diff --git a/resources/js/pages/dashboards/demo.vue b/resources/js/pages/dashboards/demo.vue
index 82c2a22..a04da55 100644
--- a/resources/js/pages/dashboards/demo.vue
+++ b/resources/js/pages/dashboards/demo.vue
@@ -8,25 +8,55 @@ import CostOverview from "@/components/CostOverview.vue";
import GeneratedLeadsCard from "@/views/dashboards/ecommerce/EcommerceGeneratedLeads.vue";
import AgGridTable from '@/views/demos/forms/tables/data-table/AgGridTable.vue';
-const ganttColumns = ref([
- { field: 'id', headerName: 'ID', sortable: true, filter: true, width: 80 },
- { field: 'todo', headerName: 'Task', sortable: true, filter: true, flex: 1 },
- { field: 'userId', headerName: 'User ID', sortable: true, filter: true, width: 100 },
- {
- field: 'completed', headerName: 'Status', sortable: true, filter: true, width: 120, cellRenderer: (params) => {
- return params.value ? '✓ Completed' : '⏳ Pending'
+const ganttColumns = ref([])
+
+const generateColumnsFromData = (data) => {
+ if (!data || data.length === 0) return []
+
+ const firstItem = data[0]
+ const columns = Object.keys(firstItem).map(key => {
+ const column = {
+ field: key,
+ headerName: key.charAt(0).toUpperCase() + key.slice(1).replace(/([A-Z])/g, ' $1'),
+ sortable: true,
+ filter: true,
+ flex: 1
}
- }
-])
+
+ if (key === 'id') {
+ column.width = 80
+ column.flex = undefined
+ }
+
+ if (key === 'completed' && typeof firstItem[key] === 'boolean') {
+ column.width = 120
+ column.flex = undefined
+ column.cellRenderer = (params) => {
+ return params.value ? '✓ Completed' : '⏳ Pending'
+ }
+ }
+
+ if (key === 'userId') {
+ column.width = 100
+ column.flex = undefined
+ }
+
+ return column
+ })
+
+ return columns
+}
const processAPIData = (response) => {
- return response.todos || response
+ const data = response.todos || response
+ ganttColumns.value = generateColumnsFromData(data)
+ return data
}
onMounted(async () => {
const grid = GridStack.init({
column: 12,
- cellHeight: '110',
+ cellHeight: '105',
float: true,
draggable: { handle: '.grid-stack-item-content' },
resizable: true,
@@ -39,7 +69,7 @@ onMounted(async () => {
-
+
@@ -64,12 +94,11 @@ onMounted(async () => {
-
diff --git a/resources/js/pages/dashboards/gantt.vue b/resources/js/pages/dashboards/gantt.vue
index 489094c..b63025d 100644
--- a/resources/js/pages/dashboards/gantt.vue
+++ b/resources/js/pages/dashboards/gantt.vue
@@ -776,642 +776,6 @@ onBeforeUnmount(() => {
-
-
-
\ No newline at end of file
diff --git a/resources/js/views/demos/forms/tables/data-table/AgGridTable.vue b/resources/js/views/demos/forms/tables/data-table/AgGridTable.vue
index 3d8e960..ba12524 100644
--- a/resources/js/views/demos/forms/tables/data-table/AgGridTable.vue
+++ b/resources/js/views/demos/forms/tables/data-table/AgGridTable.vue
@@ -14,75 +14,83 @@ import autoTable from 'jspdf-autotable'
ModuleRegistry.registerModules([AllCommunityModule])
const props = defineProps({
- apiUrl: {
- type: String,
- default: ''
- },
- apiHeaders: {
- type: Object,
- default: () => ({})
- },
- data: {
- type: Array,
- default: () => []
- },
- columns: {
- type: Array,
- default: () => []
- },
- height: {
- type: String,
- default: '600px'
- },
- mobileHeight: {
- type: String,
- default: '400px'
- },
- enableExport: {
- type: Boolean,
- default: true
- },
- enableEdit: {
- type: Boolean,
- default: true
- },
- enableDelete: {
- type: Boolean,
- default: true
- },
- enableSearch: {
- type: Boolean,
- default: true
- },
- loading: {
- type: Boolean,
- default: false
- },
- customActions: {
- type: Array,
- default: () => []
- },
- dataProcessor: {
- type: Function,
- default: null
- },
- schemaMap: {
- type: Object,
- default: () => ({})
- },
- keepOriginalKeys: {
- type: Boolean,
- default: false
- }
+ apiUrl: {
+ type: String,
+ default: ''
+ },
+ apiHeaders: {
+ type: Object,
+ default: () => ({})
+ },
+ data: {
+ type: Array,
+ default: () => []
+ },
+ columns: {
+ type: Array,
+ default: () => []
+ },
+ height: {
+ type: String,
+ default: '600px'
+ },
+ mobileHeight: {
+ type: String,
+ default: '400px'
+ },
+ enableExport: {
+ type: Boolean,
+ default: true
+ },
+ enableEdit: {
+ type: Boolean,
+ default: true
+ },
+ enableDelete: {
+ type: Boolean,
+ default: true
+ },
+ enableSearch: {
+ type: Boolean,
+ default: true
+ },
+ loading: {
+ type: Boolean,
+ default: false
+ },
+ customActions: {
+ type: Array,
+ default: () => []
+ },
+ dataProcessor: {
+ type: Function,
+ default: null
+ },
+ schemaMap: {
+ type: Object,
+ default: () => ({})
+ },
+ keepOriginalKeys: {
+ type: Boolean,
+ default: false
+ },
+ maxCellTextLength: {
+ type: Number,
+ default: 30
+ },
+ tableId: {
+ type: String,
+ default: 'default'
+ }
})
const emit = defineEmits([
- 'data-updated',
- 'row-selected',
- 'row-deleted',
- 'row-edited',
- 'export-completed',
- 'api-error'
+ 'data-updated',
+ 'row-selected',
+ 'row-deleted',
+ 'row-edited',
+ 'export-completed',
+ 'api-error'
])
const { isMobile, isTablet, windowWidth } = useResponsive()
@@ -127,13 +135,42 @@ const processedData = computed(() => {
})
})
+// Custom cell renderer component for long text with tooltip
+const LongTextCellRenderer = {
+ template: `
+
+ {{ displayText }}
+
+ `,
+ setup(props) {
+ const fullText = props.value || ''
+ const maxLength = props.colDef?.cellRendererParams?.maxLength || 30
+ const displayText = fullText.length > maxLength
+ ? fullText.substring(0, maxLength) + '...'
+ : fullText
+
+ return {
+ fullText,
+ displayText
+ }
+ }
+}
+
// Dynamically infer columns from data when not provided
const inferredColumns = computed(() => {
if (Array.isArray(props.columns) && props.columns.length) return props.columns
const rows = Array.isArray(processedData.value) ? processedData.value.slice(0, 50) : []
if (!rows.length) return []
- // Collect union of keys across sample rows
const keySet = new Set()
rows.forEach(r => {
if (r && typeof r === 'object') {
@@ -143,7 +180,7 @@ const inferredColumns = computed(() => {
}
})
- const keys = Array.from(keySet)
+ const keys = Array.from(keySet).filter(k => k !== 'action')
const toTitle = (k) => k
.replace(/_/g, ' ')
.replace(/([a-z])([A-Z])/g, '$1 $2')
@@ -151,20 +188,28 @@ const inferredColumns = computed(() => {
const isLikelyDate = (v) => {
if (typeof v !== 'string') return false
- // ISO or yyyy-mm-dd or timestamps in string
if (/^\d{4}-\d{2}-\d{2}/.test(v) || /T\d{2}:\d{2}:\d{2}/.test(v)) return true
const d = new Date(v)
return !isNaN(d.getTime())
}
- return keys.map(k => {
- // Detect type by scanning a few rows
- let sampleVal = undefined
- for (let i = 0; i < rows.length; i++) {
+ const isLongText = (samples) => {
+ return samples.some(val =>
+ typeof val === 'string' && val.length > props.maxCellTextLength
+ )
+ }
+
+ const columns = keys.map(k => {
+ const sampleValues = []
+ for (let i = 0; i < Math.min(10, rows.length); i++) {
const val = rows[i]?.[k]
- if (val !== undefined && val !== null) { sampleVal = val; break }
+ if (val !== undefined && val !== null) {
+ sampleValues.push(val)
+ }
}
+ const sampleVal = sampleValues[0]
+
const col = {
field: k,
headerName: toTitle(k),
@@ -173,6 +218,7 @@ const inferredColumns = computed(() => {
editable: false,
minWidth: 100,
flex: 1,
+ suppressMovable: true
}
if (typeof sampleVal === 'number') {
@@ -189,16 +235,42 @@ const inferredColumns = computed(() => {
const d = new Date(p.value)
return isNaN(d.getTime()) ? String(p.value) : d.toISOString().split('T')[0]
}
+ } else if (typeof sampleVal === 'string' && isLongText(sampleValues)) {
+ col.flex = 1
+ col.cellRenderer = LongTextCellRenderer
+ col.cellRendererParams = {
+ maxLength: props.maxCellTextLength
+ }
+ col.tooltipField = k
} else {
col.flex = 1
}
+
return col
})
+
+ columns.push({
+ headerName: "ACTION",
+ field: "action",
+ sortable: false,
+ filter: false,
+ flex: 1,
+ minWidth: 100,
+ hide: false,
+ editable: false,
+ cellRenderer: actionButtonsRenderer,
+ suppressHeaderMenuButton: true,
+ suppressMovable: true,
+ lockPosition: true,
+ colId: "action"
+ })
+
+ return columns
})
-// Build grid with inferred columns (or props.columns if provided)
-// Note: Passing inferredColumns.value at setup time; if you need live re-init on schema change, we can add a watcher.
-let gridSetup = useDataTableGrid(processedData, isMobile, isTablet, inferredColumns)
+
+let gridSetup = useDataTableGrid(processedData, isMobile, isTablet, inferredColumns, props.tableId)
+console.log('AgGridTable props.tableId:', props.tableId);
const gridApi = gridSetup.gridApi
const columnDefs = gridSetup.columnDefs
const rowData = gridSetup.rowData
@@ -228,344 +300,339 @@ watch(
)
const fetchData = async () => {
- if (!props.apiUrl) return
-
- isLoading.value = true
- try {
- const response = await fetch(props.apiUrl, {
- headers: {
- 'Content-Type': 'application/json',
- ...props.apiHeaders
- }
- })
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`)
- }
-
- const result = await response.json()
-
- let processedApiData = []
- if (props.dataProcessor && typeof props.dataProcessor === 'function') {
- processedApiData = props.dataProcessor(result)
- } else {
- processedApiData = result.todos || result.data || result.users || result.products || (Array.isArray(result) ? result : [])
- }
-
- fetchedData.value = Array.isArray(processedApiData) ? processedApiData : []
- console.log('Fetched data set to:', fetchedData.value)
- emit('data-updated', fetchedData.value)
- } catch (error) {
- console.error('Error fetching data:', error)
- emit('api-error', error)
- } finally {
- isLoading.value = false
- }
+ if (!props.apiUrl) return
+
+ isLoading.value = true
+ try {
+ const response = await fetch(props.apiUrl, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...props.apiHeaders
+ }
+ })
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+
+ const result = await response.json()
+
+ let processedApiData = []
+ if (props.dataProcessor && typeof props.dataProcessor === 'function') {
+ processedApiData = props.dataProcessor(result)
+ } else {
+ processedApiData = result.todos || result.data || result.users || result.products || (Array.isArray(result) ? result : [])
+ }
+
+ fetchedData.value = Array.isArray(processedApiData) ? processedApiData : []
+ console.log('Fetched data set to:', fetchedData.value)
+ emit('data-updated', fetchedData.value)
+ } catch (error) {
+ console.error('Error fetching data:', error)
+ emit('api-error', error)
+ } finally {
+ isLoading.value = false
+ }
}
const saveToAPI = async (userData, isUpdate = false) => {
- if (!props.apiUrl) return
-
- try {
- const method = isUpdate ? 'PUT' : 'POST'
- const url = isUpdate ? `${props.apiUrl}/${userData.id}` : props.apiUrl
-
- const response = await fetch(url, {
- method,
- headers: {
- 'Content-Type': 'application/json',
- ...props.apiHeaders
- },
- body: JSON.stringify(userData)
- })
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`)
- }
-
- await fetchData()
- return await response.json()
- } catch (error) {
- console.error('Error saving data:', error)
- emit('api-error', error)
- throw error
- }
+ if (!props.apiUrl) return
+
+ try {
+ const method = isUpdate ? 'PUT' : 'POST'
+ const url = isUpdate ? `${props.apiUrl}/${userData.id}` : props.apiUrl
+
+ const response = await fetch(url, {
+ method,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...props.apiHeaders
+ },
+ body: JSON.stringify(userData)
+ })
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+
+ await fetchData()
+ return await response.json()
+ } catch (error) {
+ console.error('Error saving data:', error)
+ emit('api-error', error)
+ throw error
+ }
}
const deleteFromAPI = async (id) => {
- if (!props.apiUrl) return
-
- try {
- const response = await fetch(`${props.apiUrl}/${id}`, {
- method: 'DELETE',
- headers: {
- 'Content-Type': 'application/json',
- ...props.apiHeaders
- }
- })
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`)
- }
-
- await fetchData()
- return true
- } catch (error) {
- console.error('Error deleting data:', error)
- emit('api-error', error)
- throw error
- }
+ if (!props.apiUrl) return
+
+ try {
+ const response = await fetch(`${props.apiUrl}/${id}`, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...props.apiHeaders
+ }
+ })
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+
+ await fetchData()
+ return true
+ } catch (error) {
+ console.error('Error deleting data:', error)
+ emit('api-error', error)
+ throw error
+ }
}
const {
- setupGlobalHandlers,
- cleanupGlobalHandlers,
- exportToCSV,
- exportToExcel,
- saveUser,
- confirmDelete,
- cancelDelete,
- showDetailsDialog,
- selectedRowData,
- deleteRowData,
- showDeleteDialog,
- deleteRow
-} = useDataTableActions(gridApi, rowData)
+ setupGlobalHandlers,
+ cleanupGlobalHandlers,
+ exportToCSV,
+ exportToExcel,
+ saveUser,
+ confirmDelete,
+ cancelDelete,
+ showDetailsDialog,
+ selectedRowData,
+ deleteRowData,
+ showDeleteDialog,
+ deleteRow
+} = useDataTableActions(gridApi, rowData, props.tableId)
const handleSaveUser = async (updatedData) => {
-try {
- // Optimistic update: update grid first
- saveUser(updatedData)
+ try {
+ // Optimistic update: update grid first
+ saveUser(updatedData)
- if (props.apiUrl) {
- try {
- await saveToAPI(updatedData, updatedData.id ? true : false)
- } catch (apiErr) {
- console.error('API save failed, refetching to sync:', apiErr)
- // Re-sync from server on failure
- await fetchData()
- emit('api-error', apiErr)
- return
+ if (props.apiUrl) {
+ try {
+ await saveToAPI(updatedData, updatedData.id ? true : false)
+ } catch (apiErr) {
+ console.error('API save failed, refetching to sync:', apiErr)
+ // Re-sync from server on failure
+ await fetchData()
+ emit('api-error', apiErr)
+ return
+ }
}
+ emit('row-edited', updatedData)
+ } catch (error) {
+ console.error('Error saving user:', error)
}
- emit('row-edited', updatedData)
-} catch (error) {
- console.error('Error saving user:', error)
-}
}
const handleConfirmDelete = async () => {
-try {
- // Optimistic remove from grid first
- confirmDelete()
+ try {
+ // Optimistic remove from grid first
+ confirmDelete()
- if (props.apiUrl && deleteRowData.value?.id) {
- try {
- await deleteFromAPI(deleteRowData.value.id)
- } catch (apiErr) {
- console.error('API delete failed, refetching to restore:', apiErr)
- await fetchData()
- emit('api-error', apiErr)
- return
+ if (props.apiUrl && deleteRowData.value?.id) {
+ try {
+ await deleteFromAPI(deleteRowData.value.id)
+ } catch (apiErr) {
+ console.error('API delete failed, refetching to restore:', apiErr)
+ await fetchData()
+ emit('api-error', apiErr)
+ return
+ }
}
+ emit('row-deleted', deleteRowData.value)
+ } catch (error) {
+ console.error('Error deleting row:', error)
}
- emit('row-deleted', deleteRowData.value)
-} catch (error) {
- console.error('Error deleting row:', error)
-}
}
const quickFilter = ref('')
watch(quickFilter, (newValue) => {
- if (gridApi.value) {
- gridApi.value.setGridOption('quickFilterText', newValue || '')
- }
+ if (gridApi.value) {
+ gridApi.value.setGridOption('quickFilterText', newValue || '')
+ }
}, { immediate: false })
const onGridReady = params => {
- gridReady(params)
- setupGlobalHandlers()
-
- if (props.apiUrl) {
- fetchData()
- }
+ gridReady(params)
+ setupGlobalHandlers()
- // Observe container size to recalc pagination
- try {
- const container = gridContainer?.value
- if (container && typeof ResizeObserver !== 'undefined') {
- const ro = new ResizeObserver(() => {
- if (gridApi.value) {
- updatePagination()
- setTimeout(() => {
- gridApi.value?.sizeColumnsToFit?.()
- }, 50)
- }
- })
- ro.observe(container)
- }
- } catch (e) {
- // no-op
- }
+ setTimeout(() => {
+ const allPageSizeWrappers = document.querySelectorAll('.ag-paging-page-size .ag-picker-field-wrapper');
+ allPageSizeWrappers.forEach((pageSizeWrapper, index) => {
+ if (!pageSizeWrapper.hasAttribute('data-fixed')) {
+ pageSizeWrapper.setAttribute('data-fixed', 'true');
+
+ pageSizeWrapper.addEventListener('mousedown', (e) => {
+ if (e.button !== 0) {
+ e.preventDefault();
+ e.stopPropagation();
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ const isExpanded = pageSizeWrapper.getAttribute('aria-expanded') === 'true';
+ pageSizeWrapper.setAttribute('aria-expanded', !isExpanded);
+ pageSizeWrapper.classList.toggle('ag-picker-collapsed', isExpanded);
+ pageSizeWrapper.classList.toggle('ag-picker-expanded', !isExpanded);
+ const selectList = pageSizeWrapper.parentNode.querySelector('[id^="ag-select-list"]');
+ if (selectList) {
+ selectList.style.display = isExpanded ? 'none' : 'block';
+ }
+ });
+
+ pageSizeWrapper.addEventListener('contextmenu', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ });
+ }
+ });
+ }, 1000);
+
+ if (props.apiUrl) {
+ fetchData()
+ }
+
+ try {
+ const container = gridContainer?.value
+ if (container && typeof ResizeObserver !== 'undefined') {
+ const ro = new ResizeObserver(() => {
+ if (gridApi.value) {
+ updatePagination()
+ setTimeout(() => {
+ gridApi.value?.sizeColumnsToFit?.()
+ }, 50)
+ }
+ })
+ ro.observe(container)
+ }
+ } catch (e) { }
}
const handleQuickFilterUpdate = (value) => {
- quickFilter.value = value || ''
-
- if (gridApi.value) {
- gridApi.value.setGridOption('quickFilterText', value || '')
- }
+ quickFilter.value = value || ''
+
+ if (gridApi.value) {
+ gridApi.value.setGridOption('quickFilterText', value || '')
+ }
}
const refreshData = () => {
- if (props.apiUrl) {
- fetchData()
- }
+ if (props.apiUrl) {
+ fetchData()
+ }
}
defineExpose({
- refreshData,
- fetchData,
- gridApi
+ refreshData,
+ fetchData,
+ gridApi
})
onMounted(() => {
- setupGlobalHandlers()
- window.addEventListener('resize', handleResize)
+ setupGlobalHandlers()
+ window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
- cleanupGlobalHandlers()
- window.removeEventListener('resize', handleResize)
+ cleanupGlobalHandlers()
+ window.removeEventListener('resize', handleResize)
})
const handleResize = () => {
- windowWidth.value = window.innerWidth
- if (gridApi.value) {
- updateColumnVisibility()
- updatePagination()
- setTimeout(() => {
- if (gridApi.value && gridApi.value.sizeColumnsToFit) {
- gridApi.value.sizeColumnsToFit()
- }
- }, 100)
- }
+ windowWidth.value = window.innerWidth
+ if (gridApi.value) {
+ updateColumnVisibility()
+ updatePagination()
+ setTimeout(() => {
+ if (gridApi.value) {
+ gridApi.value.sizeColumnsToFit();
+ }
+ }, 100)
+ }
}
-const exportToPDF = () => {
- if (!gridApi.value) {
- console.error('Grid API not available')
- return
- }
- try {
- const doc = new jsPDF()
- const rowDataForPDF = []
- const headers = []
-
- columnDefs.value.forEach(col => {
- if (col.headerName && col.field && col.field !== 'action') {
- headers.push(col.headerName)
- }
- })
-
- gridApi.value.forEachNodeAfterFilterAndSort(node => {
- const row = []
- columnDefs.value.forEach(col => {
- if (col.field && col.field !== 'action') {
- let value = node.data[col.field] || ''
- row.push(value)
- }
- })
- rowDataForPDF.push(row)
- })
-
- autoTable(doc, {
- head: [headers],
- body: rowDataForPDF,
- styles: {
- fontSize: 8,
- cellPadding: 2
- },
- headStyles: {
- fillColor: [41, 128, 185],
- textColor: 255,
- fontStyle: 'bold'
- },
- alternateRowStyles: {
- fillColor: [245, 245, 245]
- },
- margin: { top: 20 }
- })
-
- doc.save(`data-table-${new Date().toISOString().split('T')[0]}.pdf`)
- emit('export-completed', 'pdf')
- } catch (error) {
- console.error('Error exporting PDF:', error)
- }
+const exportToPDF = () => {
+ if (!gridApi.value) {
+ console.error('Grid API not available')
+ return
+ }
+
+ try {
+ const doc = new jsPDF()
+ const rowDataForPDF = []
+ const headers = []
+
+ columnDefs.value.forEach(col => {
+ if (col.headerName && col.field && col.field !== 'action') {
+ headers.push(col.headerName)
+ }
+ })
+
+ gridApi.value.forEachNodeAfterFilterAndSort(node => {
+ const row = []
+ columnDefs.value.forEach(col => {
+ if (col.field && col.field !== 'action') {
+ let value = node.data[col.field] || ''
+ row.push(value)
+ }
+ })
+ rowDataForPDF.push(row)
+ })
+
+ autoTable(doc, {
+ head: [headers],
+ body: rowDataForPDF,
+ styles: {
+ fontSize: 8,
+ cellPadding: 2
+ },
+ headStyles: {
+ fillColor: [41, 128, 185],
+ textColor: 255,
+ fontStyle: 'bold'
+ },
+ alternateRowStyles: {
+ fillColor: [245, 245, 245]
+ },
+ margin: { top: 20 }
+ })
+
+ doc.save(`data-table-${new Date().toISOString().split('T')[0]}.pdf`)
+ emit('export-completed', 'pdf')
+ } catch (error) {
+ console.error('Error exporting PDF:', error)
+ }
}
-
-
-
-
+
+
+
+
-
+
-
+
-
-
-
+
+
+
-
+
-
-
-
\ No newline at end of file
+
+
+
diff --git a/resources/js/views/demos/forms/tables/data-table/composables/useDataTableGrid.js b/resources/js/views/demos/forms/tables/data-table/composables/useDataTableGrid.js
index 1a205a9..1b2604d 100644
--- a/resources/js/views/demos/forms/tables/data-table/composables/useDataTableGrid.js
+++ b/resources/js/views/demos/forms/tables/data-table/composables/useDataTableGrid.js
@@ -470,40 +470,42 @@ export function useDataTableGrid(
return []
}
- const processCustomColumns = (columns) => {
- if (!Array.isArray(columns) || columns.length === 0) {
- const base = createDefaultColumns();
- return ensureSingleSelectionColumn(base);
- }
+ const processCustomColumns = (columns) => {
+ if (!Array.isArray(columns) || columns.length === 0) {
+ const base = createDefaultColumns();
+ return ensureSingleSelectionColumn(base);
+ }
- const processedColumns = columns.map(col => ({
- ...col,
- // Default to inline-editable unless explicitly disabled, skip action column
- editable: col.field === 'action' ? false : (col.editable !== undefined ? col.editable : true),
- suppressHeaderMenuButton: col.suppressHeaderMenuButton !== undefined ? col.suppressHeaderMenuButton : false,
- }));
+ const processedColumns = columns.map(col => ({
+ ...col,
+ editable: col.field === 'action' ? false : (col.editable !== undefined ? col.editable : true),
+ suppressHeaderMenuButton: col.suppressHeaderMenuButton !== undefined ? col.suppressHeaderMenuButton : false,
+ }));
- const withSelection = ensureSingleSelectionColumn(processedColumns);
+ const withSelection = ensureSingleSelectionColumn(processedColumns);
+
+ const actionIndex = withSelection.findIndex(col => col.field === 'action');
+
+ if (actionIndex !== -1) {
+ const actionColumn = withSelection.splice(actionIndex, 1)[0];
+ withSelection.push(actionColumn);
+ } else {
+ withSelection.push({
+ headerName: "ACTION",
+ field: "action",
+ sortable: false,
+ filter: false,
+ flex: 1,
+ minWidth: 100,
+ hide: false,
+ editable: false,
+ cellRenderer: actionButtonsRenderer,
+ suppressHeaderMenuButton: true,
+ });
+ }
- const hasActionColumn = processedColumns.some(col => col.field === 'action');
-
- if (!hasActionColumn) {
- withSelection.push({
- headerName: "ACTION",
- field: "action",
- sortable: false,
- filter: false,
- flex: 1,
- minWidth: 100,
- hide: false,
- editable: false,
- cellRenderer: actionButtonsRenderer,
- suppressHeaderMenuButton: true,
- });
- }
-
- return withSelection;
- };
+ return withSelection;
+};
const columnDefs = computed(() => {
const cols = resolveColumns()
@@ -529,6 +531,7 @@ export function useDataTableGrid(
suppressHeaderMenuButton: false,
}));
+ // 🔥 FIX: Improved gridOptions with better pagination settings
const gridOptions = computed(() => ({
theme: "legacy",
headerHeight: isMobile.value ? 48 : 56,
@@ -541,9 +544,14 @@ export function useDataTableGrid(
rowSelection: 'multiple',
rowMultiSelectWithClick: true,
suppressRowClickSelection: false,
+
+ // 🔥 FIXED PAGINATION SETTINGS
pagination: true,
- paginationPageSize: isMobile.value ? 5 : 10,
- paginationPageSizeSelector: isMobile.value ? [5, 10, 20] : [5, 10, 20, 50],
+ paginationPageSize: isMobile.value ? 10 : 20,
+ paginationPageSizeSelector: [5, 10, 20, 50, 100],
+ paginationAutoPageSize: false, // 🔥 KEY: Disable auto page sizing
+ suppressPaginationPanel: false, // 🔥 KEY: Show pagination controls
+
enableCellTextSelection: true,
suppressHorizontalScroll: false,
alwaysShowHorizontalScroll: false,
@@ -597,29 +605,18 @@ export function useDataTableGrid(
}
};
+ // 🔥 SIMPLIFIED UPDATE PAGINATION - Only for responsive page sizes
const updatePagination = () => {
if (!gridApi.value) return;
try {
- const gui = gridApi.value.getGui && gridApi.value.getGui();
- const containerHeight = gui ? gui.clientHeight : 0;
-
- const headerH = Number(gridOptions.value.headerHeight || (isMobile.value ? 48 : 56));
- // Estimate footer height based on overrides (mobile/desktop)
- const footerH = isMobile.value ? 48 : 56;
- const rowH = Number(gridOptions.value.rowHeight || (isMobile.value ? 44 : 52));
- const padding = 8;
-
- let available = containerHeight - headerH - footerH - padding;
- if (!Number.isFinite(available) || available <= 0) {
- available = (isMobile.value ? 360 : 520) - headerH - footerH - padding;
- }
-
- let rowsPerPage = Math.floor(available / rowH);
- rowsPerPage = Math.max(3, Math.min(rowsPerPage, 100));
-
+ // Simple responsive pagination size adjustment
+ const newPageSize = isMobile.value ? 10 : 20;
+
if (typeof gridApi.value.paginationSetPageSize === 'function') {
- gridApi.value.paginationSetPageSize(rowsPerPage);
+ gridApi.value.paginationSetPageSize(newPageSize);
+ } else if (typeof gridApi.value.setGridOption === 'function') {
+ gridApi.value.setGridOption('paginationPageSize', newPageSize);
}
} catch (error) {
console.warn('Error updating pagination:', error);
@@ -632,7 +629,7 @@ export function useDataTableGrid(
console.log('Grid API set:', gridApi.value)
updateColumnVisibility();
- updatePagination();
+ // 🔥 FIX: Don't call updatePagination immediately - let AG Grid handle it
setTimeout(() => {
if (gridApi.value && gridApi.value.sizeColumnsToFit) {
@@ -664,7 +661,7 @@ export function useDataTableGrid(
watch([isMobile, isTablet], () => {
updateColumnVisibility();
- updatePagination();
+ updatePagination(); // Only for responsive size changes
});
watch(data, (newData) => {
@@ -687,7 +684,6 @@ export function useDataTableGrid(
}
// Helpers
-
function ensureSingleSelectionColumn(columns) {
if (!Array.isArray(columns)) return columns;
@@ -714,6 +710,4 @@ function ensureSingleSelectionColumn(columns) {
}
return unique;
-}
-
-// removed selection column injection
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/resources/styles/ag-grid-overrides.scss b/resources/styles/ag-grid-overrides.scss
index 9a926df..2e8e340 100644
--- a/resources/styles/ag-grid-overrides.scss
+++ b/resources/styles/ag-grid-overrides.scss
@@ -607,31 +607,7 @@
justify-content: space-between !important;
transition: all 0.2s ease-in-out !important;
position: relative !important;
- overflow: visible !important;
-}
-
-/* Ensure page-size picker can overflow and is clickable */
-.ag-theme-alpine-dark.vuetify-grid .ag-paging-page-size,
-.ag-theme-alpine.vuetify-grid .ag-paging-page-size {
- position: relative !important;
- overflow: visible !important;
- z-index: 2 !important;
-}
-
-.ag-theme-alpine-dark.vuetify-grid .ag-wrapper.ag-picker-field-wrapper,
-.ag-theme-alpine.vuetify-grid .ag-wrapper.ag-picker-field-wrapper {
- position: relative !important;
- pointer-events: auto !important;
-}
-
-/* Raise ag-Grid popups above footer */
-.ag-theme-alpine-dark.vuetify-grid .ag-popup,
-.ag-theme-alpine.vuetify-grid .ag-popup,
-.ag-theme-alpine-dark.vuetify-grid .ag-select-list,
-.ag-theme-alpine.vuetify-grid .ag-select-list,
-.ag-theme-alpine-dark.vuetify-grid .ag-menu,
-.ag-theme-alpine.vuetify-grid .ag-menu {
- z-index: 9999 !important;
+ overflow: hidden !important;
}
.ag-theme-alpine-dark.vuetify-grid .ag-paging-panel:hover {
@@ -971,7 +947,7 @@
justify-content: space-between !important;
transition: all 0.2s ease-in-out !important;
position: relative !important;
- overflow: visible !important;
+ overflow: hidden !important;
font-size: 14px !important;
}
diff --git a/resources/styles/gantt-chart.scss b/resources/styles/gantt-chart.scss
new file mode 100644
index 0000000..a03ebb6
--- /dev/null
+++ b/resources/styles/gantt-chart.scss
@@ -0,0 +1,772 @@
+.gantt-container {
+ width: 100%;
+ height: 650px;
+ background: rgb(var(--v-theme-surface));
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+ border-radius: 12px;
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ color: rgb(var(--v-theme-on-surface));
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+ overflow: hidden;
+}
+
+.gantt-toolbar {
+ padding: 16px 20px;
+ background: rgb(var(--v-theme-surface));
+ border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+ display: flex;
+ gap: 12px;
+}
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ padding: 10px 16px;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.2s ease;
+ gap: 8px;
+}
+
+.btn-icon {
+ flex-shrink: 0;
+}
+
+.btn-primary {
+ background: rgb(var(--v-theme-primary));
+ color: rgb(var(--v-theme-on-primary));
+ box-shadow: 0 2px 4px rgba(var(--v-theme-primary), 0.3);
+}
+
+.btn-primary:hover {
+ background: rgba(var(--v-theme-primary), 0.8);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px rgba(var(--v-theme-primary), 0.4);
+}
+
+.btn-danger {
+ background: rgb(var(--v-theme-error));
+ color: rgb(var(--v-theme-on-error));
+ box-shadow: 0 2px 4px rgba(var(--v-theme-error), 0.3);
+}
+
+.btn-danger:hover {
+ background: rgba(var(--v-theme-error), 0.8);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px rgba(var(--v-theme-error), 0.4);
+}
+
+.btn-success {
+ background: rgb(var(--v-theme-success));
+ color: rgb(var(--v-theme-on-success));
+ box-shadow: 0 2px 4px rgba(var(--v-theme-success), 0.3);
+}
+
+.btn-success:hover {
+ background: rgba(var(--v-theme-success), 0.8);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px rgba(var(--v-theme-success), 0.4);
+}
+
+.btn-secondary {
+ background: rgba(var(--v-theme-on-surface), 0.05);
+ color: rgb(var(--v-theme-on-surface));
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+}
+
+.btn-secondary:hover {
+ background: rgba(var(--v-theme-on-surface), 0.1);
+ border-color: rgba(var(--v-border-color), 0.5);
+ transform: translateY(-1px);
+}
+
+.gantt-chart {
+ height: calc(100% - 77px);
+ min-height: 500px;
+}
+
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(4px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10000;
+ animation: modalOverlayFadeIn 0.3s ease;
+}
+
+@keyframes modalOverlayFadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.modal-container {
+ background: rgb(var(--v-theme-surface));
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+ border-radius: 12px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
+ width: 90%;
+ max-width: 520px;
+ max-height: 80vh;
+ overflow: hidden;
+ animation: modalSlideIn 0.3s ease;
+}
+
+@keyframes modalSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-20px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 20px 24px;
+ border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+ background: rgb(var(--v-theme-surface));
+}
+
+.modal-title {
+ display: flex;
+ align-items: center;
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: rgb(var(--v-theme-on-surface));
+}
+
+.modal-icon {
+ margin-right: 10px;
+ flex-shrink: 0;
+ padding: 4px;
+ border-radius: 4px;
+}
+
+.delete-icon {
+ color: rgb(var(--v-theme-error));
+ background: rgba(var(--v-theme-error), 0.1);
+}
+
+.edit-icon {
+ color: rgb(var(--v-theme-primary));
+ background: rgba(var(--v-theme-primary), 0.1);
+}
+
+.add-icon {
+ color: rgb(var(--v-theme-success));
+ background: rgba(var(--v-theme-success), 0.1);
+}
+
+.modal-close {
+ background: rgba(var(--v-theme-on-surface), 0.05);
+ border: none;
+ font-size: 18px;
+ color: rgb(var(--v-theme-on-surface));
+ cursor: pointer;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 6px;
+ transition: all 0.2s ease;
+}
+
+.modal-close:hover {
+ background: rgba(var(--v-theme-on-surface), 0.1);
+ color: rgb(var(--v-theme-error));
+}
+
+.modal-body {
+ padding: 24px;
+ background: rgb(var(--v-theme-surface));
+}
+
+.modal-message {
+ font-size: 15px;
+ color: rgb(var(--v-theme-on-surface));
+ margin: 0 0 8px 0;
+ line-height: 1.5;
+}
+
+.modal-warning {
+ font-size: 13px;
+ color: rgba(var(--v-theme-on-surface), 0.6);
+ margin: 0;
+ font-style: italic;
+}
+
+.form-group {
+ margin-bottom: 20px;
+}
+
+.form-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 16px;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: 600;
+ color: rgb(var(--v-theme-on-surface));
+ font-size: 13px;
+}
+
+.form-input {
+ width: 100%;
+ padding: 12px 14px;
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+ border-radius: 6px;
+ font-size: 14px;
+ color: rgb(var(--v-theme-on-surface));
+ background: rgb(var(--v-theme-surface));
+ transition: all 0.2s ease;
+ box-sizing: border-box;
+}
+
+.form-input:focus {
+ outline: none;
+ border-color: rgb(var(--v-theme-primary));
+ box-shadow: 0 0 0 2px rgba(var(--v-theme-primary), 0.2);
+}
+
+.form-input:invalid {
+ border-color: rgb(var(--v-theme-error));
+ box-shadow: 0 0 0 2px rgba(var(--v-theme-error), 0.2);
+}
+
+.progress-input-container {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.form-range {
+ flex: 1;
+ height: 4px;
+ background: rgba(var(--v-theme-on-surface), 0.1);
+ border-radius: 2px;
+ outline: none;
+}
+
+.form-range::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 16px;
+ height: 16px;
+ background: rgb(var(--v-theme-primary));
+ border-radius: 50%;
+ cursor: pointer;
+ border: 2px solid rgb(var(--v-theme-surface));
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+}
+
+.form-range::-moz-range-thumb {
+ width: 16px;
+ height: 16px;
+ background: rgb(var(--v-theme-primary));
+ border-radius: 50%;
+ cursor: pointer;
+ border: 2px solid rgb(var(--v-theme-surface));
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+}
+
+.progress-value {
+ min-width: 40px;
+ text-align: right;
+ font-weight: 600;
+ color: rgb(var(--v-theme-primary));
+ font-size: 12px;
+ padding: 2px 6px;
+ background: rgba(var(--v-theme-primary), 0.1);
+ border-radius: 4px;
+}
+
+.modal-footer {
+ padding: 16px 24px;
+ display: flex;
+ gap: 12px;
+ justify-content: flex-end;
+ background: rgb(var(--v-theme-surface));
+ border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+}
+
+.notification {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ min-width: 300px;
+ background: rgb(var(--v-theme-surface));
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+ border-radius: 8px;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+ z-index: 10001;
+ animation: notificationSlideIn 0.3s ease;
+}
+
+@keyframes notificationSlideIn {
+ from {
+ opacity: 0;
+ transform: translateX(100%);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.notification-content {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px;
+}
+
+.notification-message {
+ font-size: 14px;
+ color: rgb(var(--v-theme-on-surface));
+ font-weight: 500;
+}
+
+.notification-close {
+ background: none;
+ border: none;
+ font-size: 16px;
+ color: rgba(var(--v-theme-on-surface), 0.6);
+ cursor: pointer;
+ padding: 0;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 3px;
+ transition: all 0.2s ease;
+}
+
+.notification-close:hover {
+ background: rgba(var(--v-theme-on-surface), 0.1);
+ color: rgb(var(--v-theme-on-surface));
+}
+
+.notification-success {
+ border-left: 3px solid rgb(var(--v-theme-success));
+}
+
+.notification-error {
+ border-left: 3px solid rgb(var(--v-theme-error));
+}
+
+.notification-warning {
+ border-left: 3px solid rgb(var(--v-theme-warning));
+}
+
+.notification-info {
+ border-left: 3px solid rgb(var(--v-theme-info));
+}
+
+.gantt-container:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+}
+
+.gantt_container {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: 13px;
+ background: rgb(var(--v-theme-surface));
+ color: rgb(var(--v-theme-on-surface));
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+ border-radius: 12px;
+ overflow: hidden;
+}
+
+.gantt_layout_content {
+ background: rgb(var(--v-theme-surface)) !important;
+ color: rgb(var(--v-theme-on-surface)) !important;
+}
+
+.gantt_grid_head_cell,
+.gantt_grid_data .gantt_cell {
+ border-right: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+ border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+}
+
+.gantt_grid_head_cell {
+ background: rgb(var(--v-theme-surface));
+ font-weight: 600;
+ text-align: center;
+ padding: 12px 10px;
+ color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.gantt_cell {
+ padding: 10px 12px;
+ vertical-align: middle;
+ line-height: 1.4;
+ color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
+ background: inherit;
+}
+
+.gantt_tree_content {
+ padding-left: 6px;
+ color: rgb(var(--v-theme-on-surface));
+ font-weight: 500;
+}
+
+.gantt_tree_icon {
+ width: 14px;
+ height: 14px;
+ margin-right: 6px;
+ opacity: 0.6;
+ color: rgb(var(--v-theme-on-surface));
+}
+
+.gantt_task_line.completed-task {
+ background: rgb(var(--v-theme-success));
+ border: 1px solid rgba(var(--v-theme-success), 0.8);
+ border-radius: 4px;
+ box-shadow: 0 1px 3px rgba(var(--v-theme-success), 0.3);
+}
+
+.gantt_task_line.high-progress-task {
+ background: rgb(var(--v-theme-primary));
+ border: 1px solid rgba(var(--v-theme-primary), 0.8);
+ border-radius: 4px;
+ box-shadow: 0 1px 3px rgba(var(--v-theme-primary), 0.3);
+}
+
+.gantt_task_line.medium-progress-task {
+ background: rgb(var(--v-theme-warning));
+ border: 1px solid rgba(var(--v-theme-warning), 0.8);
+ border-radius: 4px;
+ box-shadow: 0 1px 3px rgba(var(--v-theme-warning), 0.3);
+}
+
+.gantt_task_line.low-progress-task {
+ background: rgb(var(--v-theme-error));
+ border: 1px solid rgba(var(--v-theme-error), 0.8);
+ border-radius: 4px;
+ box-shadow: 0 1px 3px rgba(var(--v-theme-error), 0.3);
+}
+
+.summary-row {
+ background: rgba(var(--v-theme-primary), 0.1);
+ font-weight: 600;
+ color: rgb(var(--v-theme-on-surface));
+ border-left: 3px solid rgb(var(--v-theme-primary));
+}
+
+.gantt_task_progress {
+ background: rgba(255, 255, 255, 0.3);
+ border-radius: 2px;
+}
+
+.gantt_scale_cell {
+ border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+ border-right: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+ background: rgb(var(--v-theme-surface));
+ font-weight: 500;
+ color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
+ font-size: 11px;
+ text-align: center;
+}
+
+.gantt_task_line {
+ border-radius: 4px;
+ height: 20px;
+ transition: all 0.2s ease;
+}
+
+.gantt_task_line:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+
+.gantt_side_content {
+ color: rgba(var(--v-theme-on-surface), 0.6);
+ font-size: 11px;
+ margin-top: 2px;
+ font-weight: 500;
+}
+
+.completed-mark {
+ color: rgb(var(--v-theme-success));
+ font-weight: bold;
+ font-size: 12px;
+}
+
+.gantt_grid_data .gantt_row {
+ background: rgb(var(--v-theme-surface));
+}
+
+.gantt_grid_data .gantt_row.gantt_row_odd {
+ background: rgba(var(--v-theme-on-surface), 0.02);
+}
+
+.gantt_grid_data .gantt_row:hover {
+ background: rgba(var(--v-theme-primary), 0.08) !important;
+}
+
+.gantt_row:hover {
+ background: rgba(var(--v-theme-primary), 0.08) !important;
+}
+
+.gantt_row_task:hover {
+ background: rgba(var(--v-theme-primary), 0.08) !important;
+}
+
+.gantt_row.gantt_row_odd:hover {
+ background: rgba(var(--v-theme-primary), 0.1) !important;
+}
+
+.gantt_row:hover .gantt_cell {
+ background: inherit !important;
+}
+
+.gantt_row:hover .gantt_tree_content {
+ color: rgb(var(--v-theme-on-surface)) !important;
+}
+
+.gantt_selected .gantt_cell {
+ background: rgba(var(--v-theme-primary), 0.12) !important;
+ color: rgb(var(--v-theme-on-surface)) !important;
+ border-left: 3px solid rgb(var(--v-theme-primary)) !important;
+}
+
+.gantt_row_task.gantt_selected {
+ background: rgba(var(--v-theme-primary), 0.12) !important;
+}
+
+.gantt_row.gantt_selected {
+ background: rgba(var(--v-theme-primary), 0.12) !important;
+}
+
+.gantt_row.gantt_selected .gantt_tree_content {
+ color: rgb(var(--v-theme-on-surface)) !important;
+ font-weight: 600;
+}
+
+.gantt_row.gantt_selected.gantt_row_odd {
+ background: rgba(var(--v-theme-primary), 0.15) !important;
+}
+
+.gantt_selected {
+ background: rgba(var(--v-theme-primary), 0.12) !important;
+}
+
+.gantt_task_link {
+ stroke: rgba(var(--v-theme-on-surface), 0.4);
+ stroke-width: 1.5;
+}
+
+.gantt_task_link .gantt_link_arrow {
+ fill: rgba(var(--v-theme-on-surface), 0.4);
+}
+
+.gantt_milestone {
+ background: rgb(var(--v-theme-secondary));
+ border: 2px solid rgba(var(--v-theme-secondary), 0.8);
+ box-shadow: 0 1px 3px rgba(var(--v-theme-secondary), 0.3);
+}
+
+.gantt-context-menu {
+ background: rgb(var(--v-theme-surface));
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+ border-radius: 8px;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+ padding: 6px;
+ min-width: 180px;
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: 13px;
+ z-index: 9999;
+ animation: contextMenuFadeIn 0.2s ease;
+}
+
+@keyframes contextMenuFadeIn {
+ from {
+ opacity: 0;
+ transform: scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.context-menu-item {
+ display: flex;
+ align-items: center;
+ padding: 8px 10px;
+ cursor: pointer;
+ color: rgb(var(--v-theme-on-surface));
+ transition: background-color 0.1s ease;
+ border-radius: 4px;
+ font-size: 13px;
+ line-height: 1.4;
+ user-select: none;
+}
+
+.context-menu-item:hover {
+ background: rgba(var(--v-theme-primary), 0.1);
+}
+
+.context-menu-item:active {
+ background: rgba(var(--v-theme-primary), 0.15);
+}
+
+.menu-icon {
+ margin-right: 8px;
+ opacity: 0.6;
+ flex-shrink: 0;
+ color: rgb(var(--v-theme-on-surface));
+}
+
+.context-menu-separator {
+ height: 1px;
+ background: rgba(var(--v-border-color), var(--v-border-opacity));
+ margin: 4px 0;
+}
+
+.gantt_layout_root {
+ background: rgb(var(--v-theme-surface));
+}
+
+.gantt_layout_root .gantt_layout_cell {
+ background: rgb(var(--v-theme-surface));
+}
+
+.gantt_grid_scale .gantt_scale_line {
+ background: rgb(var(--v-theme-surface));
+ border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+}
+
+.gantt_scale_line {
+ color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
+}
+
+.gantt_task_content {
+ color: rgb(var(--v-theme-on-primary));
+ font-weight: 500;
+ font-size: 12px;
+}
+
+.gantt_tooltip {
+ background: rgb(var(--v-theme-surface));
+ color: rgb(var(--v-theme-on-surface));
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+ border-radius: 6px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ padding: 8px 12px;
+ font-size: 12px;
+}
+
+.gantt_grid {
+ background: rgb(var(--v-theme-surface));
+}
+
+.gantt_grid_scale {
+ background: rgb(var(--v-theme-surface));
+}
+
+.gantt_task_bg {
+ background: rgba(var(--v-theme-on-surface), 0.05);
+}
+
+.gantt_task_bg .gantt_task_row {
+ background: rgb(var(--v-theme-surface)) !important;
+}
+
+.gantt_task_bg .gantt_task_row.odd {
+ background: rgba(var(--v-theme-on-surface), 0.02) !important;
+}
+
+.gantt_task_bg .gantt_task_cell {
+ background: inherit;
+ border-right: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+}
+
+.gantt_data_area {
+ background: rgb(var(--v-theme-surface));
+}
+
+.gantt_task_scale {
+ background: rgb(var(--v-theme-surface)) !important;
+}
+
+.gantt_hor_scroll {
+ background: rgb(var(--v-theme-surface));
+}
+
+.gantt_ver_scroll {
+ background: rgb(var(--v-theme-surface));
+}
+
+.gantt_hor_scroll .gantt_scroll_hor {
+ background: rgba(var(--v-theme-on-surface), 0.2);
+}
+
+.gantt_ver_scroll .gantt_scroll_ver {
+ background: rgba(var(--v-theme-on-surface), 0.2);
+}
+
+.gantt_hor_scroll .gantt_scroll_hor:hover,
+.gantt_ver_scroll .gantt_scroll_ver:hover {
+ background: rgba(var(--v-theme-on-surface), 0.4);
+}
+
+.gantt_task_scale .gantt_scale_line {
+ background: rgb(var(--v-theme-surface)) !important;
+ color: rgb(var(--v-theme-on-surface)) !important;
+}
+
+.gantt_task_scale .gantt_scale_cell {
+ background: rgb(var(--v-theme-surface)) !important;
+ color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important;
+ border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
+}
+
+.gantt_bars_area {
+ background: rgb(var(--v-theme-surface)) !important;
+}
+
+.gantt_links_area {
+ background: rgb(var(--v-theme-surface)) !important;
+}
+
+.gantt_task_link .gantt_link_line_left,
+.gantt_task_link .gantt_link_line_right,
+.gantt_task_link .gantt_link_line_up,
+.gantt_task_link .gantt_link_line_down {
+ background: rgba(var(--v-theme-on-surface), 0.4) !important;
+}
+
+.gantt_task_link .gantt_link_corner {
+ border-color: rgba(var(--v-theme-on-surface), 0.4) !important;
+}
+
+.gantt_task_link .gantt_link_arrow:before {
+ border-color: rgba(var(--v-theme-on-surface), 0.4) !important;
+}
\ No newline at end of file