@7365admin1/layer-common 1.10.6 → 1.10.7
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/AccessCardQrTagging.vue +314 -34
- package/components/AccessCardQrTaggingPrintQr.vue +75 -0
- package/components/AreaChecklistHistoryLogs.vue +9 -0
- package/components/BuildingForm.vue +36 -5
- package/components/BuildingManagement/buildings.vue +18 -9
- package/components/BuildingManagement/units.vue +12 -114
- package/components/BuildingUnitFormAdd.vue +39 -30
- package/components/BuildingUnitFormEdit.vue +265 -116
- package/components/CleaningScheduleMain.vue +60 -13
- package/components/Dialog/DeleteConfirmation.vue +2 -2
- package/components/Dialog/UpdateMoreAction.vue +2 -2
- package/components/EntryPassInformation.vue +215 -0
- package/components/Input/InputPhoneNumberV2.vue +8 -0
- package/components/ManageChecklistMain.vue +29 -3
- package/components/ScheduleAreaMain.vue +56 -0
- package/components/TableHygiene.vue +27 -0
- package/components/UnitPersonCard.vue +63 -0
- package/components/VehicleAddSelection.vue +2 -2
- package/components/VehicleForm.vue +78 -19
- package/components/VehicleManagement.vue +164 -40
- package/components/VisitorForm.vue +19 -0
- package/composables/useAccessManagement.ts +52 -0
- package/composables/usePeople.ts +4 -3
- package/composables/useVehicle.ts +15 -1
- package/package.json +3 -2
- package/types/cleaner-schedule.d.ts +1 -0
- package/types/html2pdf.d.ts +19 -0
- package/types/people.d.ts +2 -1
- package/types/site.d.ts +8 -0
- package/types/vehicle.d.ts +2 -3
- 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
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<v-skeleton-loader v-if="loading" type="list-item-two-line" width="100%" />
|
|
4
|
+
<template v-else>
|
|
5
|
+
<v-divider class="mb-4" />
|
|
6
|
+
<InputLabel title="Pass Type (Optional)" />
|
|
7
|
+
<v-row no-gutters class="ga-3 mt-1">
|
|
8
|
+
<v-col>
|
|
9
|
+
<v-card
|
|
10
|
+
:variant="passType === 'QR' ? 'tonal' : 'outlined'"
|
|
11
|
+
:color="passType === 'QR' ? 'primary' : undefined"
|
|
12
|
+
class="pa-3 d-flex flex-column align-center ga-2 cursor-pointer position-relative"
|
|
13
|
+
rounded="lg"
|
|
14
|
+
@click="passType = passType === 'QR' ? null : 'QR'"
|
|
15
|
+
>
|
|
16
|
+
<v-icon v-if="passType === 'QR'" size="18" color="primary" class="selected-check">mdi-check-circle</v-icon>
|
|
17
|
+
<v-icon size="32">mdi-qrcode</v-icon>
|
|
18
|
+
<span class="text-subtitle-2 font-weight-bold">QR Pass</span>
|
|
19
|
+
</v-card>
|
|
20
|
+
</v-col>
|
|
21
|
+
<v-col v-if="nfcEnabled">
|
|
22
|
+
<v-card
|
|
23
|
+
:variant="passType === 'NFC' ? 'tonal' : 'outlined'"
|
|
24
|
+
:color="passType === 'NFC' ? 'primary' : undefined"
|
|
25
|
+
class="pa-3 d-flex flex-column align-center ga-2 cursor-pointer position-relative"
|
|
26
|
+
rounded="lg"
|
|
27
|
+
@click="passType = passType === 'NFC' ? null : 'NFC'"
|
|
28
|
+
>
|
|
29
|
+
<v-icon v-if="passType === 'NFC'" size="18" color="primary" class="selected-check">mdi-check-circle</v-icon>
|
|
30
|
+
<v-icon size="32">mdi-nfc</v-icon>
|
|
31
|
+
<span class="text-subtitle-2 font-weight-bold">NFC</span>
|
|
32
|
+
</v-card>
|
|
33
|
+
</v-col>
|
|
34
|
+
</v-row>
|
|
35
|
+
|
|
36
|
+
<!-- QR Pass section -->
|
|
37
|
+
<template v-if="passType === 'QR'">
|
|
38
|
+
<v-alert
|
|
39
|
+
v-if="!hasPrinter"
|
|
40
|
+
type="warning"
|
|
41
|
+
variant="tonal"
|
|
42
|
+
density="compact"
|
|
43
|
+
class="mt-3"
|
|
44
|
+
icon="mdi-printer-alert"
|
|
45
|
+
>
|
|
46
|
+
No printer is configured in the settings. QR Pass may not print.
|
|
47
|
+
</v-alert>
|
|
48
|
+
<template v-else>
|
|
49
|
+
<InputLabel title="Quantity" class="mt-3" />
|
|
50
|
+
<v-text-field
|
|
51
|
+
v-model.number="quantity"
|
|
52
|
+
type="number"
|
|
53
|
+
density="comfortable"
|
|
54
|
+
:min="1"
|
|
55
|
+
hide-details
|
|
56
|
+
/>
|
|
57
|
+
</template>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<!-- NFC Pass section -->
|
|
61
|
+
<template v-if="passType === 'NFC'">
|
|
62
|
+
<div class="mt-3">
|
|
63
|
+
<InputLabel title="Select Cards" />
|
|
64
|
+
<v-select
|
|
65
|
+
v-model="selectedCards"
|
|
66
|
+
:items="cards"
|
|
67
|
+
:loading="cardsLoading"
|
|
68
|
+
item-title="name"
|
|
69
|
+
item-value="_id"
|
|
70
|
+
density="comfortable"
|
|
71
|
+
multiple
|
|
72
|
+
chips
|
|
73
|
+
closable-chips
|
|
74
|
+
hide-details
|
|
75
|
+
placeholder="Select cards..."
|
|
76
|
+
>
|
|
77
|
+
<template #item="{ props: itemProps, item }">
|
|
78
|
+
<v-list-item v-bind="itemProps">
|
|
79
|
+
<template #subtitle>
|
|
80
|
+
<span class="text-caption text-grey">{{ item.raw.cardNumber }}</span>
|
|
81
|
+
</template>
|
|
82
|
+
</v-list-item>
|
|
83
|
+
</template>
|
|
84
|
+
</v-select>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="mt-3">
|
|
88
|
+
<InputLabel title="Quantity" />
|
|
89
|
+
<v-text-field
|
|
90
|
+
:model-value="selectedCards.length"
|
|
91
|
+
type="number"
|
|
92
|
+
density="comfortable"
|
|
93
|
+
disabled
|
|
94
|
+
hide-details
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<v-row no-gutters class="ga-3 mt-3">
|
|
99
|
+
<v-col>
|
|
100
|
+
<v-btn
|
|
101
|
+
variant="outlined"
|
|
102
|
+
color="primary"
|
|
103
|
+
block
|
|
104
|
+
prepend-icon="mdi-barcode-scan"
|
|
105
|
+
@click="emit('scan:barcode')"
|
|
106
|
+
>
|
|
107
|
+
Scan Via BarCode
|
|
108
|
+
</v-btn>
|
|
109
|
+
</v-col>
|
|
110
|
+
<v-col>
|
|
111
|
+
<v-btn
|
|
112
|
+
variant="outlined"
|
|
113
|
+
color="primary"
|
|
114
|
+
block
|
|
115
|
+
prepend-icon="mdi-camera"
|
|
116
|
+
@click="emit('scan:camera')"
|
|
117
|
+
>
|
|
118
|
+
Scan with Camera
|
|
119
|
+
</v-btn>
|
|
120
|
+
</v-col>
|
|
121
|
+
</v-row>
|
|
122
|
+
</template>
|
|
123
|
+
</template>
|
|
124
|
+
<v-divider class="mt-6" />
|
|
125
|
+
</div>
|
|
126
|
+
</template>
|
|
127
|
+
|
|
128
|
+
<script setup lang="ts">
|
|
129
|
+
const props = defineProps({
|
|
130
|
+
settings: {
|
|
131
|
+
type: Object as PropType<Record<string, any> | null>,
|
|
132
|
+
default: null,
|
|
133
|
+
},
|
|
134
|
+
loading: {
|
|
135
|
+
type: Boolean,
|
|
136
|
+
default: false,
|
|
137
|
+
},
|
|
138
|
+
modelValue: {
|
|
139
|
+
type: String as PropType<string | null>,
|
|
140
|
+
default: null,
|
|
141
|
+
},
|
|
142
|
+
quantity: {
|
|
143
|
+
type: Number as PropType<number | null>,
|
|
144
|
+
default: null,
|
|
145
|
+
},
|
|
146
|
+
cards: {
|
|
147
|
+
type: Array as PropType<string[]>,
|
|
148
|
+
default: () => [],
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const emit = defineEmits(["update:modelValue", "update:quantity", "update:cards", "scan:barcode", "scan:camera"]);
|
|
153
|
+
|
|
154
|
+
const { getAll: getAllCards } = useCard();
|
|
155
|
+
|
|
156
|
+
const nfcEnabled = computed(
|
|
157
|
+
() => props.settings?.data?.settings?.nfcPass ?? false
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const printer = computed(
|
|
161
|
+
() => props.settings?.data?.settings?.printer ?? { vendorId: null, productId: null }
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const hasPrinter = computed(
|
|
165
|
+
() => !!printer.value.vendorId && !!printer.value.productId
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const passType = computed({
|
|
169
|
+
get: () => props.modelValue,
|
|
170
|
+
set: (val) => emit("update:modelValue", val),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const quantity = computed({
|
|
174
|
+
get: () => props.quantity,
|
|
175
|
+
set: (val) => emit("update:quantity", val),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const selectedCards = computed({
|
|
179
|
+
get: () => props.cards,
|
|
180
|
+
set: (val) => {
|
|
181
|
+
emit("update:cards", val);
|
|
182
|
+
emit("update:quantity", val.length || null);
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const cardItems = ref<TCard[]>([]);
|
|
187
|
+
const cardsLoading = ref(false);
|
|
188
|
+
|
|
189
|
+
const cards = computed(() => cardItems.value);
|
|
190
|
+
|
|
191
|
+
async function fetchCards() {
|
|
192
|
+
cardsLoading.value = true;
|
|
193
|
+
try {
|
|
194
|
+
const res = await getAllCards({ assignFilter: "available", limit: 100 });
|
|
195
|
+
cardItems.value = res?.data ?? [];
|
|
196
|
+
} finally {
|
|
197
|
+
cardsLoading.value = false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
watch(
|
|
202
|
+
() => passType.value,
|
|
203
|
+
(val) => {
|
|
204
|
+
if (val === "NFC") fetchCards();
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
</script>
|
|
208
|
+
|
|
209
|
+
<style scoped>
|
|
210
|
+
.selected-check {
|
|
211
|
+
position: absolute;
|
|
212
|
+
top: 6px;
|
|
213
|
+
right: 6px;
|
|
214
|
+
}
|
|
215
|
+
</style>
|
|
@@ -141,6 +141,14 @@ watch(input, (newInput) => {
|
|
|
141
141
|
else phone.value = prefix + newInput
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
+
watch(
|
|
145
|
+
() => props.modelValue,
|
|
146
|
+
(val) => {
|
|
147
|
+
phone.value = val || ''
|
|
148
|
+
},
|
|
149
|
+
{ immediate: true }
|
|
150
|
+
)
|
|
151
|
+
|
|
144
152
|
|
|
145
153
|
watch(phone, (newVal) => {
|
|
146
154
|
const prefix = phonePrefix.value
|
|
@@ -8,6 +8,13 @@
|
|
|
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
20
|
ref="tableHygieneRef"
|
|
@@ -22,8 +29,8 @@
|
|
|
22
29
|
:loading="loading"
|
|
23
30
|
:no-data-text="`No checklist found`"
|
|
24
31
|
:show-header="true"
|
|
25
|
-
:can-manage-schedule-tasks="canManageScheduleTasks"
|
|
26
|
-
:can-add-remarks="canAddRemarks"
|
|
32
|
+
:can-manage-schedule-tasks="!isScheduleClosed && canManageScheduleTasks"
|
|
33
|
+
:can-add-remarks="!isScheduleClosed && canAddRemarks"
|
|
27
34
|
@refresh="getUnitCleanerChecklistRefresh"
|
|
28
35
|
@update:selected="selectedItems = $event"
|
|
29
36
|
@action-click="handleActionClick"
|
|
@@ -113,6 +120,9 @@ const { back } = useUtils();
|
|
|
113
120
|
const { canAddRemarks, canManageScheduleTasks } =
|
|
114
121
|
useCleaningSchedulePermission();
|
|
115
122
|
|
|
123
|
+
const selectedScheduleStatus = useState<string>('selectedScheduleStatus', () => '')
|
|
124
|
+
const isScheduleClosed = computed(() => selectedScheduleStatus.value.toLowerCase() === 'closed')
|
|
125
|
+
|
|
116
126
|
const page = ref<number>(1);
|
|
117
127
|
const pages = ref<number>(0);
|
|
118
128
|
const pageRange = ref<string>("-- - -- of --");
|
|
@@ -241,6 +251,11 @@ function openCompletionDialog({
|
|
|
241
251
|
approvedItems: Array<{ key: string; item: any; action: "approve" }>;
|
|
242
252
|
lastApprovedKey: string | null;
|
|
243
253
|
}): void {
|
|
254
|
+
if (isScheduleClosed.value) {
|
|
255
|
+
showMessage("This schedule is closed. No actions can be performed.", "error");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
244
259
|
// If user does not need to add remarks, perform approvals immediately
|
|
245
260
|
if (!canAddRemarks.value) {
|
|
246
261
|
// call update directly without opening modal
|
|
@@ -283,6 +298,11 @@ async function handleActionClick(data: {
|
|
|
283
298
|
item: TFlattenedUnitItem;
|
|
284
299
|
action: "approve" | "reject";
|
|
285
300
|
}): Promise<void> {
|
|
301
|
+
if (isScheduleClosed.value) {
|
|
302
|
+
showMessage("This schedule is closed. No actions can be performed.", "error");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
286
306
|
const { item, action } = data;
|
|
287
307
|
|
|
288
308
|
if (!item?.unit || item?.set === undefined) {
|
|
@@ -322,9 +342,15 @@ async function handleActionClick(data: {
|
|
|
322
342
|
await getUnitCleanerChecklistRefresh();
|
|
323
343
|
} catch (error: any) {
|
|
324
344
|
console.error("Error updating unit checklist:", error);
|
|
325
|
-
showMessage(
|
|
345
|
+
showMessage(
|
|
346
|
+
error?.data?.message || error?.message || "Failed to update checklist",
|
|
347
|
+
"error"
|
|
348
|
+
);
|
|
326
349
|
} finally {
|
|
327
350
|
submitting.value = false;
|
|
351
|
+
if (action === "approve") {
|
|
352
|
+
tableHygieneRef.value?.stopLoadingAction(`${item.unit}_${item.set}`);
|
|
353
|
+
}
|
|
328
354
|
}
|
|
329
355
|
}
|
|
330
356
|
|
|
@@ -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>
|
|
@@ -94,6 +94,16 @@
|
|
|
94
94
|
>
|
|
95
95
|
Set {{ group.set }}
|
|
96
96
|
</span>
|
|
97
|
+
<v-chip
|
|
98
|
+
v-if="group.isScheduleTask"
|
|
99
|
+
size="x-small"
|
|
100
|
+
color="info"
|
|
101
|
+
variant="tonal"
|
|
102
|
+
prepend-icon="mdi-calendar-clock"
|
|
103
|
+
class="text-none"
|
|
104
|
+
>
|
|
105
|
+
Schedule Task
|
|
106
|
+
</v-chip>
|
|
97
107
|
<v-chip
|
|
98
108
|
v-if="group.completedByName && isGroupComplete(group)"
|
|
99
109
|
size="x-small"
|
|
@@ -264,6 +274,8 @@
|
|
|
264
274
|
: 'text'
|
|
265
275
|
"
|
|
266
276
|
color="success"
|
|
277
|
+
:loading="!!loadingActions[getKey(item, group.set)]"
|
|
278
|
+
:disabled="!!loadingActions[getKey(item, group.set)]"
|
|
267
279
|
@click.stop="
|
|
268
280
|
handleActionClick(item, group.set, 'approve')
|
|
269
281
|
"
|
|
@@ -448,12 +460,14 @@ const emits = defineEmits([
|
|
|
448
460
|
|
|
449
461
|
defineExpose({
|
|
450
462
|
revertSetApprovals,
|
|
463
|
+
stopLoadingAction,
|
|
451
464
|
});
|
|
452
465
|
|
|
453
466
|
const internalPage = ref(props.page);
|
|
454
467
|
const selected = shallowRef<any[]>(props.selected);
|
|
455
468
|
const activeActions = reactive<Record<string, "approve" | "reject">>({});
|
|
456
469
|
const persistedActions = reactive<Record<string, "approve" | "reject">>({});
|
|
470
|
+
const loadingActions = reactive<Record<string, boolean>>({});
|
|
457
471
|
const completedSets = ref<Set<number>>(new Set());
|
|
458
472
|
const itemOrderMap = new Map<string, number>();
|
|
459
473
|
|
|
@@ -497,6 +511,7 @@ const groupedItems = computed(() => {
|
|
|
497
511
|
|
|
498
512
|
return {
|
|
499
513
|
set: item.set,
|
|
514
|
+
isScheduleTask: item.isScheduleTask ?? false,
|
|
500
515
|
completedByName: item.completedByName ?? null,
|
|
501
516
|
attachments:
|
|
502
517
|
(item.attachment as string[] | undefined) ??
|
|
@@ -709,6 +724,13 @@ function handleActionClick(
|
|
|
709
724
|
return;
|
|
710
725
|
}
|
|
711
726
|
}
|
|
727
|
+
|
|
728
|
+
// Non-completing approve: undo optimistic check and show loading instead
|
|
729
|
+
if (!isPersisted) {
|
|
730
|
+
delete activeActions[key];
|
|
731
|
+
loadingActions[key] = true;
|
|
732
|
+
}
|
|
733
|
+
|
|
712
734
|
console.debug("TableHygiene: emitting action-click (approve immediate)", {
|
|
713
735
|
item: { ...item, set },
|
|
714
736
|
action,
|
|
@@ -717,6 +739,10 @@ function handleActionClick(
|
|
|
717
739
|
}
|
|
718
740
|
}
|
|
719
741
|
|
|
742
|
+
function stopLoadingAction(key: string): void {
|
|
743
|
+
delete loadingActions[key];
|
|
744
|
+
}
|
|
745
|
+
|
|
720
746
|
function revertSetApprovals(setNumber: number): void {
|
|
721
747
|
const group = groupedItems.value.find((g) => g.set === setNumber);
|
|
722
748
|
if (group) {
|
|
@@ -736,6 +762,7 @@ watch(
|
|
|
736
762
|
() => props.items,
|
|
737
763
|
() => {
|
|
738
764
|
completedSets.value.clear();
|
|
765
|
+
Object.keys(loadingActions).forEach((k) => delete loadingActions[k]);
|
|
739
766
|
},
|
|
740
767
|
{ deep: true }
|
|
741
768
|
);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card-text>
|
|
3
|
+
<v-row>
|
|
4
|
+
<v-col cols="12" md="4" class="mt-2">
|
|
5
|
+
<InputLabel class="font-weight-bold" title="Name" />
|
|
6
|
+
<v-text-field :model-value="person.name" density="compact" :readonly="readOnly" :class="{'no-pointer': readOnly}"/>
|
|
7
|
+
</v-col>
|
|
8
|
+
|
|
9
|
+
<v-col cols="12" md="4" class="mt-2">
|
|
10
|
+
<InputLabel class="font-weight-bold" title="Email Address" />
|
|
11
|
+
<v-text-field :model-value="person.email" density="compact" :readonly="readOnly" :class="{'no-pointer': readOnly}"/>
|
|
12
|
+
</v-col>
|
|
13
|
+
|
|
14
|
+
<v-col cols="12" md="4" class="mt-2">
|
|
15
|
+
<InputLabel class="font-weight-bold" title="NRIC" />
|
|
16
|
+
<v-text-field :model-value="person.nric" density="compact" :readonly="readOnly" :class="{'no-pointer': readOnly}"/>
|
|
17
|
+
</v-col>
|
|
18
|
+
|
|
19
|
+
<v-col cols="12" md="4" class="mt-2">
|
|
20
|
+
<InputLabel class="font-weight-bold" title="Mobile Number" />
|
|
21
|
+
<v-text-field :model-value="person.contact" density="compact" :readonly="readOnly" :class="{'no-pointer': readOnly}"/>
|
|
22
|
+
</v-col>
|
|
23
|
+
</v-row>
|
|
24
|
+
|
|
25
|
+
<v-row>
|
|
26
|
+
<v-col cols="12" md="4" class="mt-2">
|
|
27
|
+
<InputLabel class="font-weight-bold" title="Other Documents and Files" :viewMode="readOnly" />
|
|
28
|
+
|
|
29
|
+
<p
|
|
30
|
+
v-if="!person.files?.length"
|
|
31
|
+
class="text-blue-darken-2 font-weight-thin text-caption ml-2 mt-5"
|
|
32
|
+
>
|
|
33
|
+
**No documents to display**
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<InputFileV2
|
|
37
|
+
v-if="person.files?.length"
|
|
38
|
+
:files="person.files"
|
|
39
|
+
:viewMode="true"
|
|
40
|
+
/>
|
|
41
|
+
</v-col>
|
|
42
|
+
</v-row>
|
|
43
|
+
</v-card-text>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script setup lang="ts">
|
|
47
|
+
defineProps({
|
|
48
|
+
person: {
|
|
49
|
+
type: Object as PropType<TPeople>,
|
|
50
|
+
required: true,
|
|
51
|
+
},
|
|
52
|
+
readOnly: {
|
|
53
|
+
type: Boolean,
|
|
54
|
+
default: false,
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<style lang="scss" scoped>
|
|
60
|
+
.no-pointer {
|
|
61
|
+
pointer-events: none;
|
|
62
|
+
}
|
|
63
|
+
</style>
|
|
@@ -40,7 +40,7 @@ const prop = defineProps({
|
|
|
40
40
|
|
|
41
41
|
const emit = defineEmits(['cancel', 'select']);
|
|
42
42
|
|
|
43
|
-
const selection = computed<{label: string, value: TVehicleType}[]>(() => {
|
|
43
|
+
const selection = computed<{label: string, value: TVehicleType | 'seasonpass'}[]>(() => {
|
|
44
44
|
return [
|
|
45
45
|
{ label: "Whitelist", value: "whitelist" },
|
|
46
46
|
{ label: "Season Pass", value: "seasonpass" },
|
|
@@ -52,7 +52,7 @@ function cancel() {
|
|
|
52
52
|
emit("cancel");
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function select(value:
|
|
55
|
+
function select(value: TVehicleType | 'seasonpass') {
|
|
56
56
|
emit("select", value);
|
|
57
57
|
}
|
|
58
58
|
</script>
|