@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.
- package/CHANGELOG.md +12 -0
- package/components/AccessCardQrTagging.vue +314 -34
- package/components/AccessCardQrTaggingPrintQr.vue +75 -0
- package/components/{AddSupplyForm.vue → AddEqupmentForm.vue} +5 -5
- package/components/AreaChecklistHistoryLogs.vue +9 -0
- package/components/BuildingForm.vue +36 -5
- package/components/BuildingManagement/buildings.vue +18 -9
- package/components/BuildingManagement/units.vue +13 -115
- package/components/BuildingUnitFormAdd.vue +42 -33
- package/components/BuildingUnitFormEdit.vue +334 -139
- package/components/CleaningScheduleMain.vue +60 -13
- package/components/Dialog/DeleteConfirmation.vue +2 -2
- package/components/Dialog/UpdateMoreAction.vue +2 -2
- package/components/EntryPassInformation.vue +443 -0
- package/components/{CheckoutItemMain.vue → EquipmentItemMain.vue} +88 -85
- package/components/{SupplyManagement.vue → EquipmentManagement.vue} +3 -3
- package/components/Input/DateTimePicker.vue +17 -11
- package/components/Input/InputPhoneNumberV2.vue +8 -0
- package/components/ManageChecklistMain.vue +400 -36
- package/components/ScheduleAreaMain.vue +56 -0
- package/components/TableHygiene.vue +47 -430
- package/components/UnitPersonCard.vue +123 -0
- package/components/VehicleAddSelection.vue +2 -2
- package/components/VehicleForm.vue +78 -19
- package/components/VehicleManagement.vue +164 -40
- package/components/VisitorForm.vue +95 -20
- package/components/VisitorFormSelection.vue +13 -2
- package/components/VisitorManagement.vue +83 -55
- package/composables/useAccessManagement.ts +52 -0
- package/composables/useCleaningPermission.ts +7 -7
- package/composables/useDashboardData.ts +2 -2
- package/composables/{useSupply.ts → useEquipment.ts} +11 -11
- package/composables/{useCheckout.ts → useEquipmentItem.ts} +7 -7
- package/composables/{useCheckoutPermission.ts → useEquipmentItemPermission.ts} +13 -13
- package/composables/useEquipmentManagementPermission.ts +96 -0
- package/composables/{useSupplyPermission.ts → useEquipmentPermission.ts} +9 -9
- package/composables/usePeople.ts +4 -3
- package/composables/useVehicle.ts +35 -2
- package/composables/useVisitor.ts +3 -3
- package/composables/useWorkOrder.ts +25 -3
- package/package.json +3 -2
- package/types/building.d.ts +1 -1
- package/types/cleaner-schedule.d.ts +1 -0
- package/types/{checkout-item.d.ts → equipment-item.d.ts} +3 -3
- package/types/{supply.d.ts → equipment.d.ts} +2 -2
- package/types/html2pdf.d.ts +19 -0
- package/types/people.d.ts +5 -2
- package/types/site.d.ts +8 -0
- package/types/vehicle.d.ts +4 -3
- package/types/visitor.d.ts +2 -1
- package/.playground/app.vue +0 -41
- package/.playground/eslint.config.mjs +0 -6
- package/.playground/nuxt.config.ts +0 -22
- 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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
value:
|
|
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
|
-
//
|
|
225
|
-
if (setNumber !== undefined
|
|
226
|
-
|
|
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
|
|
283
|
-
item:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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 ||
|
|
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 ===
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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(
|
|
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>
|