feat: reusable ag grid

This commit is contained in:
2025-09-03 17:28:04 +03:30
parent dafb07375a
commit c382d97d8d
7 changed files with 1311 additions and 560 deletions

View File

@@ -1,12 +1,27 @@
<script setup> <script setup>
import { onMounted, nextTick } from 'vue' import { onMounted, nextTick, ref } from 'vue'
import { GridStack } from 'gridstack' import { GridStack } from 'gridstack'
import ProjectActivityBarChart from "@/components/ProjectActivityBarChart.vue"; import ProjectActivityBarChart from "@/components/ProjectActivityBarChart.vue";
import AnalysisCard from "@/components/AnalysisCard.vue"; import AnalysisCard from "@/components/AnalysisCard.vue";
import CostOverview from "@/components/CostOverview.vue"; import CostOverview from "@/components/CostOverview.vue";
import GeneratedLeadsCard from "@/views/dashboards/ecommerce/EcommerceGeneratedLeads.vue"; import GeneratedLeadsCard from "@/views/dashboards/ecommerce/EcommerceGeneratedLeads.vue";
import GanttChart from './gantt.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 ? '<span style="color: green; font-weight: bold;">✓ Completed</span>' : '<span style="color: orange; font-weight: bold;">⏳ Pending</span>'
}
}
])
const processAPIData = (response) => {
return response.todos || response
}
onMounted(async () => { onMounted(async () => {
const grid = GridStack.init({ const grid = GridStack.init({
@@ -49,9 +64,12 @@ onMounted(async () => {
<AnalysisCard /> <AnalysisCard />
</div> </div>
</div> </div>
<div class="grid-stack-item" gs-w="12" gs-h="8" gs-max-h="8"> <div class="grid-stack-item" gs-w="12" gs-h="4" gs-max-h="8">
<div class="grid-stack-item-content"> <div class="grid-stack-item-content">
<GanttChart /> <AgGridTable :columns="ganttColumns" api-url="https://dummyjson.com/todos"
:data-processor="processAPIData" :enable-export="true" :enable-edit="true" :enable-delete="true"
:enable-search="true" height="500px" />
</div> </div>
</div> </div>
</div> </div>
@@ -69,11 +87,11 @@ onMounted(async () => {
margin-bottom: 3.2rem; margin-bottom: 3.2rem;
} }
.grid-stack-item:has(GanttChart) { .grid-stack-item:has(AgGridTable) {
height: 100vh !important; height: 100vh !important;
} }
.grid-stack-item:has(GanttChart) .grid-stack-item-content { .grid-stack-item:has(AgGridTable) .grid-stack-item-content {
height: 100vh; height: 100vh;
} }
</style> </style>

View File

@@ -1,9 +1,7 @@
<script setup> <script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue' import { ref, watch, onMounted, onUnmounted, computed } from 'vue'
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
import data from '@/views/demos/forms/tables/data-table/datatable'
import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community' import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'
import { ExcelExportModule } from 'ag-grid-enterprise'
import DataTableHeader from './components/DataTableHeader.vue' import DataTableHeader from './components/DataTableHeader.vue'
import UserDetailsDialog from './components/UserDetailsDialog.vue' import UserDetailsDialog from './components/UserDetailsDialog.vue'
import ConfirmDeleteDialog from './components/ConfirmDeleteDialog.vue' import ConfirmDeleteDialog from './components/ConfirmDeleteDialog.vue'
@@ -13,199 +11,561 @@ import { useResponsive } from './composables/useResponsive'
import jsPDF from 'jspdf' import jsPDF from 'jspdf'
import autoTable from 'jspdf-autotable' import autoTable from 'jspdf-autotable'
ModuleRegistry.registerModules([AllCommunityModule, ExcelExportModule]) 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
}
})
const emit = defineEmits([
'data-updated',
'row-selected',
'row-deleted',
'row-edited',
'export-completed',
'api-error'
])
const { isMobile, isTablet, windowWidth } = useResponsive() const { isMobile, isTablet, windowWidth } = useResponsive()
const gridContainer = ref(null)
const fetchedData = ref([])
const isLoading = ref(false)
const normalizeRecord = (record, index) => {
if (!props.schemaMap || Object.keys(props.schemaMap).length === 0) {
return record
}
const normalized = {}
for (const [canonicalKey, candidates] of Object.entries(props.schemaMap)) {
const keys = Array.isArray(candidates) ? candidates : [candidates]
let found = undefined
for (const k of keys) {
if (record[k] !== undefined) {
found = record[k]
break
}
}
if (canonicalKey === 'id' && (found === undefined || found === null)) {
normalized[canonicalKey] = record.id ?? index + 1
} else {
normalized[canonicalKey] = found
}
}
return props.keepOriginalKeys ? { ...record, ...normalized } : normalized
}
const processedData = computed(() => {
const sourceData = props.data?.length ? props.data : fetchedData.value
if (!Array.isArray(sourceData)) return []
return sourceData.map((item, index) => {
const base = normalizeRecord(item, index)
return {
...base,
id: base?.id ?? item?.id ?? index + 1,
}
})
})
// 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') {
Object.keys(r).forEach(k => {
if (k !== 'action' && r[k] !== undefined && typeof r[k] !== 'object') keySet.add(k)
})
}
})
const keys = Array.from(keySet)
const toTitle = (k) => k
.replace(/_/g, ' ')
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/^./, s => s.toUpperCase())
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 val = rows[i]?.[k]
if (val !== undefined && val !== null) { sampleVal = val; break }
}
const col = {
field: k,
headerName: toTitle(k),
sortable: true,
filter: true,
editable: false,
minWidth: 100,
flex: 1,
}
if (typeof sampleVal === 'number') {
col.filter = true
col.width = 120
col.valueFormatter = p => (typeof p.value === 'number' ? p.value : p.value ?? '')
} else if (typeof sampleVal === 'boolean') {
col.width = 120
col.valueFormatter = p => (p.value ? 'Yes' : 'No')
} else if (isLikelyDate(sampleVal)) {
col.width = 140
col.valueFormatter = p => {
if (!p.value) return ''
const d = new Date(p.value)
return isNaN(d.getTime()) ? String(p.value) : d.toISOString().split('T')[0]
}
} else {
col.flex = 1
}
return col
})
})
// 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)
const gridApi = gridSetup.gridApi
const columnDefs = gridSetup.columnDefs
const rowData = gridSetup.rowData
const defaultColDef = gridSetup.defaultColDef
const gridOptions = gridSetup.gridOptions
const gridReady = gridSetup.onGridReady
const onCellValueChanged = gridSetup.onCellValueChanged
const updateColumnVisibility = gridSetup.updateColumnVisibility
const updatePagination = gridSetup.updatePagination
watch(
processedData,
(newData) => {
if (Array.isArray(newData) && gridApi.value) {
try {
if (typeof gridApi.value.setRowData === 'function') {
gridApi.value.setRowData(newData)
} else if (typeof gridApi.value.setGridOption === 'function') {
gridApi.value.setGridOption("rowData", newData)
}
} catch (error) {
console.warn('Error updating grid data:', error)
}
}
},
{ deep: true, immediate: true }
)
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
}
}
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
}
}
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
}
}
const { const {
gridApi, setupGlobalHandlers,
columnDefs, cleanupGlobalHandlers,
rowData, exportToCSV,
defaultColDef, exportToExcel,
gridOptions, saveUser,
onGridReady: gridReady, confirmDelete,
onCellValueChanged, cancelDelete,
updateColumnVisibility, showDetailsDialog,
updatePagination selectedRowData,
} = useDataTableGrid(data, isMobile, isTablet) deleteRowData,
showDeleteDialog,
const { deleteRow
setupGlobalHandlers,
cleanupGlobalHandlers,
exportToCSV,
exportToExcel,
saveUser,
confirmDelete,
cancelDelete,
showDetailsDialog,
selectedRowData,
deleteRowData,
showDeleteDialog,
deleteRow
} = useDataTableActions(gridApi, rowData) } = useDataTableActions(gridApi, rowData)
const handleSaveUser = async (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
}
}
emit('row-edited', updatedData)
} catch (error) {
console.error('Error saving user:', error)
}
}
const handleConfirmDelete = async () => {
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
}
}
emit('row-deleted', deleteRowData.value)
} catch (error) {
console.error('Error deleting row:', error)
}
}
const quickFilter = ref('') const quickFilter = ref('')
watch(quickFilter, (newValue) => { watch(quickFilter, (newValue) => {
if (gridApi.value) { if (gridApi.value) {
gridApi.value.setGridOption('quickFilterText', newValue || '') gridApi.value.setGridOption('quickFilterText', newValue || '')
} }
}, { immediate: false }) }, { immediate: false })
const onGridReady = params => { const onGridReady = params => {
gridReady(params) gridReady(params)
setupGlobalHandlers() setupGlobalHandlers()
}
const handleSaveUser = updatedData => saveUser(updatedData) if (props.apiUrl) {
fetchData()
}
// 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
}
}
const handleQuickFilterUpdate = (value) => { const handleQuickFilterUpdate = (value) => {
quickFilter.value = value || '' quickFilter.value = value || ''
if (gridApi.value) { if (gridApi.value) {
gridApi.value.setGridOption('quickFilterText', value || '') gridApi.value.setGridOption('quickFilterText', value || '')
} }
} }
const refreshData = () => {
if (props.apiUrl) {
fetchData()
}
}
defineExpose({
refreshData,
fetchData,
gridApi
})
onMounted(() => { onMounted(() => {
setupGlobalHandlers() setupGlobalHandlers()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
}) })
onUnmounted(() => { onUnmounted(() => {
cleanupGlobalHandlers() cleanupGlobalHandlers()
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
}) })
const handleResize = () => { const handleResize = () => {
windowWidth.value = window.innerWidth windowWidth.value = window.innerWidth
if (gridApi.value) { if (gridApi.value) {
updateColumnVisibility() updateColumnVisibility()
updatePagination() updatePagination()
setTimeout(() => { setTimeout(() => {
if (gridApi.value && gridApi.value.sizeColumnsToFit) { if (gridApi.value && gridApi.value.sizeColumnsToFit) {
gridApi.value.sizeColumnsToFit() gridApi.value.sizeColumnsToFit()
} }
}, 100) }, 100)
} }
} }
const exportToPDF = () => { const exportToPDF = () => {
if (!gridApi.value) { if (!gridApi.value) {
console.error('Grid API not available') console.error('Grid API not available')
return return
} }
try { try {
const doc = new jsPDF() const doc = new jsPDF()
const rowDataForPDF = [] const rowDataForPDF = []
const headers = [] const headers = []
columnDefs.value.forEach(col => { columnDefs.value.forEach(col => {
if (col.headerName && col.field && col.field !== 'action') { if (col.headerName && col.field && col.field !== 'action') {
headers.push(col.headerName) headers.push(col.headerName)
} }
}) })
gridApi.value.forEachNodeAfterFilterAndSort(node => { gridApi.value.forEachNodeAfterFilterAndSort(node => {
const row = [] const row = []
columnDefs.value.forEach(col => { columnDefs.value.forEach(col => {
if (col.field && col.field !== 'action') { if (col.field && col.field !== 'action') {
let value = node.data[col.field] || '' let value = node.data[col.field] || ''
row.push(value)
}
})
rowDataForPDF.push(row)
})
if (col.field === 'salary' && value) { autoTable(doc, {
value = `$${Math.floor(Number(value)).toLocaleString()}` head: [headers],
} else if (col.field === 'startDate' && value) { body: rowDataForPDF,
const date = new Date(value) styles: {
if (!isNaN(date.getTime())) { fontSize: 8,
const day = date.getDate().toString().padStart(2, '0') cellPadding: 2
const month = (date.getMonth() + 1).toString().padStart(2, '0') },
const year = date.getFullYear() headStyles: {
value = `${day}/${month}/${year}` fillColor: [41, 128, 185],
} textColor: 255,
} else if (col.field === 'status') { fontStyle: 'bold'
const statusMap = { },
0: 'Applied', alternateRowStyles: {
1: 'Current', fillColor: [245, 245, 245]
2: 'Professional', },
3: 'Rejected', margin: { top: 20 }
4: 'Resigned' })
}
value = statusMap[value] || 'Applied'
}
row.push(value) doc.save(`data-table-${new Date().toISOString().split('T')[0]}.pdf`)
} emit('export-completed', 'pdf')
}) } catch (error) {
rowDataForPDF.push(row) console.error('Error exporting PDF:', error)
}) }
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`)
console.log('PDF exported successfully')
} catch (error) {
console.error('Error exporting PDF:', error)
}
} }
</script> </script>
<template> <template>
<v-card class="elevation-2"> <v-card class="elevation-2">
<DataTableHeader <v-overlay
:is-mobile="isMobile" v-model="isLoading"
:is-tablet="isTablet" contained
:quick-filter="quickFilter" class="align-center justify-center"
@update:quick-filter="handleQuickFilterUpdate" >
@export-csv="exportToCSV" <v-progress-circular
@export-excel="exportToExcel" indeterminate
@export-pdf="exportToPDF" size="64"
color="primary"
></v-progress-circular>
</v-overlay>
<DataTableHeader
v-if="enableSearch || enableExport"
:is-mobile="isMobile"
:is-tablet="isTablet"
:quick-filter="quickFilter"
:enable-search="enableSearch"
:enable-export="enableExport"
@update:quick-filter="handleQuickFilterUpdate"
@export-csv="exportToCSV"
@export-excel="exportToExcel"
@export-pdf="exportToPDF"
/>
<v-divider v-if="enableSearch || enableExport" />
<v-card-text class="pa-0">
<AgGridVue
class="vuetify-grid ag-theme-alpine-dark"
:style="`height:${isMobile ? mobileHeight : height}; width:100%`"
:columnDefs="columnDefs"
:rowData="processedData"
:defaultColDef="defaultColDef"
:gridOptions="gridOptions"
@grid-ready="onGridReady"
@cell-value-changed="onCellValueChanged"
/> />
</v-card-text>
<v-divider /> <UserDetailsDialog
v-if="enableEdit"
v-model="showDetailsDialog"
:selected-row-data="selectedRowData"
:columns="columnDefs"
:is-mobile="isMobile"
@save-user="handleSaveUser"
/>
<v-card-text class="pa-0"> <ConfirmDeleteDialog
<AgGridVue v-if="enableDelete"
class="vuetify-grid ag-theme-alpine-dark" v-model="showDeleteDialog"
:style="`height:${isMobile ? '400px' : '600px'}; width:100%`" :selected-row-data="deleteRowData"
:columnDefs="columnDefs" :columns="columnDefs"
:rowData="rowData" :is-mobile="isMobile"
:defaultColDef="defaultColDef" @confirm="handleConfirmDelete"
:gridOptions="gridOptions" @cancel="cancelDelete"
@grid-ready="onGridReady" />
</v-card>
/>
</v-card-text>
<UserDetailsDialog
v-model="showDetailsDialog"
:selected-row-data="selectedRowData"
:is-mobile="isMobile"
@save-user="handleSaveUser"
/>
<ConfirmDeleteDialog
v-model="showDeleteDialog"
:selected-row-data="deleteRowData"
:is-mobile="isMobile"
@confirm="confirmDelete"
@cancel="cancelDelete"
/>
</v-card>
</template> </template>

View File

@@ -15,7 +15,7 @@
<v-card-text class="py-6"> <v-card-text class="py-6">
<div class="text-body-1 mb-4"> <div class="text-body-1 mb-4">
Are you sure you want to delete this user? Are you sure you want to delete this record?
</div> </div>
<v-card <v-card
@@ -24,15 +24,14 @@
class="pa-3 bg-grey-lighten-5" class="pa-3 bg-grey-lighten-5"
> >
<div class="d-flex flex-column gap-2"> <div class="d-flex flex-column gap-2">
<div class="d-flex align-center"> <div
<VIcon icon="tabler-user" size="small" class="me-2" /> v-for="(val, key) in previewFields"
<strong class="me-2">Name:</strong> :key="key"
{{ selectedRowData.fullName || 'Unknown' }} class="d-flex align-center"
</div> >
<div class="d-flex align-center"> <VIcon icon="tabler-dot" size="small" class="me-2" />
<VIcon icon="tabler-mail" size="small" class="me-2" /> <strong class="me-2">{{ key }}:</strong>
<strong class="me-2">Email:</strong> {{ formatValue(val) }}
{{ selectedRowData.email || 'Unknown' }}
</div> </div>
</div> </div>
</v-card> </v-card>
@@ -85,7 +84,7 @@
</template> </template>
<script setup> <script setup>
defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Boolean, type: Boolean,
default: false default: false
@@ -97,6 +96,10 @@ defineProps({
isMobile: { isMobile: {
type: Boolean, type: Boolean,
default: false default: false
},
columns: {
type: [Array, Object],
default: () => []
} }
}) })
@@ -118,6 +121,26 @@ const handleConfirm = () => {
emit('update:model-value', false) emit('update:model-value', false)
emit('confirm') emit('confirm')
} }
const previewFields = computed(() => {
const cols = Array.isArray(props.columns) ? props.columns : (props.columns?.value || [])
const prefer = ['title','name','fullName','todo','email','id','userId']
const keys = []
for (const p of prefer) {
if (props.selectedRowData && props.selectedRowData[p] !== undefined) keys.push(p)
}
for (const c of cols) {
if (c.field && !keys.includes(c.field) && c.field !== 'action' && keys.length < 4) keys.push(c.field)
}
const map = {}
keys.forEach(k => { map[k] = props.selectedRowData?.[k] })
return map
})
const formatValue = (v) => {
if (Array.isArray(v) || typeof v === 'object') return JSON.stringify(v)
return v ?? 'N/A'
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -2,7 +2,11 @@
const props = defineProps({ const props = defineProps({
modelValue: Boolean, modelValue: Boolean,
selectedRowData: Object, selectedRowData: Object,
isMobile: Boolean isMobile: Boolean,
columns: {
type: [Array, Object],
default: () => []
}
}) })
const emit = defineEmits(['update:modelValue', 'save-user']) const emit = defineEmits(['update:modelValue', 'save-user'])
@@ -111,6 +115,11 @@ const getFieldIcon = (field) => {
} }
return iconMap[field] || 'tabler-info-circle' return iconMap[field] || 'tabler-info-circle'
} }
const visibleColumns = computed(() => {
const list = Array.isArray(props.columns) ? props.columns : (props.columns?.value || [])
return list.filter(c => c.field && c.field !== 'action')
})
</script> </script>
<template> <template>
@@ -179,7 +188,11 @@ const getFieldIcon = (field) => {
<v-form @keyup.enter="handleEnterKey"> <v-form @keyup.enter="handleEnterKey">
<v-container fluid class="pa-0"> <v-container fluid class="pa-0">
<v-row> <v-row>
<v-col cols="12" sm="6" class="mb-2"> <v-col
v-for="col in visibleColumns"
:key="col.field"
cols="12" sm="6" class="mb-2"
>
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant"> <v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-avatar <v-avatar
@@ -192,212 +205,35 @@ const getFieldIcon = (field) => {
border: '1px solid rgba(var(--v-theme-primary), 0.3)' border: '1px solid rgba(var(--v-theme-primary), 0.3)'
}" }"
> >
<v-icon :icon="getFieldIcon('name')" color="primary" /> <v-icon :icon="getFieldIcon(col.field)" color="primary" />
</v-avatar> </v-avatar>
<div class="flex-grow-1"> <div class="flex-grow-1">
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Full Name</v-card-subtitle> <v-card-subtitle class="pa-0 text-caption text-medium-emphasis">{{ col.headerName || col.field }}</v-card-subtitle>
<v-card-title v-if="!isEditMode" class="pa-0 text-h6 text-high-emphasis"> <template v-if="!isEditMode">
{{ selectedRowData.fullName || 'N/A' }} <v-card-title class="pa-0 text-body-1 text-high-emphasis">
</v-card-title> {{ Array.isArray(selectedRowData[col.field]) || typeof selectedRowData[col.field] === 'object' ? JSON.stringify(selectedRowData[col.field]) : (selectedRowData[col.field] ?? 'N/A') }}
<v-text-field </v-card-title>
v-else </template>
v-model="editedData.fullName" <template v-else>
variant="outlined" <v-text-field
density="compact" v-if="col.editable !== false && col.field !== 'status'"
hide-details v-model="editedData[col.field]"
class="mt-1" variant="outlined"
/> density="compact"
</div> hide-details
</div> class="mt-1"
</v-card> :type="col.field.toLowerCase().includes('date') ? 'date' : (typeof selectedRowData[col.field] === 'number' ? 'number' : 'text')"
</v-col> />
<v-select
<v-col cols="12" sm="6" class="mb-2"> v-else-if="col.field === 'status'"
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant"> v-model="editedData.status"
<div class="d-flex align-center"> :items="statusOptions"
<v-avatar variant="outlined"
class="me-4" density="compact"
size="40" hide-details
:style="{ class="mt-1"
background: 'rgba(var(--v-theme-secondary), 0.2)', />
backdropFilter: 'blur(10px)', </template>
webkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(var(--v-theme-secondary), 0.3)'
}"
>
<v-icon :icon="getFieldIcon('email')" color="secondary" />
</v-avatar>
<div class="flex-grow-1">
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Email Address</v-card-subtitle>
<v-card-title v-if="!isEditMode" class="pa-0 text-body-1 text-high-emphasis">
{{ selectedRowData.email || 'N/A' }}
</v-card-title>
<v-text-field
v-else
v-model="editedData.email"
variant="outlined"
density="compact"
hide-details
type="email"
class="mt-1"
/>
</div>
</div>
</v-card>
</v-col>
<v-col cols="12" sm="6" class="mb-2">
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
<div class="d-flex align-center">
<v-avatar
class="me-4"
size="40"
:style="{
background: 'rgba(var(--v-theme-info), 0.2)',
backdropFilter: 'blur(10px)',
webkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(var(--v-theme-info), 0.3)'
}"
>
<v-icon :icon="getFieldIcon('startDate')" color="info" />
</v-avatar>
<div class="flex-grow-1">
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Start Date</v-card-subtitle>
<v-card-title v-if="!isEditMode" class="pa-0 text-h6 text-high-emphasis">
{{ formatDate(selectedRowData.startDate) }}
</v-card-title>
<v-text-field
v-else
v-model="editedData.startDate"
variant="outlined"
density="compact"
hide-details
type="date"
class="mt-1"
/>
</div>
</div>
</v-card>
</v-col>
<v-col cols="12" sm="6" class="mb-2">
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
<div class="d-flex align-center">
<v-avatar
class="me-4"
size="40"
:style="{
background: 'rgba(var(--v-theme-success), 0.2)',
backdropFilter: 'blur(10px)',
webkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(var(--v-theme-success), 0.3)'
}"
>
<v-icon :icon="getFieldIcon('salary')" color="success" />
</v-avatar>
<div class="flex-grow-1">
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Salary</v-card-subtitle>
<v-card-title v-if="!isEditMode" class="pa-0 text-h6 text-success font-weight-bold">
{{ formatSalary(selectedRowData.salary) }}
</v-card-title>
<v-text-field
v-else
v-model="editedData.salary"
variant="outlined"
density="compact"
hide-details
type="number"
prefix="$"
class="mt-1"
/>
</div>
</div>
</v-card>
</v-col>
<v-col cols="12" sm="6" class="mb-2">
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
<div class="d-flex align-center">
<v-avatar
class="me-4"
size="40"
:style="{
background: 'rgba(var(--v-theme-warning), 0.2)',
backdropFilter: 'blur(10px)',
webkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(var(--v-theme-warning), 0.3)'
}"
>
<v-icon :icon="getFieldIcon('age')" color="warning" />
</v-avatar>
<div class="flex-grow-1">
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Age</v-card-subtitle>
<v-card-title v-if="!isEditMode" class="pa-0 text-h6 text-high-emphasis">
{{ selectedRowData.age ? selectedRowData.age + ' years' : 'N/A' }}
</v-card-title>
<v-text-field
v-else
v-model="editedData.age"
variant="outlined"
density="compact"
hide-details
type="number"
suffix="years"
class="mt-1"
/>
</div>
</div>
</v-card>
</v-col>
<v-col cols="12" sm="6" class="mb-2">
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
<div class="d-flex align-center">
<v-avatar
class="me-4"
size="40"
:style="{
background: `rgba(var(--v-theme-${getStatusColor(isEditMode ? editedData.status : selectedRowData.status)}), 0.2)`,
backdropFilter: 'blur(10px)',
webkitBackdropFilter: 'blur(10px)',
border: `1px solid rgba(var(--v-theme-${getStatusColor(isEditMode ? editedData.status : selectedRowData.status)}), 0.3)`
}"
>
<v-icon :icon="getFieldIcon('status')" :color="getStatusColor(isEditMode ? editedData.status : selectedRowData.status)" />
</v-avatar>
<div class="flex-grow-1">
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Status</v-card-subtitle>
<div v-if="!isEditMode" class="pt-1">
<v-chip
:color="getStatusColor(selectedRowData.status)"
size="default"
variant="flat"
class="font-weight-medium"
>
<v-icon start icon="tabler-check" size="small" />
{{ getStatusLabel(selectedRowData.status) }}
</v-chip>
</div>
<v-select
v-else
v-model="editedData.status"
:items="statusOptions"
variant="outlined"
density="compact"
hide-details
class="mt-1"
>
<template #item="{ props, item }">
<v-list-item v-bind="props" :title="item.title">
<template #prepend>
<v-chip :color="item.raw.color" size="small" class="me-2">{{ item.title }}</v-chip>
</template>
</v-list-item>
</template>
<template #selection="{ item }">
<v-chip :color="item.raw.color" size="small">{{ item.title }}</v-chip>
</template>
</v-select>
</div> </div>
</div> </div>
</v-card> </v-card>

View File

@@ -5,15 +5,23 @@ export function useDataTableActions(gridApi, rowData) {
const showDeleteDialog = ref(false) const showDeleteDialog = ref(false)
const selectedRowData = ref(null) const selectedRowData = ref(null)
const deleteRowData = ref(null) const deleteRowData = ref(null)
const selectedRowNodeId = ref(null)
const deleteRowNodeId = ref(null)
const getRowDataByIndex = (rowIndex) => { const getRowDataByIndex = (rowIndex, nodeId) => {
if (!gridApi.value) { if (!gridApi.value) {
console.error('Grid API not available') console.error('Grid API not available')
return null return null
} }
try { try {
const node = gridApi.value.getDisplayedRowAtIndex(rowIndex) let node = null
if (nodeId != null && gridApi.value.getRowNode) {
node = gridApi.value.getRowNode(nodeId)
}
if (!node) {
node = gridApi.value.getDisplayedRowAtIndex(rowIndex)
}
if (node && node.data) { if (node && node.data) {
return node.data return node.data
} }
@@ -33,9 +41,10 @@ export function useDataTableActions(gridApi, rowData) {
const showDetails = (rowIndex, nodeId) => { const showDetails = (rowIndex, nodeId) => {
console.log('Show details called with:', { rowIndex, nodeId }) console.log('Show details called with:', { rowIndex, nodeId })
const rowData = getRowDataByIndex(rowIndex) const rowDataItem = getRowDataByIndex(rowIndex, nodeId)
if (rowData) { if (rowDataItem) {
selectedRowData.value = { ...rowData } selectedRowData.value = { ...rowDataItem }
selectedRowNodeId.value = nodeId
showDetailsDialog.value = true showDetailsDialog.value = true
console.log('Selected row data:', selectedRowData.value) console.log('Selected row data:', selectedRowData.value)
} else { } else {
@@ -52,11 +61,15 @@ export function useDataTableActions(gridApi, rowData) {
} }
try { try {
gridApi.value.startEditingCell({ const columnDefs = typeof gridApi.value.getColumnDefs === 'function' ? gridApi.value.getColumnDefs() : []
rowIndex: rowIndex, const firstEditableCol = Array.isArray(columnDefs) ? columnDefs.find(col => col && col.editable && col.field && col.field !== 'action') : null
colKey: 'fullName'
}) if (firstEditableCol && typeof gridApi.value.startEditingCell === 'function') {
console.log('Started inline editing for row:', rowIndex) gridApi.value.ensureIndexVisible?.(rowIndex)
gridApi.value.setFocusedCell?.(rowIndex, firstEditableCol.field)
gridApi.value.startEditingCell({ rowIndex, colKey: firstEditableCol.field })
console.log('Started inline editing for row:', rowIndex)
}
} catch (error) { } catch (error) {
console.error('Error starting inline edit:', error) console.error('Error starting inline edit:', error)
} }
@@ -65,9 +78,10 @@ export function useDataTableActions(gridApi, rowData) {
const deleteRow = (rowIndex, nodeId) => { const deleteRow = (rowIndex, nodeId) => {
console.log('Delete row called with:', { rowIndex, nodeId }) console.log('Delete row called with:', { rowIndex, nodeId })
const rowData = getRowDataByIndex(rowIndex) const rowDataItem = getRowDataByIndex(rowIndex, nodeId)
if (rowData) { if (rowDataItem) {
deleteRowData.value = { ...rowData } deleteRowData.value = { ...rowDataItem }
deleteRowNodeId.value = nodeId
showDeleteDialog.value = true showDeleteDialog.value = true
console.log('Delete row data:', deleteRowData.value) console.log('Delete row data:', deleteRowData.value)
} else { } else {
@@ -78,26 +92,22 @@ export function useDataTableActions(gridApi, rowData) {
const confirmDelete = () => { const confirmDelete = () => {
if (deleteRowData.value && gridApi.value) { if (deleteRowData.value && gridApi.value) {
try { try {
const indexToDelete = rowData.value.findIndex(item => { // Prefer removing via grid transaction using the exact object
return ( gridApi.value.applyTransaction({ remove: [deleteRowData.value] })
(item.id && item.id === deleteRowData.value.id) ||
(item.email && item.email === deleteRowData.value.email) ||
(item.fullName === deleteRowData.value.fullName &&
item.salary === deleteRowData.value.salary)
)
})
// Also sync our local rowData list. Try by id first, else fallback to object equality
let indexToDelete = -1
if (deleteRowData.value.id != null) {
indexToDelete = rowData.value.findIndex(item => item && item.id === deleteRowData.value.id)
}
if (indexToDelete === -1) {
indexToDelete = rowData.value.findIndex(item => item === deleteRowData.value)
}
if (indexToDelete !== -1) { if (indexToDelete !== -1) {
rowData.value.splice(indexToDelete, 1) rowData.value.splice(indexToDelete, 1)
gridApi.value.applyTransaction({
remove: [deleteRowData.value]
})
console.log('Row deleted successfully')
} else {
console.error('Row not found for deletion')
} }
console.log('Row deleted successfully')
} catch (error) { } catch (error) {
console.error('Error deleting row:', error) console.error('Error deleting row:', error)
} }
@@ -105,6 +115,7 @@ export function useDataTableActions(gridApi, rowData) {
showDeleteDialog.value = false showDeleteDialog.value = false
deleteRowData.value = null deleteRowData.value = null
deleteRowNodeId.value = null
} }
const cancelDelete = () => { const cancelDelete = () => {
@@ -116,25 +127,33 @@ export function useDataTableActions(gridApi, rowData) {
if (!editedData || !gridApi.value) return if (!editedData || !gridApi.value) return
try { try {
const indexToUpdate = rowData.value.findIndex(item => { // Update the grid row via nodeId if available
return ( let updated = false
(item.id && item.id === editedData.id) || if (selectedRowNodeId.value != null && gridApi.value.getRowNode) {
(item.email && item.email === selectedRowData.value.email) || const node = gridApi.value.getRowNode(selectedRowNodeId.value)
(item.fullName === selectedRowData.value.fullName)
)
})
if (indexToUpdate !== -1) {
rowData.value[indexToUpdate] = { ...editedData }
const node = gridApi.value.getRowNode(indexToUpdate)
if (node) { if (node) {
node.setData(editedData) node.setData(editedData)
updated = true
} }
}
console.log('User updated successfully:', editedData) // Sync local rowData list
let indexToUpdate = -1
if (editedData.id != null) {
indexToUpdate = rowData.value.findIndex(item => item && item.id === editedData.id)
}
if (indexToUpdate === -1 && selectedRowData.value) {
indexToUpdate = rowData.value.findIndex(item => item === selectedRowData.value)
}
if (indexToUpdate !== -1) {
rowData.value[indexToUpdate] = { ...editedData }
updated = true
}
if (updated) {
console.log('Row updated successfully:', editedData)
} else { } else {
console.error('User not found for update') console.error('Row not found for update')
} }
} catch (error) { } catch (error) {
console.error('Error updating user:', error) console.error('Error updating user:', error)
@@ -142,6 +161,7 @@ export function useDataTableActions(gridApi, rowData) {
showDetailsDialog.value = false showDetailsDialog.value = false
selectedRowData.value = null selectedRowData.value = null
selectedRowNodeId.value = null
} }
const exportToCSV = () => { const exportToCSV = () => {
@@ -163,7 +183,7 @@ export function useDataTableActions(gridApi, rowData) {
} }
const exportToExcel = () => { const exportToExcel = () => {
if (gridApi.value) { if (gridApi.value && gridApi.value.exportDataAsExcel) {
gridApi.value.exportDataAsExcel({ gridApi.value.exportDataAsExcel({
fileName: `datatable-export-${new Date().toISOString().split('T')[0]}.xlsx`, fileName: `datatable-export-${new Date().toISOString().split('T')[0]}.xlsx`,
sheetName: 'Data Export', sheetName: 'Data Export',
@@ -176,6 +196,8 @@ export function useDataTableActions(gridApi, rowData) {
allColumns: false, allColumns: false,
onlySelectedAllPages: false onlySelectedAllPages: false
}) })
} else {
console.warn('Excel export not available - requires AG Grid Enterprise')
} }
} }

View File

@@ -1,47 +1,52 @@
import { ref, computed } from "vue"; import { ref, computed, watch } from "vue";
export function useDataTableGrid(data, isMobile, isTablet) { export function useDataTableGrid(
data,
isMobile,
isTablet,
customColumns = null
) {
let gridApi = ref(null); let gridApi = ref(null);
const rowData = ref([]); const rowData = ref([]);
const statusCellRenderer = (params) => { const statusCellRenderer = (params) => {
const s = params.value; const s = params.value;
let label = "Applied"; let label = "Applied";
let colorClass = "info"; let colorClass = "info";
if (s === 1) { if (s === 1) {
label = "Current"; label = "Current";
colorClass = "primary"; colorClass = "primary";
} }
if (s === 2) { if (s === 2) {
label = "Professional"; label = "Professional";
colorClass = "success"; colorClass = "success";
} }
if (s === 3) { if (s === 3) {
label = "Rejected"; label = "Rejected";
colorClass = "error"; colorClass = "error";
} }
if (s === 4) { if (s === 4) {
label = "Resigned"; label = "Resigned";
colorClass = "warning"; colorClass = "warning";
} }
const chipSize = isMobile.value ? "x-small" : "small"; const chipSize = isMobile.value ? "x-small" : "small";
const fontSize = isMobile.value ? "10px" : "12px"; const fontSize = isMobile.value ? "10px" : "12px";
return `<div class="v-chip v-chip--size-${chipSize} v-chip--variant-flat bg-${colorClass}" return `<div class="v-chip v-chip--size-${chipSize} v-chip--variant-flat bg-${colorClass}"
style=" style="
height:${isMobile.value ? "20px" : "24px"}; height:${isMobile.value ? "20px" : "24px"};
padding:0 ${isMobile.value ? "6px" : "8px"}; padding:0 ${isMobile.value ? "6px" : "8px"};
display:inline-flex; display:inline-flex;
align-items:center; align-items:center;
border-radius:12px; border-radius:12px;
font-size:${fontSize}; font-size:${fontSize};
font-weight:500; font-weight:500;
color:white; color:white;
border: inherit; border: inherit;
">${label}</div>`; ">${label}</div>`;
}; };
const statusCellEditor = () => { const statusCellEditor = () => {
const statusOptions = [ const statusOptions = [
@@ -239,7 +244,7 @@ const statusCellRenderer = (params) => {
`; `;
}; };
const columnDefs = ref([ const createDefaultColumns = () => [
{ {
headerName: "NAME", headerName: "NAME",
field: "fullName", field: "fullName",
@@ -253,9 +258,6 @@ const statusCellRenderer = (params) => {
cellEditorParams: { cellEditorParams: {
maxLength: 50, maxLength: 50,
}, },
onCellValueChanged: (params) => {
console.log("Name changed:", params.newValue);
},
}, },
{ {
headerName: "EMAIL", headerName: "EMAIL",
@@ -270,9 +272,6 @@ const statusCellRenderer = (params) => {
cellEditorParams: { cellEditorParams: {
maxLength: 100, maxLength: 100,
}, },
onCellValueChanged: (params) => {
console.log("Email changed:", params.newValue);
},
}, },
{ {
headerName: "DATE", headerName: "DATE",
@@ -284,26 +283,17 @@ const statusCellRenderer = (params) => {
hide: false, hide: false,
editable: true, editable: true,
cellEditor: "agDateCellEditor", cellEditor: "agDateCellEditor",
cellEditorParams: {
preventEdit: (params) => {
return false;
},
},
valueFormatter: (params) => { valueFormatter: (params) => {
if (!params.value) return ""; if (!params.value) return "";
const date = new Date(params.value); const date = new Date(params.value);
if (isNaN(date.getTime())) return ""; if (isNaN(date.getTime())) return "";
const day = date.getDate().toString().padStart(2, "0"); const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0"); const month = (date.getMonth() + 1).toString().padStart(2, "0");
const year = date.getFullYear(); const year = date.getFullYear();
return `${day}/${month}/${year}`; return `${day}/${month}/${year}`;
}, },
valueParser: (params) => { valueParser: (params) => {
if (!params.newValue) return null; if (!params.newValue) return null;
const dateStr = params.newValue; const dateStr = params.newValue;
if (typeof dateStr === "string" && dateStr.includes("/")) { if (typeof dateStr === "string" && dateStr.includes("/")) {
const parts = dateStr.split("/"); const parts = dateStr.split("/");
@@ -311,14 +301,12 @@ const statusCellRenderer = (params) => {
const day = parseInt(parts[0]); const day = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1; const month = parseInt(parts[1]) - 1;
const year = parseInt(parts[2]); const year = parseInt(parts[2]);
const date = new Date(year, month, day); const date = new Date(year, month, day);
if (!isNaN(date.getTime())) { if (!isNaN(date.getTime())) {
return date; return date;
} }
} }
} }
return new Date(dateStr); return new Date(dateStr);
}, },
filterParams: { filterParams: {
@@ -327,14 +315,11 @@ const statusCellRenderer = (params) => {
suppressAndOrCondition: true, suppressAndOrCondition: true,
comparator: (filterLocalDateAtMidnight, cellValue) => { comparator: (filterLocalDateAtMidnight, cellValue) => {
if (!cellValue) return -1; if (!cellValue) return -1;
const cellDate = new Date(cellValue); const cellDate = new Date(cellValue);
cellDate.setHours(0, 0, 0, 0); cellDate.setHours(0, 0, 0, 0);
if (filterLocalDateAtMidnight.getTime() === cellDate.getTime()) { if (filterLocalDateAtMidnight.getTime() === cellDate.getTime()) {
return 0; return 0;
} }
return filterLocalDateAtMidnight < cellDate ? -1 : 1; return filterLocalDateAtMidnight < cellDate ? -1 : 1;
}, },
}, },
@@ -408,7 +393,6 @@ const statusCellRenderer = (params) => {
precision: 0, precision: 0,
showStepperButtons: false, showStepperButtons: false,
}, },
valueFormatter: (params) => { valueFormatter: (params) => {
if ( if (
params.value === null || params.value === null ||
@@ -420,7 +404,6 @@ const statusCellRenderer = (params) => {
const numValue = Number(params.value); const numValue = Number(params.value);
return isNaN(numValue) ? "" : Math.floor(numValue).toString(); return isNaN(numValue) ? "" : Math.floor(numValue).toString();
}, },
onCellValueChanged: (params) => { onCellValueChanged: (params) => {
const newValue = params.newValue; const newValue = params.newValue;
@@ -477,11 +460,60 @@ const statusCellRenderer = (params) => {
hide: false, hide: false,
editable: false, editable: false,
cellRenderer: actionButtonsRenderer, cellRenderer: actionButtonsRenderer,
suppressMenu: true, suppressHeaderMenuButton: true,
suppressSorting: true,
suppressFilter: true,
}, },
]); ];
const resolveColumns = () => {
if (Array.isArray(customColumns)) return customColumns
if (customColumns && Array.isArray(customColumns.value)) return customColumns.value
return []
}
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 withSelection = ensureSingleSelectionColumn(processedColumns);
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;
};
const columnDefs = computed(() => {
const cols = resolveColumns()
if (cols && Array.isArray(cols) && cols.length > 0) {
const processed = processCustomColumns(cols);
return processed;
}
const defaultCols = createDefaultColumns();
return ensureSingleSelectionColumn(defaultCols);
});
const defaultColDef = computed(() => ({ const defaultColDef = computed(() => ({
resizable: true, resizable: true,
@@ -494,7 +526,7 @@ const statusCellRenderer = (params) => {
maxWidth: 400, maxWidth: 400,
wrapText: false, wrapText: false,
autoHeight: false, autoHeight: false,
suppressMenu: false, suppressHeaderMenuButton: false,
})); }));
const gridOptions = computed(() => ({ const gridOptions = computed(() => ({
@@ -502,12 +534,16 @@ const statusCellRenderer = (params) => {
headerHeight: isMobile.value ? 48 : 56, headerHeight: isMobile.value ? 48 : 56,
rowHeight: isMobile.value ? 44 : 52, rowHeight: isMobile.value ? 44 : 52,
animateRows: true, animateRows: true,
rowSelection: { type: "multiRow" }, getRowId: params => {
const value = params.data && (params.data.id ?? params.data.ID ?? params.data._id)
return value != null ? String(value) : String(params.rowIndex)
},
rowSelection: 'multiple',
rowMultiSelectWithClick: true,
suppressRowClickSelection: false,
pagination: true, pagination: true,
paginationPageSize: isMobile.value ? 5 : 10, paginationPageSize: isMobile.value ? 5 : 10,
paginationPageSizeSelector: isMobile.value ? [5, 10, 20] : [5, 10, 20, 50], paginationPageSizeSelector: isMobile.value ? [5, 10, 20] : [5, 10, 20, 50],
suppressRowClickSelection: false,
rowMultiSelectWithClick: true,
enableCellTextSelection: true, enableCellTextSelection: true,
suppressHorizontalScroll: false, suppressHorizontalScroll: false,
alwaysShowHorizontalScroll: false, alwaysShowHorizontalScroll: false,
@@ -527,55 +563,81 @@ const statusCellRenderer = (params) => {
cacheQuickFilter: true, cacheQuickFilter: true,
enableAdvancedFilter: false, enableAdvancedFilter: false,
includeHiddenColumnsInAdvancedFilter: false, includeHiddenColumnsInAdvancedFilter: false,
suppressBrowserResizeObserver: false,
maintainColumnOrder: true, maintainColumnOrder: true,
suppressMenuHide: true, suppressMenuHide: true,
enableRangeSelection: true, loading: false,
enableFillHandle: false, suppressNoRowsOverlay: false,
enableRangeHandle: false,
})); }));
const updateColumnVisibility = () => { const updateColumnVisibility = () => {
if (!gridApi.value) return; if (!gridApi.value) return;
if (isMobile.value) { const columns = ['email', 'startDate', 'age'];
gridApi.value.setColumnVisible("email", false);
gridApi.value.setColumnVisible("startDate", false); try {
gridApi.value.setColumnVisible("age", false); columns.forEach(colId => {
} else if (isTablet.value) { const column = gridApi.value.getColumn ? gridApi.value.getColumn(colId) : null;
gridApi.value.setColumnVisible("email", true); if (!column) return;
gridApi.value.setColumnVisible("startDate", false);
gridApi.value.setColumnVisible("age", true); let visible = true;
} else { if (isMobile.value) {
gridApi.value.setColumnVisible("email", true); visible = false;
gridApi.value.setColumnVisible("startDate", true); } else if (isTablet.value) {
gridApi.value.setColumnVisible("age", true); visible = colId !== 'startDate';
}
if (gridApi.value.setColumnVisible) {
gridApi.value.setColumnVisible(colId, visible);
} else if (gridApi.value.columnApi && gridApi.value.columnApi.setColumnVisible) {
gridApi.value.columnApi.setColumnVisible(colId, visible);
}
});
} catch (error) {
console.warn('Column visibility update not supported in this AG Grid version');
} }
}; };
const updatePagination = () => { const updatePagination = () => {
if (!gridApi.value) return; if (!gridApi.value) return;
const pageSize = isMobile.value ? 5 : 10; try {
const pageSizeSelector = isMobile.value ? [5, 10, 20] : [5, 10, 20, 50]; const gui = gridApi.value.getGui && gridApi.value.getGui();
const containerHeight = gui ? gui.clientHeight : 0;
gridApi.value.paginationSetPageSize(pageSize); const headerH = Number(gridOptions.value.headerHeight || (isMobile.value ? 48 : 56));
gridApi.value.setGridOption("paginationPageSizeSelector", pageSizeSelector); // 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));
if (typeof gridApi.value.paginationSetPageSize === 'function') {
gridApi.value.paginationSetPageSize(rowsPerPage);
}
} catch (error) {
console.warn('Error updating pagination:', error);
}
}; };
function onGridReady(params) { function onGridReady(params) {
console.log('Grid ready called with params:', params)
gridApi.value = params.api; gridApi.value = params.api;
rowData.value = data.map((item) => ({ console.log('Grid API set:', gridApi.value)
...item,
salary:
typeof item.salary === "string" ? parseInt(item.salary) : item.salary,
age: typeof item.age === "string" ? parseInt(item.age) : item.age,
}));
updateColumnVisibility(); updateColumnVisibility();
updatePagination(); updatePagination();
setTimeout(() => { setTimeout(() => {
gridApi.value.sizeColumnsToFit(); if (gridApi.value && gridApi.value.sizeColumnsToFit) {
gridApi.value.sizeColumnsToFit();
}
}, 100); }, 100);
} }
@@ -600,6 +662,17 @@ const statusCellRenderer = (params) => {
} }
} }
watch([isMobile, isTablet], () => {
updateColumnVisibility();
updatePagination();
});
watch(data, (newData) => {
console.log('Data watch triggered with:', newData)
rowData.value = newData || [];
console.log('rowData updated to:', rowData.value)
}, { immediate: true, deep: true });
return { return {
gridApi, gridApi,
columnDefs, columnDefs,
@@ -612,3 +685,35 @@ const statusCellRenderer = (params) => {
updatePagination, updatePagination,
}; };
} }
// Helpers
function ensureSingleSelectionColumn(columns) {
if (!Array.isArray(columns)) return columns;
const isSelectionCol = (c) => !!(c && (c.checkboxSelection || c.headerCheckboxSelection || c.field === '__selection__' || c.field === 'selection'));
const firstIndex = columns.findIndex(isSelectionCol);
if (firstIndex === -1) return columns;
const unique = [];
columns.forEach((c, idx) => {
if (isSelectionCol(c)) {
if (unique.some(isSelectionCol)) return; // skip duplicates
unique.push({ ...c, pinned: 'left' });
} else {
unique.push(c);
}
});
// Move the selection column to the first position
const selIdx = unique.findIndex(isSelectionCol);
if (selIdx > 0) {
const [sel] = unique.splice(selIdx, 1);
unique.unshift(sel);
}
return unique;
}
// removed selection column injection

View File

@@ -1,6 +1,320 @@
.ag-theme-alpine-dark .ag-cell { .ag-theme-alpine-dark .ag-cell {
cursor: text !important; cursor: text !important;
} }
.ag-theme-alpine.vuetify-grid .ag-cell,
.ag-theme-alpine-dark.vuetify-grid .ag-cell {
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
}
/* Inputs (filters, quick filter, tool panels) */
.ag-theme-alpine.vuetify-grid .ag-input-field-input,
.ag-theme-alpine-dark.vuetify-grid .ag-input-field-input {
background-color: rgba(var(--v-theme-on-surface), 0.04) !important;
color: rgb(var(--v-theme-on-surface)) !important;
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
border-radius: 8px !important;
padding: 6px 8px !important;
height: auto !important;
line-height: normal !important;
box-sizing: border-box !important;
display: block !important;
width: 100% !important;
}
.ag-theme-alpine.vuetify-grid .ag-input-field-input:focus,
.ag-theme-alpine-dark.vuetify-grid .ag-input-field-input:focus {
outline: none !important;
border-color: rgb(var(--v-theme-primary)) !important;
box-shadow: 0 0 0 3px rgba(var(--v-theme-primary), 0.2) !important;
}
/* Checkboxes (row select, set filter items) */
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper,
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper {
width: 18px !important;
height: 18px !important;
border-radius: 6px !important;
background: transparent !important;
border: 0 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
padding: 0 !important;
}
.ag-theme-alpine.vuetify-grid .ag-checkbox-input,
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input {
width: 14px !important;
height: 14px !important;
border-radius: 4px !important;
}
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"],
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"] {
width: auto !important;
height: auto !important;
border-radius: 0 !important;
border: 0 !important;
background: transparent !important;
position: static !important;
opacity: 1 !important;
}
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:checked,
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:checked { transform: none; }
/* keep AG Grid's own checkmark */
/* Center and tidy header selection checkbox */
.ag-theme-alpine.vuetify-grid .ag-header-select-all,
.ag-theme-alpine-dark.vuetify-grid .ag-header-select-all {
display: flex !important;
align-items: center !important;
justify-content: center !important;
height: 100% !important;
}
.ag-theme-alpine.vuetify-grid .ag-header-cell .ag-selection-checkbox,
.ag-theme-alpine-dark.vuetify-grid .ag-header-cell .ag-selection-checkbox {
margin: 0 auto !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 18px !important;
height: 18px !important;
}
/* Center selection checkbox inside body cells as well */
.ag-theme-alpine.vuetify-grid .ag-cell .ag-selection-checkbox,
.ag-theme-alpine-dark.vuetify-grid .ag-cell .ag-selection-checkbox {
margin: 0 auto !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 18px !important;
height: 18px !important;
}
/* Prevent nested visual duplication: wrapper stays transparent, input renders the box */
.ag-theme-alpine.vuetify-grid .ag-header-cell .ag-checkbox-input-wrapper,
.ag-theme-alpine-dark.vuetify-grid .ag-header-cell .ag-checkbox-input-wrapper {
background: transparent !important;
border: 0 !important;
width: 18px !important;
height: 18px !important;
}
/* Unify checkbox input look & interactions */
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper,
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper {
width: 18px !important;
height: 18px !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
}
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"],
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"] {
transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease !important;
}
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:hover,
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:hover {
border-color: rgb(var(--v-theme-primary)) !important;
}
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:focus-visible,
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:focus-visible {
outline: none !important;
box-shadow: 0 0 0 3px rgba(var(--v-theme-primary), 0.25) !important;
}
/* Improve alignment for narrow first column without forcing brittle indices */
.ag-theme-alpine.vuetify-grid .ag-header-cell.ag-column-first,
.ag-theme-alpine-dark.vuetify-grid .ag-header-cell.ag-column-first {
padding-left: 6px !important;
padding-right: 6px !important;
}
/* Make selection column narrower and center contents */
.ag-theme-alpine.vuetify-grid .ag-header-cell.ag-column-first,
.ag-theme-alpine-dark.vuetify-grid .ag-header-cell.ag-column-first {
padding-left: 6px !important;
padding-right: 6px !important;
}
.ag-theme-alpine.vuetify-grid .ag-center-cols-container .ag-cell.ag-cell-not-inline-editing.ag-cell-focus.ag-cell-value.ag-cell-normal-height[aria-colindex="1"],
.ag-theme-alpine-dark.vuetify-grid .ag-center-cols-container .ag-cell.ag-cell-not-inline-editing.ag-cell-focus.ag-cell-value.ag-cell-normal-height[aria-colindex="1"] {
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
/* Set filter items aesthetics */
.ag-theme-alpine.vuetify-grid .ag-set-filter-item,
.ag-theme-alpine-dark.vuetify-grid .ag-set-filter-item {
padding: 6px 8px !important;
border-radius: 8px !important;
}
.ag-theme-alpine.vuetify-grid .ag-set-filter-item:hover,
.ag-theme-alpine-dark.vuetify-grid .ag-set-filter-item:hover {
background-color: rgba(var(--v-theme-primary), 0.08) !important;
}
/* Status cell: ensure chip text aligns nicely */
.ag-theme-alpine.vuetify-grid .ag-cell[col-id="status"],
.ag-theme-alpine-dark.vuetify-grid .ag-cell[col-id="status"] {
padding-top: 6px !important;
padding-bottom: 6px !important;
}
.ag-theme-alpine .ag-cell,
.ag-theme-alpine-dark .ag-cell {
font-size: 0.95rem !important;
}
/* Set Filter, Column Tool Panel, Picker styles to match panel */
.ag-theme-alpine .ag-menu,
.ag-theme-alpine .ag-filter,
.ag-theme-alpine .ag-set-filter,
.ag-theme-alpine .ag-column-panel,
.ag-theme-alpine .ag-tool-panel-wrapper,
.ag-theme-alpine .ag-picker-field-wrapper {
background-color: rgb(var(--v-theme-surface)) !important;
color: rgb(var(--v-theme-on-surface)) !important;
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
}
.ag-theme-alpine-dark .ag-menu,
.ag-theme-alpine-dark .ag-filter,
.ag-theme-alpine-dark .ag-set-filter,
.ag-theme-alpine-dark .ag-column-panel,
.ag-theme-alpine-dark .ag-tool-panel-wrapper,
.ag-theme-alpine-dark .ag-picker-field-wrapper {
background-color: rgb(var(--v-theme-surface)) !important;
color: rgb(var(--v-theme-on-surface)) !important;
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
}
.ag-theme-alpine .ag-set-filter-item,
.ag-theme-alpine-dark .ag-set-filter-item {
color: rgb(var(--v-theme-on-surface)) !important;
}
/* Target the exact AG Grid checkbox structure you shared */
.ag-theme-alpine.vuetify-grid .ag-checkbox .ag-input-wrapper,
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox .ag-input-wrapper {
position: static !important;
width: auto !important;
height: auto !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
border: 0 !important;
background: transparent !important;
}
.ag-theme-alpine.vuetify-grid .ag-checkbox .ag-input-wrapper:hover,
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox .ag-input-wrapper:hover { transform: none; }
.ag-theme-alpine.vuetify-grid .ag-checkbox .ag-input-wrapper.ag-checked,
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox .ag-input-wrapper.ag-checked { transform: none; }
.ag-theme-alpine.vuetify-grid .ag-checkbox .ag-input-wrapper.ag-checked::after,
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox .ag-input-wrapper.ag-checked::after { content: none !important; }
.ag-theme-alpine.vuetify-grid .ag-checkbox .ag-input-field-input,
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox .ag-input-field-input {
position: static !important;
opacity: 1 !important;
}
.ag-theme-alpine.vuetify-grid .ag-checkbox .ag-input-field-input:focus-visible + .ag-input-wrapper,
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox .ag-input-field-input:focus-visible + .ag-input-wrapper { transform: none; }
/* Ensure horizontal/vertical centering of checkbox in header and cells */
.ag-theme-alpine.vuetify-grid .ag-header-cell .ag-checkbox,
.ag-theme-alpine-dark.vuetify-grid .ag-header-cell .ag-checkbox,
.ag-theme-alpine.vuetify-grid .ag-cell .ag-checkbox,
.ag-theme-alpine-dark.vuetify-grid .ag-cell .ag-checkbox {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 100% !important;
height: 100% !important;
}
/* Final neutralization: let AG Grid draw the checkbox visuals */
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"],
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"] {
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:checked,
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:checked {
background: transparent !important;
border-color: transparent !important;
}
.ag-theme-alpine .ag-set-filter-item:hover,
.ag-theme-alpine-dark .ag-set-filter-item:hover {
background-color: rgba(var(--v-theme-primary), 0.08) !important;
}
/* Remove generic checkbox box styling to avoid double visuals; rely on theme defaults */
.ag-theme-alpine .ag-checkbox-input-wrapper input[type="checkbox"],
.ag-theme-alpine-dark .ag-checkbox-input-wrapper input[type="checkbox"] {
background: initial !important;
border: initial !important;
}
.ag-theme-alpine .ag-checkbox-input-wrapper input[type="checkbox"]:checked,
.ag-theme-alpine-dark .ag-checkbox-input-wrapper input[type="checkbox"]:checked {
background: initial !important;
border-color: initial !important;
}
.ag-theme-alpine .ag-selection-checkbox,
.ag-theme-alpine-dark .ag-selection-checkbox {
width: 18px !important;
height: 18px !important;
border-radius: 4px !important;
}
.ag-theme-alpine .ag-header-cell-menu-button,
.ag-theme-alpine-dark .ag-header-cell-menu-button {
color: rgb(var(--v-theme-primary)) !important;
}
.ag-theme-alpine .ag-header-cell:hover .ag-icon-filter,
.ag-theme-alpine-dark .ag-header-cell:hover .ag-icon-filter {
color: rgb(var(--v-theme-primary)) !important;
}
.ag-theme-alpine .ag-picker-field-display,
.ag-theme-alpine-dark .ag-picker-field-display {
color: rgb(var(--v-theme-on-surface)) !important;
}
.ag-theme-alpine .ag-select .ag-picker-field-wrapper,
.ag-theme-alpine-dark .ag-select .ag-picker-field-wrapper {
background-color: rgba(var(--v-theme-on-surface), 0.05) !important;
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
border-radius: 6px !important;
padding: 4px 8px !important;
min-height: 32px !important;
}
.ag-theme-alpine .ag-list-item,
.ag-theme-alpine-dark .ag-list-item {
color: rgb(var(--v-theme-on-surface)) !important;
}
.ag-theme-alpine-dark .ag-cell:hover { .ag-theme-alpine-dark .ag-cell:hover {
cursor: text !important; cursor: text !important;
@@ -150,45 +464,25 @@
} }
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"] { .ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"] {
background-color: rgb(var(--v-theme-surface)) !important; background: transparent !important;
border: 2px solid rgba(var(--v-theme-on-surface), 0.6) !important; border: 0 !important;
} }
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:checked { .ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"]:checked {
background-color: rgb(var(--v-theme-primary)) !important; background: transparent !important;
border-color: rgb(var(--v-theme-primary)) !important; border-color: transparent !important;
} }
.ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox { .ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox {
width: 20px !important; width: 18px !important;
height: 20px !important; height: 18px !important;
border-radius: 4px !important;
border: 2px solid rgba(var(--v-theme-on-surface), 0.6) !important;
background-color: rgb(var(--v-theme-surface)) !important;
position: relative !important;
transition: all 0.2s ease-in-out !important;
} }
.ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox:checked { .ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox:checked { transform: none; }
background-color: rgb(var(--v-theme-primary)) !important;
border-color: rgb(var(--v-theme-primary)) !important;
}
.ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox:checked::after { .ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox:checked::after { content: none !important; }
content: "" !important;
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
color: rgb(var(--v-theme-on-primary)) !important;
font-size: 12px !important;
font-weight: bold !important;
}
.ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox:hover { .ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox:hover { transform: none; }
border-color: rgb(var(--v-theme-primary)) !important;
background-color: rgba(var(--v-theme-primary), 0.08) !important;
}
.ag-theme-alpine-dark.vuetify-grid .ag-menu { .ag-theme-alpine-dark.vuetify-grid .ag-menu {
background-color: rgb(var(--v-theme-surface)) !important; background-color: rgb(var(--v-theme-surface)) !important;
@@ -313,7 +607,31 @@
justify-content: space-between !important; justify-content: space-between !important;
transition: all 0.2s ease-in-out !important; transition: all 0.2s ease-in-out !important;
position: relative !important; position: relative !important;
overflow: hidden !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;
} }
.ag-theme-alpine-dark.vuetify-grid .ag-paging-panel:hover { .ag-theme-alpine-dark.vuetify-grid .ag-paging-panel:hover {
@@ -653,7 +971,7 @@
justify-content: space-between !important; justify-content: space-between !important;
transition: all 0.2s ease-in-out !important; transition: all 0.2s ease-in-out !important;
position: relative !important; position: relative !important;
overflow: hidden !important; overflow: visible !important;
font-size: 14px !important; font-size: 14px !important;
} }
@@ -999,3 +1317,72 @@
.ag-theme-alpine-dark.vuetify-grid .ag-paging-row-summary-panel { .ag-theme-alpine-dark.vuetify-grid .ag-paging-row-summary-panel {
color: rgb(var(--v-theme-on-surface)) !important; color: rgb(var(--v-theme-on-surface)) !important;
} }
/* === FINAL MINIMAL CHECKBOX FIXES (reset visuals, keep centering) === */
/* Reset wrappers/inputs to AG Grid default rendering so no double visuals occur */
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper,
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper {
width: auto !important;
height: auto !important;
background: transparent !important;
border: 0 !important;
display: inline-block !important;
padding: 0 !important;
}
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox .ag-input-wrapper,
.ag-theme-alpine.vuetify-grid .ag-checkbox .ag-input-wrapper {
width: auto !important;
height: auto !important;
background: transparent !important;
border: 0 !important;
position: static !important;
box-shadow: none !important;
}
.ag-theme-alpine-dark.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"],
.ag-theme-alpine.vuetify-grid .ag-checkbox-input-wrapper input[type="checkbox"] {
position: static !important;
width: 16px !important;
height: 16px !important;
opacity: 1 !important;
cursor: pointer !important;
background: transparent !important;
border: 0 !important;
box-shadow: none !important;
}
/* Center selection checkboxes in header and cells without altering visuals */
.ag-theme-alpine-dark.vuetify-grid .ag-header-cell .ag-header-select-all,
.ag-theme-alpine.vuetify-grid .ag-header-cell .ag-header-select-all {
display: flex !important;
align-items: center !important;
justify-content: center !important;
height: 100% !important;
}
/* Ensure checkbox container is centered */
.ag-theme-alpine-dark.vuetify-grid .ag-selection-checkbox,
.ag-theme-alpine.vuetify-grid .ag-selection-checkbox,
.ag-theme-alpine-dark.vuetify-grid .ag-header-select-all,
.ag-theme-alpine.vuetify-grid .ag-header-select-all,
.ag-theme-alpine-dark.vuetify-grid .ag-cell .ag-checkbox,
.ag-theme-alpine.vuetify-grid .ag-cell .ag-checkbox {
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.ag-theme-alpine-dark.vuetify-grid .ag-centered,
.ag-theme-alpine.vuetify-grid .ag-centered {
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin: 0 auto !important;
}
/* Optional: use AG Grid var to unify checkbox size if supported */
.ag-theme-alpine-dark.vuetify-grid,
.ag-theme-alpine.vuetify-grid {
--ag-checkbox-size: 18px;
}