@7365admin1/layer-common 1.10.0 → 1.10.2

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 (86) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/components/AcceptDialog.vue +44 -0
  3. package/components/AccessCard/AvailableStats.vue +55 -0
  4. package/components/AccessCardAddForm.vue +284 -19
  5. package/components/AccessCardAssignToUnitForm.vue +440 -0
  6. package/components/AccessManagement.vue +218 -85
  7. package/components/AddSupplyForm.vue +165 -0
  8. package/components/AreaChecklistHistoryLogs.vue +235 -0
  9. package/components/AreaChecklistHistoryMain.vue +176 -0
  10. package/components/AreaFormDialog.vue +266 -0
  11. package/components/AreaMain.vue +863 -0
  12. package/components/AttendanceCheckInOutDialog.vue +416 -0
  13. package/components/AttendanceDetailsDialog.vue +184 -0
  14. package/components/AttendanceMain.vue +155 -0
  15. package/components/AttendanceMapSearchDialog.vue +393 -0
  16. package/components/AttendanceSettingsDialog.vue +398 -0
  17. package/components/BuildingManagement/buildings.vue +5 -5
  18. package/components/BuildingManagement/units.vue +5 -5
  19. package/components/BulletinBoardManagement.vue +322 -0
  20. package/components/ChecklistItemRow.vue +54 -0
  21. package/components/CheckoutItemMain.vue +705 -0
  22. package/components/CleaningScheduleMain.vue +271 -0
  23. package/components/DocumentManagement.vue +4 -0
  24. package/components/EntryPass/QrTemplatePreview.vue +104 -0
  25. package/components/EntryPassMain.vue +252 -200
  26. package/components/HygieneUpdateMoreAction.vue +238 -0
  27. package/components/ManageChecklistMain.vue +384 -0
  28. package/components/MemberMain.vue +48 -20
  29. package/components/MyAttendanceMain.vue +224 -0
  30. package/components/OnlineFormsConfiguration.vue +9 -2
  31. package/components/PhotoUpload.vue +410 -0
  32. package/components/ScheduleAreaMain.vue +313 -0
  33. package/components/ScheduleTaskAreaFormDialog.vue +144 -0
  34. package/components/ScheduleTaskAreaUpdateMoreAction.vue +109 -0
  35. package/components/ScheduleTaskForm.vue +471 -0
  36. package/components/ScheduleTaskMain.vue +345 -0
  37. package/components/ScheduleTastTicketMain.vue +182 -0
  38. package/components/SignaturePad.vue +17 -5
  39. package/components/StockCard.vue +191 -0
  40. package/components/SupplyManagementMain.vue +557 -0
  41. package/components/TableHygiene.vue +617 -0
  42. package/components/UnitMain.vue +451 -0
  43. package/components/VisitorManagement.vue +28 -15
  44. package/composables/useAccessManagement.ts +163 -0
  45. package/composables/useAreaPermission.ts +51 -0
  46. package/composables/useAreas.ts +99 -0
  47. package/composables/useAttendance.ts +89 -0
  48. package/composables/useAttendancePermission.ts +68 -0
  49. package/composables/useBuilding.ts +2 -2
  50. package/composables/useBuildingUnit.ts +2 -2
  51. package/composables/useBulletin.ts +82 -0
  52. package/composables/useCard.ts +2 -0
  53. package/composables/useCheckout.ts +61 -0
  54. package/composables/useCheckoutPermission.ts +80 -0
  55. package/composables/useCleaningPermission.ts +229 -0
  56. package/composables/useCleaningSchedulePermission.ts +58 -0
  57. package/composables/useCleaningSchedules.ts +233 -0
  58. package/composables/useCountry.ts +8 -0
  59. package/composables/useDashboardData.ts +2 -2
  60. package/composables/useFeedback.ts +1 -1
  61. package/composables/useLocation.ts +78 -0
  62. package/composables/useOnlineForm.ts +16 -9
  63. package/composables/usePeople.ts +87 -72
  64. package/composables/useQR.ts +29 -0
  65. package/composables/useScheduleTask.ts +89 -0
  66. package/composables/useScheduleTaskArea.ts +85 -0
  67. package/composables/useScheduleTaskPermission.ts +68 -0
  68. package/composables/useSiteEntryPassSettings.ts +4 -15
  69. package/composables/useStock.ts +45 -0
  70. package/composables/useSupply.ts +63 -0
  71. package/composables/useSupplyPermission.ts +92 -0
  72. package/composables/useUnitPermission.ts +51 -0
  73. package/composables/useUnits.ts +82 -0
  74. package/composables/useWebUsb.ts +389 -0
  75. package/composables/useWorkOrder.ts +1 -1
  76. package/nuxt.config.ts +3 -0
  77. package/package.json +4 -1
  78. package/types/area.d.ts +22 -0
  79. package/types/attendance.d.ts +38 -0
  80. package/types/checkout-item.d.ts +27 -0
  81. package/types/cleaner-schedule.d.ts +54 -0
  82. package/types/location.d.ts +42 -0
  83. package/types/schedule-task.d.ts +18 -0
  84. package/types/stock.d.ts +16 -0
  85. package/types/supply.d.ts +11 -0
  86. package/utils/acm-crypto.ts +30 -0
@@ -0,0 +1,863 @@
1
+ <template>
2
+ <v-row no-gutters align="center" justify="center">
3
+ <v-col cols="12">
4
+ <TableMain
5
+ :title="tableTitle"
6
+ :items="items"
7
+ :headers="headers"
8
+ :loading="loading"
9
+ :show-header="true"
10
+ v-model:page="page"
11
+ :pages="pages"
12
+ :pageRange="pageRange"
13
+ :no-data-text="noDataText"
14
+ @refresh="getTaskRefresh"
15
+ :canCreate="canCreateArea"
16
+ :createLabel="createLabel"
17
+ @create="onCreateItem"
18
+ @row-click="handleRowClick"
19
+ >
20
+ <template #extension>
21
+ <v-row no-gutters class="w-100 d-flex flex-column">
22
+ <v-card
23
+ class="w-100 px-3 d-flex align-center ga-5 py-2"
24
+ flat
25
+ :height="60"
26
+ >
27
+ <v-select
28
+ v-model="areaTypeFilter"
29
+ :items="typeOptions"
30
+ density="compact"
31
+ hide-details
32
+ class="mx-2"
33
+ style="max-width: 160px"
34
+ item-title="title"
35
+ item-value="value"
36
+ label="Type"
37
+ />
38
+ </v-card>
39
+ </v-row>
40
+ </template>
41
+
42
+ <template #item.type="{ item }">
43
+ <span class="text-capitalize">{{ item.type }}</span>
44
+ </template>
45
+ <template #actions>
46
+ <v-row no-gutters align="center" class="w-100">
47
+ <v-col cols="auto">
48
+ <div class="d-flex align-center">
49
+ <!-- Primary action -->
50
+ <v-btn
51
+ v-if="canCreateArea"
52
+ class="text-none mr-3"
53
+ rounded="pill"
54
+ variant="tonal"
55
+ size="large"
56
+ @click="onCreateItem"
57
+ >
58
+ {{ createLabel }}
59
+ </v-btn>
60
+
61
+ <!-- Secondary actions as icon buttons with tooltips -->
62
+ <v-tooltip v-if="canCreateArea" location="top">
63
+ <template #activator="{ props: t }">
64
+ <v-btn
65
+ v-bind="t"
66
+ class="ma-1"
67
+ variant="tonal"
68
+ size="large"
69
+ rounded="pill"
70
+ href="/files/area-sample-data.xlsx"
71
+ target="_blank"
72
+ download
73
+ >
74
+ <v-icon>mdi-download</v-icon>
75
+ </v-btn>
76
+ </template>
77
+ <span>Download Template</span>
78
+ </v-tooltip>
79
+
80
+ <v-tooltip v-if="canImportArea" location="top">
81
+ <template #activator="{ props: t }">
82
+ <v-btn
83
+ v-bind="t"
84
+ class="ma-1"
85
+ variant="tonal"
86
+ size="large"
87
+ rounded="pill"
88
+ @click="onImportData"
89
+ >
90
+ <v-icon>mdi-upload</v-icon>
91
+ </v-btn>
92
+ </template>
93
+ <span>Import Data</span>
94
+ </v-tooltip>
95
+
96
+ <v-tooltip v-if="canViewAreas" location="top">
97
+ <template #activator="{ props: t }">
98
+ <v-btn
99
+ v-bind="t"
100
+ class="ma-1"
101
+ variant="tonal"
102
+ size="large"
103
+ rounded="pill"
104
+ @click="handleDownloadExcel"
105
+ :loading="isDownloading"
106
+ >
107
+ <v-icon>mdi-file-excel</v-icon>
108
+ </v-btn>
109
+ </template>
110
+ <span>Download Excel</span>
111
+ </v-tooltip>
112
+ </div>
113
+ </v-col>
114
+
115
+ <v-spacer />
116
+
117
+ <v-col cols="auto">
118
+ <v-text-field
119
+ v-model="searchInput"
120
+ density="compact"
121
+ placeholder="Search"
122
+ clearable
123
+ width="300"
124
+ append-inner-icon="mdi-magnify"
125
+ hide-details
126
+ />
127
+ </v-col>
128
+ </v-row>
129
+ </template>
130
+
131
+ <template #type="{ item }">
132
+ <span class="text-capitalize">{{ item.type }}</span>
133
+ </template>
134
+ </TableMain>
135
+ </v-col>
136
+ </v-row>
137
+
138
+ <AreaFormDialog
139
+ v-model="dialogShowForm"
140
+ v-model:name="itemName"
141
+ v-model:type="itemType"
142
+ v-model:set="itemSet"
143
+ v-model:units="itemUnits"
144
+ :mode="dialogMode"
145
+ :areaData="editingItem"
146
+ :label="formLabel"
147
+ :entityType="entityTypeLabel"
148
+ :showUnits="true"
149
+ :site="props.site"
150
+ @saved="_createArea"
151
+ />
152
+
153
+ <v-dialog v-model="dialogShowMoreActions" width="400" persistent>
154
+ <HygieneUpdateMoreAction
155
+ :title="selectedItem?.name || `${entityTypeLabel} Actions`"
156
+ :canUpdate="canUpdateArea"
157
+ :canDelete="canDeleteArea"
158
+ :showQrButton="isToiletLocation"
159
+ @close="dialogShowMoreActions = false"
160
+ @edit="onEditFromMoreAction"
161
+ @delete="onDeleteFromMoreAction"
162
+ @manage="onManageFromMoreAction"
163
+ @showqr="onShowQRAction"
164
+ >
165
+ <template #content>
166
+ <v-row no-gutters>
167
+ <v-col v-if="selectedItem" cols="12" class="mb-2">
168
+ <v-row no-gutters class="mb-2">
169
+ <v-col cols="5" class="text-subtitle-2 font-weight-bold">
170
+ Name:
171
+ </v-col>
172
+ <v-col cols="7" class="text-subtitle-2">
173
+ {{ selectedItem.name || "N/A" }}
174
+ </v-col>
175
+ </v-row>
176
+
177
+ <v-row no-gutters class="mb-2">
178
+ <v-col cols="5" class="text-subtitle-2 font-weight-bold">
179
+ Type:
180
+ </v-col>
181
+ <v-col cols="7" class="text-subtitle-2 text-capitalize">
182
+ {{ selectedItem.type || "N/A" }}
183
+ </v-col>
184
+ </v-row>
185
+
186
+ <v-row no-gutters class="mb-2">
187
+ <v-col cols="5" class="text-subtitle-2 font-weight-bold">
188
+ No. of Visits:
189
+ </v-col>
190
+ <v-col cols="7" class="text-subtitle-2">
191
+ {{ selectedItem.set || "N/A" }}
192
+ </v-col>
193
+ </v-row>
194
+
195
+ <v-row
196
+ v-if="selectedItem.units && selectedItem.units.length > 0"
197
+ no-gutters
198
+ class="mb-2"
199
+ >
200
+ <v-col cols="5" class="text-subtitle-2 font-weight-bold">
201
+ Units:
202
+ </v-col>
203
+ <v-col cols="7" class="text-subtitle-2">
204
+ <div
205
+ v-for="(unit, idx) in selectedItem.units"
206
+ :key="idx"
207
+ class="mb-1"
208
+ >
209
+ {{ unit.name || unit }}
210
+ </div>
211
+ </v-col>
212
+ </v-row>
213
+ </v-col>
214
+
215
+ <v-col v-if="message" cols="12" class="my-2">
216
+ <span class="text-subtitle-2 text-error">{{ message }}</span>
217
+ </v-col>
218
+ </v-row>
219
+ </template>
220
+ </HygieneUpdateMoreAction>
221
+ </v-dialog>
222
+
223
+ <v-dialog v-model="dialogDeleteItem" max-width="450">
224
+ <v-card>
225
+ <v-toolbar>
226
+ <v-row no-gutters class="fill-height px-6" align="center">
227
+ <span class="font-weight-bold text-h6 text-capitalize">
228
+ Delete {{ selectedItem?.name || entityTypeLabel }}
229
+ </span>
230
+ </v-row>
231
+ </v-toolbar>
232
+ <v-card-text>
233
+ <span class="text-subtitle-2"
234
+ >Are you sure you want to delete this {{ entityTypeLabel }}?</span
235
+ >
236
+ </v-card-text>
237
+
238
+ <v-toolbar class="pa-0" density="compact">
239
+ <v-row no-gutters>
240
+ <v-col cols="6" class="pa-0">
241
+ <v-btn
242
+ block
243
+ variant="text"
244
+ class="text-none"
245
+ size="large"
246
+ @click="dialogDeleteItem = false"
247
+ height="48"
248
+ >
249
+ Cancel
250
+ </v-btn>
251
+ </v-col>
252
+
253
+ <v-col cols="6" class="pa-0">
254
+ <v-btn
255
+ block
256
+ variant="flat"
257
+ color="black"
258
+ class="text-none font-weight-bold rounded-0"
259
+ height="48"
260
+ :loading="submitting"
261
+ @click="_deleteItem"
262
+ >
263
+ Delete
264
+ </v-btn>
265
+ </v-col>
266
+ </v-row>
267
+ </v-toolbar>
268
+ </v-card>
269
+ </v-dialog>
270
+
271
+ <v-dialog v-if="isToiletLocation" v-model="dialogShowQR" max-width="500">
272
+ <v-card>
273
+ <v-toolbar>
274
+ <v-row no-gutters class="fill-height px-6" align="center">
275
+ <span class="font-weight-bold text-h6 text-capitalize">
276
+ QR Code for {{ selectedItem?.name || "Toilet Location" }}
277
+ </span>
278
+ </v-row>
279
+ </v-toolbar>
280
+ <v-card-text class="d-flex justify-center pa-6">
281
+ <v-row no-gutters class="d-flex justify-center">
282
+ <v-col cols="12" class="text-center">
283
+ <div v-if="qrLoading" class="d-flex flex-column align-center">
284
+ <v-progress-circular indeterminate color="primary" size="64" />
285
+ <p class="text-subtitle-2 mt-4">Generating QR Code...</p>
286
+ </div>
287
+ <div v-else-if="qrError" class="d-flex flex-column align-center">
288
+ <v-icon size="80" color="error" class="mb-4">
289
+ mdi-alert-circle
290
+ </v-icon>
291
+ <p class="text-subtitle-2 text-error">{{ qrError }}</p>
292
+ </div>
293
+ <div v-else-if="qrPath" class="d-flex flex-column align-center">
294
+ <div>
295
+ <QrcodeVue
296
+ :value="qrPath"
297
+ :level="'M'"
298
+ :render-as="'canvas'"
299
+ :size="300"
300
+ style="display: block"
301
+ />
302
+ </div>
303
+ <p class="text-caption mt-3 text-grey">
304
+ Scan this QR code to access the cleaning schedule
305
+ </p>
306
+ </div>
307
+ </v-col>
308
+ </v-row>
309
+ </v-card-text>
310
+
311
+ <v-toolbar class="pa-0" density="compact">
312
+ <v-row no-gutters>
313
+ <v-col cols="6" class="pa-0">
314
+ <v-btn
315
+ block
316
+ variant="text"
317
+ class="text-none"
318
+ size="large"
319
+ @click="dialogShowQR = false"
320
+ height="48"
321
+ >
322
+ Cancel
323
+ </v-btn>
324
+ </v-col>
325
+
326
+ <v-col cols="6" class="pa-0">
327
+ <v-btn
328
+ block
329
+ variant="flat"
330
+ color="black"
331
+ class="text-none font-weight-bold rounded-0"
332
+ height="48"
333
+ :loading="qrLoading"
334
+ @click="onDownloadQrCode"
335
+ >
336
+ Download
337
+ </v-btn>
338
+ </v-col>
339
+ </v-row>
340
+ </v-toolbar>
341
+ </v-card>
342
+ </v-dialog>
343
+
344
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
345
+
346
+ <input
347
+ ref="fileInput"
348
+ type="file"
349
+ accept=".csv,.xlsx,.xls"
350
+ style="display: none"
351
+ @change="onFileSelected"
352
+ />
353
+ </template>
354
+
355
+ <script setup lang="ts">
356
+ import QrcodeVue from "qrcode.vue";
357
+ import useAreas from "../composables/useAreas";
358
+ import useUnits from "../composables/useUnits";
359
+ import { useAreaPermission } from "../composables/useAreaPermission";
360
+ import useSiteSettings from "../composables/useSiteSettings";
361
+
362
+ const props = defineProps({
363
+ orgId: { type: String, default: "" },
364
+ site: { type: String, default: "" },
365
+ type: { type: String, default: "" },
366
+ });
367
+
368
+ const isCleanerArea = computed(() => {
369
+ return props.type === "cleaner";
370
+ });
371
+
372
+ const isToiletArea = computed(() => {
373
+ return props.type === "toilet";
374
+ });
375
+
376
+ const isToiletLocation = computed(() => {
377
+ return selectedItem.value?.type?.toLowerCase() === "toilet";
378
+ });
379
+
380
+ const isScheduledArea = computed(() => {
381
+ return props.type === "schedule-task";
382
+ });
383
+
384
+ const areaTypeFilter = ref<TAreaType>("all");
385
+ const typeOptions = [
386
+ { title: "All", value: "all" },
387
+ { title: "Common", value: "common" },
388
+ { title: "Toilet", value: "toilet" },
389
+ ];
390
+
391
+ const entityTypeLabel = computed(() => {
392
+ if (isToiletArea.value) return "toilet location";
393
+ return "area";
394
+ });
395
+
396
+ const formLabel = computed(() => {
397
+ if (isToiletArea.value) return "Toilet Location Name";
398
+ return "Area Name";
399
+ });
400
+
401
+ const tableTitle = computed(() => {
402
+ if (isToiletArea.value) return `${siteName.value} Toilet Location`;
403
+ return `${siteName.value} Areas`;
404
+ });
405
+
406
+ const createLabel = computed(() => {
407
+ if (isToiletArea.value) return "Add Toilet Location";
408
+ return "Add Area";
409
+ });
410
+
411
+ const noDataText = computed(() => {
412
+ if (isToiletArea.value)
413
+ return `No toilet location found for ${siteName.value}.`;
414
+ return `No areas found for ${siteName.value}.`;
415
+ });
416
+
417
+ const items = ref<Array<Record<string, any>>>([]);
418
+ const siteName = ref<string>("");
419
+ const submitting = ref(false);
420
+ const isDownloading = ref(false);
421
+
422
+ const headers = [
423
+ { title: "AREA", value: "name" },
424
+ { title: "TYPE", value: "type" },
425
+ { title: "SETS", value: "set" },
426
+ ];
427
+
428
+ const { debounce } = useUtils();
429
+
430
+ const {
431
+ getAreas,
432
+ createArea,
433
+ updateArea,
434
+ deleteArea,
435
+ uploadAreas,
436
+ getAreaById,
437
+ exportAreas,
438
+ } = useAreas();
439
+
440
+ const { getSiteById } = useSiteSettings();
441
+
442
+ const { getUnits } = useUnits();
443
+
444
+ const searchInput = ref("");
445
+ const page = ref(1);
446
+ const pages = ref(0);
447
+ const pageRange = ref("-- - -- of --");
448
+
449
+ const {
450
+ data: getTaskReq,
451
+ refresh: getTaskRefresh,
452
+ pending: loading,
453
+ } = await useLazyAsyncData(
454
+ `get-all-task`,
455
+ () =>
456
+ getAreas({
457
+ page: page.value,
458
+ site: props.site,
459
+ search: searchInput.value,
460
+ type: areaTypeFilter.value === "all" ? undefined : areaTypeFilter.value,
461
+ }),
462
+ {
463
+ watch: [page, () => props.site, () => props.type, areaTypeFilter],
464
+ }
465
+ );
466
+
467
+ const { data: getUnitsReq } = await useLazyAsyncData(
468
+ `get-all-units-${props.site}`,
469
+ () =>
470
+ getUnits({
471
+ site: props.site,
472
+ limit: 100,
473
+ }),
474
+ {
475
+ watch: [() => props.site],
476
+ }
477
+ );
478
+
479
+ const allUnits = computed(() => getUnitsReq.value?.items || []);
480
+
481
+ watchEffect(() => {
482
+ if (getTaskReq.value) {
483
+ items.value = getTaskReq.value.items;
484
+ pages.value = getTaskReq.value.pages;
485
+ pageRange.value = getTaskReq.value.pageRange;
486
+ }
487
+ });
488
+
489
+ const debounceSearch = debounce(getTaskRefresh, 500);
490
+
491
+ watch(
492
+ [searchInput],
493
+ () => {
494
+ debounceSearch();
495
+ },
496
+ { immediate: false, deep: true }
497
+ );
498
+
499
+ const selectedItem = ref<Record<string, any> | null>(null);
500
+ const message = ref("");
501
+ const messageSnackbar = ref(false);
502
+ const messageColor = ref("");
503
+ const fileInput = ref<HTMLInputElement | null>(null);
504
+
505
+ const dialogShowMoreActions = ref(false);
506
+ const dialogShowForm = ref(false);
507
+ const dialogDeleteItem = ref(false);
508
+ const dialogShowQR = ref(false);
509
+ const dialogMode = ref<"add" | "edit">("add");
510
+
511
+ const editingItem = ref<Record<string, any>>({});
512
+ const itemName = ref("");
513
+ const itemType = ref("");
514
+ const itemSet = ref(1);
515
+ const itemUnits = ref<string[]>([]);
516
+
517
+ const qrPath = ref("");
518
+ const qrLoading = ref(false);
519
+ const qrError = ref("");
520
+
521
+ watch(
522
+ () => props.site,
523
+ async (val) => {
524
+ if (!val) {
525
+ siteName.value = "";
526
+ return;
527
+ }
528
+
529
+ try {
530
+ const siteResp = await getSiteById(val);
531
+ siteName.value = (siteResp && (siteResp.name || siteResp.title)) || val;
532
+ } catch (err) {
533
+ siteName.value = val;
534
+ }
535
+ },
536
+ { immediate: true }
537
+ );
538
+
539
+ function showMessage(msg: string, color: string = "error") {
540
+ message.value = msg;
541
+ messageColor.value = color;
542
+ messageSnackbar.value = true;
543
+ }
544
+
545
+ const {
546
+ canCreateArea,
547
+ canUpdateArea,
548
+ canDeleteArea,
549
+ canImportArea,
550
+ canViewAreas,
551
+ } = useAreaPermission();
552
+
553
+ async function handleDownloadExcel() {
554
+ try {
555
+ isDownloading.value = true;
556
+
557
+ if (!props.site || !/^[0-9a-fA-F]{24}$/.test(props.site)) {
558
+ showMessage("Invalid site id for export.", "error");
559
+ return;
560
+ }
561
+
562
+ const excelBuffer = await exportAreas(props.site);
563
+
564
+ if (!excelBuffer) {
565
+ showMessage("Failed to download areas.", "error");
566
+ return;
567
+ }
568
+
569
+ const blob = new Blob([excelBuffer], {
570
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
571
+ });
572
+ const url = URL.createObjectURL(blob);
573
+ const a = document.createElement("a");
574
+
575
+ const date = new Date();
576
+ const formattedDate = `${String(date.getMonth() + 1).padStart(
577
+ 2,
578
+ "0"
579
+ )}-${String(date.getDate()).padStart(2, "0")}-${date.getFullYear()}`;
580
+
581
+ a.href = url;
582
+ a.download = `areas-${siteName.value || "export"}-${formattedDate}.xlsx`;
583
+ document.body.appendChild(a);
584
+ a.click();
585
+ document.body.removeChild(a);
586
+ URL.revokeObjectURL(url);
587
+
588
+ showMessage("Areas exported successfully.", "success");
589
+ } catch (error: any) {
590
+ showMessage(error?.message || "Failed to download areas.", "error");
591
+ } finally {
592
+ isDownloading.value = false;
593
+ }
594
+ }
595
+
596
+ function onImportData() {
597
+ fileInput.value?.click();
598
+ }
599
+
600
+ async function onFileSelected(event: Event) {
601
+ const target = event.target as HTMLInputElement;
602
+ const file = target.files?.[0];
603
+
604
+ if (!file) return;
605
+
606
+ try {
607
+ submitting.value = true;
608
+ const response = await uploadAreas(file, props.site);
609
+ showMessage(response?.message, "success");
610
+
611
+ await getTaskRefresh();
612
+ } catch (err: any) {
613
+ showMessage(err?.data?.message, "error");
614
+ } finally {
615
+ submitting.value = false;
616
+ if (target) target.value = "";
617
+ }
618
+ }
619
+
620
+ function onCreateItem() {
621
+ dialogMode.value = "add";
622
+ editingItem.value = {};
623
+ itemName.value = "";
624
+ itemType.value = "";
625
+ itemSet.value = 1;
626
+ itemUnits.value = [];
627
+ dialogShowForm.value = true;
628
+ }
629
+
630
+ const units = computed(() => {
631
+ return itemUnits.value.map((unitId) => {
632
+ const unit = allUnits.value.find((u: any) => (u._id || u.id) === unitId);
633
+
634
+ return {
635
+ unit: unit?._id || unit?.id || "",
636
+ name: unit?.name || "",
637
+ };
638
+ });
639
+ });
640
+
641
+ async function _createArea() {
642
+ submitting.value = true;
643
+ try {
644
+ const payload: TAreaCreate = {
645
+ name: itemName.value,
646
+ set: itemSet.value,
647
+ type: (itemType.value || "common").toLowerCase(),
648
+ units: units.value,
649
+ };
650
+
651
+ let response;
652
+
653
+ if (dialogMode.value === "edit" && editingItem.value) {
654
+ const id =
655
+ (editingItem.value as any)._id || (editingItem.value as any).id;
656
+ if (!id)
657
+ throw new Error(`Invalid ${entityTypeLabel.value} id for update`);
658
+ response = await updateArea(id, payload);
659
+ showMessage(response?.message, "success");
660
+ } else {
661
+ response = await createArea(payload, props.site);
662
+ showMessage(response?.message, "success");
663
+ await getTaskRefresh();
664
+ }
665
+
666
+ showMessage(response?.message, "success");
667
+ dialogShowForm.value = false;
668
+ editingItem.value = {};
669
+ itemType.value = "";
670
+ itemUnits.value = [];
671
+
672
+ await getTaskRefresh();
673
+ } catch (error: any) {
674
+ console.error(error);
675
+ showMessage(error?.message, "error");
676
+ } finally {
677
+ submitting.value = false;
678
+ }
679
+ }
680
+
681
+ function editItem(item: Record<string, any> | null) {
682
+ if (!item) return;
683
+ dialogMode.value = "edit";
684
+ editingItem.value = item;
685
+ itemName.value = item.name || "";
686
+ itemType.value = item.type || "common";
687
+ itemSet.value =
688
+ typeof item.set === "string" ? parseInt(item.set, 10) : item.set || 1;
689
+
690
+ if (item.units && Array.isArray(item.units)) {
691
+ itemUnits.value = item.units.map((u: any) => {
692
+ if (u.unit) return u.unit;
693
+
694
+ return u._id || u.id;
695
+ });
696
+ } else {
697
+ itemUnits.value = [];
698
+ }
699
+ dialogShowForm.value = true;
700
+ }
701
+
702
+ async function handleRowClick(data: any) {
703
+ const item = data?.item;
704
+ const areaId = item?._id || item?.id;
705
+
706
+ if (!areaId) {
707
+ selectedItem.value = item;
708
+ dialogShowMoreActions.value = true;
709
+ message.value = "";
710
+ return;
711
+ }
712
+
713
+ try {
714
+ submitting.value = true;
715
+ const areaData = await getAreaById(areaId);
716
+ selectedItem.value = areaData;
717
+ dialogShowMoreActions.value = true;
718
+ message.value = "";
719
+ } catch (error: any) {
720
+ selectedItem.value = item;
721
+ dialogShowMoreActions.value = true;
722
+ message.value = error?.message || "Failed to load area details";
723
+ } finally {
724
+ submitting.value = false;
725
+ }
726
+ }
727
+
728
+ const onEditFromMoreAction = async () => {
729
+ dialogShowMoreActions.value = false;
730
+
731
+ const areaId =
732
+ (selectedItem.value as any)?._id || (selectedItem.value as any)?.id;
733
+ if (areaId) {
734
+ try {
735
+ loading.value = true;
736
+ const areaData = await getAreaById(areaId);
737
+ editItem(areaData);
738
+ } catch (error: any) {
739
+ showMessage(error?.message || "Failed to fetch area data", "error");
740
+ } finally {
741
+ loading.value = false;
742
+ }
743
+ } else {
744
+ editItem(selectedItem.value);
745
+ }
746
+ };
747
+
748
+ const onManageFromMoreAction = () => {
749
+ dialogShowMoreActions.value = false;
750
+ const id =
751
+ (selectedItem.value as any)?._id || (selectedItem.value as any)?.id;
752
+ if (id) {
753
+ let path = "";
754
+ if (isToiletArea.value) {
755
+ path = `/${props.orgId}/${props.site}/toilet-checklist/toilet/${id}`;
756
+ } else {
757
+ path = `/${props.orgId}/${props.site}/cleaning-schedule/area/${id}`;
758
+ }
759
+ navigateTo(path);
760
+ } else {
761
+ console.error(`No ${entityTypeLabel.value} ID found for navigation`);
762
+ }
763
+ };
764
+
765
+ const onDeleteFromMoreAction = () => {
766
+ dialogShowMoreActions.value = false;
767
+ dialogDeleteItem.value = true;
768
+ };
769
+
770
+ const onShowQRAction = () => {
771
+ dialogShowMoreActions.value = false;
772
+ dialogShowQR.value = true;
773
+ qrLoading.value = false;
774
+ qrError.value = "";
775
+
776
+ const id =
777
+ (selectedItem.value as any)?._id || (selectedItem.value as any)?.id;
778
+ const runtimeConfig = useRuntimeConfig ? useRuntimeConfig() : undefined;
779
+ const origin =
780
+ runtimeConfig?.public?.APP_HYGIENE ||
781
+ (typeof window !== "undefined" && window.location?.origin
782
+ ? window.location.origin
783
+ : "https://hygiene.app.iservice365.org");
784
+ qrPath.value = `${origin}/${props.orgId}/${
785
+ props.site
786
+ }/cleaning-schedule?qrcode=true&id=${encodeURIComponent(
787
+ id
788
+ )}&site=${encodeURIComponent(props.site)}`;
789
+ };
790
+
791
+ async function onDownloadQrCode() {
792
+ if (!qrPath.value) {
793
+ showMessage("QR code URL not available", "error");
794
+ return;
795
+ }
796
+
797
+ const areaName = selectedItem.value?.name || "toilet-location";
798
+ const filename = `qr-${areaName.toLowerCase().replace(/\s+/g, "-")}`;
799
+ const siteDisplay = siteName.value || props.site || "";
800
+ const title = siteDisplay;
801
+ const subtitle = selectedItem.value?.name || "";
802
+
803
+ try {
804
+ qrLoading.value = true;
805
+ const config = useRuntimeConfig();
806
+ const apiBase = config.public.API_BASE_URL || "";
807
+
808
+ // Fetch the PDF from the API
809
+ const downloadUrl = `${apiBase}/api/hygiene-qr/generate?url=${encodeURIComponent(
810
+ qrPath.value
811
+ )}&filename=${encodeURIComponent(filename)}&title=${encodeURIComponent(
812
+ title
813
+ )}&subtitle=${encodeURIComponent(subtitle)}&download=true`;
814
+
815
+ const response = await fetch(downloadUrl);
816
+
817
+ if (!response.ok) {
818
+ throw new Error(`Failed to generate QR code: ${response.statusText}`);
819
+ }
820
+
821
+ // Get the PDF blob
822
+ const blob = await response.blob();
823
+
824
+ // Create a download link and trigger it
825
+ const url = window.URL.createObjectURL(blob);
826
+ const link = document.createElement("a");
827
+ link.href = url;
828
+ link.download = `${filename}.pdf`;
829
+ document.body.appendChild(link);
830
+ link.click();
831
+ document.body.removeChild(link);
832
+ window.URL.revokeObjectURL(url);
833
+
834
+ showMessage("QR code downloaded successfully", "success");
835
+ } catch (error: any) {
836
+ console.error("Failed to download QR code:", error);
837
+ showMessage(error?.message || "Failed to download QR code", "error");
838
+ } finally {
839
+ qrLoading.value = false;
840
+ }
841
+ }
842
+
843
+ async function _deleteItem() {
844
+ if (!selectedItem.value) return;
845
+
846
+ try {
847
+ submitting.value = true;
848
+ const id =
849
+ (selectedItem.value as any)._id || (selectedItem.value as any).id;
850
+ if (!id) throw new Error(`Invalid ${entityTypeLabel.value} id`);
851
+
852
+ const response = await deleteArea(id);
853
+ dialogDeleteItem.value = false;
854
+ showMessage(response?.message, "success");
855
+
856
+ await getTaskRefresh();
857
+ } catch (err: any) {
858
+ showMessage(err?.message, "error");
859
+ } finally {
860
+ submitting.value = false;
861
+ }
862
+ }
863
+ </script>