@amirjalili1374/ui-kit 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +10 -10
  2. package/package.json +1 -2
  3. package/src/assets/fonts/A Massir Spray.ttf +0 -0
  4. package/src/assets/fonts/BYekan.ttf +0 -0
  5. package/src/assets/fonts/BYekan.woff +0 -0
  6. package/src/assets/fonts/BYekan.woff2 +0 -0
  7. package/src/assets/fonts/Dima Shekasteh 2 Free.ttf +0 -0
  8. package/src/assets/fonts/Dima Shekasteh Free Regular.ttf +0 -0
  9. package/src/assets/fonts/IRANSansWeb.ts +0 -1
  10. package/src/assets/fonts/IRANSansWeb.ttf +0 -0
  11. package/src/assets/fonts/IRANSansXBlack.ttf +0 -0
  12. package/src/assets/fonts/IRANSansXBold.ttf +0 -0
  13. package/src/assets/fonts/IRANSansXDemiBold.ttf +0 -0
  14. package/src/assets/fonts/IRANSansXExtraBold.ttf +0 -0
  15. package/src/assets/fonts/IRANSansXLight.ttf +0 -0
  16. package/src/assets/fonts/IRANSansXMedium.ttf +0 -0
  17. package/src/assets/fonts/IRANSansXRegular.ttf +0 -0
  18. package/src/assets/fonts/IRANSansXThin.ttf +0 -0
  19. package/src/assets/fonts/IRANSansXUltraLight.ttf +0 -0
  20. package/src/assets/fonts/IranNastaliq.ttf +0 -0
  21. package/src/assets/fonts/Vazir-Medium-FD.ttf +0 -0
  22. package/src/assets/fonts/Vazir-Medium-FD.woff +0 -0
  23. package/src/assets/fonts/Vazir-Medium-FD.woff2 +0 -0
  24. package/src/assets/fonts/Vazir-Regular-FD.eot +0 -0
  25. package/src/assets/fonts/kalamehBold.woff +0 -0
  26. package/src/assets/fonts/kalamehBold.woff2 +0 -0
  27. package/src/assets/fonts/kalamehHeavy.woff +0 -0
  28. package/src/assets/fonts/kalamehHeavy.woff2 +0 -0
  29. package/src/assets/fonts/kalamehLight.woff +0 -0
  30. package/src/assets/fonts/kalamehLight.woff2 +0 -0
  31. package/src/assets/fonts/kalamehRegular.woff +0 -0
  32. package/src/assets/fonts/kalamehRegular.woff2 +0 -0
  33. package/src/assets/images/auth/social-google.svg +0 -6
  34. package/src/assets/images/favicon.svg +0 -18
  35. package/src/assets/images/icons/icon-card.svg +0 -5
  36. package/src/assets/images/logos/logo.svg +0 -12
  37. package/src/assets/images/logos/logolight.svg +0 -12
  38. package/src/assets/images/maintenance/img-error-bg.svg +0 -34
  39. package/src/assets/images/maintenance/img-error-blue.svg +0 -43
  40. package/src/assets/images/maintenance/img-error-purple.svg +0 -42
  41. package/src/assets/images/maintenance/img-error-text.svg +0 -27
  42. package/src/assets/images/profile/profile-user-account-svgrepo-com.svg +0 -12
  43. package/src/assets/images/profile/user-round.svg +0 -15
  44. package/src/assets/images/template/template-01.ts +0 -1
  45. package/src/assets/images/vectors/colorized-bg.svg +0 -40
  46. package/src/assets/images/vectors/logo_stroke_1px.svg +0 -26
  47. package/src/assets/images/vectors/logo_stroke_2px.svg +0 -26
  48. package/src/assets/scss/components/_approval-sections.scss +0 -75
  49. package/src/assets/styles/fonts.scss +0 -77
  50. package/src/components/Loading.vue +0 -88
  51. package/src/components/common/AppStepper.vue +0 -139
  52. package/src/components/shared/BaseBreadcrumb.vue +0 -55
  53. package/src/components/shared/BaseIcon.vue +0 -27
  54. package/src/components/shared/ConfirmDialog.vue +0 -72
  55. package/src/components/shared/CustomAutocomplete.vue +0 -306
  56. package/src/components/shared/CustomDataTable.vue +0 -1859
  57. package/src/components/shared/DescriptionInput.vue +0 -204
  58. package/src/components/shared/DownloadButton.vue +0 -169
  59. package/src/components/shared/MoneyInput.vue +0 -105
  60. package/src/components/shared/PdfViewer.vue +0 -645
  61. package/src/components/shared/ShamsiDatePicker.vue +0 -444
  62. package/src/components/shared/UiChildCard.vue +0 -17
  63. package/src/components/shared/UiParentCard.vue +0 -21
  64. package/src/components/shared/VPriceTextField.vue +0 -136
  65. package/src/composables/useDataTable.ts +0 -152
  66. package/src/composables/usePermissions.ts +0 -90
  67. package/src/composables/useRouteGuard.ts +0 -36
  68. package/src/composables/useTableActions.ts +0 -207
  69. package/src/composables/useTableHeaders.ts +0 -172
  70. package/src/composables/useTableSelection.ts +0 -201
  71. package/src/constants/enums/approval.ts +0 -13
  72. package/src/constants/enums/booleanEnum.ts +0 -11
  73. package/src/constants/enums/contractType.ts +0 -11
  74. package/src/constants/enums/lcProductType.ts +0 -21
  75. package/src/constants/enums/repaymentType.ts +0 -11
  76. package/src/directives/v-digit-limit.ts +0 -15
  77. package/src/directives/v-permission.ts +0 -31
  78. package/src/features/index.ts +0 -48
  79. package/src/index.ts +0 -119
  80. package/src/plugins/key-clock.ts +0 -39
  81. package/src/plugins/mdi-icon.ts +0 -31
  82. package/src/plugins/vuetify.ts +0 -74
  83. package/src/scss/_override.scss +0 -72
  84. package/src/scss/_variables.scss +0 -124
  85. package/src/scss/components/_VButtons.scss +0 -23
  86. package/src/scss/components/_VCard.scss +0 -20
  87. package/src/scss/components/_VCustomDataTable.scss +0 -282
  88. package/src/scss/components/_VField.scss +0 -9
  89. package/src/scss/components/_VInput.scss +0 -17
  90. package/src/scss/components/_VNavigationDrawer.scss +0 -3
  91. package/src/scss/components/_VShadow.scss +0 -3
  92. package/src/scss/components/_VStepper.scss +0 -235
  93. package/src/scss/components/_VTabs.scss +0 -11
  94. package/src/scss/components/_VTextField.scss +0 -40
  95. package/src/scss/components/_approval.scss +0 -128
  96. package/src/scss/layout/_container.scss +0 -147
  97. package/src/scss/layout/_sidebar.scss +0 -138
  98. package/src/scss/layout/_topbar.scss +0 -39
  99. package/src/scss/pages/_dashboards.scss +0 -97
  100. package/src/scss/style.scss +0 -21
  101. package/src/services/apiService.ts +0 -59
  102. package/src/services/axiosInstance.ts +0 -14
  103. package/src/stores/customizer.ts +0 -55
  104. package/src/stores/permissions.ts +0 -237
  105. package/src/theme/darkThemes/DarkModernTheme.ts +0 -54
  106. package/src/theme/darkThemes/DarkOrangeTheme.ts +0 -53
  107. package/src/theme/darkThemes/DarkPurpleTheme.ts +0 -54
  108. package/src/theme/darkThemes/DarkRedTheme.ts +0 -54
  109. package/src/theme/darkThemes/DarkSilverTheme.ts +0 -53
  110. package/src/theme/darkThemes/DarkSteelTealGreen.ts +0 -53
  111. package/src/theme/darkThemes/DarkTealTheme.ts +0 -52
  112. package/src/theme/lightThemes/ModernTheme.ts +0 -55
  113. package/src/theme/lightThemes/OrangeTheme.ts +0 -54
  114. package/src/theme/lightThemes/PurpleTheme.ts +0 -54
  115. package/src/theme/lightThemes/RedTheme.ts +0 -55
  116. package/src/theme/lightThemes/SilverTheme.ts +0 -55
  117. package/src/theme/lightThemes/SteelTealGreen.ts +0 -54
  118. package/src/theme/lightThemes/TealTheme.ts +0 -54
  119. package/src/types/approval/approvalType.ts +0 -473
  120. package/src/types/cartable/cartableTypes.ts +0 -169
  121. package/src/types/componentTypes/DataTableType.ts +0 -14
  122. package/src/types/componentTypes/DataTableTypes.ts +0 -130
  123. package/src/types/enums/global.ts +0 -267
  124. package/src/types/jalaali-js.d.ts +0 -6
  125. package/src/types/models/Base.ts +0 -4
  126. package/src/types/models/env.d.ts +0 -10
  127. package/src/types/models/person.ts +0 -13
  128. package/src/types/models/userInfo.ts +0 -29
  129. package/src/types/preApproval/preApprovalTypes.ts +0 -67
  130. package/src/types/shims-tabler-icons.d.ts +0 -58
  131. package/src/types/themeTypes/ThemeType.ts +0 -47
  132. package/src/types/vue-apexcharts.d.ts +0 -1
  133. package/src/types/vue3-print-nb.d.ts +0 -1
  134. package/src/types/vue_tabler_icon.d.ts +0 -10
  135. package/src/utils/NationalCodeValidator.ts +0 -33
  136. package/src/utils/date-convertor.ts +0 -40
  137. package/src/utils/greetingUtils.ts +0 -97
  138. package/src/utils/helpers/fake-backend.ts +0 -68
  139. package/src/utils/helpers/fetch-wrapper.ts +0 -86
  140. package/src/utils/number-formatter.ts +0 -33
  141. package/src/validators/nationalCodeRule.ts +0 -6
@@ -1,1859 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * CustomDataTable.vue
4
- *
5
- * A feature-rich data table component with server-side pagination, filtering,
6
- * grouping, selection, CRUD actions, custom actions, downloads, and dialogs.
7
- *
8
- * Key features:
9
- * - Server-side pagination and infinite scroll
10
- * - Optional grouping with expand/collapse
11
- * - Row selection (single/multi) with external v-model
12
- * - Dynamic actions column with CRUD/routes/downloads/custom buttons
13
- * - Date conversion between Shamsi and Gregorian with optional timezone
14
- *
15
- * Accessibility:
16
- * - Adds ARIA attributes to group headers and busy regions
17
- * - Keyboard support for toggling groups and activating selection on rows
18
- */
19
- import MoneyInput from '@/components/shared/MoneyInput.vue';
20
- import ShamsiDatePicker from '@/components/shared/ShamsiDatePicker.vue';
21
- import { useTableSelection } from '@/composables/useTableSelection';
22
- import apiService from '@/services/apiService';
23
- import axiosInstance from '@/services/axiosInstance';
24
- import { DateConverter } from '@/utils/date-convertor';
25
- import { formatNumberWithCommas } from '@/utils/number-formatter';
26
- import { IconChevronDown, IconChevronRight } from '@tabler/icons-vue';
27
- import { useDebounceFn } from '@vueuse/core';
28
- import type { Component, Ref } from 'vue';
29
- import { computed, isRef, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue';
30
- import { useRouter } from 'vue-router';
31
-
32
- import type {
33
- ApiResponse,
34
- CustomAction,
35
- DataTableProps,
36
- Header,
37
- TableItem
38
- } from '@/types/componentTypes/DataTableTypes';
39
-
40
- type AutocompleteItemsSource =
41
- | any[]
42
- | Ref<any[] | undefined>
43
- | ((context?: Record<string, any>) => any[] | undefined);
44
-
45
- type EnhancedHeader = Header & {
46
- autocompleteItems?: AutocompleteItemsSource;
47
- autocompleteItemTitle?: string;
48
- autocompleteItemValue?: string;
49
- autocompleteReturnObject?: boolean;
50
- autocompleteMultiple?: boolean;
51
- };
52
-
53
- /**
54
- * Component props - using proper types from DataTableTypes
55
- */
56
- interface Props extends Omit<DataTableProps, 'routes'> {
57
- routes?: Record<string, string> | ((item: TableItem) => Record<string, string>);
58
- enableGroupDelete?: boolean; // Enable group delete functionality for bulk mode
59
- }
60
-
61
- const props = withDefaults(defineProps<Props>(), {
62
- autoFetch: true,
63
- showPagination: true,
64
- showRefreshButton: false,
65
- selectable: false,
66
- multiSelect: false,
67
- selectedItems: () => [],
68
- uniqueKey: 'id',
69
- pageSize: 10,
70
- defaultExpanded: false,
71
- dateWithTimezone: false,
72
- bulkMode: false
73
- });
74
-
75
- const emit = defineEmits<{
76
- (e: 'update:selectedItems', items: TableItem[]): void;
77
- (e: 'selection-change', items: TableItem[]): void;
78
- }>();
79
-
80
- defineOptions({ inheritAttrs: false });
81
-
82
- const items = ref<TableItem[]>([]);
83
- const originalServerData = ref<TableItem[]>([]); // Store original server data
84
- const loading = ref(false);
85
- const error = ref<string | null>(null);
86
- const dialog = ref(false);
87
- const deleteDialog = ref(false);
88
- const isEditing = ref(false);
89
- const editedItem = ref<TableItem | null>(null);
90
- const formModel = ref<Record<string, any>>({});
91
- const itemToDelete = ref<TableItem | null>(null);
92
- const snackbar = ref(false);
93
- const snackbarMessage = ref('');
94
- const router = useRouter();
95
- const itemsPerPage = ref(props.pageSize);
96
- const totalSize = ref(0);
97
- const totalPages = ref(0);
98
- const currentPage = ref(1);
99
- const sortBy = ref<{ key: string; order: 'asc' | 'desc' } | null>(null);
100
- const filterDialog = ref(false);
101
- const filterModel = ref<Record<string, any>>({});
102
- const tableRef = ref<HTMLElement | null>(null);
103
- const isLoadingMore = ref(false);
104
- const hasMore = ref(true);
105
-
106
- // Selection & grouping using composable (minimal-risk wiring)
107
- const selection = useTableSelection(items, {
108
- multiSelect: props.bulkMode ? false : props.multiSelect, // Force single select in bulk mode
109
- uniqueKey: props.uniqueKey as any,
110
- groupBy: props.groupBy as any,
111
- defaultExpanded: props.defaultExpanded
112
- });
113
-
114
- // Override toggleSelection in bulk mode to ensure single selection
115
- const originalToggleSelection = selection.toggleSelection;
116
- selection.toggleSelection = (item: any) => {
117
- if (props.bulkMode) {
118
- // In bulk mode, always clear and select only this item
119
- selection.clearSelection();
120
- selection.selectedItems.value = [item];
121
- } else {
122
- // Normal behavior for non-bulk mode
123
- originalToggleSelection(item);
124
- }
125
- };
126
- const selectedItems = selection.selectedItems;
127
- const selectAll = computed({
128
- get: () => selection.allSelected.value,
129
- set: (val: boolean) => {
130
- if (val) {
131
- selection.toggleSelectAll();
132
- } else {
133
- selection.clearSelection();
134
- }
135
- }
136
- });
137
- const groupedItems = selection.groupedItems as unknown as Ref<
138
- Array<{ groupKey: string | number; groupLabel: string; items: any[]; isExpanded: boolean; count: number }>
139
- >;
140
- const expandedGroups = selection.expandedGroups;
141
-
142
- // Computed flag to determine if any actions should be shown
143
- const hasAnyActions = computed(() => {
144
- // If bulkMode is true, don't show actions column in table
145
- if (props.bulkMode) {
146
- return false;
147
- }
148
-
149
- const hasCrudActions = Array.isArray(props.actions) && props.actions.length > 0;
150
- const hasRoutes = !!props.routes && (typeof props.routes === 'function' || Object.keys(props.routes).length > 0);
151
- const hasDownloadLinks = !!props.downloadLink && Object.keys(props.downloadLink).length > 0;
152
- const hasCustomActions = Array.isArray(props.customActions) && props.customActions.length > 0;
153
- const hasCustomButtons = (Array.isArray(props.customButtons) && props.customButtons.length > 0) || !!props.customButtonsFn;
154
- return hasCrudActions || hasRoutes || hasDownloadLinks || hasCustomActions || hasCustomButtons;
155
- });
156
-
157
- // Function to get routes for a specific item
158
- const getRoutesForItem = (item: any): Record<string, string> => {
159
- if (!props.routes) return {};
160
- if (typeof props.routes === 'function') {
161
- return props.routes(item);
162
- }
163
- return props.routes;
164
- };
165
-
166
- // Estimate auto width based on header title and type when width is not provided
167
- const estimateColumnWidth = (header: Header): number => {
168
- const title = header.title || '';
169
- const basePadding = 32; // left/right padding
170
- const avgCharWidth = 10; // heuristic average per character
171
- const typeExtra = header.type && String(header.type).toLowerCase() === 'money' ? 40 : 0;
172
- const computed = basePadding + title.length * avgCharWidth + typeExtra;
173
- const min = 100;
174
- const max = 300;
175
- return Math.min(Math.max(computed, min), max);
176
- };
177
-
178
- // Headers with auto width applied when not specified
179
- const autoHeaders = computed(() => {
180
- return props.headers.map((h) => ({
181
- ...h,
182
- width: h.width ?? estimateColumnWidth(h)
183
- }));
184
- });
185
-
186
- const selectionHeader = { title: '', key: 'selection', sortable: false, width: 50 } as const;
187
-
188
- const groupedHeaders = computed(() => {
189
- const base = [...(props.selectable ? [selectionHeader] : []), ...autoHeaders.value];
190
-
191
- if (!hasAnyActions.value) {
192
- return base;
193
- }
194
-
195
- // Calculate dynamic width based on actual button sizes (same logic as normalHeaders)
196
- let totalWidth = 0;
197
-
198
- // CRUD actions (edit, delete, view, create)
199
- if (props.actions) {
200
- props.actions.forEach((action) => {
201
- switch (action) {
202
- case 'edit':
203
- totalWidth += 140; // "ویرایش ✏️" button width
204
- break;
205
- case 'delete':
206
- totalWidth += 120; // "حذف ❌" button width
207
- break;
208
- case 'view':
209
- totalWidth += 140; // "🔍 نمایش" button width
210
- break;
211
- case 'create':
212
- totalWidth += 120; // Create button width
213
- break;
214
- }
215
- });
216
- }
217
-
218
- // Route actions
219
- if (props.routes) {
220
- Object.keys(props.routes).forEach((routeKey) => {
221
- totalWidth += 120; // Route button width (key.toUpperCase())
222
- });
223
- }
224
-
225
- // Download actions
226
- if (props.downloadLink) {
227
- Object.keys(props.downloadLink).forEach((key) => {
228
- totalWidth += 120; // Download button width
229
- });
230
- }
231
-
232
- // Custom actions
233
- if (props.customActions) {
234
- props.customActions.forEach((action) => {
235
- totalWidth += 140; // Custom action button width
236
- });
237
- }
238
-
239
- // Custom buttons
240
- if (props.customButtonsFn) {
241
- // For dynamic buttons, estimate based on typical button count
242
- totalWidth += 240; // 2 buttons * 120px each
243
- } else if (props.customButtons) {
244
- props.customButtons.forEach((button) => {
245
- totalWidth += 120; // Custom button width
246
- });
247
- }
248
-
249
- // Add spacing between buttons (8px margin per button)
250
- const buttonCount =
251
- (props.actions?.length || 0) +
252
- (props.routes ? Object.keys(props.routes).length : 0) +
253
- (props.downloadLink ? Object.keys(props.downloadLink).length : 0) +
254
- (props.customActions?.length || 0) +
255
- (props.customButtons?.length || (props.customButtonsFn ? 2 : 0));
256
-
257
- const spacingWidth = Math.max(buttonCount - 1, 0) * 8; // 8px margin between buttons
258
- totalWidth += spacingWidth;
259
-
260
- // Add padding for the cell
261
- totalWidth += 32; // 16px padding on each side
262
-
263
- // Ensure minimum width
264
- const actionWidth = Math.max(totalWidth, 200);
265
-
266
- return [...base, { title: 'عملیات', key: 'actions', sortable: false, width: actionWidth }];
267
- });
268
-
269
- const normalHeaders = computed(() => {
270
- const base = [...(props.selectable ? [selectionHeader] : []), ...autoHeaders.value];
271
-
272
- if (!hasAnyActions.value) {
273
- return base;
274
- }
275
-
276
- // Calculate dynamic width based on actual button sizes
277
- let totalWidth = 0;
278
-
279
- // CRUD actions (edit, delete, view, create)
280
- if (props.actions) {
281
- props.actions.forEach((action) => {
282
- switch (action) {
283
- case 'edit':
284
- totalWidth += 140; // "ویرایش ✏️" button width
285
- break;
286
- case 'delete':
287
- totalWidth += 120; // "حذف ❌" button width
288
- break;
289
- case 'view':
290
- totalWidth += 140; // "🔍 نمایش" button width
291
- break;
292
- case 'create':
293
- totalWidth += 120; // Create button width
294
- break;
295
- }
296
- });
297
- }
298
-
299
- // Route actions
300
- if (props.routes) {
301
- Object.keys(props.routes).forEach((routeKey) => {
302
- totalWidth += 120; // Route button width (key.toUpperCase())
303
- });
304
- }
305
-
306
- // Download actions
307
- if (props.downloadLink) {
308
- Object.keys(props.downloadLink).forEach((key) => {
309
- totalWidth += 120; // Download button width
310
- });
311
- }
312
-
313
- // Custom actions
314
- if (props.customActions) {
315
- props.customActions.forEach((action) => {
316
- totalWidth += 140; // Custom action button width
317
- });
318
- }
319
-
320
- // Custom buttons
321
- if (props.customButtonsFn) {
322
- // For dynamic buttons, estimate based on typical button count
323
- totalWidth += 240; // 2 buttons * 120px each
324
- } else if (props.customButtons) {
325
- props.customButtons.forEach((button) => {
326
- totalWidth += 120; // Custom button width
327
- });
328
- }
329
-
330
- // Add spacing between buttons (8px margin per button)
331
- const buttonCount =
332
- (props.actions?.length || 0) +
333
- (props.routes ? Object.keys(props.routes).length : 0) +
334
- (props.downloadLink ? Object.keys(props.downloadLink).length : 0) +
335
- (props.customActions?.length || 0) +
336
- (props.customButtons?.length || (props.customButtonsFn ? 2 : 0));
337
-
338
- const spacingWidth = Math.max(buttonCount - 1, 0) * 8; // 8px margin between buttons
339
- totalWidth += spacingWidth;
340
-
341
- // Add padding for the cell
342
- totalWidth += 32; // 16px padding on each side
343
-
344
- // Ensure minimum width
345
- const actionWidth = Math.max(totalWidth, 200);
346
-
347
- return [...base, { title: 'عملیات', key: 'actions', sortable: false, width: actionWidth }];
348
- });
349
-
350
- const hasAutocomplete = (header: Header): header is EnhancedHeader => {
351
- return Boolean((header as EnhancedHeader).autocompleteItems);
352
- };
353
-
354
- const resolveAutocompleteItems = (header: Header, context?: Record<string, any>): any[] => {
355
- const enhancedHeader = header as EnhancedHeader;
356
- const source = enhancedHeader.autocompleteItems;
357
-
358
- if (!source) return [];
359
-
360
- try {
361
- if (typeof source === 'function') {
362
- const baseContext = context ?? formModel.value;
363
- return source(baseContext) ?? [];
364
- }
365
-
366
- if (isRef(source)) {
367
- return source.value ?? [];
368
- }
369
-
370
- const resolved = unref(source);
371
- return Array.isArray(resolved) ? resolved : [];
372
- } catch (error) {
373
- console.error('Error resolving autocomplete items for header:', header.key, error);
374
- return [];
375
- }
376
- };
377
-
378
- const resolveHeaderDefaultValue = (header: Header, context: Record<string, any>): any => {
379
- const rawDefault = header.defaultValue;
380
-
381
- if (rawDefault === undefined) {
382
- return undefined;
383
- }
384
-
385
- try {
386
- if (typeof rawDefault === 'function') {
387
- return rawDefault(context);
388
- }
389
-
390
- if (isRef(rawDefault)) {
391
- return unref(rawDefault);
392
- }
393
-
394
- return rawDefault;
395
- } catch (error) {
396
- console.error('Error resolving default value for header:', header.key, error);
397
- return undefined;
398
- }
399
- };
400
-
401
- const resolveAutocompleteItemTitle = (header: Header): string => {
402
- return (header as EnhancedHeader).autocompleteItemTitle ?? 'title';
403
- };
404
-
405
- const resolveAutocompleteItemValue = (header: Header): string => {
406
- return (header as EnhancedHeader).autocompleteItemValue ?? 'value';
407
- };
408
-
409
- const resolveAutocompleteReturnObject = (header: Header): boolean => {
410
- const value = (header as EnhancedHeader).autocompleteReturnObject;
411
- return value !== false;
412
- };
413
-
414
- const resolveAutocompleteMultiple = (header: Header): boolean => {
415
- return (header as EnhancedHeader).autocompleteMultiple === true;
416
- };
417
-
418
- const formHeaders = computed((): Header[] => props.headers as Header[]);
419
-
420
- const isMoneyHeader = (header: Header): boolean => {
421
- if (!header || typeof header.type !== 'string') {
422
- return false;
423
- }
424
- return header.type.toLowerCase() === 'money';
425
- };
426
-
427
- const getFieldInputType = (header: Header): string | undefined => {
428
- return typeof header?.type === 'string' ? header.type : undefined;
429
- };
430
-
431
- const resolveHeaderKey = (header: Header): string => header.key;
432
- const resolveHeaderTitle = (header: Header): string => header.title;
433
- const isHeaderDisabled = (header: Header): boolean => header.editable === false;
434
-
435
- // Helper function to get unique value from item
436
- const getUniqueValue = (item: any): string | number => {
437
- if (typeof props.uniqueKey === 'function') {
438
- return props.uniqueKey(item);
439
- }
440
-
441
- if (typeof props.uniqueKey === 'string') {
442
- // Handle nested properties like "user.id"
443
- return props.uniqueKey.split('.').reduce((obj, key) => obj?.[key], item);
444
- }
445
-
446
- // Fallback to id
447
- return item.id;
448
- };
449
-
450
- // Helper function to get group value from item
451
- const getGroupValue = (item: any): string | number => {
452
- if (!props.groupBy) return '';
453
-
454
- if (typeof props.groupBy === 'function') {
455
- return props.groupBy(item);
456
- }
457
-
458
- if (typeof props.groupBy === 'string') {
459
- // Handle nested properties like "user.department"
460
- return props.groupBy.split('.').reduce((obj, key) => obj?.[key], item);
461
- }
462
-
463
- return item[props.groupBy] || '';
464
- };
465
-
466
- // Note: Grouping logic is handled by useTableSelection composable
467
-
468
- // Selection methods
469
- const toggleSelection = (item: any) => {
470
- if (!props.selectable) return;
471
- selection.toggleSelection(item);
472
- emit('update:selectedItems', selectedItems.value);
473
- emit('selection-change', selectedItems.value);
474
- };
475
-
476
- const toggleSelectAll = () => {
477
- if (!props.selectable || !props.multiSelect) return;
478
- selection.toggleSelectAll();
479
- emit('update:selectedItems', selectedItems.value);
480
- emit('selection-change', selectedItems.value);
481
- };
482
-
483
- // Group toggle function with additional safety
484
- const toggleGroup = (groupKey: string | number) => {
485
- // Small delay to ensure any focus/initialization issues are resolved
486
- setTimeout(() => {
487
- selection.toggleGroup(groupKey);
488
- }, 0);
489
- };
490
-
491
- // Expand all groups
492
- const expandAllGroups = () => selection.expandAllGroups();
493
-
494
- // Collapse all groups
495
- const collapseAllGroups = () => selection.collapseAllGroups();
496
-
497
- const isSelected = (item: any) => selection.isSelected(item);
498
-
499
- // Computed properties for selection
500
- const selectedCount = computed(() => selectedItems.value.length);
501
- const hasSelection = computed(() => selectedItems.value.length > 0);
502
-
503
- // Check if selected items are still present in current filtered data
504
- const validSelectedItems = computed(() => {
505
- if (!props.selectable || !props.bulkMode) return selectedItems.value;
506
-
507
- // Filter selected items to only include those that exist in current items
508
- return selectedItems.value.filter((selectedItem) => {
509
- const uniqueValue = getUniqueValue(selectedItem);
510
- return items.value.some((item) => getUniqueValue(item) === uniqueValue);
511
- });
512
- });
513
-
514
- // Check if we have valid selections for bulk mode
515
- const hasValidSelection = computed(() => {
516
- if (!props.selectable || !props.bulkMode) return hasSelection.value;
517
- return validSelectedItems.value.length > 0;
518
- });
519
-
520
- // Clear selection method
521
- const clearSelection = () => {
522
- selection.clearSelection();
523
- emit('update:selectedItems', selectedItems.value);
524
- emit('selection-change', selectedItems.value);
525
- };
526
-
527
- // Method for single item selection in bulk mode
528
- const selectSingleItem = (item: any) => {
529
- // Clear all selections first
530
- selection.clearSelection();
531
- // Force select only this item (don't use toggle)
532
- selection.selectedItems.value = [item];
533
- // Emit the change
534
- emit('update:selectedItems', selectedItems.value);
535
- emit('selection-change', selectedItems.value);
536
- };
537
-
538
- // Computed for radio group value
539
- const radioGroupValue = computed(() => {
540
- return selectedItems.value.length > 0 ? getUniqueValue(selectedItems.value[0]) : null;
541
- });
542
-
543
- // Memoize computed properties
544
- const cleanFilterModel = computed(() => {
545
- const model = { ...filterModel.value };
546
- Object.keys(model).forEach((key) => {
547
- if (model[key] === null || model[key] === undefined || model[key] === '') {
548
- delete model[key];
549
- }
550
- });
551
- return model;
552
- });
553
-
554
- const hasFilterComponent = computed(() => {
555
- return !!props.filterComponent;
556
- });
557
-
558
- /**
559
- * Fetches data from server using current filters, query params, and pagination.
560
- * Converts date fields to Shamsi for display and computes grouping/selection state.
561
- * @param queryParams Optional extra query params to merge with filter and pagination
562
- */
563
- const fetchData = async (queryParams?: Record<string, unknown>) => {
564
- loading.value = true;
565
- error.value = null;
566
-
567
- let params: Record<string, unknown> = {
568
- ...cleanFilterModel.value,
569
- ...props.queryParams
570
- };
571
-
572
- if (queryParams) {
573
- params = { ...params, ...queryParams };
574
- }
575
-
576
- try {
577
- const shouldPaginate = props.showPagination !== false;
578
- const requestParams = shouldPaginate
579
- ? {
580
- ...params,
581
- page: currentPage.value - 1,
582
- size: itemsPerPage.value
583
- }
584
- : params;
585
-
586
- const response = await api.fetch(requestParams) as ApiResponse<TableItem>;
587
- const serverRawData = shouldPaginate ? response.data?.content ?? [] : response.data ?? [];
588
- const serverData = Array.isArray(serverRawData) ? serverRawData : [];
589
-
590
- originalServerData.value = serverData;
591
-
592
- items.value = serverData.map((item: Record<string, any>) => {
593
- const newItem = { ...item };
594
- props.headers.forEach((header) => {
595
- if (header.isDate && newItem[header.key]) {
596
- try {
597
- newItem[header.key] = DateConverter.toShamsi(newItem[header.key]);
598
- } catch (error) {
599
- console.error(`Error converting date for field ${header.key}:`, error);
600
- }
601
- }
602
- });
603
- return newItem;
604
- });
605
-
606
- if (shouldPaginate && response.data?.page) {
607
- totalSize.value = response.data.page.totalElements;
608
- totalPages.value = response.data.page.totalPages;
609
- hasMore.value = currentPage.value < response.data.page.totalPages;
610
- } else {
611
- totalSize.value = serverData.length;
612
- totalPages.value = 1;
613
- hasMore.value = false;
614
- }
615
-
616
- if (props.defaultSelected && items.value.length > 0 && props.defaultSelected in items.value[0]) {
617
- const defaultSelectedItems = items.value.filter((item) => item[props.defaultSelected!] === true);
618
- selectedItems.value = [...defaultSelectedItems];
619
- emit('update:selectedItems', selectedItems.value);
620
- emit('selection-change', selectedItems.value);
621
- }
622
- } catch (err: any) {
623
- if (err.response) {
624
- error.value = `خطای سرور: ${err.response.status} - ${err.response.data.message || 'خطای ناشناخته'}`;
625
- } else if (err.request) {
626
- error.value = 'خطای شبکه. لطفا دوباره تلاش کنید.';
627
- } else {
628
- error.value = 'یک خطای غیرمنتظره رخ داد.';
629
- }
630
- console.error(err);
631
- } finally {
632
- loading.value = false;
633
- }
634
- };
635
-
636
- // Add debounced version after fetchData definition
637
- const debouncedFetchData = useDebounceFn(fetchData, 300);
638
-
639
- // Update watcher to use debounced function
640
- watch(
641
- [cleanFilterModel],
642
- () => {
643
- debouncedFetchData();
644
- },
645
- { deep: true }
646
- );
647
-
648
- // Watch for pageSize prop changes
649
- watch(
650
- () => props.pageSize,
651
- (newPageSize) => {
652
- itemsPerPage.value = newPageSize;
653
- // Reset to first page when page size changes
654
- currentPage.value = 1;
655
- // Refetch data with new page size
656
- debouncedFetchData();
657
- }
658
- );
659
-
660
- // Watch for items changes and clear invalid selections
661
- watch(
662
- () => items.value,
663
- (newItems) => {
664
- if (props.selectable && props.bulkMode && selectedItems.value.length > 0) {
665
- // Check if any selected items are no longer in the current data
666
- const invalidSelections = selectedItems.value.filter((selectedItem) => {
667
- const uniqueValue = getUniqueValue(selectedItem);
668
- return !newItems.some((item) => getUniqueValue(item) === uniqueValue);
669
- });
670
-
671
- // If there are invalid selections, remove them
672
- if (invalidSelections.length > 0) {
673
- const validSelections = selectedItems.value.filter((selectedItem) => {
674
- const uniqueValue = getUniqueValue(selectedItem);
675
- return newItems.some((item) => getUniqueValue(item) === uniqueValue);
676
- });
677
-
678
- selectedItems.value = validSelections;
679
- emit('update:selectedItems', selectedItems.value);
680
- emit('selection-change', selectedItems.value);
681
- }
682
- }
683
- },
684
- { deep: true }
685
- );
686
-
687
- // Cleanup on component unmount
688
- onBeforeUnmount(() => {
689
- // Remove cancel call as it's not supported
690
- });
691
-
692
- const api = apiService(axiosInstance, props.apiResource);
693
- const customActionDialog = ref(false);
694
- const customActionComponent = shallowRef<Component | null>(null);
695
- const customActionItem = ref<any>(null);
696
-
697
- // Add scroll event handler
698
- const handleScroll = async (event: Event) => {
699
- const target = event.target as HTMLElement;
700
- const { scrollTop, scrollHeight, clientHeight } = target;
701
-
702
- // Check if we're near the bottom (within 100px)
703
- if (scrollHeight - scrollTop - clientHeight < 100 && !isLoadingMore.value && hasMore.value) {
704
- await loadMore();
705
- }
706
- };
707
-
708
- /**
709
- * Loads next page and appends to current items. Preserves selection defaults.
710
- */
711
- const loadMore = async () => {
712
- if (isLoadingMore.value || !hasMore.value) return;
713
-
714
- isLoadingMore.value = true;
715
- currentPage.value++;
716
-
717
- try {
718
- const params = {
719
- ...cleanFilterModel.value,
720
- ...props.queryParams,
721
- page: currentPage.value - 1,
722
- size: itemsPerPage.value
723
- };
724
-
725
- const response = await api.fetch(params);
726
- const newItems = response.data.content || [];
727
-
728
- // Convert dates to Shamsi format
729
- const formattedItems = newItems.map((item: Record<string, any>) => {
730
- const newItem = { ...item };
731
- props.headers.forEach((header) => {
732
- if (header.isDate && newItem[header.key]) {
733
- try {
734
- newItem[header.key] = DateConverter.toShamsi(newItem[header.key]);
735
- } catch (error) {
736
- console.error(`Error converting date for field ${header.key}:`, error);
737
- }
738
- }
739
- });
740
- return newItem;
741
- });
742
-
743
- items.value = [...items.value, ...formattedItems];
744
- hasMore.value = currentPage.value < response.data.totalPages;
745
-
746
- // Auto-select new items if defaultSelected prop is provided
747
- if (props.defaultSelected && props.selectable) {
748
- const newSelectedItems = formattedItems.filter((item: any) => item[props.defaultSelected!] === true);
749
- if (newSelectedItems.length > 0) {
750
- selectedItems.value = [...selectedItems.value, ...newSelectedItems];
751
- emit('update:selectedItems', selectedItems.value);
752
- emit('selection-change', selectedItems.value);
753
- }
754
- }
755
- } catch (err) {
756
- console.error('Error loading more items:', err);
757
- currentPage.value--; // Revert page number on error
758
- } finally {
759
- isLoadingMore.value = false;
760
- }
761
- };
762
-
763
- // Expose methods to parent component
764
- defineExpose({
765
- fetchData,
766
- items,
767
- selectedItems,
768
- getSelectedItems: () => selectedItems.value,
769
- clearSelection: () => {
770
- selectedItems.value = [];
771
- selectAll.value = false;
772
- },
773
- // Grouping methods (handled by useTableSelection composable)
774
- groupedItems,
775
- toggleGroup,
776
- expandAllGroups,
777
- collapseAllGroups,
778
- formModel
779
- });
780
-
781
- /**
782
- * Opens the create/edit dialog and normalizes date fields for editing.
783
- */
784
- const openDialog = (item?: any) => {
785
- editedItem.value = item ? { ...item } : {};
786
- isEditing.value = !!item;
787
- dialog.value = true;
788
-
789
- if (!isEditing.value) {
790
- const defaultContext: Record<string, any> = { ...editedItem.value };
791
-
792
- for (const header of props.headers) {
793
- if (header.defaultValue === undefined) {
794
- continue;
795
- }
796
-
797
- const resolvedDefault = resolveHeaderDefaultValue(header, defaultContext);
798
- if (resolvedDefault !== undefined) {
799
- editedItem.value![header.key] = resolvedDefault;
800
- defaultContext[header.key] = resolvedDefault;
801
- }
802
- }
803
- }
804
-
805
- // Sync form model with editedItem
806
- formModel.value = { ...editedItem.value };
807
-
808
- // Normalize autocomplete fields based on header configuration
809
- props.headers.forEach((header) => {
810
- if (!hasAutocomplete(header)) return;
811
-
812
- const enhancedHeader = header as EnhancedHeader;
813
- const currentValue = formModel.value[header.key];
814
- const valueKey = resolveAutocompleteItemValue(header);
815
-
816
- if (enhancedHeader.autocompleteReturnObject === false) {
817
- if (Array.isArray(currentValue)) {
818
- formModel.value[header.key] = currentValue.map((item: any) =>
819
- item && typeof item === 'object' ? item[valueKey] ?? null : item
820
- );
821
- } else if (currentValue && typeof currentValue === 'object') {
822
- formModel.value[header.key] = currentValue[valueKey] ?? null;
823
- }
824
- }
825
- });
826
-
827
- // Ensure date fields in edit mode are in Gregorian YYYY-MM-DD for the date picker
828
- if (isEditing.value) {
829
- try {
830
- props.headers.forEach((header) => {
831
- if (header.isDate) {
832
- const v = formModel.value[header.key];
833
- if (typeof v === 'string' && v.includes('/')) {
834
- // Convert Shamsi jYYYY/jMM/jDD -> YYYY-MM-DD for picker
835
- formModel.value[header.key] = DateConverter.toGregorian(v);
836
- } else if (typeof v === 'string' && v.includes('T')) {
837
- // Normalize ISO to YYYY-MM-DD for picker
838
- formModel.value[header.key] = v.split('T')[0];
839
- }
840
- }
841
- });
842
- } catch (e) {
843
- console.error('Error normalizing date fields for edit form:', e);
844
- }
845
- }
846
- };
847
-
848
- const openDeleteDialog = (item: any) => {
849
- itemToDelete.value = item;
850
- deleteDialog.value = true;
851
- };
852
-
853
- const groupDeleteDialog = ref(false);
854
-
855
- const openGroupDeleteDialog = () => {
856
- groupDeleteDialog.value = true;
857
- };
858
-
859
- const deleteGroupItems = async () => {
860
- try {
861
- const selectedIds = validSelectedItems.value.map((item) => getUniqueValue(item));
862
-
863
- // Use bulk delete endpoint with comma-separated IDs
864
- const idsParam = selectedIds.join(',');
865
- await api.delete(`?ids=${idsParam}`);
866
-
867
- groupDeleteDialog.value = false;
868
- clearSelection();
869
- await fetchData();
870
-
871
- snackbarMessage.value = `✅ ${selectedIds.length} آیتم با موفقیت حذف شد`;
872
- snackbar.value = true;
873
- } catch (err) {
874
- console.error('خطا در حذف گروهی اطلاعات', err);
875
- snackbarMessage.value = '❌ خطا در حذف گروهی اطلاعات!';
876
- snackbar.value = true;
877
- }
878
- };
879
-
880
- /**
881
- * Persists the current form model, converting date fields back to Gregorian
882
- * (optionally with timezone) before sending to the API. Refreshes the table.
883
- */
884
- const saveItem = async () => {
885
- if (!formModel.value) return;
886
-
887
- try {
888
- // Normalize dates before saving
889
- const dataToSave = { ...formModel.value };
890
- props.headers.forEach((header) => {
891
- if (header.isDate && dataToSave[header.key]) {
892
- try {
893
- const raw = dataToSave[header.key];
894
- if (typeof raw === 'string') {
895
- const toIsoWithOffset = (ymd: string) => {
896
- const [y, m, d] = ymd.split('-').map(Number);
897
- const local = new Date(y, (m || 1) - 1, d || 1, 0, 0, 0, 0);
898
- const offsetMin = -local.getTimezoneOffset();
899
- const sign = offsetMin >= 0 ? '+' : '-';
900
- const abs = Math.abs(offsetMin);
901
- const hh = String(Math.floor(abs / 60)).padStart(2, '0');
902
- const mm = String(abs % 60).padStart(2, '0');
903
- const mm2 = String(m).padStart(2, '0');
904
- const dd2 = String(d).padStart(2, '0');
905
- return `${y}-${mm2}-${dd2}T00:00:00${sign}${hh}:${mm}`;
906
- };
907
-
908
- if (raw.includes('/')) {
909
- // Shamsi jYYYY/jMM/jDD -> Gregorian YYYY-MM-DD
910
- const greg = DateConverter.toGregorian(raw);
911
- dataToSave[header.key] = props.dateWithTimezone ? toIsoWithOffset(greg) : greg;
912
- } else if (raw.includes('T')) {
913
- // ISO string -> keep as is or trim time if not requested
914
- dataToSave[header.key] = props.dateWithTimezone ? raw : raw.split('T')[0];
915
- } else {
916
- // Already YYYY-MM-DD
917
- dataToSave[header.key] = props.dateWithTimezone ? toIsoWithOffset(raw) : raw;
918
- }
919
- }
920
- } catch (error) {
921
- console.error(`Error converting date for field ${header.key}:`, error);
922
- }
923
- }
924
- });
925
-
926
- if (isEditing.value && dataToSave.id) {
927
- await api.update(dataToSave);
928
- snackbarMessage.value = '✅ آیتم با موفقیت بروزرسانی شد!';
929
- } else {
930
- const response = await api.create(dataToSave);
931
- snackbarMessage.value = '✅ آیتم با موفقیت ایجاد شد!';
932
- if (response.data) items.value.push(response.data);
933
- }
934
-
935
- snackbar.value = true;
936
- dialog.value = false;
937
- await fetchData();
938
- } catch (err) {
939
- console.error('خطا در ذخیره اطلاعات', err);
940
- snackbarMessage.value = '❌ خطا در ذخیره اطلاعات!';
941
- snackbar.value = true;
942
- }
943
- };
944
-
945
- /**
946
- * Deletes item by id and refreshes the list.
947
- */
948
- const deleteItem = async (id: string) => {
949
- try {
950
- await api.delete(id);
951
- deleteDialog.value = false;
952
- items.value = items.value.filter((item) => item.id !== id);
953
- await fetchData();
954
- } catch (err) {
955
- console.error('خطا در حذف اطلاعات', err);
956
- snackbarMessage.value = '❌ خطا در حذف اطلاعات!';
957
- snackbar.value = true;
958
- }
959
- };
960
-
961
- /**
962
- * Navigates to a dynamic route constructed from the provided route template
963
- * and the selected item fields. Shows a snackbar if params are missing.
964
- */
965
- const goToRoute = (key: string, item?: any) => {
966
- const routes = getRoutesForItem(item);
967
- if (!routes || !routes[key] || !item) return;
968
-
969
- let routePath = routes[key];
970
- const missingParams: string[] = [];
971
-
972
- routePath = routePath.replace(/\{(\w+)}/g, (_, field) => {
973
- if (item[field] !== undefined) {
974
- return item[field];
975
- } else {
976
- missingParams.push(field);
977
- return `{${field}}`;
978
- }
979
- });
980
-
981
- if (missingParams.length > 0) {
982
- snackbarMessage.value = ` خطا : ${missingParams.join(' , ')} در خروجی نیست `;
983
- snackbar.value = true;
984
- return;
985
- }
986
-
987
- router.push(routePath);
988
- };
989
-
990
- /**
991
- * Downloads a file from a URL present on the item. Tries fetch and falls back
992
- * to axios with blob response, handling common server error content types.
993
- */
994
- const download = async (key: string | number, item: TableItem) => {
995
- if (!props.downloadLink || !item) return;
996
-
997
- const fileKey = props.downloadLink[key];
998
- const url = item[fileKey];
999
-
1000
- if (!url || typeof url !== 'string') {
1001
- snackbarMessage.value = `❌ لینک فایل یافت نشد.`;
1002
- snackbar.value = true;
1003
- return;
1004
- }
1005
-
1006
- // Type assertion after validation
1007
- const fileUrlString = url as string;
1008
-
1009
- try {
1010
- // Method 1: Fetch file as blob with proper headers
1011
- const response = await fetch(fileUrlString, {
1012
- method: 'GET',
1013
- headers: {
1014
- Accept: 'application/octet-stream,application/pdf,image/*,*/*'
1015
- },
1016
- credentials: 'include' // Include cookies for authentication
1017
- });
1018
-
1019
- if (!response.ok) {
1020
- throw new Error(`HTTP error! status: ${response.status}`);
1021
- }
1022
-
1023
- // Check if response is actually a file or an error
1024
- const contentType = response.headers.get('content-type');
1025
- const contentLength = response.headers.get('content-length');
1026
-
1027
- console.log('Response headers:', {
1028
- contentType,
1029
- contentLength,
1030
- url: fileUrlString
1031
- });
1032
-
1033
- // If content-type is XML, it's likely an error response
1034
- if (contentType && contentType.includes('xml')) {
1035
- const errorText = await response.text();
1036
- console.error('Server returned XML error:', errorText);
1037
- snackbarMessage.value = `❌ خطای سرور: فایل در دسترس نیست`;
1038
- snackbar.value = true;
1039
- return;
1040
- }
1041
-
1042
- // If content-length is very small, it might be an error
1043
- if (contentLength && parseInt(contentLength) < 1000) {
1044
- const responseText = await response.text();
1045
- console.log('Small response:', responseText);
1046
- if (responseText.includes('error') || responseText.includes('Error')) {
1047
- snackbarMessage.value = `❌ خطای سرور: فایل در دسترس نیست`;
1048
- snackbar.value = true;
1049
- return;
1050
- }
1051
- }
1052
-
1053
- const blob = await response.blob();
1054
- const url = window.URL.createObjectURL(blob);
1055
-
1056
- // Create download link
1057
- const link = document.createElement('a');
1058
- link.href = url;
1059
-
1060
- // Extract filename from URL or use default
1061
- const filename = fileUrlString.split('/').pop() || 'download';
1062
- link.download = filename;
1063
-
1064
- // Add to DOM, click, and cleanup
1065
- document.body.appendChild(link);
1066
- link.click();
1067
- document.body.removeChild(link);
1068
-
1069
- // Clean up the blob URL
1070
- window.URL.revokeObjectURL(url);
1071
-
1072
- snackbarMessage.value = `✅ دانلود شروع شد`;
1073
- snackbar.value = true;
1074
- } catch (error) {
1075
- console.error('Download error:', error);
1076
-
1077
- // Method 2: Try with axios instance (includes auth headers)
1078
- try {
1079
- const axiosResponse = await axiosInstance.get(fileUrlString, {
1080
- responseType: 'blob',
1081
- headers: {
1082
- Accept: 'application/octet-stream,application/pdf,image/*,*/*'
1083
- }
1084
- });
1085
-
1086
- // Check response type
1087
- const contentType = axiosResponse.headers['content-type'];
1088
- const contentLength = axiosResponse.headers['content-length'];
1089
-
1090
- console.log('Axios response headers:', {
1091
- contentType,
1092
- contentLength,
1093
- url: fileUrlString
1094
- });
1095
-
1096
- // If it's XML, convert to text to see the error
1097
- if (contentType && contentType.includes('xml')) {
1098
- const textResponse = await axiosInstance.get(fileUrlString, {
1099
- responseType: 'text'
1100
- });
1101
- console.error('Server returned XML error:', textResponse.data);
1102
- snackbarMessage.value = `❌ خطای سرور: فایل در دسترس نیست`;
1103
- snackbar.value = true;
1104
- return;
1105
- }
1106
-
1107
- const blob = new Blob([axiosResponse.data]);
1108
- const url = window.URL.createObjectURL(blob);
1109
-
1110
- const link = document.createElement('a');
1111
- link.href = url;
1112
- const filename = fileUrlString.split('/').pop() || 'download';
1113
- link.download = filename;
1114
-
1115
- document.body.appendChild(link);
1116
- link.click();
1117
- document.body.removeChild(link);
1118
-
1119
- window.URL.revokeObjectURL(url);
1120
-
1121
- snackbarMessage.value = `✅ دانلود شروع شد`;
1122
- snackbar.value = true;
1123
- } catch (axiosError: any) {
1124
- console.error('Axios download error:', axiosError);
1125
-
1126
- // Try to get the error response as text
1127
- if (axiosError.response) {
1128
- try {
1129
- const errorText = await axiosInstance.get(url, {
1130
- responseType: 'text'
1131
- });
1132
- console.error('Server error response:', errorText.data);
1133
- } catch (textError) {
1134
- console.error('Could not get error text:', textError);
1135
- }
1136
- }
1137
-
1138
- snackbarMessage.value = `❌ خطا در دانلود فایل`;
1139
- snackbar.value = true;
1140
- }
1141
- }
1142
- };
1143
-
1144
- /**
1145
- * Handles pagination page change and refetches server data.
1146
- */
1147
- const handlePageChange = (newPage: number) => {
1148
- currentPage.value = newPage;
1149
- debouncedFetchData();
1150
- };
1151
-
1152
- onMounted(() => {
1153
- if (props.autoFetch) {
1154
- fetchData();
1155
- }
1156
- });
1157
-
1158
- const openCustomActionDialog = (action: CustomAction, item: any) => {
1159
- customActionComponent.value = action.component;
1160
-
1161
- // Find the original server data for this item
1162
- const originalItem = originalServerData.value.find((originalItem) => {
1163
- const itemId = typeof props.uniqueKey === 'function' ? props.uniqueKey(item) : item[props.uniqueKey as string];
1164
- const originalId = typeof props.uniqueKey === 'function' ? props.uniqueKey(originalItem) : originalItem[props.uniqueKey as string];
1165
- return itemId === originalId;
1166
- });
1167
-
1168
- // Pass original server data to custom action components
1169
- customActionItem.value = originalItem ? { ...originalItem } : { ...item };
1170
- customActionDialog.value = true;
1171
- };
1172
-
1173
- const getColumnStyle = (column: any, item: any) => {
1174
- const header = props.headers.find((h) => h.key === column.key);
1175
- if (!header) return {};
1176
-
1177
- const baseStyle = header.style || {};
1178
- if (header.conditionalStyle) {
1179
- const conditionalStyle = header.conditionalStyle(item[column.key], item);
1180
- return { ...baseStyle, ...conditionalStyle };
1181
- }
1182
- return baseStyle;
1183
- };
1184
-
1185
- // Helper function to get nested object values
1186
- const getNestedValue = (obj: any, path: string) => {
1187
- return path.split('.').reduce((current, key) => {
1188
- return current && current[key] !== undefined ? current[key] : null;
1189
- }, obj);
1190
- };
1191
-
1192
- /**
1193
- * Returns the display value for a cell, applying custom renderer/formatter
1194
- * and enum translation when configured on the column header.
1195
- */
1196
- const getTranslatedValue = (value: any, column: any, item: any) => {
1197
- const header = props.headers.find((h) => h.key === column.key);
1198
- if (!header) return value;
1199
-
1200
- // Use custom renderer if provided
1201
- if (header.customRenderer) {
1202
- return header.customRenderer(item);
1203
- }
1204
-
1205
- // Use custom formatter if provided
1206
- if (header.formatter) {
1207
- return header.formatter(value, item);
1208
- }
1209
-
1210
- // Money type formatting with separators
1211
- if (String(header.type).toLowerCase() === 'money') {
1212
- try {
1213
- return formatNumberWithCommas(value ?? 0, 0);
1214
- } catch (e) {
1215
- return value;
1216
- }
1217
- }
1218
-
1219
- if (header.translate) {
1220
- if (header.options) {
1221
- // Find matching option for enum value
1222
- const option = header.options.find((opt) => opt.value === value);
1223
- return option?.title || value;
1224
- }
1225
- // Fallback to basic translation if no options provided
1226
- return translateValue(value);
1227
- }
1228
- return value;
1229
- };
1230
-
1231
- const translateValue = (value: string) => {
1232
- // Example translation mapping
1233
- const translations: Record<string, string> = {
1234
- ACTIVE: 'فعال',
1235
- INACTIVE: 'غیرفعال',
1236
- PENDING: 'در انتظار',
1237
- COMPLETED: 'تکمیل شده'
1238
- // Add more translations as needed
1239
- };
1240
- return translations[value] || value;
1241
- };
1242
-
1243
- /**
1244
- * Applies currently edited filters and refetches from the first page.
1245
- */
1246
- const applyFilter = () => {
1247
- currentPage.value = 1; // Reset to first page when applying new filters
1248
- debouncedFetchData();
1249
- filterDialog.value = false;
1250
- };
1251
-
1252
- /**
1253
- * Clears filters and refetches from the first page.
1254
- */
1255
- const resetFilter = () => {
1256
- filterModel.value = {};
1257
- currentPage.value = 1;
1258
- debouncedFetchData();
1259
- filterDialog.value = false;
1260
- };
1261
-
1262
- // Handle filter apply from custom filter component
1263
- /**
1264
- * Receives filter data from a custom filter component and refetches.
1265
- */
1266
- const handleFilterApply = (filterData: any) => {
1267
- filterModel.value = filterData;
1268
- currentPage.value = 1;
1269
- debouncedFetchData();
1270
- filterDialog.value = false;
1271
- };
1272
- </script>
1273
-
1274
- <template>
1275
- <!-- Page Title -->
1276
- <div v-if="props.title" class="page-title">
1277
- <h3 class="title-text">{{ props.title }}</h3>
1278
- </div>
1279
-
1280
- <!-- Action Buttons OUTSIDE the table container -->
1281
- <div class="action-buttons">
1282
- <v-btn v-if="props.actions?.includes('create')" color="green" class="me-2" @click="openDialog()">ایجاد ✅</v-btn>
1283
- <v-btn v-if="hasFilterComponent" class="me-2" @click="filterDialog = true">فیلتر 🔍</v-btn>
1284
- <v-btn v-if="props.showRefreshButton" color="blue" @click="debouncedFetchData" :loading="loading">بروزرسانی 🔄</v-btn>
1285
-
1286
- <!-- Selection Actions -->
1287
- <div v-if="props.selectable && hasSelection" class="selection-actions">
1288
- <v-chip color="primary" class="me-2"> {{ selectedCount }} آیتم انتخاب شده </v-chip>
1289
- <v-btn color="error" size="small" class="me-2" @click="clearSelection"> پاک کردن انتخاب </v-btn>
1290
- </div>
1291
-
1292
- <!-- Action Buttons for Selected Items -->
1293
- <transition name="slide-left" appear>
1294
- <div v-if="(props.bulkMode && hasValidSelection) || (props.enableGroupDelete && hasSelection)" class="selected-actions">
1295
- <!-- Group Actions -->
1296
- <v-btn v-if="props.enableGroupDelete" color="red" size="small" class="me-2" @click="openGroupDeleteDialog">
1297
- <span class="me-1">🗑️</span>
1298
- حذف گروهی ({{ selectedCount }})
1299
- </v-btn>
1300
-
1301
- <!-- Individual Actions for Selected Items (only in bulk mode) -->
1302
- <template v-if="props.bulkMode" v-for="item in validSelectedItems" :key="getUniqueValue(item)">
1303
- <!-- CRUD Actions -->
1304
- <v-btn v-if="props.actions?.includes('edit')" color="blue" size="small" class="me-2" @click="openDialog(item)">
1305
- <span class="me-1">✏️</span>
1306
- ویرایش
1307
- </v-btn>
1308
-
1309
- <v-btn v-if="props.actions?.includes('delete')" color="red" size="small" class="me-2" @click="openDeleteDialog(item)">
1310
- <span class="me-1">🗑️</span>
1311
- حذف
1312
- </v-btn>
1313
-
1314
- <v-btn v-if="props.actions?.includes('view')" color="purple" size="small" class="me-2" @click="goToRoute('view', item)">
1315
- <span class="me-1">👁️</span>
1316
- نمایش
1317
- </v-btn>
1318
-
1319
- <!-- Route Actions -->
1320
- <template v-for="(routePath, routeKey) in getRoutesForItem(item)" :key="routeKey">
1321
- <v-btn color="indigo" size="small" class="me-2" @click="goToRoute(routeKey, item)">
1322
- <span class="me-1">➡️</span>
1323
- {{ String(routeKey) }}
1324
- </v-btn>
1325
- </template>
1326
-
1327
- <!-- Download Actions -->
1328
- <v-btn v-for="(value, key) in props.downloadLink" size="small" class="me-2" :key="key" @click="download(key, item)">
1329
- <span class="me-1">⬇️</span>
1330
- {{ key }}
1331
- </v-btn>
1332
-
1333
- <!-- Custom Actions -->
1334
- <template v-for="(action, index) in props.customActions" :key="action.title || index">
1335
- <v-btn
1336
- v-if="!action.condition || action.condition(item)"
1337
- color="orange"
1338
- size="small"
1339
- class="me-2"
1340
- @click="openCustomActionDialog(action, item)"
1341
- >
1342
- {{ action.title }}
1343
- </v-btn>
1344
- </template>
1345
-
1346
- <!-- Custom Buttons -->
1347
- <template v-if="props.customButtonsFn">
1348
- <v-btn
1349
- v-for="button in props.customButtonsFn(item)"
1350
- :key="button.label"
1351
- :color="button.color || 'primary'"
1352
- size="small"
1353
- class="me-2"
1354
- :disabled="button.disabled"
1355
- @click="button.onClick(item)"
1356
- >
1357
- <span v-if="(button as any).icon" class="me-1">{{ (button as any).icon }}</span>
1358
- {{ button.label }}
1359
- </v-btn>
1360
- </template>
1361
- <template v-else>
1362
- <v-btn
1363
- v-for="button in props.customButtons"
1364
- :key="button.label"
1365
- :color="button.color || 'primary'"
1366
- size="small"
1367
- class="me-2"
1368
- @click="button.onClick(item)"
1369
- >
1370
- <span v-if="(button as any).icon" class="me-1">{{ (button as any).icon }}</span>
1371
- {{ button.label }}
1372
- </v-btn>
1373
- </template>
1374
- </template>
1375
- </div>
1376
- </transition>
1377
- </div>
1378
-
1379
- <!-- Data Table Container (fills parent height) -->
1380
- <div
1381
- class="data-table-container"
1382
- v-bind="$attrs"
1383
- role="region"
1384
- :aria-busy="loading || isLoadingMore"
1385
- :aria-live="loading || isLoadingMore ? 'polite' : 'off'"
1386
- >
1387
- <template v-if="loading && !isLoadingMore">
1388
- <div class="skeleton-container" :style="{ height: `${props.height}px` }">
1389
- <v-skeleton-loader type="table" :loading="loading" class="mx-auto" max-width="100%" :boilerplate="false" />
1390
- </div>
1391
- </template>
1392
- <template v-else>
1393
- <!-- Grouped Table Structure -->
1394
- <div v-if="props.groupBy && groupedItems.length > 0" class="grouped-table">
1395
- <!-- Group Controls -->
1396
- <div class="group-controls mb-3">
1397
- <v-btn size="small" color="primary" @click="expandAllGroups" class="me-2"> گسترش همه </v-btn>
1398
- <v-btn size="small" color="secondary" @click="collapseAllGroups" class="me-2"> جمع کردن همه </v-btn>
1399
- </div>
1400
-
1401
- <!-- Single Scrollable Container for All Groups -->
1402
- <div class="groups-scroll-container" :style="{ height: `${props.height - 120}px` }">
1403
- <div class="groups-container">
1404
- <div v-for="group in groupedItems" :key="group.groupKey" class="group-section">
1405
- <!-- Group Header -->
1406
- <div
1407
- class="group-header"
1408
- role="button"
1409
- tabindex="0"
1410
- @click.stop="toggleGroup(group.groupKey)"
1411
- @mousedown.stop
1412
- @keydown.enter.prevent="toggleGroup(group.groupKey)"
1413
- @keydown.space.prevent="toggleGroup(group.groupKey)"
1414
- :aria-expanded="group.isExpanded ? 'true' : 'false'"
1415
- :aria-controls="`group-panel-${group.groupKey}`"
1416
- :id="`group-header-${group.groupKey}`"
1417
- :class="{ expanded: group.isExpanded }"
1418
- >
1419
- <IconChevronDown v-if="group.isExpanded" class="me-2 chevron-icon" />
1420
- <IconChevronRight v-else class="me-2 chevron-icon" />
1421
- <span class="group-label">{{ group.groupLabel }}</span>
1422
- <v-chip size="small" color="darkprimary" class="ms-auto">{{ group.count }}</v-chip>
1423
- </div>
1424
-
1425
- <!-- Group Items -->
1426
- <transition name="group-expand" appear>
1427
- <div
1428
- v-if="group.isExpanded"
1429
- class="group-items"
1430
- :id="`group-panel-${group.groupKey}`"
1431
- :aria-labelledby="`group-header-${group.groupKey}`"
1432
- role="region"
1433
- >
1434
- <v-data-table
1435
- :headers="groupedHeaders"
1436
- :items="group.items"
1437
- :items-per-page="itemsPerPage"
1438
- hide-default-footer
1439
- class="elevation-1 group-table"
1440
- no-data-text="رکوردی یافت نشد"
1441
- hover
1442
- :height="'auto'"
1443
- density="compact"
1444
- >
1445
- <!-- Custom Header for Selection -->
1446
- <template v-slot:header.selection="{ column }">
1447
- <v-checkbox
1448
- v-if="props.selectable && props.multiSelect"
1449
- :model-value="selectAll"
1450
- @update:model-value="toggleSelectAll"
1451
- :indeterminate="selectedCount > 0 && selectedCount < items.length"
1452
- hide-details
1453
- density="compact"
1454
- />
1455
- </template>
1456
- <template v-slot:item="{ item, columns, index }">
1457
- <tr
1458
- :style="{
1459
- background:
1460
- isSelected(item) && props.bulkMode
1461
- ? 'rgb(var(--v-theme-primary200))'
1462
- : index % 2 === 0
1463
- ? 'rgb(var(--v-theme-surface))'
1464
- : 'rgb(var(--v-theme-lightprimary))',
1465
- cursor: props.bulkMode && props.selectable ? 'pointer' : 'default'
1466
- }"
1467
- :tabindex="props.selectable ? 0 : -1"
1468
- @keydown.enter.prevent="props.selectable && toggleSelection(item)"
1469
- @click="props.bulkMode && props.selectable && selectSingleItem(item)"
1470
- >
1471
- <td
1472
- v-for="column in columns"
1473
- :key="column.key || 'unknown'"
1474
- :style="{
1475
- ...getColumnStyle(column, item),
1476
- ...(column.width
1477
- ? { width: column.width + 'px', minWidth: column.width + 'px', maxWidth: column.width + 'px' }
1478
- : {})
1479
- }"
1480
- >
1481
- <!-- Selection Checkbox/Radio -->
1482
- <template v-if="column.key === 'selection'">
1483
- <v-radio
1484
- v-if="props.bulkMode"
1485
- :model-value="radioGroupValue"
1486
- :value="getUniqueValue(item)"
1487
- @click.stop="selectSingleItem(item)"
1488
- :disabled="!props.selectable"
1489
- hide-details
1490
- density="compact"
1491
- />
1492
- <v-checkbox
1493
- v-else
1494
- :model-value="isSelected(item)"
1495
- @update:model-value="toggleSelection(item)"
1496
- :disabled="!props.selectable"
1497
- hide-details
1498
- density="compact"
1499
- />
1500
- </template>
1501
- <template v-if="column.key === 'actions' && hasAnyActions">
1502
- <v-btn v-if="props.actions?.includes('edit')" color="blue" size="small" class="mr-2" @click="openDialog(item)">
1503
- ویرایش ✏️
1504
- </v-btn>
1505
- <v-btn
1506
- v-if="props.actions?.includes('delete')"
1507
- color="red"
1508
- size="small"
1509
- class="mr-2"
1510
- @click="openDeleteDialog(item)"
1511
- >حذف ❌
1512
- </v-btn>
1513
- <v-btn
1514
- v-if="props.actions?.includes('view')"
1515
- color="purple"
1516
- size="small"
1517
- class="mr-2"
1518
- @click="goToRoute('view', item)"
1519
- >🔍 نمایش
1520
- </v-btn>
1521
- <template v-for="(routePath, routeKey) in getRoutesForItem(item)" :key="routeKey">
1522
- <v-btn color="indigo" size="small" class="mr-2" @click="goToRoute(routeKey, item)">
1523
- {{ routeKey.toUpperCase() }}
1524
- </v-btn>
1525
- </template>
1526
- <v-btn
1527
- v-for="(value, key) in props.downloadLink"
1528
- size="small"
1529
- class="mr-2"
1530
- :key="key"
1531
- @click="download(key, item)"
1532
- >
1533
- {{ key }} ⬇️
1534
- </v-btn>
1535
- <template v-for="(action, index) in props.customActions" :key="action.title || index">
1536
- <v-btn
1537
- v-if="!action.condition || action.condition(item)"
1538
- color="orange"
1539
- size="small"
1540
- class="mr-2"
1541
- @click="openCustomActionDialog(action, item)"
1542
- >
1543
- {{ action.title }}
1544
- </v-btn>
1545
- </template>
1546
- <template v-if="props.customButtonsFn">
1547
- <v-btn
1548
- v-for="button in props.customButtonsFn(item)"
1549
- :key="button.label"
1550
- :color="button.color || 'primary'"
1551
- size="small"
1552
- class="mr-2"
1553
- :disabled="button.disabled"
1554
- @click="button.onClick(item)"
1555
- >
1556
- {{ button.label }}
1557
- </v-btn>
1558
- </template>
1559
- <template v-else>
1560
- <v-btn
1561
- v-for="button in props.customButtons"
1562
- :key="button.label"
1563
- :color="button.color || 'primary'"
1564
- size="small"
1565
- class="mr-2"
1566
- @click="button.onClick(item)"
1567
- >
1568
- {{ button.label }}
1569
- </v-btn>
1570
- </template>
1571
- </template>
1572
- <template v-else>
1573
- {{ getTranslatedValue(getNestedValue(item, column.key || ''), column, item) }}
1574
- </template>
1575
- </td>
1576
- </tr>
1577
- </template>
1578
- </v-data-table>
1579
- </div>
1580
- </transition>
1581
- </div>
1582
- </div>
1583
- </div>
1584
- </div>
1585
-
1586
- <!-- Regular Table Structure (when not grouped) -->
1587
- <v-data-table
1588
- v-else
1589
- :headers="normalHeaders"
1590
- :items="items"
1591
- :items-per-page="itemsPerPage"
1592
- hide-default-footer
1593
- class="elevation-1"
1594
- no-data-text="رکوردی یافت نشد"
1595
- hover
1596
- :height="props.height"
1597
- density="compact"
1598
- >
1599
- <!-- Custom Header for Selection -->
1600
- <template v-slot:header.selection="{ column }">
1601
- <v-checkbox
1602
- v-if="props.selectable && props.multiSelect"
1603
- :model-value="selectAll"
1604
- @update:model-value="toggleSelectAll"
1605
- :indeterminate="selectedCount > 0 && selectedCount < items.length"
1606
- hide-details
1607
- density="compact"
1608
- />
1609
- </template>
1610
- <template v-slot:item="{ item, columns, index }">
1611
- <tr
1612
- :style="{
1613
- color: isSelected(item) && props.bulkMode ? 'rgb(var(--v-theme-white))' : 'rgb(var(--v-theme-darkText))',
1614
- background:
1615
- isSelected(item) && props.bulkMode
1616
- ? 'rgb(var(--v-theme-primary))'
1617
- : index % 2 === 0
1618
- ? 'rgb(var(--v-theme-surface))'
1619
- : 'rgb(var(--v-theme-lightprimary))',
1620
- cursor: props.bulkMode && props.selectable ? 'pointer' : 'default'
1621
- }"
1622
- :tabindex="props.selectable ? 0 : -1"
1623
- @keydown.enter.prevent="props.selectable && toggleSelection(item)"
1624
- @click="props.bulkMode && props.selectable && selectSingleItem(item)"
1625
- >
1626
- <td
1627
- v-for="column in columns"
1628
- :key="column.key || 'unknown'"
1629
- :style="{
1630
- ...getColumnStyle(column, item),
1631
- ...(column.width ? { width: column.width + 'px', minWidth: column.width + 'px', maxWidth: column.width + 'px' } : {})
1632
- }"
1633
- >
1634
- <!-- Selection Checkbox/Radio -->
1635
- <template v-if="column.key === 'selection'">
1636
- <v-radio
1637
- v-if="props.bulkMode"
1638
- :model-value="radioGroupValue"
1639
- :value="getUniqueValue(item)"
1640
- @click.stop="selectSingleItem(item)"
1641
- :disabled="!props.selectable"
1642
- hide-details
1643
- density="compact"
1644
- />
1645
- <v-checkbox
1646
- v-else
1647
- :model-value="isSelected(item)"
1648
- @update:model-value="toggleSelection(item)"
1649
- :disabled="!props.selectable"
1650
- hide-details
1651
- density="compact"
1652
- />
1653
- </template>
1654
- <template v-if="column.key === 'actions' && hasAnyActions">
1655
- <v-btn v-if="props.actions?.includes('edit')" color="blue" size="small" class="mr-2" @click="openDialog(item)">
1656
- ویرایش ✏️
1657
- </v-btn>
1658
- <v-btn v-if="props.actions?.includes('delete')" color="red" size="small" class="mr-2" @click="openDeleteDialog(item)"
1659
- >حذف ❌
1660
- </v-btn>
1661
- <v-btn v-if="props.actions?.includes('view')" color="purple" size="small" class="mr-2" @click="goToRoute('view', item)"
1662
- >🔍 نمایش
1663
- </v-btn>
1664
- <template v-for="(routePath, routeKey) in getRoutesForItem(item)" :key="routeKey">
1665
- <v-btn color="indigo" size="small" class="mr-2" @click="goToRoute(routeKey, item)">
1666
- {{ routeKey.toUpperCase() }}
1667
- </v-btn>
1668
- </template>
1669
- <v-btn v-for="(value, key) in props.downloadLink" size="small" class="mr-2" :key="key" @click="download(key, item)">
1670
- {{ key }} ⬇️
1671
- </v-btn>
1672
- <template v-for="(action, index) in props.customActions" :key="action.title || index">
1673
- <v-btn
1674
- v-if="!action.condition || action.condition(item)"
1675
- color="orange"
1676
- size="small"
1677
- class="mr-2"
1678
- @click="openCustomActionDialog(action, item)"
1679
- >
1680
- {{ action.title }}
1681
- </v-btn>
1682
- </template>
1683
- <template v-if="props.customButtonsFn">
1684
- <v-btn
1685
- v-for="button in props.customButtonsFn(item)"
1686
- :key="button.label"
1687
- :color="button.color || 'primary'"
1688
- size="small"
1689
- class="mr-2"
1690
- :disabled="button.disabled"
1691
- @click="button.onClick(item)"
1692
- >
1693
- {{ button.label }}
1694
- </v-btn>
1695
- </template>
1696
- <template v-else>
1697
- <v-btn
1698
- v-for="button in props.customButtons"
1699
- :key="button.label"
1700
- :color="button.color || 'primary'"
1701
- size="small"
1702
- class="mr-2"
1703
- @click="button.onClick(item)"
1704
- >
1705
- {{ button.label }}
1706
- </v-btn>
1707
- </template>
1708
- </template>
1709
- <template v-else>
1710
- {{ getTranslatedValue(getNestedValue(item, column.key || ''), column, item) }}
1711
- </template>
1712
- </td>
1713
- </tr>
1714
- </template>
1715
- </v-data-table>
1716
-
1717
- <!-- Loading indicator for infinite scroll -->
1718
- <div v-if="isLoadingMore" class="d-flex justify-center align-center pa-4">
1719
- <v-progress-circular indeterminate color="primary"></v-progress-circular>
1720
- </div>
1721
- </template>
1722
-
1723
- <!-- Custom Pagination always visible at the bottom -->
1724
- <div v-if="props.showPagination" class="pagination-wrapper">
1725
- <div class="d-flex justify-space-between align-center pa-4">
1726
- <div class="text-subtitle-2">
1727
- نمایش {{ (currentPage - 1) * itemsPerPage + 1 }} تا {{ Math.min(currentPage * itemsPerPage, totalSize) }} از {{ totalSize }} رکورد
1728
- </div>
1729
- <v-pagination v-model="currentPage" :length="totalPages" :total-visible="5" size="small" @update:model-value="handlePageChange" />
1730
- </div>
1731
- </div>
1732
- </div>
1733
-
1734
- <v-dialog v-model="dialog" max-width="1400">
1735
- <v-card>
1736
- <v-card-title>{{ isEditing ? 'ویرایش' : 'ایجاد' }}</v-card-title>
1737
- <v-card-text>
1738
- <v-container>
1739
- <component v-if="props.formComponent" :is="props.formComponent" v-model="formModel" />
1740
- <template v-else>
1741
- <v-row>
1742
- <v-col v-for="header in formHeaders" :key="resolveHeaderKey(header)" cols="12" md="4">
1743
- <template v-if="!header.hidden">
1744
- <ShamsiDatePicker
1745
- v-if="header.isDate"
1746
- v-model="formModel[resolveHeaderKey(header)]"
1747
- :label="resolveHeaderTitle(header)"
1748
- :disabled="isHeaderDisabled(header)"
1749
- />
1750
- <v-autocomplete
1751
- v-else-if="hasAutocomplete(header)"
1752
- v-model="formModel[resolveHeaderKey(header)]"
1753
- :label="resolveHeaderTitle(header)"
1754
- :items="resolveAutocompleteItems(header, formModel.value)"
1755
- :item-title="resolveAutocompleteItemTitle(header)"
1756
- :item-value="resolveAutocompleteItemValue(header)"
1757
- :return-object="resolveAutocompleteReturnObject(header)"
1758
- :multiple="resolveAutocompleteMultiple(header)"
1759
- :chips="resolveAutocompleteMultiple(header)"
1760
- :closable-chips="resolveAutocompleteMultiple(header)"
1761
- :disabled="isHeaderDisabled(header)"
1762
- clearable
1763
- variant="outlined"
1764
- />
1765
- <MoneyInput
1766
- v-else-if="isMoneyHeader(header)"
1767
- v-model="formModel[resolveHeaderKey(header)] as number"
1768
- :label="resolveHeaderTitle(header)"
1769
- :disabled="isHeaderDisabled(header)"
1770
- />
1771
- <v-text-field
1772
- v-else
1773
- v-model="formModel[resolveHeaderKey(header)]"
1774
- :label="resolveHeaderTitle(header)"
1775
- variant="outlined"
1776
- :disabled="isHeaderDisabled(header)"
1777
- :type="getFieldInputType(header)"
1778
- />
1779
- </template>
1780
- </v-col>
1781
- </v-row>
1782
- </template>
1783
- </v-container>
1784
- </v-card-text>
1785
- <v-card-actions>
1786
- <v-btn variant="tonal" color="error" @click="dialog = false">انصراف</v-btn>
1787
- <v-btn color="primary" var @click="saveItem">{{ isEditing ? 'ذخیره' : 'ایجاد' }}</v-btn>
1788
- </v-card-actions>
1789
- </v-card>
1790
- </v-dialog>
1791
-
1792
- <v-dialog v-model="deleteDialog" max-width="400">
1793
- <v-card>
1794
- <v-card-title>حذف آیتم</v-card-title>
1795
- <v-card-text> آیا مایل به حذف این رکورد هستید ?</v-card-text>
1796
- <v-card-actions>
1797
- <v-btn color="grey" @click="deleteDialog = false">انصراف</v-btn>
1798
- <v-btn color="red" @click="deleteItem(String(itemToDelete?.id || ''))">حذف</v-btn>
1799
- </v-card-actions>
1800
- </v-card>
1801
- </v-dialog>
1802
-
1803
- <!-- Group Delete Confirmation Dialog -->
1804
- <v-dialog v-model="groupDeleteDialog" max-width="500">
1805
- <v-card>
1806
- <v-card-title class="text-h6">
1807
- <v-icon color="red" class="me-2">🗑️</v-icon>
1808
- حذف گروهی
1809
- </v-card-title>
1810
- <v-card-text>
1811
- <p>
1812
- آیا مایل به حذف <strong>{{ selectedCount }}</strong> آیتم انتخاب شده هستید؟
1813
- </p>
1814
- <v-alert type="warning" variant="tonal" class="mt-3"> این عمل قابل بازگشت نیست! </v-alert>
1815
- </v-card-text>
1816
- <v-card-actions>
1817
- <v-spacer></v-spacer>
1818
- <v-btn color="grey" @click="groupDeleteDialog = false">انصراف</v-btn>
1819
- <v-btn color="red" @click="deleteGroupItems" :loading="loading"> حذف {{ selectedCount }} آیتم </v-btn>
1820
- </v-card-actions>
1821
- </v-card>
1822
- </v-dialog>
1823
-
1824
- <v-dialog v-model="customActionDialog" max-width="1300">
1825
- <v-card>
1826
- <v-card-title>
1827
- {{ props.customActions?.find((a) => a.component === customActionComponent)?.title || '' }}
1828
- </v-card-title>
1829
- <v-card-text>
1830
- <component v-if="customActionComponent" :is="customActionComponent" :item="customActionItem" @close="customActionDialog = false" />
1831
- </v-card-text>
1832
- </v-card>
1833
- </v-dialog>
1834
-
1835
- <!-- Filter Dialog -->
1836
- <v-dialog v-model="filterDialog" max-width="800">
1837
- <v-card>
1838
- <v-card-title>فیلتر</v-card-title>
1839
- <v-card-text>
1840
- <component
1841
- v-if="props.filterComponent"
1842
- :is="props.filterComponent"
1843
- v-model="filterModel"
1844
- @update:modelValue="filterModel = $event"
1845
- @apply="handleFilterApply"
1846
- />
1847
- </v-card-text>
1848
- </v-card>
1849
- </v-dialog>
1850
-
1851
- <!-- Snackbar for messages -->
1852
- <v-snackbar v-if="snackbar" v-model="snackbar" :timeout="3000" location="top">
1853
- {{ snackbarMessage }}
1854
- <template v-slot:actions>
1855
- <v-btn color="white" variant="text" @click="snackbar = false"> بستن </v-btn>
1856
- </template>
1857
- </v-snackbar>
1858
- </template>
1859
-