@7365admin1/layer-common 1.10.6 → 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 (54) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/components/AccessCardQrTagging.vue +314 -34
  3. package/components/AccessCardQrTaggingPrintQr.vue +75 -0
  4. package/components/{AddSupplyForm.vue → AddEqupmentForm.vue} +5 -5
  5. package/components/AreaChecklistHistoryLogs.vue +9 -0
  6. package/components/BuildingForm.vue +36 -5
  7. package/components/BuildingManagement/buildings.vue +18 -9
  8. package/components/BuildingManagement/units.vue +13 -115
  9. package/components/BuildingUnitFormAdd.vue +42 -33
  10. package/components/BuildingUnitFormEdit.vue +334 -139
  11. package/components/CleaningScheduleMain.vue +60 -13
  12. package/components/Dialog/DeleteConfirmation.vue +2 -2
  13. package/components/Dialog/UpdateMoreAction.vue +2 -2
  14. package/components/EntryPassInformation.vue +443 -0
  15. package/components/{CheckoutItemMain.vue → EquipmentItemMain.vue} +88 -85
  16. package/components/{SupplyManagement.vue → EquipmentManagement.vue} +3 -3
  17. package/components/Input/DateTimePicker.vue +17 -11
  18. package/components/Input/InputPhoneNumberV2.vue +8 -0
  19. package/components/ManageChecklistMain.vue +400 -36
  20. package/components/ScheduleAreaMain.vue +56 -0
  21. package/components/TableHygiene.vue +47 -430
  22. package/components/UnitPersonCard.vue +123 -0
  23. package/components/VehicleAddSelection.vue +2 -2
  24. package/components/VehicleForm.vue +78 -19
  25. package/components/VehicleManagement.vue +164 -40
  26. package/components/VisitorForm.vue +95 -20
  27. package/components/VisitorFormSelection.vue +13 -2
  28. package/components/VisitorManagement.vue +83 -55
  29. package/composables/useAccessManagement.ts +52 -0
  30. package/composables/useCleaningPermission.ts +7 -7
  31. package/composables/useDashboardData.ts +2 -2
  32. package/composables/{useSupply.ts → useEquipment.ts} +11 -11
  33. package/composables/{useCheckout.ts → useEquipmentItem.ts} +7 -7
  34. package/composables/{useCheckoutPermission.ts → useEquipmentItemPermission.ts} +13 -13
  35. package/composables/useEquipmentManagementPermission.ts +96 -0
  36. package/composables/{useSupplyPermission.ts → useEquipmentPermission.ts} +9 -9
  37. package/composables/usePeople.ts +4 -3
  38. package/composables/useVehicle.ts +35 -2
  39. package/composables/useVisitor.ts +3 -3
  40. package/composables/useWorkOrder.ts +25 -3
  41. package/package.json +3 -2
  42. package/types/building.d.ts +1 -1
  43. package/types/cleaner-schedule.d.ts +1 -0
  44. package/types/{checkout-item.d.ts → equipment-item.d.ts} +3 -3
  45. package/types/{supply.d.ts → equipment.d.ts} +2 -2
  46. package/types/html2pdf.d.ts +19 -0
  47. package/types/people.d.ts +5 -2
  48. package/types/site.d.ts +8 -0
  49. package/types/vehicle.d.ts +4 -3
  50. package/types/visitor.d.ts +2 -1
  51. package/.playground/app.vue +0 -41
  52. package/.playground/eslint.config.mjs +0 -6
  53. package/.playground/nuxt.config.ts +0 -22
  54. package/.playground/pages/feedback.vue +0 -30
@@ -8,13 +8,18 @@
8
8
  Back
9
9
  </v-btn>
10
10
  </v-col>
11
+ <v-spacer />
12
+ <v-col v-if="isScheduleClosed" cols="auto" class="d-flex align-center">
13
+ <v-chip color="error" variant="tonal" class="text-capitalize">
14
+ <v-icon start>mdi-lock</v-icon>
15
+ Closed – View Only
16
+ </v-chip>
17
+ </v-col>
11
18
  </v-row>
12
19
  <TableHygiene
13
- ref="tableHygieneRef"
14
20
  :title="'Cleaner Checklist'"
15
21
  :headers="headers"
16
22
  :items="items"
17
- :selected="selectedItems"
18
23
  :item-value="'unit'"
19
24
  v-model:page="page"
20
25
  :pages="pages"
@@ -22,13 +27,112 @@
22
27
  :loading="loading"
23
28
  :no-data-text="`No checklist found`"
24
29
  :show-header="true"
25
- :can-manage-schedule-tasks="canManageScheduleTasks"
26
- :can-add-remarks="canAddRemarks"
27
30
  @refresh="getUnitCleanerChecklistRefresh"
28
- @update:selected="selectedItems = $event"
29
- @action-click="handleActionClick"
30
- @request-completion-dialog="openCompletionDialog"
31
- />
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>
32
136
  </v-col>
33
137
  </v-row>
34
138
 
@@ -92,12 +196,90 @@
92
196
  </v-card>
93
197
  </v-dialog>
94
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
+
95
274
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
96
275
  </template>
97
276
 
98
277
  <script setup lang="ts">
99
278
  import { useCleaningSchedulePermission } from "../composables/useCleaningSchedulePermission";
100
279
  import useCleaningSchedules from "../composables/useCleaningSchedules";
280
+ import useCustomerSite from "../composables/useCustomerSite";
281
+ import useFile from "../composables/useFile";
282
+ import useUtils from "../composables/useUtils";
101
283
 
102
284
  const props = defineProps({
103
285
  orgId: { type: String, default: "" },
@@ -113,6 +295,14 @@ const { back } = useUtils();
113
295
  const { canAddRemarks, canManageScheduleTasks } =
114
296
  useCleaningSchedulePermission();
115
297
 
298
+ const selectedScheduleStatus = useState<string>(
299
+ "selectedScheduleStatus",
300
+ () => ""
301
+ );
302
+ const isScheduleClosed = computed(
303
+ () => selectedScheduleStatus.value.toLowerCase() === "closed"
304
+ );
305
+
116
306
  const page = ref<number>(1);
117
307
  const pages = ref<number>(0);
118
308
  const pageRange = ref<string>("-- - -- of --");
@@ -125,10 +315,94 @@ const messageColor = ref<string>("");
125
315
  const categories = ref<
126
316
  Array<{ title: string; value: string; subtitle: string }>
127
317
  >([]);
128
- const tableHygieneRef = ref<any>(null);
129
-
130
318
  const headers = [{ title: "Name", value: "name" }];
131
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
+
132
406
  const { data: getCategoriesReq } = await useLazyAsyncData(
133
407
  "get-categories-for-work-order",
134
408
  () => getBySiteAsServiceProvider(props.site)
@@ -166,11 +440,12 @@ const {
166
440
  );
167
441
 
168
442
  watchEffect(() => {
169
- if (getCategoriesReq.value) {
170
- categories.value = getCategoriesReq.value.map((i: any) => ({
171
- title: i.nature.replace(/_/g, " "),
172
- subtitle: i.title,
173
- 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 || "",
174
449
  }));
175
450
  }
176
451
  });
@@ -201,6 +476,35 @@ watchEffect(() => {
201
476
  selectedItems.value = completed;
202
477
  });
203
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
+
204
508
  function showMessage(msg: string, color: string = "error"): void {
205
509
  message.value = msg;
206
510
  messageColor.value = color;
@@ -221,9 +525,9 @@ function closeModal(): void {
221
525
  showCompletionModal.value = false;
222
526
  resetModalData();
223
527
 
224
- // Notify TableHygiene to revert approval states
225
- if (setNumber !== undefined && tableHygieneRef.value) {
226
- tableHygieneRef.value.revertSetApprovals(setNumber);
528
+ // Revert optimistic approval state for this set
529
+ if (setNumber !== undefined) {
530
+ revertSetApprovals(setNumber);
227
531
  }
228
532
  }
229
533
 
@@ -241,6 +545,14 @@ function openCompletionDialog({
241
545
  approvedItems: Array<{ key: string; item: any; action: "approve" }>;
242
546
  lastApprovedKey: string | null;
243
547
  }): void {
548
+ if (isScheduleClosed.value) {
549
+ showMessage(
550
+ "This schedule is closed. No actions can be performed.",
551
+ "error"
552
+ );
553
+ return;
554
+ }
555
+
244
556
  // If user does not need to add remarks, perform approvals immediately
245
557
  if (!canAddRemarks.value) {
246
558
  // call update directly without opening modal
@@ -279,21 +591,73 @@ async function submitModal(): Promise<void> {
279
591
  closeModal();
280
592
  }
281
593
 
282
- async function handleActionClick(data: {
283
- item: TFlattenedUnitItem;
284
- action: "approve" | "reject";
285
- }): Promise<void> {
286
- const { item, action } = data;
594
+ async function handleItemActionClick(
595
+ item: TUnitChecklistItem,
596
+ set: number | undefined,
597
+ action: "approve" | "reject"
598
+ ): Promise<void> {
599
+ if (isScheduleClosed.value) {
600
+ showMessage(
601
+ "This schedule is closed. No actions can be performed.",
602
+ "error"
603
+ );
604
+ return;
605
+ }
287
606
 
288
- if (!item?.unit || item?.set === undefined) {
607
+ if (!item?.unit || set === undefined) {
289
608
  showMessage("Invalid unit or set", "error");
290
609
  return;
291
610
  }
292
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> {
293
658
  const isApproved = selectedItems.value.some(
294
- (s) => s.unit === item.unit && s.set === item.set
659
+ (s) => s.unit === item.unit && s.set === set
295
660
  );
296
-
297
661
  if (isApproved && action === "approve") {
298
662
  showMessage("Unit already approved", "info");
299
663
  return;
@@ -302,29 +666,29 @@ async function handleActionClick(data: {
302
666
  submitting.value = true;
303
667
 
304
668
  try {
305
- const checklistId = props.scheduleAreaId;
306
- const unitId = item.unit;
307
- const setNumber = item.set;
308
-
309
669
  const response = await updateUnitChecklist(
310
- checklistId,
311
- unitId,
312
- setNumber,
670
+ props.scheduleAreaId,
671
+ item.unit,
672
+ set,
313
673
  action
314
674
  );
315
-
316
675
  showMessage(
317
676
  response?.message ||
318
677
  `Unit ${action === "approve" ? "approved" : "rejected"} successfully`,
319
678
  "success"
320
679
  );
321
-
322
680
  await getUnitCleanerChecklistRefresh();
323
681
  } catch (error: any) {
324
682
  console.error("Error updating unit checklist:", error);
325
- showMessage(error?.message || "Failed to update checklist", "error");
683
+ showMessage(
684
+ error?.data?.message || error?.message || "Failed to update checklist",
685
+ "error"
686
+ );
326
687
  } finally {
327
688
  submitting.value = false;
689
+ if (action === "approve") {
690
+ stopLoadingAction(getKey(item, set));
691
+ }
328
692
  }
329
693
  }
330
694
 
@@ -68,6 +68,15 @@
68
68
  <v-spacer />
69
69
 
70
70
  <v-col cols="auto">
71
+ <v-chip
72
+ v-if="isScheduleClosed"
73
+ color="error"
74
+ variant="tonal"
75
+ class="text-capitalize mr-2"
76
+ >
77
+ <v-icon start>mdi-lock</v-icon>
78
+ Closed – View Only
79
+ </v-chip>
71
80
  <v-btn
72
81
  v-if="canViewHistory"
73
82
  class="text-none mr-2"
@@ -86,6 +95,7 @@
86
95
  aria-label="Generate"
87
96
  @click="_generateChecklist"
88
97
  :loading="submitting"
98
+ :disabled="isScheduleClosed"
89
99
  >
90
100
  Generate List
91
101
  </v-btn>
@@ -103,6 +113,15 @@
103
113
  {{ item.acceptedByName || item.acceptedBy || "Accepted" }}
104
114
  </span>
105
115
  </div>
116
+ <div v-else-if="isScheduleClosed">
117
+ <v-chip
118
+ size="small"
119
+ color="error"
120
+ variant="tonal"
121
+ class="text-capitalize"
122
+ >Closed</v-chip
123
+ >
124
+ </div>
106
125
  <div v-else>
107
126
  <v-btn
108
127
  v-if="canManageScheduleTasks"
@@ -176,6 +195,14 @@ const { formatDate, back } = useUtils();
176
195
  const { canGenerateChecklist, canViewHistory, canManageScheduleTasks } =
177
196
  useCleaningSchedulePermission();
178
197
 
198
+ const selectedScheduleStatus = useState<string>(
199
+ "selectedScheduleStatus",
200
+ () => ""
201
+ );
202
+ const isScheduleClosed = computed(
203
+ () => selectedScheduleStatus.value.toLowerCase() === "closed"
204
+ );
205
+
179
206
  const items = ref<Array<Record<string, any>>>([]);
180
207
 
181
208
  const statusFilter = ref<TScheduleAreaStatus>("All");
@@ -313,4 +340,33 @@ async function _generateChecklist() {
313
340
  submitting.value = false;
314
341
  }
315
342
  }
343
+
344
+ onMounted(() => {
345
+ try {
346
+ const { $socket } = useNuxtApp() as any;
347
+ if ($socket) {
348
+ $socket.emit("join:cleaning-area", props.scheduleAreaId);
349
+
350
+ $socket.on(
351
+ "area-checklist:generated",
352
+ (payload: { scheduleId: string; siteId: string }) => {
353
+ if (payload?.scheduleId !== props.scheduleAreaId) return;
354
+ if (typeof getScheduleAreasRefresh === "function") {
355
+ getScheduleAreasRefresh();
356
+ }
357
+ }
358
+ );
359
+ }
360
+ } catch (_) {}
361
+ });
362
+
363
+ onUnmounted(() => {
364
+ try {
365
+ const { $socket } = useNuxtApp() as any;
366
+ if ($socket) {
367
+ $socket.emit("leave:cleaning-area", props.scheduleAreaId);
368
+ $socket.off("area-checklist:generated");
369
+ }
370
+ } catch (_) {}
371
+ });
316
372
  </script>