@7365admin1/layer-common 1.10.7 → 1.10.8

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 (32) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/components/{AddSupplyForm.vue → AddEqupmentForm.vue} +5 -5
  3. package/components/BuildingManagement/units.vue +2 -2
  4. package/components/BuildingUnitFormAdd.vue +4 -4
  5. package/components/BuildingUnitFormEdit.vue +114 -68
  6. package/components/EntryPassInformation.vue +251 -23
  7. package/components/{CheckoutItemMain.vue → EquipmentItemMain.vue} +88 -85
  8. package/components/{SupplyManagement.vue → EquipmentManagement.vue} +3 -3
  9. package/components/Input/DateTimePicker.vue +17 -11
  10. package/components/ManageChecklistMain.vue +379 -41
  11. package/components/TableHygiene.vue +42 -452
  12. package/components/UnitPersonCard.vue +74 -14
  13. package/components/VisitorForm.vue +77 -21
  14. package/components/VisitorFormSelection.vue +13 -2
  15. package/components/VisitorManagement.vue +83 -55
  16. package/composables/useCleaningPermission.ts +7 -7
  17. package/composables/useDashboardData.ts +2 -2
  18. package/composables/{useSupply.ts → useEquipment.ts} +11 -11
  19. package/composables/{useCheckout.ts → useEquipmentItem.ts} +7 -7
  20. package/composables/{useCheckoutPermission.ts → useEquipmentItemPermission.ts} +13 -13
  21. package/composables/useEquipmentManagementPermission.ts +96 -0
  22. package/composables/{useSupplyPermission.ts → useEquipmentPermission.ts} +9 -9
  23. package/composables/useVehicle.ts +21 -2
  24. package/composables/useVisitor.ts +3 -3
  25. package/composables/useWorkOrder.ts +25 -3
  26. package/package.json +1 -1
  27. package/types/building.d.ts +1 -1
  28. package/types/{checkout-item.d.ts → equipment-item.d.ts} +3 -3
  29. package/types/{supply.d.ts → equipment.d.ts} +2 -2
  30. package/types/people.d.ts +3 -1
  31. package/types/vehicle.d.ts +2 -0
  32. package/types/visitor.d.ts +2 -1
@@ -17,11 +17,9 @@
17
17
  </v-col>
18
18
  </v-row>
19
19
  <TableHygiene
20
- ref="tableHygieneRef"
21
20
  :title="'Cleaner Checklist'"
22
21
  :headers="headers"
23
22
  :items="items"
24
- :selected="selectedItems"
25
23
  :item-value="'unit'"
26
24
  v-model:page="page"
27
25
  :pages="pages"
@@ -29,13 +27,112 @@
29
27
  :loading="loading"
30
28
  :no-data-text="`No checklist found`"
31
29
  :show-header="true"
32
- :can-manage-schedule-tasks="!isScheduleClosed && canManageScheduleTasks"
33
- :can-add-remarks="!isScheduleClosed && canAddRemarks"
34
30
  @refresh="getUnitCleanerChecklistRefresh"
35
- @update:selected="selectedItems = $event"
36
- @action-click="handleActionClick"
37
- @request-completion-dialog="openCompletionDialog"
38
- />
31
+ >
32
+ <!-- Status icon driven by local activeActions state -->
33
+ <template #item-prepend="{ item, group }">
34
+ <v-icon
35
+ size="20"
36
+ :color="
37
+ activeActions[getKey(item, group.set)] === 'approve'
38
+ ? 'success'
39
+ : activeActions[getKey(item, group.set)] === 'reject'
40
+ ? 'error'
41
+ : 'grey-lighten-2'
42
+ "
43
+ >
44
+ {{
45
+ activeActions[getKey(item, group.set)] === "approve"
46
+ ? "mdi-check-circle"
47
+ : activeActions[getKey(item, group.set)] === "reject"
48
+ ? "mdi-close-circle"
49
+ : "mdi-circle-outline"
50
+ }}
51
+ </v-icon>
52
+ </template>
53
+
54
+ <!-- Item name with strike-through when approved -->
55
+ <template #item-content="{ item, group }">
56
+ <v-row no-gutters>
57
+ <v-col cols="12">
58
+ <span
59
+ class="text-body-2 font-weight-medium"
60
+ :class="
61
+ activeActions[getKey(item, group.set)] === 'approve'
62
+ ? 'text-decoration-line-through text-medium-emphasis'
63
+ : ''
64
+ "
65
+ >
66
+ {{ item.name }}
67
+ </span>
68
+ </v-col>
69
+ <v-col v-if="item.timestamp" cols="12">
70
+ <v-row no-gutters align="center" class="mt-1">
71
+ <v-col
72
+ cols="auto"
73
+ class="d-flex align-center ga-1 text-caption text-medium-emphasis pa-0"
74
+ >
75
+ <v-icon size="11">mdi-clock-outline</v-icon>
76
+ {{ formatTimestamp(item.timestamp) }}
77
+ </v-col>
78
+ </v-row>
79
+ </v-col>
80
+ </v-row>
81
+ </template>
82
+
83
+ <!-- Approve / Reject action buttons -->
84
+ <template #item-append="{ item, group }">
85
+ <v-row
86
+ v-if="!isScheduleClosed && canManageScheduleTasks"
87
+ no-gutters
88
+ align="center"
89
+ >
90
+ <v-col cols="auto">
91
+ <v-btn
92
+ icon="mdi-close"
93
+ size="small"
94
+ :variant="
95
+ activeActions[getKey(item, group.set)] === 'reject'
96
+ ? 'flat'
97
+ : 'text'
98
+ "
99
+ color="error"
100
+ @click.stop="handleItemActionClick(item, group.set, 'reject')"
101
+ />
102
+ </v-col>
103
+ <v-col cols="auto">
104
+ <v-btn
105
+ icon="mdi-check"
106
+ size="small"
107
+ :variant="
108
+ activeActions[getKey(item, group.set)] === 'approve'
109
+ ? 'flat'
110
+ : 'text'
111
+ "
112
+ color="success"
113
+ :loading="!!loadingActions[getKey(item, group.set)]"
114
+ :disabled="!!loadingActions[getKey(item, group.set)]"
115
+ @click.stop="handleItemActionClick(item, group.set, 'approve')"
116
+ />
117
+ </v-col>
118
+ </v-row>
119
+ </template>
120
+ <template #group-header-append="{ group }">
121
+ <v-btn
122
+ v-if="group.attachments && group.attachments.length > 0"
123
+ size="x-small"
124
+ variant="tonal"
125
+ color="primary"
126
+ class="text-none"
127
+ prepend-icon="mdi-paperclip"
128
+ @click.stop="openAttachmentDialog(group.set, group.attachments)"
129
+ >
130
+ {{ group.attachments.length }} attachment{{
131
+ group.attachments.length > 1 ? "s" : ""
132
+ }}
133
+ </v-btn>
134
+ </template>
135
+ </TableHygiene>
39
136
  </v-col>
40
137
  </v-row>
41
138
 
@@ -99,12 +196,90 @@
99
196
  </v-card>
100
197
  </v-dialog>
101
198
 
199
+ <v-dialog v-model="showAttachmentDialog" max-width="700" scrollable>
200
+ <v-card>
201
+ <v-card-title class="d-flex align-center pa-4">
202
+ <span class="text-h6 font-weight-bold">
203
+ Set {{ attachmentDialogSetNum }} — Attachments
204
+ </span>
205
+ <v-spacer />
206
+ <v-btn
207
+ icon="mdi-close"
208
+ variant="text"
209
+ size="small"
210
+ @click="showAttachmentDialog = false"
211
+ />
212
+ </v-card-title>
213
+
214
+ <v-divider />
215
+
216
+ <v-card-text class="pa-4">
217
+ <v-row>
218
+ <v-col
219
+ v-for="(id, index) in attachmentDialogIds"
220
+ :key="index"
221
+ cols="6"
222
+ sm="4"
223
+ >
224
+ <v-sheet
225
+ rounded="lg"
226
+ class="overflow-hidden"
227
+ style="aspect-ratio: 1"
228
+ >
229
+ <v-img
230
+ :src="getFileUrl(id)"
231
+ aspect-ratio="1"
232
+ cover
233
+ class="rounded-lg"
234
+ @click="openFullImage(getFileUrl(id))"
235
+ style="cursor: zoom-in"
236
+ >
237
+ <template v-slot:placeholder>
238
+ <v-row
239
+ class="fill-height ma-0"
240
+ align="center"
241
+ justify="center"
242
+ >
243
+ <v-progress-circular indeterminate color="grey-lighten-4" />
244
+ </v-row>
245
+ </template>
246
+ <template v-slot:error>
247
+ <v-row
248
+ class="fill-height ma-0"
249
+ align="center"
250
+ justify="center"
251
+ >
252
+ <v-icon icon="mdi-image-broken" size="40" color="grey" />
253
+ </v-row>
254
+ </template>
255
+ </v-img>
256
+ </v-sheet>
257
+ </v-col>
258
+ </v-row>
259
+ </v-card-text>
260
+ </v-card>
261
+ </v-dialog>
262
+
263
+ <v-dialog v-model="showLightbox" max-width="900">
264
+ <v-card>
265
+ <v-card-actions class="pa-2 justify-end">
266
+ <v-btn icon="mdi-close" variant="text" @click="showLightbox = false" />
267
+ </v-card-actions>
268
+ <v-card-text class="pa-2 pt-0">
269
+ <v-img :src="lightboxSrc" contain max-height="80vh" />
270
+ </v-card-text>
271
+ </v-card>
272
+ </v-dialog>
273
+
102
274
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
103
275
  </template>
104
276
 
105
277
  <script setup lang="ts">
106
278
  import { useCleaningSchedulePermission } from "../composables/useCleaningSchedulePermission";
107
279
  import useCleaningSchedules from "../composables/useCleaningSchedules";
280
+ import useCustomerSite from "../composables/useCustomerSite";
281
+ import useFile from "../composables/useFile";
282
+ import useUtils from "../composables/useUtils";
108
283
 
109
284
  const props = defineProps({
110
285
  orgId: { type: String, default: "" },
@@ -120,8 +295,13 @@ const { back } = useUtils();
120
295
  const { canAddRemarks, canManageScheduleTasks } =
121
296
  useCleaningSchedulePermission();
122
297
 
123
- const selectedScheduleStatus = useState<string>('selectedScheduleStatus', () => '')
124
- const isScheduleClosed = computed(() => selectedScheduleStatus.value.toLowerCase() === 'closed')
298
+ const selectedScheduleStatus = useState<string>(
299
+ "selectedScheduleStatus",
300
+ () => ""
301
+ );
302
+ const isScheduleClosed = computed(
303
+ () => selectedScheduleStatus.value.toLowerCase() === "closed"
304
+ );
125
305
 
126
306
  const page = ref<number>(1);
127
307
  const pages = ref<number>(0);
@@ -135,10 +315,94 @@ const messageColor = ref<string>("");
135
315
  const categories = ref<
136
316
  Array<{ title: string; value: string; subtitle: string }>
137
317
  >([]);
138
- const tableHygieneRef = ref<any>(null);
139
-
140
318
  const headers = [{ title: "Name", value: "name" }];
141
319
 
320
+ // ── Action state (approve / reject per item key) ───────────────────────────
321
+ const activeActions = reactive<Record<string, "approve" | "reject">>({});
322
+ const persistedActions = reactive<Record<string, "approve" | "reject">>({});
323
+ const loadingActions = reactive<Record<string, boolean>>({});
324
+ const completedSets = ref<Set<number>>(new Set());
325
+ const lastApprovedKey = ref<string | null>(null);
326
+
327
+ function getKey(item: any, set?: number): string {
328
+ return `${item.unit}_${set ?? ""}`;
329
+ }
330
+
331
+ function isSetFullyApproved(setNumber: number): boolean {
332
+ const group = items.value.find((g: any) => g.set === setNumber);
333
+ if (!group) return false;
334
+ return (group.units || []).every(
335
+ (unit: any) => activeActions[getKey(unit, setNumber)] === "approve"
336
+ );
337
+ }
338
+
339
+ function getNewApprovedItemsForSet(
340
+ setNumber: number
341
+ ): Array<{ key: string; item: any; action: "approve" }> {
342
+ const group = items.value.find((g: any) => g.set === setNumber);
343
+ if (!group) return [];
344
+ return (group.units || [])
345
+ .filter((unit: any) => {
346
+ const key = getKey(unit, setNumber);
347
+ return activeActions[key] === "approve" && !(key in persistedActions);
348
+ })
349
+ .map((unit: any) => ({
350
+ key: getKey(unit, setNumber),
351
+ item: { ...unit, set: setNumber },
352
+ action: "approve" as const,
353
+ }));
354
+ }
355
+
356
+ function revertSetApprovals(setNumber: number): void {
357
+ const group = items.value.find((g: any) => g.set === setNumber);
358
+ if (group) {
359
+ (group.units || []).forEach((unit: any) => {
360
+ const key = getKey(unit, setNumber);
361
+ if (!(key in persistedActions)) {
362
+ delete activeActions[key];
363
+ }
364
+ });
365
+ }
366
+ completedSets.value.delete(setNumber);
367
+ }
368
+
369
+ function stopLoadingAction(key: string): void {
370
+ delete loadingActions[key];
371
+ }
372
+
373
+ // ── Attachment dialog ──────────────────────────────────────────────────────
374
+ const showAttachmentDialog = ref(false);
375
+ const attachmentDialogSetNum = ref<number | undefined>(undefined);
376
+ const attachmentDialogIds = ref<string[]>([]);
377
+ const showLightbox = ref(false);
378
+ const lightboxSrc = ref("");
379
+
380
+ const { getFileUrl } = useFile();
381
+
382
+ function openAttachmentDialog(setNumber: number, attachments: string[]) {
383
+ attachmentDialogSetNum.value = setNumber;
384
+ attachmentDialogIds.value = attachments;
385
+ showAttachmentDialog.value = true;
386
+ }
387
+
388
+ function openFullImage(src: string) {
389
+ lightboxSrc.value = src;
390
+ showLightbox.value = true;
391
+ }
392
+
393
+ function formatTimestamp(ts: string): string {
394
+ if (!ts) return "";
395
+ const date = new Date(ts);
396
+ return date.toLocaleString("en-SG", {
397
+ day: "2-digit",
398
+ month: "short",
399
+ year: "numeric",
400
+ hour: "2-digit",
401
+ minute: "2-digit",
402
+ hour12: true,
403
+ });
404
+ }
405
+
142
406
  const { data: getCategoriesReq } = await useLazyAsyncData(
143
407
  "get-categories-for-work-order",
144
408
  () => getBySiteAsServiceProvider(props.site)
@@ -176,11 +440,12 @@ const {
176
440
  );
177
441
 
178
442
  watchEffect(() => {
179
- if (getCategoriesReq.value) {
180
- categories.value = getCategoriesReq.value.map((i: any) => ({
181
- title: i.nature.replace(/_/g, " "),
182
- subtitle: i.title,
183
- value: i._id.org,
443
+ const data = getCategoriesReq.value as any;
444
+ if (data) {
445
+ categories.value = (data.categories ?? data).map((cat: any) => ({
446
+ title: cat.name,
447
+ value: cat.id,
448
+ subtitle: cat.description || "",
184
449
  }));
185
450
  }
186
451
  });
@@ -211,6 +476,35 @@ watchEffect(() => {
211
476
  selectedItems.value = completed;
212
477
  });
213
478
 
479
+ watch(
480
+ items,
481
+ (newItems) => {
482
+ if (!Array.isArray(newItems)) return;
483
+
484
+ Object.keys(persistedActions).forEach(
485
+ (key) => delete persistedActions[key]
486
+ );
487
+
488
+ newItems.forEach((group: any) => {
489
+ const set = group.set;
490
+ (group.units || []).forEach((unit: any) => {
491
+ const key = getKey(unit, set);
492
+ if ((unit as any).approve === true) {
493
+ activeActions[key] = "approve";
494
+ persistedActions[key] = "approve";
495
+ } else if ((unit as any).reject === true) {
496
+ activeActions[key] = "reject";
497
+ persistedActions[key] = "reject";
498
+ }
499
+ });
500
+ });
501
+
502
+ completedSets.value.clear();
503
+ Object.keys(loadingActions).forEach((k) => delete loadingActions[k]);
504
+ },
505
+ { immediate: true }
506
+ );
507
+
214
508
  function showMessage(msg: string, color: string = "error"): void {
215
509
  message.value = msg;
216
510
  messageColor.value = color;
@@ -231,9 +525,9 @@ function closeModal(): void {
231
525
  showCompletionModal.value = false;
232
526
  resetModalData();
233
527
 
234
- // Notify TableHygiene to revert approval states
235
- if (setNumber !== undefined && tableHygieneRef.value) {
236
- tableHygieneRef.value.revertSetApprovals(setNumber);
528
+ // Revert optimistic approval state for this set
529
+ if (setNumber !== undefined) {
530
+ revertSetApprovals(setNumber);
237
531
  }
238
532
  }
239
533
 
@@ -252,7 +546,10 @@ function openCompletionDialog({
252
546
  lastApprovedKey: string | null;
253
547
  }): void {
254
548
  if (isScheduleClosed.value) {
255
- showMessage("This schedule is closed. No actions can be performed.", "error");
549
+ showMessage(
550
+ "This schedule is closed. No actions can be performed.",
551
+ "error"
552
+ );
256
553
  return;
257
554
  }
258
555
 
@@ -294,26 +591,73 @@ async function submitModal(): Promise<void> {
294
591
  closeModal();
295
592
  }
296
593
 
297
- async function handleActionClick(data: {
298
- item: TFlattenedUnitItem;
299
- action: "approve" | "reject";
300
- }): Promise<void> {
594
+ async function handleItemActionClick(
595
+ item: TUnitChecklistItem,
596
+ set: number | undefined,
597
+ action: "approve" | "reject"
598
+ ): Promise<void> {
301
599
  if (isScheduleClosed.value) {
302
- showMessage("This schedule is closed. No actions can be performed.", "error");
600
+ showMessage(
601
+ "This schedule is closed. No actions can be performed.",
602
+ "error"
603
+ );
303
604
  return;
304
605
  }
305
606
 
306
- const { item, action } = data;
307
-
308
- if (!item?.unit || item?.set === undefined) {
607
+ if (!item?.unit || set === undefined) {
309
608
  showMessage("Invalid unit or set", "error");
310
609
  return;
311
610
  }
312
611
 
612
+ const key = getKey(item, set);
613
+ const isPersisted = key in persistedActions;
614
+
615
+ // Toggle off optimistic state if clicking the same un-persisted action
616
+ if (activeActions[key] === action && !isPersisted) {
617
+ delete activeActions[key];
618
+ return;
619
+ }
620
+
621
+ activeActions[key] = action;
622
+
623
+ if (action === "reject") {
624
+ await _callUpdateUnitChecklist(item, set, action);
625
+ return;
626
+ }
627
+
628
+ // Approve: check if this completes the entire set
629
+ lastApprovedKey.value = key;
630
+
631
+ if (isSetFullyApproved(set) && !completedSets.value.has(set)) {
632
+ const newApprovedItems = getNewApprovedItemsForSet(set);
633
+ if (newApprovedItems.length > 0) {
634
+ completedSets.value.add(set);
635
+ openCompletionDialog({
636
+ setNumber: set,
637
+ approvedItems: newApprovedItems,
638
+ lastApprovedKey: lastApprovedKey.value,
639
+ });
640
+ return;
641
+ }
642
+ }
643
+
644
+ // Single item approve (does not complete the set yet)
645
+ if (!isPersisted) {
646
+ delete activeActions[key];
647
+ loadingActions[key] = true;
648
+ }
649
+
650
+ await _callUpdateUnitChecklist(item, set, action);
651
+ }
652
+
653
+ async function _callUpdateUnitChecklist(
654
+ item: TUnitChecklistItem,
655
+ set: number,
656
+ action: "approve" | "reject"
657
+ ): Promise<void> {
313
658
  const isApproved = selectedItems.value.some(
314
- (s) => s.unit === item.unit && s.set === item.set
659
+ (s) => s.unit === item.unit && s.set === set
315
660
  );
316
-
317
661
  if (isApproved && action === "approve") {
318
662
  showMessage("Unit already approved", "info");
319
663
  return;
@@ -322,23 +666,17 @@ async function handleActionClick(data: {
322
666
  submitting.value = true;
323
667
 
324
668
  try {
325
- const checklistId = props.scheduleAreaId;
326
- const unitId = item.unit;
327
- const setNumber = item.set;
328
-
329
669
  const response = await updateUnitChecklist(
330
- checklistId,
331
- unitId,
332
- setNumber,
670
+ props.scheduleAreaId,
671
+ item.unit,
672
+ set,
333
673
  action
334
674
  );
335
-
336
675
  showMessage(
337
676
  response?.message ||
338
677
  `Unit ${action === "approve" ? "approved" : "rejected"} successfully`,
339
678
  "success"
340
679
  );
341
-
342
680
  await getUnitCleanerChecklistRefresh();
343
681
  } catch (error: any) {
344
682
  console.error("Error updating unit checklist:", error);
@@ -349,7 +687,7 @@ async function handleActionClick(data: {
349
687
  } finally {
350
688
  submitting.value = false;
351
689
  if (action === "approve") {
352
- tableHygieneRef.value?.stopLoadingAction(`${item.unit}_${item.set}`);
690
+ stopLoadingAction(getKey(item, set));
353
691
  }
354
692
  }
355
693
  }