@7365admin1/layer-common 1.10.0 → 1.10.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 (81) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/components/AcceptDialog.vue +44 -0
  3. package/components/AccessCardAddForm.vue +101 -13
  4. package/components/AccessManagement.vue +130 -47
  5. package/components/AddSupplyForm.vue +165 -0
  6. package/components/AreaChecklistHistoryLogs.vue +235 -0
  7. package/components/AreaChecklistHistoryMain.vue +176 -0
  8. package/components/AreaFormDialog.vue +266 -0
  9. package/components/AreaMain.vue +841 -0
  10. package/components/AttendanceCheckInOutDialog.vue +416 -0
  11. package/components/AttendanceDetailsDialog.vue +184 -0
  12. package/components/AttendanceMain.vue +155 -0
  13. package/components/AttendanceMapSearchDialog.vue +393 -0
  14. package/components/AttendanceSettingsDialog.vue +398 -0
  15. package/components/BuildingManagement/buildings.vue +5 -5
  16. package/components/BuildingManagement/units.vue +5 -5
  17. package/components/ChecklistItemRow.vue +54 -0
  18. package/components/CheckoutItemMain.vue +705 -0
  19. package/components/CleaningScheduleMain.vue +271 -0
  20. package/components/DocumentManagement.vue +4 -0
  21. package/components/EntryPass/QrTemplatePreview.vue +104 -0
  22. package/components/EntryPassMain.vue +252 -200
  23. package/components/HygieneUpdateMoreAction.vue +238 -0
  24. package/components/ManageChecklistMain.vue +384 -0
  25. package/components/MemberMain.vue +48 -20
  26. package/components/MyAttendanceMain.vue +224 -0
  27. package/components/OnlineFormsConfiguration.vue +9 -2
  28. package/components/PhotoUpload.vue +410 -0
  29. package/components/ScheduleAreaMain.vue +313 -0
  30. package/components/ScheduleTaskAreaFormDialog.vue +144 -0
  31. package/components/ScheduleTaskAreaUpdateMoreAction.vue +109 -0
  32. package/components/ScheduleTaskForm.vue +471 -0
  33. package/components/ScheduleTaskMain.vue +345 -0
  34. package/components/ScheduleTastTicketMain.vue +182 -0
  35. package/components/StockCard.vue +191 -0
  36. package/components/SupplyManagementMain.vue +557 -0
  37. package/components/TableHygiene.vue +617 -0
  38. package/components/UnitMain.vue +451 -0
  39. package/components/VisitorManagement.vue +28 -15
  40. package/composables/useAccessManagement.ts +90 -0
  41. package/composables/useAreaPermission.ts +51 -0
  42. package/composables/useAreas.ts +99 -0
  43. package/composables/useAttendance.ts +89 -0
  44. package/composables/useAttendancePermission.ts +68 -0
  45. package/composables/useBuilding.ts +2 -2
  46. package/composables/useBuildingUnit.ts +2 -2
  47. package/composables/useCard.ts +2 -0
  48. package/composables/useCheckout.ts +61 -0
  49. package/composables/useCheckoutPermission.ts +80 -0
  50. package/composables/useCleaningPermission.ts +229 -0
  51. package/composables/useCleaningSchedulePermission.ts +58 -0
  52. package/composables/useCleaningSchedules.ts +233 -0
  53. package/composables/useCountry.ts +8 -0
  54. package/composables/useDashboardData.ts +2 -2
  55. package/composables/useFeedback.ts +1 -1
  56. package/composables/useLocation.ts +78 -0
  57. package/composables/useOnlineForm.ts +16 -9
  58. package/composables/usePeople.ts +87 -72
  59. package/composables/useQR.ts +29 -0
  60. package/composables/useScheduleTask.ts +89 -0
  61. package/composables/useScheduleTaskArea.ts +85 -0
  62. package/composables/useScheduleTaskPermission.ts +68 -0
  63. package/composables/useSiteEntryPassSettings.ts +4 -15
  64. package/composables/useStock.ts +45 -0
  65. package/composables/useSupply.ts +63 -0
  66. package/composables/useSupplyPermission.ts +92 -0
  67. package/composables/useUnitPermission.ts +51 -0
  68. package/composables/useUnits.ts +82 -0
  69. package/composables/useWebUsb.ts +389 -0
  70. package/composables/useWorkOrder.ts +1 -1
  71. package/nuxt.config.ts +3 -0
  72. package/package.json +4 -1
  73. package/types/area.d.ts +22 -0
  74. package/types/attendance.d.ts +38 -0
  75. package/types/checkout-item.d.ts +27 -0
  76. package/types/cleaner-schedule.d.ts +54 -0
  77. package/types/location.d.ts +42 -0
  78. package/types/schedule-task.d.ts +18 -0
  79. package/types/stock.d.ts +16 -0
  80. package/types/supply.d.ts +11 -0
  81. package/utils/acm-crypto.ts +30 -0
@@ -0,0 +1,617 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <!-- Top Actions -->
4
+ <v-col cols="12" class="mb-2" v-if="canCreate || $slots.actions">
5
+ <v-row no-gutters>
6
+ <slot name="actions">
7
+ <v-btn
8
+ v-if="canCreate"
9
+ class="text-none"
10
+ rounded="pill"
11
+ variant="tonal"
12
+ size="large"
13
+ @click="emits('create')"
14
+ >
15
+ {{ createLabel }}
16
+ </v-btn>
17
+ </slot>
18
+ </v-row>
19
+ </v-col>
20
+
21
+ <!-- List Card -->
22
+ <v-col cols="12">
23
+ <v-card
24
+ width="100%"
25
+ variant="outlined"
26
+ border="thin"
27
+ rounded="lg"
28
+ :loading="loading"
29
+ >
30
+ <!-- Toolbar -->
31
+ <v-toolbar
32
+ density="compact"
33
+ color="grey-lighten-4"
34
+ :extension-height="extensionHeight"
35
+ >
36
+ <template #prepend>
37
+ <v-btn fab icon density="comfortable" @click="emits('refresh')">
38
+ <v-icon>mdi-refresh</v-icon>
39
+ </v-btn>
40
+ <slot name="prepend-additional" />
41
+ </template>
42
+
43
+ <v-toolbar-title v-if="title" class="text-subtitle-1 font-weight-medium">
44
+ {{ title }}
45
+ </v-toolbar-title>
46
+
47
+ <template #append>
48
+ <v-row no-gutters justify="end" align="center">
49
+ <span class="mr-2 text-caption text-fontgray">
50
+ {{ pageRange }}
51
+ </span>
52
+ <local-pagination
53
+ v-model="internalPage"
54
+ :length="pages"
55
+ @update:value="emits('update:page', internalPage)"
56
+ />
57
+ </v-row>
58
+ </template>
59
+
60
+ <template v-if="$slots.extension" #extension>
61
+ <slot name="extension" />
62
+ </template>
63
+ </v-toolbar>
64
+
65
+ <v-divider />
66
+
67
+ <!-- List Items -->
68
+ <v-list
69
+ :style="`max-height: calc(100vh - (${offset}px)); overflow-y: auto;`"
70
+ >
71
+ <v-list-item
72
+ v-if="groupedItems.length === 0"
73
+ class="py-6 text-center text-medium-emphasis"
74
+ >
75
+ {{ noDataText }}
76
+ </v-list-item>
77
+
78
+ <template
79
+ v-for="(group, groupIndex) in groupedItems"
80
+ :key="`group-${groupIndex}`"
81
+ >
82
+ <div
83
+ v-if="group.set !== undefined"
84
+ class="d-flex align-center justify-space-between px-4 py-1 text-caption text-medium-emphasis"
85
+ style="min-height: 32px"
86
+ >
87
+ <span>Set {{ group.set }}</span>
88
+ <div class="d-flex align-center ga-2">
89
+ <span v-if="group.completedByName">
90
+ Completed by: {{ group.completedByName }}
91
+ </span>
92
+ <v-btn
93
+ v-if="group.attachments && group.attachments.length > 0"
94
+ size="x-small"
95
+ variant="tonal"
96
+ color="primary"
97
+ class="text-none"
98
+ prepend-icon="mdi-paperclip"
99
+ @click.stop="
100
+ openAttachmentDialog(group.set, group.attachments)
101
+ "
102
+ >
103
+ View Attachment ({{ group.attachments.length }})
104
+ </v-btn>
105
+ </div>
106
+ </div>
107
+
108
+ <v-list-item
109
+ v-for="item in group.items"
110
+ :key="item[itemValue]"
111
+ class="py-3"
112
+ :class="
113
+ isItemSelected(item, group.set)
114
+ ? ['bg-grey-lighten-4', 'rounded']
115
+ : []
116
+ "
117
+ >
118
+ <slot name="list-item" :item="item">
119
+ <v-list-item-title v-if="headers[0]">
120
+ {{ getItemValue(item, headers[0].value) }}
121
+ </v-list-item-title>
122
+
123
+ <v-list-item-subtitle
124
+ v-if="headers[1]"
125
+ class="mb-1 text-high-emphasis opacity-100"
126
+ >
127
+ {{ getItemValue(item, headers[1].value) || "N/A" }}
128
+ </v-list-item-subtitle>
129
+
130
+ <v-list-item-subtitle
131
+ v-if="headers[2]"
132
+ class="text-high-emphasis"
133
+ >
134
+ {{ getItemValue(item, headers[2].value) || "N/A" }}
135
+ </v-list-item-subtitle>
136
+ </slot>
137
+
138
+ <template v-slot:append>
139
+ <slot
140
+ name="list-item-append"
141
+ :item="item"
142
+ :isSelected="isItemSelected(item, group.set)"
143
+ >
144
+ <v-list-item-action class="d-flex flex-row ga-2">
145
+ <template v-if="canManageScheduleTasks">
146
+ <v-btn
147
+ icon="mdi-close"
148
+ size="small"
149
+ :variant="
150
+ activeActions[getKey(item, group.set)] === 'reject'
151
+ ? 'flat'
152
+ : 'text'
153
+ "
154
+ color="error"
155
+ @click.stop="
156
+ handleActionClick(item, group.set, 'reject')
157
+ "
158
+ />
159
+
160
+ <v-btn
161
+ icon="mdi-check"
162
+ size="small"
163
+ :variant="
164
+ activeActions[getKey(item, group.set)] === 'approve'
165
+ ? 'flat'
166
+ : 'text'
167
+ "
168
+ color="success"
169
+ @click.stop="
170
+ handleActionClick(item, group.set, 'approve')
171
+ "
172
+ />
173
+ </template>
174
+ </v-list-item-action>
175
+ </slot>
176
+ </template>
177
+ </v-list-item>
178
+ </template>
179
+ </v-list>
180
+
181
+ <slot name="footer" />
182
+ </v-card>
183
+ </v-col>
184
+ </v-row>
185
+
186
+ <!-- Attachment Preview Dialog -->
187
+ <v-dialog v-model="showAttachmentDialog" max-width="700" scrollable>
188
+ <v-card>
189
+ <v-card-title class="d-flex align-center pa-4">
190
+ <span class="text-h6 font-weight-bold">
191
+ Set {{ attachmentDialogSet }} — Attachments
192
+ </span>
193
+ <v-spacer />
194
+ <v-btn
195
+ icon="mdi-close"
196
+ variant="text"
197
+ size="small"
198
+ @click="showAttachmentDialog = false"
199
+ />
200
+ </v-card-title>
201
+
202
+ <v-divider />
203
+
204
+ <v-card-text class="pa-4">
205
+ <v-row>
206
+ <v-col
207
+ v-for="(id, index) in attachmentDialogIds"
208
+ :key="index"
209
+ cols="6"
210
+ sm="4"
211
+ >
212
+ <v-sheet
213
+ rounded="lg"
214
+ class="overflow-hidden"
215
+ style="aspect-ratio: 1"
216
+ >
217
+ <v-img
218
+ :src="getFileUrl(id)"
219
+ aspect-ratio="1"
220
+ cover
221
+ class="rounded-lg"
222
+ @click="openFullImage(getFileUrl(id))"
223
+ style="cursor: zoom-in"
224
+ >
225
+ <template v-slot:placeholder>
226
+ <v-row
227
+ class="fill-height ma-0"
228
+ align="center"
229
+ justify="center"
230
+ >
231
+ <v-progress-circular indeterminate color="grey-lighten-4" />
232
+ </v-row>
233
+ </template>
234
+ <template v-slot:error>
235
+ <v-row
236
+ class="fill-height ma-0"
237
+ align="center"
238
+ justify="center"
239
+ >
240
+ <v-icon icon="mdi-image-broken" size="40" color="grey" />
241
+ </v-row>
242
+ </template>
243
+ </v-img>
244
+ </v-sheet>
245
+ </v-col>
246
+ </v-row>
247
+ </v-card-text>
248
+ </v-card>
249
+ </v-dialog>
250
+
251
+ <!-- Full Image Lightbox -->
252
+ <v-dialog v-model="showLightbox" max-width="900">
253
+ <v-card>
254
+ <v-card-actions class="pa-2 justify-end">
255
+ <v-btn icon="mdi-close" variant="text" @click="showLightbox = false" />
256
+ </v-card-actions>
257
+ <v-card-text class="pa-2 pt-0">
258
+ <v-img :src="lightboxSrc" contain max-height="80vh" />
259
+ </v-card-text>
260
+ </v-card>
261
+ </v-dialog>
262
+ </template>
263
+
264
+ <script lang="ts" setup>
265
+ defineOptions({ inheritAttrs: false });
266
+
267
+ const props = defineProps({
268
+ title: {
269
+ type: String,
270
+ default: "",
271
+ },
272
+ noDataText: {
273
+ type: String,
274
+ default: "No data available",
275
+ },
276
+ headers: {
277
+ type: Array as PropType<Array<Record<string, any>>>,
278
+ required: true,
279
+ },
280
+ items: {
281
+ type: Array as PropType<Array<Record<string, any>>>,
282
+ default: () => [],
283
+ },
284
+ loading: {
285
+ type: Boolean,
286
+ default: false,
287
+ },
288
+ canCreate: {
289
+ type: Boolean,
290
+ default: false,
291
+ },
292
+ createLabel: {
293
+ type: String,
294
+ default: "Add",
295
+ },
296
+ itemValue: {
297
+ type: String,
298
+ default: "_id",
299
+ },
300
+ itemsPerPage: {
301
+ type: Number,
302
+ default: 20,
303
+ },
304
+ page: {
305
+ type: Number,
306
+ default: 1,
307
+ },
308
+ pages: {
309
+ type: Number,
310
+ default: 0,
311
+ },
312
+ pageRange: {
313
+ type: String,
314
+ default: "-- - -- of --",
315
+ },
316
+ showHeader: {
317
+ type: Boolean,
318
+ default: false,
319
+ },
320
+ canManageScheduleTasks: {
321
+ type: Boolean,
322
+ default: true,
323
+ },
324
+ canAddRemarks: {
325
+ type: Boolean,
326
+ default: true,
327
+ },
328
+ extensionHeight: {
329
+ type: Number,
330
+ default: 50,
331
+ },
332
+ offset: {
333
+ type: Number,
334
+ default: 200,
335
+ },
336
+ selected: {
337
+ type: Array as PropType<any[]>,
338
+ default: () => [],
339
+ },
340
+ });
341
+
342
+ const emits = defineEmits([
343
+ "create",
344
+ "refresh",
345
+ "update:page",
346
+ "row-click",
347
+ "update:selected",
348
+ "action-click",
349
+ "request-completion-dialog",
350
+ ]);
351
+
352
+ defineExpose({
353
+ revertSetApprovals,
354
+ });
355
+
356
+ const internalPage = ref(props.page);
357
+ const selected = shallowRef<any[]>(props.selected);
358
+ const activeActions = reactive<Record<string, "approve" | "reject">>({});
359
+ const persistedActions = reactive<Record<string, "approve" | "reject">>({});
360
+ const completedSets = ref<Set<number>>(new Set());
361
+ const itemOrderMap = new Map<string, number>();
362
+
363
+ const showAttachmentDialog = ref(false);
364
+ const attachmentDialogSet = ref<number | undefined>(undefined);
365
+ const attachmentDialogIds = ref<string[]>([]);
366
+ const showLightbox = ref(false);
367
+ const lightboxSrc = ref("");
368
+
369
+ const { getFileUrl } = useFile();
370
+
371
+ function openAttachmentDialog(setNumber: number, attachments: string[]) {
372
+ attachmentDialogSet.value = setNumber;
373
+ attachmentDialogIds.value = attachments;
374
+ showAttachmentDialog.value = true;
375
+ }
376
+
377
+ function openFullImage(src: string) {
378
+ lightboxSrc.value = src;
379
+ showLightbox.value = true;
380
+ }
381
+
382
+ const groupedItems = computed(() => {
383
+ return props.items.map((item: any) => {
384
+ const units = item.units || [item];
385
+
386
+ units.forEach((unit: any, index: number) => {
387
+ const key = getKey(unit, item.set);
388
+ if (!itemOrderMap.has(key)) {
389
+ itemOrderMap.set(key, itemOrderMap.size);
390
+ }
391
+ });
392
+
393
+ const sortedUnits = [...units].sort((a: any, b: any) => {
394
+ const keyA = getKey(a, item.set);
395
+ const keyB = getKey(b, item.set);
396
+ const orderA = itemOrderMap.get(keyA) ?? 0;
397
+ const orderB = itemOrderMap.get(keyB) ?? 0;
398
+ return orderA - orderB;
399
+ });
400
+
401
+ return {
402
+ set: item.set,
403
+ completedByName: item.completedByName ?? null,
404
+ attachments:
405
+ (item.attachment as string[] | undefined) ??
406
+ (item.attachments as string[] | undefined) ??
407
+ [],
408
+ items: sortedUnits,
409
+ };
410
+ });
411
+ });
412
+
413
+ const totalItemsCount = computed(() => {
414
+ return groupedItems.value.reduce((acc, group) => acc + group.items.length, 0);
415
+ });
416
+
417
+ const approvedItemsCount = computed(() => {
418
+ return Object.values(activeActions).filter((action) => action === "approve")
419
+ .length;
420
+ });
421
+
422
+ const hasRejectedItems = computed(() => {
423
+ return Object.values(activeActions).some((action) => action === "reject");
424
+ });
425
+
426
+ const allItemsApproved = computed(() => {
427
+ return (
428
+ totalItemsCount.value > 0 &&
429
+ approvedItemsCount.value === totalItemsCount.value &&
430
+ !hasRejectedItems.value
431
+ );
432
+ });
433
+
434
+ function isSetFullyApproved(setNumber: number): boolean {
435
+ const group = groupedItems.value.find((g) => g.set === setNumber);
436
+ if (!group) return false;
437
+
438
+ return group.items.every((item: any) => {
439
+ const key = getKey(item, setNumber);
440
+ return activeActions[key] === "approve";
441
+ });
442
+ }
443
+
444
+ function getNewApprovedItemsForSet(
445
+ setNumber: number,
446
+ ): Array<{ key: string; item: any; action: "approve" }> {
447
+ const group = groupedItems.value.find((g) => g.set === setNumber);
448
+ if (!group) return [];
449
+
450
+ const approvedItems: Array<{ key: string; item: any; action: "approve" }> =
451
+ [];
452
+
453
+ group.items.forEach((item: any) => {
454
+ const key = getKey(item, setNumber);
455
+ if (activeActions[key] === "approve" && !(key in persistedActions)) {
456
+ approvedItems.push({
457
+ key,
458
+ item: { ...item, set: setNumber },
459
+ action: "approve",
460
+ });
461
+ }
462
+ });
463
+
464
+ return approvedItems;
465
+ }
466
+
467
+ const lastApprovedKey = ref<string | null>(null);
468
+
469
+ watch(
470
+ () => props.page,
471
+ (val) => {
472
+ internalPage.value = val;
473
+
474
+ itemOrderMap.clear();
475
+ },
476
+ );
477
+
478
+ watch(
479
+ () => props.selected,
480
+ (val) => {
481
+ selected.value = val;
482
+ },
483
+ );
484
+
485
+ watch(selected, (val) => {
486
+ emits("update:selected", val);
487
+ });
488
+
489
+ watch(
490
+ () => props.items,
491
+ (items) => {
492
+ if (!items || !Array.isArray(items)) return;
493
+
494
+ Object.keys(persistedActions).forEach(
495
+ (key) => delete persistedActions[key],
496
+ );
497
+
498
+ items.forEach((group: any) => {
499
+ const set = group.set;
500
+ const units = group.units || [];
501
+
502
+ units.forEach((unit: any) => {
503
+ const key = getKey(unit, set);
504
+ if (unit.approve === true) {
505
+ activeActions[key] = "approve";
506
+ persistedActions[key] = "approve";
507
+ } else if (unit.reject === true) {
508
+ activeActions[key] = "reject";
509
+ persistedActions[key] = "reject";
510
+ }
511
+ });
512
+ });
513
+ },
514
+ { immediate: true },
515
+ );
516
+
517
+ function getKey(item: any, set?: number): string {
518
+ return `${item[props.itemValue]}_${set ?? ""}`;
519
+ }
520
+
521
+ function getItemValue(item: any, key: string): string {
522
+ if (!key) return "";
523
+ return key.split(".").reduce((obj, k) => obj?.[k], item) ?? "";
524
+ }
525
+
526
+ function isItemSelected(item: any, set?: number): boolean {
527
+ if (!Array.isArray(selected.value) || selected.value.length === 0) {
528
+ return false;
529
+ }
530
+
531
+ if (typeof selected.value[0] === "object" && "unit" in selected.value[0]) {
532
+ return selected.value.some(
533
+ (s: any) => s.unit === item[props.itemValue] && s.set === set,
534
+ );
535
+ }
536
+
537
+ return selected.value.includes(item[props.itemValue]);
538
+ }
539
+
540
+ function handleActionClick(
541
+ item: any,
542
+ set: number | undefined,
543
+ action: "approve" | "reject",
544
+ ): void {
545
+ const key = getKey(item, set);
546
+
547
+ const isPersisted = key in persistedActions;
548
+
549
+ if (activeActions[key] === action && !isPersisted) {
550
+ delete activeActions[key];
551
+ return;
552
+ }
553
+
554
+ activeActions[key] = action;
555
+
556
+ if (action === "reject") {
557
+ console.debug("TableHygiene: emitting action-click", {
558
+ item: { ...item, set },
559
+ action,
560
+ });
561
+ emits("action-click", { item: { ...item, set }, action });
562
+ }
563
+
564
+ if (action === "approve") {
565
+ lastApprovedKey.value = key;
566
+
567
+ if (
568
+ set !== undefined &&
569
+ isSetFullyApproved(set) &&
570
+ !completedSets.value.has(set)
571
+ ) {
572
+ const newApprovedItems = getNewApprovedItemsForSet(set);
573
+
574
+ if (newApprovedItems.length > 0) {
575
+ // Mark this set as completed
576
+ completedSets.value.add(set);
577
+
578
+ // Emit request to open completion dialog
579
+ emits("request-completion-dialog", {
580
+ setNumber: set,
581
+ approvedItems: newApprovedItems,
582
+ lastApprovedKey: lastApprovedKey.value,
583
+ });
584
+ return;
585
+ }
586
+ }
587
+ console.debug("TableHygiene: emitting action-click (approve immediate)", {
588
+ item: { ...item, set },
589
+ action,
590
+ });
591
+ emits("action-click", { item: { ...item, set }, action });
592
+ }
593
+ }
594
+
595
+ function revertSetApprovals(setNumber: number): void {
596
+ const group = groupedItems.value.find((g) => g.set === setNumber);
597
+ if (group) {
598
+ group.items.forEach((item: any) => {
599
+ const key = getKey(item, setNumber);
600
+
601
+ if (!(key in persistedActions)) {
602
+ delete activeActions[key];
603
+ }
604
+ });
605
+ }
606
+
607
+ completedSets.value.delete(setNumber);
608
+ }
609
+
610
+ watch(
611
+ () => props.items,
612
+ () => {
613
+ completedSets.value.clear();
614
+ },
615
+ { deep: true },
616
+ );
617
+ </script>