2025-08-04 16:33:07 +03:30
|
|
|
<script setup>
|
2025-09-03 17:28:04 +03:30
|
|
|
import { ref, watch, onMounted, onUnmounted, computed } from 'vue'
|
2025-08-04 16:33:07 +03:30
|
|
|
import { AgGridVue } from 'ag-grid-vue3'
|
|
|
|
|
import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'
|
|
|
|
|
import DataTableHeader from './components/DataTableHeader.vue'
|
|
|
|
|
import UserDetailsDialog from './components/UserDetailsDialog.vue'
|
|
|
|
|
import ConfirmDeleteDialog from './components/ConfirmDeleteDialog.vue'
|
|
|
|
|
import { useDataTableGrid } from './composables/useDataTableGrid'
|
|
|
|
|
import { useDataTableActions } from './composables/useDataTableActions'
|
|
|
|
|
import { useResponsive } from './composables/useResponsive'
|
|
|
|
|
import jsPDF from 'jspdf'
|
|
|
|
|
import autoTable from 'jspdf-autotable'
|
|
|
|
|
|
2025-09-03 17:28:04 +03:30
|
|
|
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'
|
|
|
|
|
])
|
2025-08-04 16:33:07 +03:30
|
|
|
|
|
|
|
|
const { isMobile, isTablet, windowWidth } = useResponsive()
|
2025-09-03 17:28:04 +03:30
|
|
|
const gridContainer = ref(null)
|
2025-08-04 16:33:07 +03:30
|
|
|
|
2025-09-03 17:28:04 +03:30
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-04 16:33:07 +03:30
|
|
|
|
|
|
|
|
const {
|
2025-09-03 17:28:04 +03:30
|
|
|
setupGlobalHandlers,
|
|
|
|
|
cleanupGlobalHandlers,
|
|
|
|
|
exportToCSV,
|
|
|
|
|
exportToExcel,
|
|
|
|
|
saveUser,
|
|
|
|
|
confirmDelete,
|
|
|
|
|
cancelDelete,
|
|
|
|
|
showDetailsDialog,
|
|
|
|
|
selectedRowData,
|
|
|
|
|
deleteRowData,
|
|
|
|
|
showDeleteDialog,
|
|
|
|
|
deleteRow
|
2025-08-04 16:33:07 +03:30
|
|
|
} = useDataTableActions(gridApi, rowData)
|
|
|
|
|
|
2025-09-03 17:28:04 +03:30
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-04 16:33:07 +03:30
|
|
|
const quickFilter = ref('')
|
|
|
|
|
|
|
|
|
|
watch(quickFilter, (newValue) => {
|
2025-09-03 17:28:04 +03:30
|
|
|
if (gridApi.value) {
|
|
|
|
|
gridApi.value.setGridOption('quickFilterText', newValue || '')
|
|
|
|
|
}
|
2025-08-04 16:33:07 +03:30
|
|
|
}, { immediate: false })
|
|
|
|
|
|
|
|
|
|
const onGridReady = params => {
|
2025-09-03 17:28:04 +03:30
|
|
|
gridReady(params)
|
|
|
|
|
setupGlobalHandlers()
|
|
|
|
|
|
|
|
|
|
if (props.apiUrl) {
|
|
|
|
|
fetchData()
|
|
|
|
|
}
|
2025-08-04 16:33:07 +03:30
|
|
|
|
2025-09-03 17:28:04 +03:30
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-04 16:33:07 +03:30
|
|
|
|
|
|
|
|
const handleQuickFilterUpdate = (value) => {
|
2025-09-03 17:28:04 +03:30
|
|
|
quickFilter.value = value || ''
|
|
|
|
|
|
|
|
|
|
if (gridApi.value) {
|
|
|
|
|
gridApi.value.setGridOption('quickFilterText', value || '')
|
|
|
|
|
}
|
2025-08-04 16:33:07 +03:30
|
|
|
}
|
|
|
|
|
|
2025-09-03 17:28:04 +03:30
|
|
|
const refreshData = () => {
|
|
|
|
|
if (props.apiUrl) {
|
|
|
|
|
fetchData()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defineExpose({
|
|
|
|
|
refreshData,
|
|
|
|
|
fetchData,
|
|
|
|
|
gridApi
|
|
|
|
|
})
|
|
|
|
|
|
2025-08-04 16:33:07 +03:30
|
|
|
onMounted(() => {
|
2025-09-03 17:28:04 +03:30
|
|
|
setupGlobalHandlers()
|
|
|
|
|
window.addEventListener('resize', handleResize)
|
2025-08-04 16:33:07 +03:30
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
2025-09-03 17:28:04 +03:30
|
|
|
cleanupGlobalHandlers()
|
|
|
|
|
window.removeEventListener('resize', handleResize)
|
2025-08-04 16:33:07 +03:30
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const handleResize = () => {
|
2025-09-03 17:28:04 +03:30
|
|
|
windowWidth.value = window.innerWidth
|
|
|
|
|
if (gridApi.value) {
|
|
|
|
|
updateColumnVisibility()
|
|
|
|
|
updatePagination()
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (gridApi.value && gridApi.value.sizeColumnsToFit) {
|
|
|
|
|
gridApi.value.sizeColumnsToFit()
|
|
|
|
|
}
|
|
|
|
|
}, 100)
|
|
|
|
|
}
|
2025-08-04 16:33:07 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const exportToPDF = () => {
|
2025-09-03 17:28:04 +03:30
|
|
|
if (!gridApi.value) {
|
|
|
|
|
console.error('Grid API not available')
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-08-04 16:33:07 +03:30
|
|
|
|
2025-09-03 17:28:04 +03:30
|
|
|
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)
|
|
|
|
|
}
|
2025-08-04 16:33:07 +03:30
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2025-09-03 17:28:04 +03:30
|
|
|
<v-card class="elevation-2">
|
|
|
|
|
<v-overlay
|
|
|
|
|
v-model="isLoading"
|
|
|
|
|
contained
|
|
|
|
|
class="align-center justify-center"
|
|
|
|
|
>
|
|
|
|
|
<v-progress-circular
|
|
|
|
|
indeterminate
|
|
|
|
|
size="64"
|
|
|
|
|
color="primary"
|
|
|
|
|
></v-progress-circular>
|
|
|
|
|
</v-overlay>
|
2025-08-04 16:33:07 +03:30
|
|
|
|
2025-09-03 17:28:04 +03:30
|
|
|
<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"
|
|
|
|
|
/>
|
2025-08-04 16:33:07 +03:30
|
|
|
|
2025-09-03 17:28:04 +03:30
|
|
|
<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"
|
2025-08-04 16:33:07 +03:30
|
|
|
/>
|
2025-09-03 17:28:04 +03:30
|
|
|
</v-card-text>
|
|
|
|
|
|
|
|
|
|
<UserDetailsDialog
|
|
|
|
|
v-if="enableEdit"
|
|
|
|
|
v-model="showDetailsDialog"
|
|
|
|
|
:selected-row-data="selectedRowData"
|
|
|
|
|
:columns="columnDefs"
|
|
|
|
|
:is-mobile="isMobile"
|
|
|
|
|
@save-user="handleSaveUser"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<ConfirmDeleteDialog
|
|
|
|
|
v-if="enableDelete"
|
|
|
|
|
v-model="showDeleteDialog"
|
|
|
|
|
:selected-row-data="deleteRowData"
|
|
|
|
|
:columns="columnDefs"
|
|
|
|
|
:is-mobile="isMobile"
|
|
|
|
|
@confirm="handleConfirmDelete"
|
|
|
|
|
@cancel="cancelDelete"
|
|
|
|
|
/>
|
|
|
|
|
</v-card>
|
2025-08-04 16:33:07 +03:30
|
|
|
</template>
|