@7365admin1/layer-common 1.10.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 +6 -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 +4 -0
- package/components/EntryPass/QrTemplatePreview.vue +104 -0
- package/components/EntryPassMain.vue +252 -200
- package/components/HygieneUpdateMoreAction.vue +238 -0
- 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/PhotoUpload.vue +410 -0
- 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/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/useDashboardData.ts +2 -2
- package/composables/useFeedback.ts +1 -1
- 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/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/utils/acm-crypto.ts +30 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row no-gutters>
|
|
3
|
+
<!-- Top Actions -->
|
|
4
|
+
<v-col cols="12" class="mb-2" v-if="canCreate || $slots.actions">
|
|
5
|
+
<v-row no-gutters>
|
|
6
|
+
<slot name="actions">
|
|
7
|
+
<v-btn
|
|
8
|
+
v-if="canCreate"
|
|
9
|
+
class="text-none"
|
|
10
|
+
rounded="pill"
|
|
11
|
+
variant="tonal"
|
|
12
|
+
size="large"
|
|
13
|
+
@click="emits('create')"
|
|
14
|
+
>
|
|
15
|
+
{{ createLabel }}
|
|
16
|
+
</v-btn>
|
|
17
|
+
</slot>
|
|
18
|
+
</v-row>
|
|
19
|
+
</v-col>
|
|
20
|
+
|
|
21
|
+
<!-- List Card -->
|
|
22
|
+
<v-col cols="12">
|
|
23
|
+
<v-card
|
|
24
|
+
width="100%"
|
|
25
|
+
variant="outlined"
|
|
26
|
+
border="thin"
|
|
27
|
+
rounded="lg"
|
|
28
|
+
:loading="loading"
|
|
29
|
+
>
|
|
30
|
+
<!-- Toolbar -->
|
|
31
|
+
<v-toolbar
|
|
32
|
+
density="compact"
|
|
33
|
+
color="grey-lighten-4"
|
|
34
|
+
:extension-height="extensionHeight"
|
|
35
|
+
>
|
|
36
|
+
<template #prepend>
|
|
37
|
+
<v-btn fab icon density="comfortable" @click="emits('refresh')">
|
|
38
|
+
<v-icon>mdi-refresh</v-icon>
|
|
39
|
+
</v-btn>
|
|
40
|
+
<slot name="prepend-additional" />
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<v-toolbar-title v-if="title" class="text-subtitle-1 font-weight-medium">
|
|
44
|
+
{{ title }}
|
|
45
|
+
</v-toolbar-title>
|
|
46
|
+
|
|
47
|
+
<template #append>
|
|
48
|
+
<v-row no-gutters justify="end" align="center">
|
|
49
|
+
<span class="mr-2 text-caption text-fontgray">
|
|
50
|
+
{{ pageRange }}
|
|
51
|
+
</span>
|
|
52
|
+
<local-pagination
|
|
53
|
+
v-model="internalPage"
|
|
54
|
+
:length="pages"
|
|
55
|
+
@update:value="emits('update:page', internalPage)"
|
|
56
|
+
/>
|
|
57
|
+
</v-row>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<template v-if="$slots.extension" #extension>
|
|
61
|
+
<slot name="extension" />
|
|
62
|
+
</template>
|
|
63
|
+
</v-toolbar>
|
|
64
|
+
|
|
65
|
+
<v-divider />
|
|
66
|
+
|
|
67
|
+
<!-- List Items -->
|
|
68
|
+
<v-list
|
|
69
|
+
:style="`max-height: calc(100vh - (${offset}px)); overflow-y: auto;`"
|
|
70
|
+
>
|
|
71
|
+
<v-list-item
|
|
72
|
+
v-if="groupedItems.length === 0"
|
|
73
|
+
class="py-6 text-center text-medium-emphasis"
|
|
74
|
+
>
|
|
75
|
+
{{ noDataText }}
|
|
76
|
+
</v-list-item>
|
|
77
|
+
|
|
78
|
+
<template
|
|
79
|
+
v-for="(group, groupIndex) in groupedItems"
|
|
80
|
+
:key="`group-${groupIndex}`"
|
|
81
|
+
>
|
|
82
|
+
<div
|
|
83
|
+
v-if="group.set !== undefined"
|
|
84
|
+
class="d-flex align-center justify-space-between px-4 py-1 text-caption text-medium-emphasis"
|
|
85
|
+
style="min-height: 32px"
|
|
86
|
+
>
|
|
87
|
+
<span>Set {{ group.set }}</span>
|
|
88
|
+
<div class="d-flex align-center ga-2">
|
|
89
|
+
<span v-if="group.completedByName">
|
|
90
|
+
Completed by: {{ group.completedByName }}
|
|
91
|
+
</span>
|
|
92
|
+
<v-btn
|
|
93
|
+
v-if="group.attachments && group.attachments.length > 0"
|
|
94
|
+
size="x-small"
|
|
95
|
+
variant="tonal"
|
|
96
|
+
color="primary"
|
|
97
|
+
class="text-none"
|
|
98
|
+
prepend-icon="mdi-paperclip"
|
|
99
|
+
@click.stop="
|
|
100
|
+
openAttachmentDialog(group.set, group.attachments)
|
|
101
|
+
"
|
|
102
|
+
>
|
|
103
|
+
View Attachment ({{ group.attachments.length }})
|
|
104
|
+
</v-btn>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<v-list-item
|
|
109
|
+
v-for="item in group.items"
|
|
110
|
+
:key="item[itemValue]"
|
|
111
|
+
class="py-3"
|
|
112
|
+
:class="
|
|
113
|
+
isItemSelected(item, group.set)
|
|
114
|
+
? ['bg-grey-lighten-4', 'rounded']
|
|
115
|
+
: []
|
|
116
|
+
"
|
|
117
|
+
>
|
|
118
|
+
<slot name="list-item" :item="item">
|
|
119
|
+
<v-list-item-title v-if="headers[0]">
|
|
120
|
+
{{ getItemValue(item, headers[0].value) }}
|
|
121
|
+
</v-list-item-title>
|
|
122
|
+
|
|
123
|
+
<v-list-item-subtitle
|
|
124
|
+
v-if="headers[1]"
|
|
125
|
+
class="mb-1 text-high-emphasis opacity-100"
|
|
126
|
+
>
|
|
127
|
+
{{ getItemValue(item, headers[1].value) || "N/A" }}
|
|
128
|
+
</v-list-item-subtitle>
|
|
129
|
+
|
|
130
|
+
<v-list-item-subtitle
|
|
131
|
+
v-if="headers[2]"
|
|
132
|
+
class="text-high-emphasis"
|
|
133
|
+
>
|
|
134
|
+
{{ getItemValue(item, headers[2].value) || "N/A" }}
|
|
135
|
+
</v-list-item-subtitle>
|
|
136
|
+
</slot>
|
|
137
|
+
|
|
138
|
+
<template v-slot:append>
|
|
139
|
+
<slot
|
|
140
|
+
name="list-item-append"
|
|
141
|
+
:item="item"
|
|
142
|
+
:isSelected="isItemSelected(item, group.set)"
|
|
143
|
+
>
|
|
144
|
+
<v-list-item-action class="d-flex flex-row ga-2">
|
|
145
|
+
<template v-if="canManageScheduleTasks">
|
|
146
|
+
<v-btn
|
|
147
|
+
icon="mdi-close"
|
|
148
|
+
size="small"
|
|
149
|
+
:variant="
|
|
150
|
+
activeActions[getKey(item, group.set)] === 'reject'
|
|
151
|
+
? 'flat'
|
|
152
|
+
: 'text'
|
|
153
|
+
"
|
|
154
|
+
color="error"
|
|
155
|
+
@click.stop="
|
|
156
|
+
handleActionClick(item, group.set, 'reject')
|
|
157
|
+
"
|
|
158
|
+
/>
|
|
159
|
+
|
|
160
|
+
<v-btn
|
|
161
|
+
icon="mdi-check"
|
|
162
|
+
size="small"
|
|
163
|
+
:variant="
|
|
164
|
+
activeActions[getKey(item, group.set)] === 'approve'
|
|
165
|
+
? 'flat'
|
|
166
|
+
: 'text'
|
|
167
|
+
"
|
|
168
|
+
color="success"
|
|
169
|
+
@click.stop="
|
|
170
|
+
handleActionClick(item, group.set, 'approve')
|
|
171
|
+
"
|
|
172
|
+
/>
|
|
173
|
+
</template>
|
|
174
|
+
</v-list-item-action>
|
|
175
|
+
</slot>
|
|
176
|
+
</template>
|
|
177
|
+
</v-list-item>
|
|
178
|
+
</template>
|
|
179
|
+
</v-list>
|
|
180
|
+
|
|
181
|
+
<slot name="footer" />
|
|
182
|
+
</v-card>
|
|
183
|
+
</v-col>
|
|
184
|
+
</v-row>
|
|
185
|
+
|
|
186
|
+
<!-- Attachment Preview Dialog -->
|
|
187
|
+
<v-dialog v-model="showAttachmentDialog" max-width="700" scrollable>
|
|
188
|
+
<v-card>
|
|
189
|
+
<v-card-title class="d-flex align-center pa-4">
|
|
190
|
+
<span class="text-h6 font-weight-bold">
|
|
191
|
+
Set {{ attachmentDialogSet }} — Attachments
|
|
192
|
+
</span>
|
|
193
|
+
<v-spacer />
|
|
194
|
+
<v-btn
|
|
195
|
+
icon="mdi-close"
|
|
196
|
+
variant="text"
|
|
197
|
+
size="small"
|
|
198
|
+
@click="showAttachmentDialog = false"
|
|
199
|
+
/>
|
|
200
|
+
</v-card-title>
|
|
201
|
+
|
|
202
|
+
<v-divider />
|
|
203
|
+
|
|
204
|
+
<v-card-text class="pa-4">
|
|
205
|
+
<v-row>
|
|
206
|
+
<v-col
|
|
207
|
+
v-for="(id, index) in attachmentDialogIds"
|
|
208
|
+
:key="index"
|
|
209
|
+
cols="6"
|
|
210
|
+
sm="4"
|
|
211
|
+
>
|
|
212
|
+
<v-sheet
|
|
213
|
+
rounded="lg"
|
|
214
|
+
class="overflow-hidden"
|
|
215
|
+
style="aspect-ratio: 1"
|
|
216
|
+
>
|
|
217
|
+
<v-img
|
|
218
|
+
:src="getFileUrl(id)"
|
|
219
|
+
aspect-ratio="1"
|
|
220
|
+
cover
|
|
221
|
+
class="rounded-lg"
|
|
222
|
+
@click="openFullImage(getFileUrl(id))"
|
|
223
|
+
style="cursor: zoom-in"
|
|
224
|
+
>
|
|
225
|
+
<template v-slot:placeholder>
|
|
226
|
+
<v-row
|
|
227
|
+
class="fill-height ma-0"
|
|
228
|
+
align="center"
|
|
229
|
+
justify="center"
|
|
230
|
+
>
|
|
231
|
+
<v-progress-circular indeterminate color="grey-lighten-4" />
|
|
232
|
+
</v-row>
|
|
233
|
+
</template>
|
|
234
|
+
<template v-slot:error>
|
|
235
|
+
<v-row
|
|
236
|
+
class="fill-height ma-0"
|
|
237
|
+
align="center"
|
|
238
|
+
justify="center"
|
|
239
|
+
>
|
|
240
|
+
<v-icon icon="mdi-image-broken" size="40" color="grey" />
|
|
241
|
+
</v-row>
|
|
242
|
+
</template>
|
|
243
|
+
</v-img>
|
|
244
|
+
</v-sheet>
|
|
245
|
+
</v-col>
|
|
246
|
+
</v-row>
|
|
247
|
+
</v-card-text>
|
|
248
|
+
</v-card>
|
|
249
|
+
</v-dialog>
|
|
250
|
+
|
|
251
|
+
<!-- Full Image Lightbox -->
|
|
252
|
+
<v-dialog v-model="showLightbox" max-width="900">
|
|
253
|
+
<v-card>
|
|
254
|
+
<v-card-actions class="pa-2 justify-end">
|
|
255
|
+
<v-btn icon="mdi-close" variant="text" @click="showLightbox = false" />
|
|
256
|
+
</v-card-actions>
|
|
257
|
+
<v-card-text class="pa-2 pt-0">
|
|
258
|
+
<v-img :src="lightboxSrc" contain max-height="80vh" />
|
|
259
|
+
</v-card-text>
|
|
260
|
+
</v-card>
|
|
261
|
+
</v-dialog>
|
|
262
|
+
</template>
|
|
263
|
+
|
|
264
|
+
<script lang="ts" setup>
|
|
265
|
+
defineOptions({ inheritAttrs: false });
|
|
266
|
+
|
|
267
|
+
const props = defineProps({
|
|
268
|
+
title: {
|
|
269
|
+
type: String,
|
|
270
|
+
default: "",
|
|
271
|
+
},
|
|
272
|
+
noDataText: {
|
|
273
|
+
type: String,
|
|
274
|
+
default: "No data available",
|
|
275
|
+
},
|
|
276
|
+
headers: {
|
|
277
|
+
type: Array as PropType<Array<Record<string, any>>>,
|
|
278
|
+
required: true,
|
|
279
|
+
},
|
|
280
|
+
items: {
|
|
281
|
+
type: Array as PropType<Array<Record<string, any>>>,
|
|
282
|
+
default: () => [],
|
|
283
|
+
},
|
|
284
|
+
loading: {
|
|
285
|
+
type: Boolean,
|
|
286
|
+
default: false,
|
|
287
|
+
},
|
|
288
|
+
canCreate: {
|
|
289
|
+
type: Boolean,
|
|
290
|
+
default: false,
|
|
291
|
+
},
|
|
292
|
+
createLabel: {
|
|
293
|
+
type: String,
|
|
294
|
+
default: "Add",
|
|
295
|
+
},
|
|
296
|
+
itemValue: {
|
|
297
|
+
type: String,
|
|
298
|
+
default: "_id",
|
|
299
|
+
},
|
|
300
|
+
itemsPerPage: {
|
|
301
|
+
type: Number,
|
|
302
|
+
default: 20,
|
|
303
|
+
},
|
|
304
|
+
page: {
|
|
305
|
+
type: Number,
|
|
306
|
+
default: 1,
|
|
307
|
+
},
|
|
308
|
+
pages: {
|
|
309
|
+
type: Number,
|
|
310
|
+
default: 0,
|
|
311
|
+
},
|
|
312
|
+
pageRange: {
|
|
313
|
+
type: String,
|
|
314
|
+
default: "-- - -- of --",
|
|
315
|
+
},
|
|
316
|
+
showHeader: {
|
|
317
|
+
type: Boolean,
|
|
318
|
+
default: false,
|
|
319
|
+
},
|
|
320
|
+
canManageScheduleTasks: {
|
|
321
|
+
type: Boolean,
|
|
322
|
+
default: true,
|
|
323
|
+
},
|
|
324
|
+
canAddRemarks: {
|
|
325
|
+
type: Boolean,
|
|
326
|
+
default: true,
|
|
327
|
+
},
|
|
328
|
+
extensionHeight: {
|
|
329
|
+
type: Number,
|
|
330
|
+
default: 50,
|
|
331
|
+
},
|
|
332
|
+
offset: {
|
|
333
|
+
type: Number,
|
|
334
|
+
default: 200,
|
|
335
|
+
},
|
|
336
|
+
selected: {
|
|
337
|
+
type: Array as PropType<any[]>,
|
|
338
|
+
default: () => [],
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const emits = defineEmits([
|
|
343
|
+
"create",
|
|
344
|
+
"refresh",
|
|
345
|
+
"update:page",
|
|
346
|
+
"row-click",
|
|
347
|
+
"update:selected",
|
|
348
|
+
"action-click",
|
|
349
|
+
"request-completion-dialog",
|
|
350
|
+
]);
|
|
351
|
+
|
|
352
|
+
defineExpose({
|
|
353
|
+
revertSetApprovals,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const internalPage = ref(props.page);
|
|
357
|
+
const selected = shallowRef<any[]>(props.selected);
|
|
358
|
+
const activeActions = reactive<Record<string, "approve" | "reject">>({});
|
|
359
|
+
const persistedActions = reactive<Record<string, "approve" | "reject">>({});
|
|
360
|
+
const completedSets = ref<Set<number>>(new Set());
|
|
361
|
+
const itemOrderMap = new Map<string, number>();
|
|
362
|
+
|
|
363
|
+
const showAttachmentDialog = ref(false);
|
|
364
|
+
const attachmentDialogSet = ref<number | undefined>(undefined);
|
|
365
|
+
const attachmentDialogIds = ref<string[]>([]);
|
|
366
|
+
const showLightbox = ref(false);
|
|
367
|
+
const lightboxSrc = ref("");
|
|
368
|
+
|
|
369
|
+
const { getFileUrl } = useFile();
|
|
370
|
+
|
|
371
|
+
function openAttachmentDialog(setNumber: number, attachments: string[]) {
|
|
372
|
+
attachmentDialogSet.value = setNumber;
|
|
373
|
+
attachmentDialogIds.value = attachments;
|
|
374
|
+
showAttachmentDialog.value = true;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function openFullImage(src: string) {
|
|
378
|
+
lightboxSrc.value = src;
|
|
379
|
+
showLightbox.value = true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const groupedItems = computed(() => {
|
|
383
|
+
return props.items.map((item: any) => {
|
|
384
|
+
const units = item.units || [item];
|
|
385
|
+
|
|
386
|
+
units.forEach((unit: any, index: number) => {
|
|
387
|
+
const key = getKey(unit, item.set);
|
|
388
|
+
if (!itemOrderMap.has(key)) {
|
|
389
|
+
itemOrderMap.set(key, itemOrderMap.size);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const sortedUnits = [...units].sort((a: any, b: any) => {
|
|
394
|
+
const keyA = getKey(a, item.set);
|
|
395
|
+
const keyB = getKey(b, item.set);
|
|
396
|
+
const orderA = itemOrderMap.get(keyA) ?? 0;
|
|
397
|
+
const orderB = itemOrderMap.get(keyB) ?? 0;
|
|
398
|
+
return orderA - orderB;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
set: item.set,
|
|
403
|
+
completedByName: item.completedByName ?? null,
|
|
404
|
+
attachments:
|
|
405
|
+
(item.attachment as string[] | undefined) ??
|
|
406
|
+
(item.attachments as string[] | undefined) ??
|
|
407
|
+
[],
|
|
408
|
+
items: sortedUnits,
|
|
409
|
+
};
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const totalItemsCount = computed(() => {
|
|
414
|
+
return groupedItems.value.reduce((acc, group) => acc + group.items.length, 0);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const approvedItemsCount = computed(() => {
|
|
418
|
+
return Object.values(activeActions).filter((action) => action === "approve")
|
|
419
|
+
.length;
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const hasRejectedItems = computed(() => {
|
|
423
|
+
return Object.values(activeActions).some((action) => action === "reject");
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const allItemsApproved = computed(() => {
|
|
427
|
+
return (
|
|
428
|
+
totalItemsCount.value > 0 &&
|
|
429
|
+
approvedItemsCount.value === totalItemsCount.value &&
|
|
430
|
+
!hasRejectedItems.value
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
function isSetFullyApproved(setNumber: number): boolean {
|
|
435
|
+
const group = groupedItems.value.find((g) => g.set === setNumber);
|
|
436
|
+
if (!group) return false;
|
|
437
|
+
|
|
438
|
+
return group.items.every((item: any) => {
|
|
439
|
+
const key = getKey(item, setNumber);
|
|
440
|
+
return activeActions[key] === "approve";
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function getNewApprovedItemsForSet(
|
|
445
|
+
setNumber: number,
|
|
446
|
+
): Array<{ key: string; item: any; action: "approve" }> {
|
|
447
|
+
const group = groupedItems.value.find((g) => g.set === setNumber);
|
|
448
|
+
if (!group) return [];
|
|
449
|
+
|
|
450
|
+
const approvedItems: Array<{ key: string; item: any; action: "approve" }> =
|
|
451
|
+
[];
|
|
452
|
+
|
|
453
|
+
group.items.forEach((item: any) => {
|
|
454
|
+
const key = getKey(item, setNumber);
|
|
455
|
+
if (activeActions[key] === "approve" && !(key in persistedActions)) {
|
|
456
|
+
approvedItems.push({
|
|
457
|
+
key,
|
|
458
|
+
item: { ...item, set: setNumber },
|
|
459
|
+
action: "approve",
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return approvedItems;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const lastApprovedKey = ref<string | null>(null);
|
|
468
|
+
|
|
469
|
+
watch(
|
|
470
|
+
() => props.page,
|
|
471
|
+
(val) => {
|
|
472
|
+
internalPage.value = val;
|
|
473
|
+
|
|
474
|
+
itemOrderMap.clear();
|
|
475
|
+
},
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
watch(
|
|
479
|
+
() => props.selected,
|
|
480
|
+
(val) => {
|
|
481
|
+
selected.value = val;
|
|
482
|
+
},
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
watch(selected, (val) => {
|
|
486
|
+
emits("update:selected", val);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
watch(
|
|
490
|
+
() => props.items,
|
|
491
|
+
(items) => {
|
|
492
|
+
if (!items || !Array.isArray(items)) return;
|
|
493
|
+
|
|
494
|
+
Object.keys(persistedActions).forEach(
|
|
495
|
+
(key) => delete persistedActions[key],
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
items.forEach((group: any) => {
|
|
499
|
+
const set = group.set;
|
|
500
|
+
const units = group.units || [];
|
|
501
|
+
|
|
502
|
+
units.forEach((unit: any) => {
|
|
503
|
+
const key = getKey(unit, set);
|
|
504
|
+
if (unit.approve === true) {
|
|
505
|
+
activeActions[key] = "approve";
|
|
506
|
+
persistedActions[key] = "approve";
|
|
507
|
+
} else if (unit.reject === true) {
|
|
508
|
+
activeActions[key] = "reject";
|
|
509
|
+
persistedActions[key] = "reject";
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
},
|
|
514
|
+
{ immediate: true },
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
function getKey(item: any, set?: number): string {
|
|
518
|
+
return `${item[props.itemValue]}_${set ?? ""}`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function getItemValue(item: any, key: string): string {
|
|
522
|
+
if (!key) return "";
|
|
523
|
+
return key.split(".").reduce((obj, k) => obj?.[k], item) ?? "";
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function isItemSelected(item: any, set?: number): boolean {
|
|
527
|
+
if (!Array.isArray(selected.value) || selected.value.length === 0) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (typeof selected.value[0] === "object" && "unit" in selected.value[0]) {
|
|
532
|
+
return selected.value.some(
|
|
533
|
+
(s: any) => s.unit === item[props.itemValue] && s.set === set,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return selected.value.includes(item[props.itemValue]);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function handleActionClick(
|
|
541
|
+
item: any,
|
|
542
|
+
set: number | undefined,
|
|
543
|
+
action: "approve" | "reject",
|
|
544
|
+
): void {
|
|
545
|
+
const key = getKey(item, set);
|
|
546
|
+
|
|
547
|
+
const isPersisted = key in persistedActions;
|
|
548
|
+
|
|
549
|
+
if (activeActions[key] === action && !isPersisted) {
|
|
550
|
+
delete activeActions[key];
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
activeActions[key] = action;
|
|
555
|
+
|
|
556
|
+
if (action === "reject") {
|
|
557
|
+
console.debug("TableHygiene: emitting action-click", {
|
|
558
|
+
item: { ...item, set },
|
|
559
|
+
action,
|
|
560
|
+
});
|
|
561
|
+
emits("action-click", { item: { ...item, set }, action });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (action === "approve") {
|
|
565
|
+
lastApprovedKey.value = key;
|
|
566
|
+
|
|
567
|
+
if (
|
|
568
|
+
set !== undefined &&
|
|
569
|
+
isSetFullyApproved(set) &&
|
|
570
|
+
!completedSets.value.has(set)
|
|
571
|
+
) {
|
|
572
|
+
const newApprovedItems = getNewApprovedItemsForSet(set);
|
|
573
|
+
|
|
574
|
+
if (newApprovedItems.length > 0) {
|
|
575
|
+
// Mark this set as completed
|
|
576
|
+
completedSets.value.add(set);
|
|
577
|
+
|
|
578
|
+
// Emit request to open completion dialog
|
|
579
|
+
emits("request-completion-dialog", {
|
|
580
|
+
setNumber: set,
|
|
581
|
+
approvedItems: newApprovedItems,
|
|
582
|
+
lastApprovedKey: lastApprovedKey.value,
|
|
583
|
+
});
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
console.debug("TableHygiene: emitting action-click (approve immediate)", {
|
|
588
|
+
item: { ...item, set },
|
|
589
|
+
action,
|
|
590
|
+
});
|
|
591
|
+
emits("action-click", { item: { ...item, set }, action });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function revertSetApprovals(setNumber: number): void {
|
|
596
|
+
const group = groupedItems.value.find((g) => g.set === setNumber);
|
|
597
|
+
if (group) {
|
|
598
|
+
group.items.forEach((item: any) => {
|
|
599
|
+
const key = getKey(item, setNumber);
|
|
600
|
+
|
|
601
|
+
if (!(key in persistedActions)) {
|
|
602
|
+
delete activeActions[key];
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
completedSets.value.delete(setNumber);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
watch(
|
|
611
|
+
() => props.items,
|
|
612
|
+
() => {
|
|
613
|
+
completedSets.value.clear();
|
|
614
|
+
},
|
|
615
|
+
{ deep: true },
|
|
616
|
+
);
|
|
617
|
+
</script>
|