@7365admin1/layer-common 1.9.0 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/components/AcceptDialog.vue +44 -0
- package/components/AccessCardAddForm.vue +101 -13
- package/components/AccessManagement.vue +130 -47
- package/components/AddSupplyForm.vue +165 -0
- package/components/AreaChecklistHistoryLogs.vue +235 -0
- package/components/AreaChecklistHistoryMain.vue +176 -0
- package/components/AreaFormDialog.vue +266 -0
- package/components/AreaMain.vue +841 -0
- package/components/AttendanceCheckInOutDialog.vue +416 -0
- package/components/AttendanceDetailsDialog.vue +184 -0
- package/components/AttendanceMain.vue +155 -0
- package/components/AttendanceMapSearchDialog.vue +393 -0
- package/components/AttendanceSettingsDialog.vue +398 -0
- package/components/BuildingManagement/buildings.vue +5 -5
- package/components/BuildingManagement/units.vue +5 -5
- package/components/ChecklistItemRow.vue +54 -0
- package/components/CheckoutItemMain.vue +705 -0
- package/components/CleaningScheduleMain.vue +271 -0
- package/components/DocumentManagement.vue +8 -9
- package/components/EntryPass/QrTemplatePreview.vue +104 -0
- package/components/EntryPassMain.vue +252 -200
- package/components/HygieneUpdateMoreAction.vue +238 -0
- package/components/IncidentReport/Authorities.vue +226 -0
- package/components/IncidentReport/IncidentInformation.vue +258 -0
- package/components/IncidentReport/affectedEntities.vue +167 -0
- package/components/InvitationMain.vue +19 -17
- package/components/ManageChecklistMain.vue +384 -0
- package/components/MemberMain.vue +48 -20
- package/components/MyAttendanceMain.vue +224 -0
- package/components/OnlineFormsConfiguration.vue +9 -2
- package/components/PasswordConfirmation.vue +95 -0
- package/components/PhotoUpload.vue +410 -0
- package/components/RolePermissionMain.vue +17 -15
- package/components/ScheduleAreaMain.vue +313 -0
- package/components/ScheduleTaskAreaFormDialog.vue +144 -0
- package/components/ScheduleTaskAreaUpdateMoreAction.vue +109 -0
- package/components/ScheduleTaskForm.vue +471 -0
- package/components/ScheduleTaskMain.vue +345 -0
- package/components/ScheduleTastTicketMain.vue +182 -0
- package/components/ServiceProviderMain.vue +27 -7
- package/components/StockCard.vue +191 -0
- package/components/SupplyManagementMain.vue +557 -0
- package/components/TableHygiene.vue +617 -0
- package/components/UnitMain.vue +451 -0
- package/components/VisitorManagement.vue +28 -15
- package/composables/useAccessManagement.ts +90 -0
- package/composables/useAreaPermission.ts +51 -0
- package/composables/useAreas.ts +99 -0
- package/composables/useAttendance.ts +89 -0
- package/composables/useAttendancePermission.ts +68 -0
- package/composables/useBuilding.ts +2 -2
- package/composables/useBuildingUnit.ts +2 -2
- package/composables/useCard.ts +2 -0
- package/composables/useCheckout.ts +61 -0
- package/composables/useCheckoutPermission.ts +80 -0
- package/composables/useCleaningPermission.ts +229 -0
- package/composables/useCleaningSchedulePermission.ts +58 -0
- package/composables/useCleaningSchedules.ts +233 -0
- package/composables/useCountry.ts +8 -0
- package/composables/useDOBEntries.ts +13 -0
- package/composables/useDashboardData.ts +2 -2
- package/composables/useDocument.ts +3 -2
- package/composables/useFeedback.ts +1 -1
- package/composables/useFile.ts +4 -6
- package/composables/useLocation.ts +78 -0
- package/composables/useOnlineForm.ts +16 -9
- package/composables/usePeople.ts +87 -72
- package/composables/useQR.ts +29 -0
- package/composables/useRole.ts +3 -2
- package/composables/useScheduleTask.ts +89 -0
- package/composables/useScheduleTaskArea.ts +85 -0
- package/composables/useScheduleTaskPermission.ts +68 -0
- package/composables/useSiteEntryPassSettings.ts +4 -15
- package/composables/useStock.ts +45 -0
- package/composables/useSupply.ts +63 -0
- package/composables/useSupplyPermission.ts +92 -0
- package/composables/useUnitPermission.ts +51 -0
- package/composables/useUnits.ts +82 -0
- package/composables/useWebUsb.ts +389 -0
- package/composables/useWorkOrder.ts +1 -1
- package/nuxt.config.ts +3 -0
- package/package.json +4 -1
- package/types/area.d.ts +22 -0
- package/types/attendance.d.ts +38 -0
- package/types/checkout-item.d.ts +27 -0
- package/types/cleaner-schedule.d.ts +54 -0
- package/types/location.d.ts +42 -0
- package/types/schedule-task.d.ts +18 -0
- package/types/stock.d.ts +16 -0
- package/types/supply.d.ts +11 -0
- package/types/verification.d.ts +1 -1
- package/utils/acm-crypto.ts +30 -0
|
@@ -0,0 +1,841 @@
|
|
|
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="submitting"
|
|
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
|
+
|
|
361
|
+
const props = defineProps({
|
|
362
|
+
orgId: { type: String, default: "" },
|
|
363
|
+
site: { type: String, default: "" },
|
|
364
|
+
type: { type: String, default: "" },
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const isCleanerArea = computed(() => {
|
|
368
|
+
return props.type === "cleaner";
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const isToiletArea = computed(() => {
|
|
372
|
+
return props.type === "toilet";
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const isToiletLocation = computed(() => {
|
|
376
|
+
return selectedItem.value?.type?.toLowerCase() === "toilet";
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const isScheduledArea = computed(() => {
|
|
380
|
+
return props.type === "schedule-task";
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const areaTypeFilter = ref<TAreaType>("all");
|
|
384
|
+
const typeOptions = [
|
|
385
|
+
{ title: "All", value: "all" },
|
|
386
|
+
{ title: "Common", value: "common" },
|
|
387
|
+
{ title: "Toilet", value: "toilet" },
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
const entityTypeLabel = computed(() => {
|
|
391
|
+
if (isToiletArea.value) return "toilet location";
|
|
392
|
+
return "area";
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const formLabel = computed(() => {
|
|
396
|
+
if (isToiletArea.value) return "Toilet Location Name";
|
|
397
|
+
return "Area Name";
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const tableTitle = computed(() => {
|
|
401
|
+
if (isToiletArea.value) return `${siteName.value} Toilet Location`;
|
|
402
|
+
return `${siteName.value} Areas`;
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const createLabel = computed(() => {
|
|
406
|
+
if (isToiletArea.value) return "Add Toilet Location";
|
|
407
|
+
return "Add Area";
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const noDataText = computed(() => {
|
|
411
|
+
if (isToiletArea.value)
|
|
412
|
+
return `No toilet location found for ${siteName.value}.`;
|
|
413
|
+
return `No areas found for ${siteName.value}.`;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const items = ref<Array<Record<string, any>>>([]);
|
|
417
|
+
const siteName = ref<string>("");
|
|
418
|
+
const submitting = ref(false);
|
|
419
|
+
const isDownloading = ref(false);
|
|
420
|
+
|
|
421
|
+
const headers = [
|
|
422
|
+
{ title: "AREA", value: "name" },
|
|
423
|
+
{ title: "TYPE", value: "type" },
|
|
424
|
+
{ title: "SETS", value: "set" },
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
const { debounce } = useUtils();
|
|
428
|
+
|
|
429
|
+
const {
|
|
430
|
+
getAreas,
|
|
431
|
+
createArea,
|
|
432
|
+
updateArea,
|
|
433
|
+
deleteArea,
|
|
434
|
+
uploadAreas,
|
|
435
|
+
getAreaById,
|
|
436
|
+
exportAreas,
|
|
437
|
+
} = useAreas();
|
|
438
|
+
|
|
439
|
+
const { getUnits } = useUnits();
|
|
440
|
+
|
|
441
|
+
const searchInput = ref("");
|
|
442
|
+
const page = ref(1);
|
|
443
|
+
const pages = ref(0);
|
|
444
|
+
const pageRange = ref("-- - -- of --");
|
|
445
|
+
|
|
446
|
+
const {
|
|
447
|
+
data: getTaskReq,
|
|
448
|
+
refresh: getTaskRefresh,
|
|
449
|
+
pending: loading,
|
|
450
|
+
} = await useLazyAsyncData(
|
|
451
|
+
`get-all-task`,
|
|
452
|
+
() =>
|
|
453
|
+
getAreas({
|
|
454
|
+
page: page.value,
|
|
455
|
+
site: props.site,
|
|
456
|
+
search: searchInput.value,
|
|
457
|
+
type: areaTypeFilter.value === "all" ? undefined : areaTypeFilter.value,
|
|
458
|
+
}),
|
|
459
|
+
{
|
|
460
|
+
watch: [page, () => props.site, () => props.type, areaTypeFilter],
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
const { data: getUnitsReq } = await useLazyAsyncData(
|
|
465
|
+
`get-all-units-${props.site}`,
|
|
466
|
+
() =>
|
|
467
|
+
getUnits({
|
|
468
|
+
site: props.site,
|
|
469
|
+
limit: 100,
|
|
470
|
+
}),
|
|
471
|
+
{
|
|
472
|
+
watch: [() => props.site],
|
|
473
|
+
}
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
const allUnits = computed(() => getUnitsReq.value?.items || []);
|
|
477
|
+
|
|
478
|
+
watchEffect(() => {
|
|
479
|
+
if (getTaskReq.value) {
|
|
480
|
+
items.value = getTaskReq.value.items;
|
|
481
|
+
pages.value = getTaskReq.value.pages;
|
|
482
|
+
pageRange.value = getTaskReq.value.pageRange;
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const debounceSearch = debounce(getTaskRefresh, 500);
|
|
487
|
+
|
|
488
|
+
watch(
|
|
489
|
+
[searchInput],
|
|
490
|
+
() => {
|
|
491
|
+
debounceSearch();
|
|
492
|
+
},
|
|
493
|
+
{ immediate: false, deep: true }
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const selectedItem = ref<Record<string, any> | null>(null);
|
|
497
|
+
const message = ref("");
|
|
498
|
+
const messageSnackbar = ref(false);
|
|
499
|
+
const messageColor = ref("");
|
|
500
|
+
const fileInput = ref<HTMLInputElement | null>(null);
|
|
501
|
+
|
|
502
|
+
const dialogShowMoreActions = ref(false);
|
|
503
|
+
const dialogShowForm = ref(false);
|
|
504
|
+
const dialogDeleteItem = ref(false);
|
|
505
|
+
const dialogShowQR = ref(false);
|
|
506
|
+
const dialogMode = ref<"add" | "edit">("add");
|
|
507
|
+
|
|
508
|
+
const editingItem = ref<Record<string, any>>({});
|
|
509
|
+
const itemName = ref("");
|
|
510
|
+
const itemType = ref("");
|
|
511
|
+
const itemSet = ref(1);
|
|
512
|
+
const itemUnits = ref<string[]>([]);
|
|
513
|
+
|
|
514
|
+
const qrPath = ref("");
|
|
515
|
+
const qrLoading = ref(false);
|
|
516
|
+
const qrError = ref("");
|
|
517
|
+
|
|
518
|
+
function showMessage(msg: string, color: string = "error") {
|
|
519
|
+
message.value = msg;
|
|
520
|
+
messageColor.value = color;
|
|
521
|
+
messageSnackbar.value = true;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const {
|
|
525
|
+
canCreateArea,
|
|
526
|
+
canUpdateArea,
|
|
527
|
+
canDeleteArea,
|
|
528
|
+
canImportArea,
|
|
529
|
+
canViewAreas,
|
|
530
|
+
} = useAreaPermission();
|
|
531
|
+
|
|
532
|
+
async function handleDownloadExcel() {
|
|
533
|
+
try {
|
|
534
|
+
isDownloading.value = true;
|
|
535
|
+
|
|
536
|
+
// Validate site id (must be a 24-char hex ObjectId)
|
|
537
|
+
if (!props.site || !/^[0-9a-fA-F]{24}$/.test(props.site)) {
|
|
538
|
+
showMessage("Invalid site id for export.", "error");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const excelBuffer = await exportAreas(props.site);
|
|
543
|
+
|
|
544
|
+
if (!excelBuffer) {
|
|
545
|
+
showMessage("Failed to download areas.", "error");
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const blob = new Blob([excelBuffer], {
|
|
550
|
+
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
551
|
+
});
|
|
552
|
+
const url = URL.createObjectURL(blob);
|
|
553
|
+
const a = document.createElement("a");
|
|
554
|
+
|
|
555
|
+
const date = new Date();
|
|
556
|
+
const formattedDate = `${String(date.getMonth() + 1).padStart(
|
|
557
|
+
2,
|
|
558
|
+
"0"
|
|
559
|
+
)}-${String(date.getDate()).padStart(2, "0")}-${date.getFullYear()}`;
|
|
560
|
+
|
|
561
|
+
a.href = url;
|
|
562
|
+
a.download = `areas-${siteName.value || "export"}-${formattedDate}.xlsx`;
|
|
563
|
+
document.body.appendChild(a);
|
|
564
|
+
a.click();
|
|
565
|
+
document.body.removeChild(a);
|
|
566
|
+
URL.revokeObjectURL(url);
|
|
567
|
+
|
|
568
|
+
showMessage("Areas exported successfully.", "success");
|
|
569
|
+
} catch (error: any) {
|
|
570
|
+
showMessage(error?.message || "Failed to download areas.", "error");
|
|
571
|
+
} finally {
|
|
572
|
+
isDownloading.value = false;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function onImportData() {
|
|
577
|
+
fileInput.value?.click();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function onFileSelected(event: Event) {
|
|
581
|
+
const target = event.target as HTMLInputElement;
|
|
582
|
+
const file = target.files?.[0];
|
|
583
|
+
|
|
584
|
+
if (!file) return;
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
submitting.value = true;
|
|
588
|
+
const response = await uploadAreas(file, props.site);
|
|
589
|
+
showMessage(response?.message, "success");
|
|
590
|
+
|
|
591
|
+
await getTaskRefresh();
|
|
592
|
+
} catch (err: any) {
|
|
593
|
+
showMessage(err?.data?.message, "error");
|
|
594
|
+
} finally {
|
|
595
|
+
submitting.value = false;
|
|
596
|
+
if (target) target.value = "";
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function onCreateItem() {
|
|
601
|
+
dialogMode.value = "add";
|
|
602
|
+
editingItem.value = {};
|
|
603
|
+
itemName.value = "";
|
|
604
|
+
itemType.value = "";
|
|
605
|
+
itemSet.value = 1;
|
|
606
|
+
itemUnits.value = [];
|
|
607
|
+
dialogShowForm.value = true;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const units = computed(() => {
|
|
611
|
+
return itemUnits.value.map((unitId) => {
|
|
612
|
+
const unit = allUnits.value.find((u: any) => (u._id || u.id) === unitId);
|
|
613
|
+
|
|
614
|
+
return {
|
|
615
|
+
unit: unit?._id || unit?.id || "",
|
|
616
|
+
name: unit?.name || "",
|
|
617
|
+
};
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
async function _createArea() {
|
|
622
|
+
submitting.value = true;
|
|
623
|
+
try {
|
|
624
|
+
const payload: TAreaCreate = {
|
|
625
|
+
name: itemName.value,
|
|
626
|
+
set: itemSet.value,
|
|
627
|
+
type: (itemType.value || "common").toLowerCase(),
|
|
628
|
+
units: units.value,
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
let response;
|
|
632
|
+
|
|
633
|
+
if (dialogMode.value === "edit" && editingItem.value) {
|
|
634
|
+
const id =
|
|
635
|
+
(editingItem.value as any)._id || (editingItem.value as any).id;
|
|
636
|
+
if (!id)
|
|
637
|
+
throw new Error(`Invalid ${entityTypeLabel.value} id for update`);
|
|
638
|
+
response = await updateArea(id, payload);
|
|
639
|
+
showMessage(response?.message, "success");
|
|
640
|
+
} else {
|
|
641
|
+
response = await createArea(payload, props.site);
|
|
642
|
+
showMessage(response?.message, "success");
|
|
643
|
+
await getTaskRefresh();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
showMessage(response?.message, "success");
|
|
647
|
+
dialogShowForm.value = false;
|
|
648
|
+
editingItem.value = {};
|
|
649
|
+
itemType.value = "";
|
|
650
|
+
itemUnits.value = [];
|
|
651
|
+
|
|
652
|
+
await getTaskRefresh();
|
|
653
|
+
} catch (error: any) {
|
|
654
|
+
console.error(error);
|
|
655
|
+
showMessage(error?.message, "error");
|
|
656
|
+
} finally {
|
|
657
|
+
submitting.value = false;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function editItem(item: Record<string, any> | null) {
|
|
662
|
+
if (!item) return;
|
|
663
|
+
dialogMode.value = "edit";
|
|
664
|
+
editingItem.value = item;
|
|
665
|
+
itemName.value = item.name || "";
|
|
666
|
+
itemType.value = item.type || "common";
|
|
667
|
+
itemSet.value =
|
|
668
|
+
typeof item.set === "string" ? parseInt(item.set, 10) : item.set || 1;
|
|
669
|
+
|
|
670
|
+
if (item.units && Array.isArray(item.units)) {
|
|
671
|
+
itemUnits.value = item.units.map((u: any) => {
|
|
672
|
+
if (u.unit) return u.unit;
|
|
673
|
+
|
|
674
|
+
return u._id || u.id;
|
|
675
|
+
});
|
|
676
|
+
} else {
|
|
677
|
+
itemUnits.value = [];
|
|
678
|
+
}
|
|
679
|
+
dialogShowForm.value = true;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function handleRowClick(data: any) {
|
|
683
|
+
const item = data?.item;
|
|
684
|
+
const areaId = item?._id || item?.id;
|
|
685
|
+
|
|
686
|
+
if (!areaId) {
|
|
687
|
+
selectedItem.value = item;
|
|
688
|
+
dialogShowMoreActions.value = true;
|
|
689
|
+
message.value = "";
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
try {
|
|
694
|
+
submitting.value = true;
|
|
695
|
+
const areaData = await getAreaById(areaId);
|
|
696
|
+
selectedItem.value = areaData;
|
|
697
|
+
dialogShowMoreActions.value = true;
|
|
698
|
+
message.value = "";
|
|
699
|
+
} catch (error: any) {
|
|
700
|
+
selectedItem.value = item;
|
|
701
|
+
dialogShowMoreActions.value = true;
|
|
702
|
+
message.value = error?.message || "Failed to load area details";
|
|
703
|
+
} finally {
|
|
704
|
+
submitting.value = false;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const onEditFromMoreAction = async () => {
|
|
709
|
+
dialogShowMoreActions.value = false;
|
|
710
|
+
|
|
711
|
+
const areaId =
|
|
712
|
+
(selectedItem.value as any)?._id || (selectedItem.value as any)?.id;
|
|
713
|
+
if (areaId) {
|
|
714
|
+
try {
|
|
715
|
+
loading.value = true;
|
|
716
|
+
const areaData = await getAreaById(areaId);
|
|
717
|
+
editItem(areaData);
|
|
718
|
+
} catch (error: any) {
|
|
719
|
+
showMessage(error?.message || "Failed to fetch area data", "error");
|
|
720
|
+
} finally {
|
|
721
|
+
loading.value = false;
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
editItem(selectedItem.value);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const onManageFromMoreAction = () => {
|
|
729
|
+
dialogShowMoreActions.value = false;
|
|
730
|
+
const id =
|
|
731
|
+
(selectedItem.value as any)?._id || (selectedItem.value as any)?.id;
|
|
732
|
+
if (id) {
|
|
733
|
+
let path = "";
|
|
734
|
+
if (isToiletArea.value) {
|
|
735
|
+
path = `/${props.orgId}/${props.site}/toilet-checklist/toilet/${id}`;
|
|
736
|
+
} else {
|
|
737
|
+
path = `/${props.orgId}/${props.site}/cleaning-schedule/area/${id}`;
|
|
738
|
+
}
|
|
739
|
+
navigateTo(path);
|
|
740
|
+
} else {
|
|
741
|
+
console.error(`No ${entityTypeLabel.value} ID found for navigation`);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const onDeleteFromMoreAction = () => {
|
|
746
|
+
dialogShowMoreActions.value = false;
|
|
747
|
+
dialogDeleteItem.value = true;
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
const onShowQRAction = () => {
|
|
751
|
+
dialogShowMoreActions.value = false;
|
|
752
|
+
dialogShowQR.value = true;
|
|
753
|
+
qrLoading.value = false;
|
|
754
|
+
qrError.value = "";
|
|
755
|
+
|
|
756
|
+
const id =
|
|
757
|
+
(selectedItem.value as any)?._id || (selectedItem.value as any)?.id;
|
|
758
|
+
const runtimeConfig = useRuntimeConfig ? useRuntimeConfig() : undefined;
|
|
759
|
+
const origin =
|
|
760
|
+
runtimeConfig?.public?.APP_HYGIENE ||
|
|
761
|
+
(typeof window !== "undefined" && window.location?.origin
|
|
762
|
+
? window.location.origin
|
|
763
|
+
: "https://hygiene.app.iservice365.org");
|
|
764
|
+
qrPath.value = `${origin}/${props.orgId}/${
|
|
765
|
+
props.site
|
|
766
|
+
}/cleaning-schedule?qrcode=true&id=${encodeURIComponent(
|
|
767
|
+
id
|
|
768
|
+
)}&site=${encodeURIComponent(props.site)}`;
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
async function onDownloadQrCode() {
|
|
772
|
+
if (!qrPath.value) {
|
|
773
|
+
showMessage("QR code URL not available", "error");
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const areaName = selectedItem.value?.name || "toilet-location";
|
|
778
|
+
const filename = `qr-${areaName.toLowerCase().replace(/\s+/g, "-")}`;
|
|
779
|
+
const title = "Seven365 Pte Ltd";
|
|
780
|
+
|
|
781
|
+
try {
|
|
782
|
+
qrLoading.value = true;
|
|
783
|
+
const config = useRuntimeConfig();
|
|
784
|
+
const apiBase = config.public.API_BASE_URL || "";
|
|
785
|
+
|
|
786
|
+
// Fetch the PDF from the API
|
|
787
|
+
const downloadUrl = `${apiBase}/api/hygiene-qr/generate?url=${encodeURIComponent(
|
|
788
|
+
qrPath.value
|
|
789
|
+
)}&filename=${encodeURIComponent(filename)}&title=${encodeURIComponent(
|
|
790
|
+
title
|
|
791
|
+
)}&download=true`;
|
|
792
|
+
|
|
793
|
+
const response = await fetch(downloadUrl);
|
|
794
|
+
|
|
795
|
+
if (!response.ok) {
|
|
796
|
+
throw new Error(`Failed to generate QR code: ${response.statusText}`);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Get the PDF blob
|
|
800
|
+
const blob = await response.blob();
|
|
801
|
+
|
|
802
|
+
// Create a download link and trigger it
|
|
803
|
+
const url = window.URL.createObjectURL(blob);
|
|
804
|
+
const link = document.createElement("a");
|
|
805
|
+
link.href = url;
|
|
806
|
+
link.download = `${filename}.pdf`;
|
|
807
|
+
document.body.appendChild(link);
|
|
808
|
+
link.click();
|
|
809
|
+
document.body.removeChild(link);
|
|
810
|
+
window.URL.revokeObjectURL(url);
|
|
811
|
+
|
|
812
|
+
showMessage("QR code downloaded successfully", "success");
|
|
813
|
+
} catch (error: any) {
|
|
814
|
+
console.error("Failed to download QR code:", error);
|
|
815
|
+
showMessage(error?.message || "Failed to download QR code", "error");
|
|
816
|
+
} finally {
|
|
817
|
+
qrLoading.value = false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async function _deleteItem() {
|
|
822
|
+
if (!selectedItem.value) return;
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
submitting.value = true;
|
|
826
|
+
const id =
|
|
827
|
+
(selectedItem.value as any)._id || (selectedItem.value as any).id;
|
|
828
|
+
if (!id) throw new Error(`Invalid ${entityTypeLabel.value} id`);
|
|
829
|
+
|
|
830
|
+
const response = await deleteArea(id);
|
|
831
|
+
dialogDeleteItem.value = false;
|
|
832
|
+
showMessage(response?.message, "success");
|
|
833
|
+
|
|
834
|
+
await getTaskRefresh();
|
|
835
|
+
} catch (err: any) {
|
|
836
|
+
showMessage(err?.message, "error");
|
|
837
|
+
} finally {
|
|
838
|
+
submitting.value = false;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
</script>
|