@7365admin1/layer-common 1.10.5 → 1.10.6
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/AccessCardDetailsDialog.vue +144 -0
- package/components/AccessCardPreviewDialog.vue +7 -2
- package/components/AccessManagement.vue +5 -1
- package/components/Input/InputPhoneNumberV2.vue +3 -0
- package/components/PlateNumberDisplay.vue +9 -1
- package/components/TableHygiene.vue +238 -113
- package/components/VehicleForm.vue +101 -38
- package/components/VehicleManagement.vue +11 -9
- package/components/VisitorManagement.vue +1 -1
- package/components/WorkOrder/Main.vue +2 -1
- package/composables/useAccessManagement.ts +11 -0
- package/composables/useFeedback.ts +2 -2
- package/composables/usePeople.ts +10 -0
- package/composables/useWorkOrder.ts +2 -2
- package/package.json +1 -1
- package/types/people.d.ts +3 -0
- package/types/vehicle.d.ts +1 -1
- package/components/AccessCardHistoryDialog.vue +0 -133
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog :model-value="modelValue" width="480" persistent>
|
|
3
|
+
<v-card width="100%">
|
|
4
|
+
<v-toolbar density="compact" color="black">
|
|
5
|
+
<v-toolbar-title class="text-subtitle-1 font-weight-medium">
|
|
6
|
+
Card Details — {{ card?.cardNo ?? "N/A" }}
|
|
7
|
+
</v-toolbar-title>
|
|
8
|
+
<v-btn icon @click="emit('update:modelValue', false)">
|
|
9
|
+
<v-icon>mdi-close</v-icon>
|
|
10
|
+
</v-btn>
|
|
11
|
+
</v-toolbar>
|
|
12
|
+
|
|
13
|
+
<v-card-text style="max-height: 70vh; overflow-y: auto" class="pa-4">
|
|
14
|
+
<div v-if="pending" class="d-flex justify-center align-center py-8">
|
|
15
|
+
<v-progress-circular indeterminate color="black" />
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div v-else-if="!details" class="text-center text-grey py-8">
|
|
19
|
+
No details available.
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div v-else class="d-flex flex-column ga-3">
|
|
23
|
+
<div>
|
|
24
|
+
<div class="text-caption text-grey">Card No</div>
|
|
25
|
+
<div class="text-body-2 font-weight-medium">{{ details.cardNo ?? "N/A" }}</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div>
|
|
29
|
+
<div class="text-caption text-grey">Type</div>
|
|
30
|
+
<div class="text-body-2 font-weight-medium">{{ details.type ?? "N/A" }}</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div>
|
|
34
|
+
<div class="text-caption text-grey">Status</div>
|
|
35
|
+
<v-chip
|
|
36
|
+
:color="statusColor(details.status)"
|
|
37
|
+
variant="flat"
|
|
38
|
+
size="x-small"
|
|
39
|
+
class="text-capitalize font-weight-medium mt-1"
|
|
40
|
+
>
|
|
41
|
+
{{ details.status ?? "N/A" }}
|
|
42
|
+
</v-chip>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div>
|
|
46
|
+
<div class="text-caption text-grey">Activated</div>
|
|
47
|
+
<div class="text-body-2 font-weight-medium">{{ details.isActivated ? "Yes" : "No" }}</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div>
|
|
51
|
+
<div class="text-caption text-grey">Site</div>
|
|
52
|
+
<div class="text-body-2 font-weight-medium">{{ details.site?.name ?? "N/A" }}</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div>
|
|
56
|
+
<div class="text-caption text-grey">User</div>
|
|
57
|
+
<div class="text-body-2 font-weight-medium">{{ details.user?.name ?? "N/A" }} ({{ details.user?.email ?? "N/A" }})</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div>
|
|
61
|
+
<div class="text-caption text-grey">Created At</div>
|
|
62
|
+
<div class="text-body-2 font-weight-medium">{{ formatDate(details.createdAt) }}</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div>
|
|
66
|
+
<div class="text-caption text-grey">Updated At</div>
|
|
67
|
+
<div class="text-body-2 font-weight-medium">{{ formatDate(details.updatedAt) }}</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div v-if="details.remarks">
|
|
71
|
+
<div class="text-caption text-grey">Remarks</div>
|
|
72
|
+
<div class="text-body-2 font-weight-medium">{{ details.remarks }}</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</v-card-text>
|
|
76
|
+
</v-card>
|
|
77
|
+
</v-dialog>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<script setup lang="ts">
|
|
81
|
+
const props = defineProps({
|
|
82
|
+
modelValue: {
|
|
83
|
+
type: Boolean,
|
|
84
|
+
default: false,
|
|
85
|
+
},
|
|
86
|
+
card: {
|
|
87
|
+
type: Object as PropType<Record<string, any> | null>,
|
|
88
|
+
default: null,
|
|
89
|
+
},
|
|
90
|
+
siteId: {
|
|
91
|
+
type: String,
|
|
92
|
+
default: "",
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const emit = defineEmits<{
|
|
97
|
+
"update:modelValue": [value: boolean];
|
|
98
|
+
}>();
|
|
99
|
+
|
|
100
|
+
const { getCardDetails } = useAccessManagement();
|
|
101
|
+
|
|
102
|
+
const details = ref<Record<string, any> | null>(null);
|
|
103
|
+
const pending = ref(false);
|
|
104
|
+
|
|
105
|
+
watch(
|
|
106
|
+
() => props.modelValue,
|
|
107
|
+
async (val) => {
|
|
108
|
+
if (val && props.card?._id && props.siteId) {
|
|
109
|
+
pending.value = true;
|
|
110
|
+
try {
|
|
111
|
+
const res = await getCardDetails({ siteId: props.siteId, cardId: props.card._id });
|
|
112
|
+
details.value = res?.data ?? null;
|
|
113
|
+
} finally {
|
|
114
|
+
pending.value = false;
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
details.value = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
function statusColor(status: string) {
|
|
123
|
+
const map: Record<string, string> = {
|
|
124
|
+
active: "success",
|
|
125
|
+
assigned: "primary",
|
|
126
|
+
replaced: "orange",
|
|
127
|
+
deleted: "error",
|
|
128
|
+
available: "grey",
|
|
129
|
+
};
|
|
130
|
+
return map[status] ?? "grey";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function formatDate(date: string) {
|
|
134
|
+
if (!date) return "N/A";
|
|
135
|
+
return new Intl.DateTimeFormat("en-GB", {
|
|
136
|
+
day: "2-digit",
|
|
137
|
+
month: "short",
|
|
138
|
+
year: "numeric",
|
|
139
|
+
hour: "2-digit",
|
|
140
|
+
minute: "2-digit",
|
|
141
|
+
hour12: true,
|
|
142
|
+
}).format(new Date(date));
|
|
143
|
+
}
|
|
144
|
+
</script>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<AccessCardDetailsDialog
|
|
3
3
|
v-model="historyDialog"
|
|
4
4
|
:card="selectedCardInUnit"
|
|
5
|
+
:site-id="siteId"
|
|
5
6
|
/>
|
|
6
7
|
|
|
7
8
|
<v-dialog :model-value="modelValue" width="450" persistent>
|
|
@@ -221,7 +222,7 @@
|
|
|
221
222
|
</v-list-item>
|
|
222
223
|
<v-list-item :disabled="!isSelectedCardPhysical" @click="historyDialog = true">
|
|
223
224
|
<v-list-item-title class="text-subtitle-2 cursor-pointer">
|
|
224
|
-
Card
|
|
225
|
+
Card Details
|
|
225
226
|
</v-list-item-title>
|
|
226
227
|
</v-list-item>
|
|
227
228
|
<v-list-item
|
|
@@ -277,6 +278,10 @@ const props = defineProps({
|
|
|
277
278
|
type: Boolean,
|
|
278
279
|
default: false,
|
|
279
280
|
},
|
|
281
|
+
siteId: {
|
|
282
|
+
type: String,
|
|
283
|
+
default: "",
|
|
284
|
+
},
|
|
280
285
|
});
|
|
281
286
|
|
|
282
287
|
const emit = defineEmits<{
|
|
@@ -112,6 +112,7 @@
|
|
|
112
112
|
:can-delete-access-card="canDeleteAccessCard"
|
|
113
113
|
:is-selected-card-assigned-physical="isSelectedCardAssignedPhysical"
|
|
114
114
|
:is-selected-card-physical="isSelectedCardPhysical"
|
|
115
|
+
:site-id="siteId"
|
|
115
116
|
@replace="openReplaceDialog()"
|
|
116
117
|
@delete="openDeleteDialog()"
|
|
117
118
|
/>
|
|
@@ -354,8 +355,11 @@ async function handleDeleteCard(remarks: string) {
|
|
|
354
355
|
selectedCardId.value = null;
|
|
355
356
|
confirmDialog.value = false;
|
|
356
357
|
previewDialog.value = false;
|
|
358
|
+
showMessage("Access card deleted successfully!", "success");
|
|
357
359
|
} catch (error: any) {
|
|
358
|
-
|
|
360
|
+
const msg = error?.response?._data?.message || "Failed to delete card";
|
|
361
|
+
deleteError.value = msg;
|
|
362
|
+
showMessage(msg, "error");
|
|
359
363
|
} finally {
|
|
360
364
|
deleteLoading.value = false;
|
|
361
365
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
style="max-width: 95px"
|
|
14
14
|
:rules="[...props.rules]"
|
|
15
15
|
:readonly="props.readOnly"
|
|
16
|
+
:disabled="props.disabled"
|
|
16
17
|
@update:model-value="handleUpdateCountry"
|
|
17
18
|
>
|
|
18
19
|
<template v-slot:item="{ props: itemProps, item }">
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
:prefix="phonePrefix || ''"
|
|
41
42
|
persistent-placeholder
|
|
42
43
|
:density="density"
|
|
44
|
+
:disabled="props.disabled"
|
|
43
45
|
:placeholder="placeholder || currentMask"
|
|
44
46
|
/>
|
|
45
47
|
</v-col>
|
|
@@ -64,6 +66,7 @@ const props = defineProps({
|
|
|
64
66
|
hideDetails: { type: Boolean, default: false },
|
|
65
67
|
loading: { type: Boolean, default: false },
|
|
66
68
|
readOnly: { type: Boolean, default: false },
|
|
69
|
+
disabled: { type: Boolean, default: false },
|
|
67
70
|
})
|
|
68
71
|
|
|
69
72
|
const emit = defineEmits(['update:modelValue'])
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</span>
|
|
6
6
|
|
|
7
7
|
<v-chip
|
|
8
|
-
v-if="extraCount > 0"
|
|
8
|
+
v-if="extraCount > 0 && !showAll"
|
|
9
9
|
density="comfortable"
|
|
10
10
|
size="small"
|
|
11
11
|
>
|
|
@@ -23,12 +23,20 @@ const props = defineProps({
|
|
|
23
23
|
defaultValue: {
|
|
24
24
|
type: String,
|
|
25
25
|
default: ""
|
|
26
|
+
},
|
|
27
|
+
showAll: {
|
|
28
|
+
type: Boolean,
|
|
29
|
+
default: false
|
|
26
30
|
}
|
|
27
31
|
})
|
|
28
32
|
|
|
29
33
|
const formatted = computed(() => {
|
|
30
34
|
if (!props.plateNumbers?.length) return props.defaultValue || ""
|
|
31
35
|
|
|
36
|
+
if (props.showAll) {
|
|
37
|
+
return props.plateNumbers.map((v: any) => v?.plateNumber || "").join(", ")
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
const firstTwo = props.plateNumbers
|
|
33
41
|
.slice(0, 2)
|
|
34
42
|
.map((v: any) => v?.plateNumber || "")
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-row no-gutters>
|
|
3
|
-
<!-- Top Actions -->
|
|
4
3
|
<v-col cols="12" class="mb-2" v-if="canCreate || $slots.actions">
|
|
5
4
|
<v-row no-gutters>
|
|
6
5
|
<slot name="actions">
|
|
@@ -18,7 +17,6 @@
|
|
|
18
17
|
</v-row>
|
|
19
18
|
</v-col>
|
|
20
19
|
|
|
21
|
-
<!-- List Card -->
|
|
22
20
|
<v-col cols="12">
|
|
23
21
|
<v-card
|
|
24
22
|
width="100%"
|
|
@@ -27,7 +25,6 @@
|
|
|
27
25
|
rounded="lg"
|
|
28
26
|
:loading="loading"
|
|
29
27
|
>
|
|
30
|
-
<!-- Toolbar -->
|
|
31
28
|
<v-toolbar
|
|
32
29
|
density="compact"
|
|
33
30
|
color="grey-lighten-4"
|
|
@@ -40,7 +37,10 @@
|
|
|
40
37
|
<slot name="prepend-additional" />
|
|
41
38
|
</template>
|
|
42
39
|
|
|
43
|
-
<v-toolbar-title
|
|
40
|
+
<v-toolbar-title
|
|
41
|
+
v-if="title"
|
|
42
|
+
class="text-subtitle-1 font-weight-medium"
|
|
43
|
+
>
|
|
44
44
|
{{ title }}
|
|
45
45
|
</v-toolbar-title>
|
|
46
46
|
|
|
@@ -64,126 +64,224 @@
|
|
|
64
64
|
|
|
65
65
|
<v-divider />
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
<v-sheet
|
|
68
|
+
:max-height="`calc(100vh - (${offset}px))`"
|
|
69
|
+
class="overflow-y-auto"
|
|
70
70
|
>
|
|
71
|
-
<v-
|
|
71
|
+
<v-row
|
|
72
72
|
v-if="groupedItems.length === 0"
|
|
73
|
-
|
|
73
|
+
no-gutters
|
|
74
|
+
justify="center"
|
|
75
|
+
class="py-10"
|
|
74
76
|
>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
<v-col
|
|
78
|
+
cols="auto"
|
|
79
|
+
class="text-center text-medium-emphasis text-body-2"
|
|
80
|
+
>
|
|
81
|
+
{{ noDataText }}
|
|
82
|
+
</v-col>
|
|
83
|
+
</v-row>
|
|
77
84
|
|
|
78
85
|
<template
|
|
79
86
|
v-for="(group, groupIndex) in groupedItems"
|
|
80
87
|
:key="`group-${groupIndex}`"
|
|
81
88
|
>
|
|
82
|
-
<
|
|
83
|
-
v-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
89
|
+
<v-sheet color="grey-lighten-4" border="b t">
|
|
90
|
+
<v-row no-gutters align="center" class="px-4 py-2">
|
|
91
|
+
<v-col cols="auto" class="d-flex align-center ga-2">
|
|
92
|
+
<span
|
|
93
|
+
class="text-caption font-weight-bold text-medium-emphasis text-uppercase"
|
|
94
|
+
>
|
|
95
|
+
Set {{ group.set }}
|
|
96
|
+
</span>
|
|
97
|
+
<v-chip
|
|
98
|
+
v-if="group.completedByName && isGroupComplete(group)"
|
|
99
|
+
size="x-small"
|
|
100
|
+
color="success"
|
|
101
|
+
variant="tonal"
|
|
102
|
+
prepend-icon="mdi-check-circle-outline"
|
|
103
|
+
class="text-none"
|
|
104
|
+
>
|
|
105
|
+
Completed · {{ group.completedByName }}
|
|
106
|
+
</v-chip>
|
|
107
|
+
<v-chip
|
|
108
|
+
v-else-if="
|
|
109
|
+
group.completedByName && isGroupInProgress(group)
|
|
110
|
+
"
|
|
111
|
+
size="x-small"
|
|
112
|
+
color="warning"
|
|
113
|
+
variant="tonal"
|
|
114
|
+
prepend-icon="mdi-progress-clock"
|
|
115
|
+
class="text-none"
|
|
116
|
+
>
|
|
117
|
+
Ongoing · {{ group.completedByName }}
|
|
118
|
+
</v-chip>
|
|
119
|
+
</v-col>
|
|
120
|
+
<v-spacer />
|
|
121
|
+
<v-col cols="auto">
|
|
122
|
+
<v-btn
|
|
123
|
+
v-if="group.attachments && group.attachments.length > 0"
|
|
124
|
+
size="x-small"
|
|
125
|
+
variant="tonal"
|
|
126
|
+
color="primary"
|
|
127
|
+
class="text-none"
|
|
128
|
+
prepend-icon="mdi-paperclip"
|
|
129
|
+
@click.stop="
|
|
130
|
+
openAttachmentDialog(group.set, group.attachments)
|
|
131
|
+
"
|
|
132
|
+
>
|
|
133
|
+
{{ group.attachments.length }} attachment{{
|
|
134
|
+
group.attachments.length > 1 ? "s" : ""
|
|
135
|
+
}}
|
|
136
|
+
</v-btn>
|
|
137
|
+
</v-col>
|
|
138
|
+
</v-row>
|
|
139
|
+
</v-sheet>
|
|
140
|
+
|
|
141
|
+
<v-sheet
|
|
109
142
|
v-for="item in group.items"
|
|
110
143
|
:key="item[itemValue]"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
isItemSelected(item, group.set)
|
|
114
|
-
? ['bg-grey-lighten-4', 'rounded']
|
|
115
|
-
: []
|
|
144
|
+
:color="
|
|
145
|
+
isItemSelected(item, group.set) ? 'grey-lighten-4' : 'white'
|
|
116
146
|
"
|
|
147
|
+
border="b"
|
|
117
148
|
>
|
|
118
|
-
<
|
|
119
|
-
<v-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
name="list-item
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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')
|
|
149
|
+
<v-row no-gutters align="center" class="px-4 py-2">
|
|
150
|
+
<v-col cols="auto" class="mr-3">
|
|
151
|
+
<v-icon
|
|
152
|
+
size="20"
|
|
153
|
+
:color="
|
|
154
|
+
activeActions[getKey(item, group.set)] === 'approve'
|
|
155
|
+
? 'success'
|
|
156
|
+
: activeActions[getKey(item, group.set)] === 'reject'
|
|
157
|
+
? 'error'
|
|
158
|
+
: 'grey-lighten-2'
|
|
159
|
+
"
|
|
160
|
+
>
|
|
161
|
+
{{
|
|
162
|
+
activeActions[getKey(item, group.set)] === "approve"
|
|
163
|
+
? "mdi-check-circle"
|
|
164
|
+
: activeActions[getKey(item, group.set)] === "reject"
|
|
165
|
+
? "mdi-close-circle"
|
|
166
|
+
: "mdi-circle-outline"
|
|
167
|
+
}}
|
|
168
|
+
</v-icon>
|
|
169
|
+
</v-col>
|
|
170
|
+
<v-col>
|
|
171
|
+
<slot name="list-item" :item="item">
|
|
172
|
+
<v-row no-gutters align-center>
|
|
173
|
+
<v-col cols="12">
|
|
174
|
+
<span
|
|
175
|
+
class="text-body-2 font-weight-medium"
|
|
176
|
+
:class="
|
|
177
|
+
activeActions[getKey(item, group.set)] === 'approve'
|
|
178
|
+
? 'text-decoration-line-through text-medium-emphasis'
|
|
179
|
+
: ''
|
|
180
|
+
"
|
|
181
|
+
>
|
|
182
|
+
{{ getItemValue(item, headers[0].value) }}
|
|
183
|
+
</span>
|
|
184
|
+
</v-col>
|
|
185
|
+
<v-col
|
|
186
|
+
v-if="
|
|
187
|
+
item.timestamp ||
|
|
188
|
+
(headers[1] &&
|
|
189
|
+
getItemValue(item, headers[1].value)) ||
|
|
190
|
+
(headers[2] && getItemValue(item, headers[2].value))
|
|
171
191
|
"
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
192
|
+
cols="12"
|
|
193
|
+
>
|
|
194
|
+
<v-row
|
|
195
|
+
no-gutters
|
|
196
|
+
align="center"
|
|
197
|
+
class="ga-3 flex-wrap mt-1"
|
|
198
|
+
>
|
|
199
|
+
<v-col
|
|
200
|
+
v-if="item.timestamp"
|
|
201
|
+
cols="auto"
|
|
202
|
+
class="d-flex align-center ga-1 text-caption text-medium-emphasis pa-0"
|
|
203
|
+
>
|
|
204
|
+
<v-icon size="11">mdi-clock-outline</v-icon>
|
|
205
|
+
{{ formatTimestamp(item.timestamp) }}
|
|
206
|
+
</v-col>
|
|
207
|
+
<v-col
|
|
208
|
+
v-if="
|
|
209
|
+
headers[1] && getItemValue(item, headers[1].value)
|
|
210
|
+
"
|
|
211
|
+
cols="auto"
|
|
212
|
+
class="text-caption text-medium-emphasis pa-0"
|
|
213
|
+
>
|
|
214
|
+
{{ getItemValue(item, headers[1].value) }}
|
|
215
|
+
</v-col>
|
|
216
|
+
<v-col
|
|
217
|
+
v-if="
|
|
218
|
+
headers[2] && getItemValue(item, headers[2].value)
|
|
219
|
+
"
|
|
220
|
+
cols="auto"
|
|
221
|
+
class="text-caption text-medium-emphasis pa-0"
|
|
222
|
+
>
|
|
223
|
+
{{ getItemValue(item, headers[2].value) }}
|
|
224
|
+
</v-col>
|
|
225
|
+
</v-row>
|
|
226
|
+
</v-col>
|
|
227
|
+
</v-row>
|
|
228
|
+
</slot>
|
|
229
|
+
</v-col>
|
|
230
|
+
|
|
231
|
+
<v-col cols="auto">
|
|
232
|
+
<slot
|
|
233
|
+
name="list-item-append"
|
|
234
|
+
:item="item"
|
|
235
|
+
:isSelected="isItemSelected(item, group.set)"
|
|
236
|
+
>
|
|
237
|
+
<v-row
|
|
238
|
+
v-if="canManageScheduleTasks"
|
|
239
|
+
no-gutters
|
|
240
|
+
align="center"
|
|
241
|
+
>
|
|
242
|
+
<v-col cols="auto">
|
|
243
|
+
<v-btn
|
|
244
|
+
icon="mdi-close"
|
|
245
|
+
size="small"
|
|
246
|
+
:variant="
|
|
247
|
+
activeActions[getKey(item, group.set)] === 'reject'
|
|
248
|
+
? 'flat'
|
|
249
|
+
: 'text'
|
|
250
|
+
"
|
|
251
|
+
color="error"
|
|
252
|
+
@click.stop="
|
|
253
|
+
handleActionClick(item, group.set, 'reject')
|
|
254
|
+
"
|
|
255
|
+
/>
|
|
256
|
+
</v-col>
|
|
257
|
+
<v-col cols="auto">
|
|
258
|
+
<v-btn
|
|
259
|
+
icon="mdi-check"
|
|
260
|
+
size="small"
|
|
261
|
+
:variant="
|
|
262
|
+
activeActions[getKey(item, group.set)] === 'approve'
|
|
263
|
+
? 'flat'
|
|
264
|
+
: 'text'
|
|
265
|
+
"
|
|
266
|
+
color="success"
|
|
267
|
+
@click.stop="
|
|
268
|
+
handleActionClick(item, group.set, 'approve')
|
|
269
|
+
"
|
|
270
|
+
/>
|
|
271
|
+
</v-col>
|
|
272
|
+
</v-row>
|
|
273
|
+
</slot>
|
|
274
|
+
</v-col>
|
|
275
|
+
</v-row>
|
|
276
|
+
</v-sheet>
|
|
178
277
|
</template>
|
|
179
|
-
</v-
|
|
278
|
+
</v-sheet>
|
|
180
279
|
|
|
181
280
|
<slot name="footer" />
|
|
182
281
|
</v-card>
|
|
183
282
|
</v-col>
|
|
184
283
|
</v-row>
|
|
185
284
|
|
|
186
|
-
<!-- Attachment Preview Dialog -->
|
|
187
285
|
<v-dialog v-model="showAttachmentDialog" max-width="700" scrollable>
|
|
188
286
|
<v-card>
|
|
189
287
|
<v-card-title class="d-flex align-center pa-4">
|
|
@@ -248,7 +346,6 @@
|
|
|
248
346
|
</v-card>
|
|
249
347
|
</v-dialog>
|
|
250
348
|
|
|
251
|
-
<!-- Full Image Lightbox -->
|
|
252
349
|
<v-dialog v-model="showLightbox" max-width="900">
|
|
253
350
|
<v-card>
|
|
254
351
|
<v-card-actions class="pa-2 justify-end">
|
|
@@ -431,6 +528,34 @@ const allItemsApproved = computed(() => {
|
|
|
431
528
|
);
|
|
432
529
|
});
|
|
433
530
|
|
|
531
|
+
function formatTimestamp(ts: string): string {
|
|
532
|
+
if (!ts) return "";
|
|
533
|
+
const date = new Date(ts);
|
|
534
|
+
return date.toLocaleString("en-SG", {
|
|
535
|
+
day: "2-digit",
|
|
536
|
+
month: "short",
|
|
537
|
+
year: "numeric",
|
|
538
|
+
hour: "2-digit",
|
|
539
|
+
minute: "2-digit",
|
|
540
|
+
hour12: true,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function isGroupComplete(group: { items: any[] }): boolean {
|
|
545
|
+
return (
|
|
546
|
+
group.items.length > 0 &&
|
|
547
|
+
group.items.every((item: any) => item.approve === true)
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function isGroupInProgress(group: { items: any[] }): boolean {
|
|
552
|
+
return (
|
|
553
|
+
group.items.some(
|
|
554
|
+
(item: any) => item.approve === true || item.reject === true
|
|
555
|
+
) && !isGroupComplete(group)
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
434
559
|
function isSetFullyApproved(setNumber: number): boolean {
|
|
435
560
|
const group = groupedItems.value.find((g) => g.set === setNumber);
|
|
436
561
|
if (!group) return false;
|
|
@@ -442,7 +567,7 @@ function isSetFullyApproved(setNumber: number): boolean {
|
|
|
442
567
|
}
|
|
443
568
|
|
|
444
569
|
function getNewApprovedItemsForSet(
|
|
445
|
-
setNumber: number
|
|
570
|
+
setNumber: number
|
|
446
571
|
): Array<{ key: string; item: any; action: "approve" }> {
|
|
447
572
|
const group = groupedItems.value.find((g) => g.set === setNumber);
|
|
448
573
|
if (!group) return [];
|
|
@@ -472,14 +597,14 @@ watch(
|
|
|
472
597
|
internalPage.value = val;
|
|
473
598
|
|
|
474
599
|
itemOrderMap.clear();
|
|
475
|
-
}
|
|
600
|
+
}
|
|
476
601
|
);
|
|
477
602
|
|
|
478
603
|
watch(
|
|
479
604
|
() => props.selected,
|
|
480
605
|
(val) => {
|
|
481
606
|
selected.value = val;
|
|
482
|
-
}
|
|
607
|
+
}
|
|
483
608
|
);
|
|
484
609
|
|
|
485
610
|
watch(selected, (val) => {
|
|
@@ -492,7 +617,7 @@ watch(
|
|
|
492
617
|
if (!items || !Array.isArray(items)) return;
|
|
493
618
|
|
|
494
619
|
Object.keys(persistedActions).forEach(
|
|
495
|
-
(key) => delete persistedActions[key]
|
|
620
|
+
(key) => delete persistedActions[key]
|
|
496
621
|
);
|
|
497
622
|
|
|
498
623
|
items.forEach((group: any) => {
|
|
@@ -511,7 +636,7 @@ watch(
|
|
|
511
636
|
});
|
|
512
637
|
});
|
|
513
638
|
},
|
|
514
|
-
{ immediate: true }
|
|
639
|
+
{ immediate: true }
|
|
515
640
|
);
|
|
516
641
|
|
|
517
642
|
function getKey(item: any, set?: number): string {
|
|
@@ -530,7 +655,7 @@ function isItemSelected(item: any, set?: number): boolean {
|
|
|
530
655
|
|
|
531
656
|
if (typeof selected.value[0] === "object" && "unit" in selected.value[0]) {
|
|
532
657
|
return selected.value.some(
|
|
533
|
-
(s: any) => s.unit === item[props.itemValue] && s.set === set
|
|
658
|
+
(s: any) => s.unit === item[props.itemValue] && s.set === set
|
|
534
659
|
);
|
|
535
660
|
}
|
|
536
661
|
|
|
@@ -540,7 +665,7 @@ function isItemSelected(item: any, set?: number): boolean {
|
|
|
540
665
|
function handleActionClick(
|
|
541
666
|
item: any,
|
|
542
667
|
set: number | undefined,
|
|
543
|
-
action: "approve" | "reject"
|
|
668
|
+
action: "approve" | "reject"
|
|
544
669
|
): void {
|
|
545
670
|
const key = getKey(item, set);
|
|
546
671
|
|
|
@@ -612,6 +737,6 @@ watch(
|
|
|
612
737
|
() => {
|
|
613
738
|
completedSets.value.clear();
|
|
614
739
|
},
|
|
615
|
-
{ deep: true }
|
|
740
|
+
{ deep: true }
|
|
616
741
|
);
|
|
617
742
|
</script>
|
|
@@ -32,47 +32,53 @@
|
|
|
32
32
|
</v-combobox>
|
|
33
33
|
</v-col>
|
|
34
34
|
|
|
35
|
+
<v-col v-if="shouldShowField('nric')" cols="12">
|
|
36
|
+
<InputLabel class="text-capitalize" title="NRIC" required />
|
|
37
|
+
<InputNRICNumber v-model="vehicle.nric" density="comfortable" :rules="[requiredRule]" />
|
|
38
|
+
</v-col>
|
|
39
|
+
|
|
35
40
|
<v-col v-if="shouldShowField('name')" cols="12">
|
|
36
41
|
<v-row>
|
|
37
42
|
<v-col cols="12">
|
|
38
43
|
<InputLabel class="text-capitalize" title="Full Name" required />
|
|
39
|
-
<v-text-field v-model.trim="vehicle.name" density="comfortable" :rules="[requiredRule]" />
|
|
44
|
+
<v-text-field v-model.trim="vehicle.name" density="comfortable" :rules="[requiredRule]" :disabled="disablePrefilledInputs" />
|
|
40
45
|
</v-col>
|
|
41
46
|
</v-row>
|
|
42
47
|
</v-col>
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
<InputLabel class="text-capitalize" title="NRIC" required />
|
|
46
|
-
<InputNRICNumber v-model="vehicle.nric" density="comfortable" :rules="[requiredRule]" />
|
|
47
|
-
</v-col>
|
|
49
|
+
|
|
48
50
|
|
|
49
51
|
<v-col v-if="shouldShowField('phone')" cols="12">
|
|
50
52
|
<InputLabel class="text-capitalize" title="Phone Number" required />
|
|
51
|
-
<InputPhoneNumberV2 v-model="vehicle.phoneNumber" density="comfortable" :rules="[requiredRule]" />
|
|
53
|
+
<InputPhoneNumberV2 v-model="vehicle.phoneNumber" density="comfortable" :rules="[requiredRule]" :disabled="disablePrefilledInputs" />
|
|
52
54
|
</v-col>
|
|
53
55
|
|
|
54
56
|
<v-col v-if="shouldShowField('block')" cols="12">
|
|
55
57
|
<InputLabel class="text-capitalize" title="Block" required />
|
|
56
58
|
<v-select v-model="vehicle.block" :items="blocksArray" item-value="value" item-title="title"
|
|
57
|
-
@update:model-value="handleChangeBlock" density="comfortable" :rules="[requiredRule]" />
|
|
59
|
+
@update:model-value="handleChangeBlock" density="comfortable" :rules="[requiredRule]" :disabled="disablePrefilledInputs" />
|
|
58
60
|
</v-col>
|
|
59
61
|
|
|
60
62
|
<v-col v-if="shouldShowField('level')" cols="12">
|
|
61
63
|
<InputLabel class="text-capitalize" title="Level" required />
|
|
62
|
-
<v-select v-model="vehicle.level" :items="levelsArray" density="comfortable" :disabled="!vehicle.block"
|
|
64
|
+
<v-select v-model="vehicle.level" :items="levelsArray" density="comfortable" :disabled="!vehicle.block || disablePrefilledInputs"
|
|
63
65
|
@update:model-value="handleChangeLevel" :rules="[requiredRule]" />
|
|
64
66
|
</v-col>
|
|
65
67
|
|
|
66
68
|
<v-col v-if="shouldShowField('unit')" cols="12">
|
|
67
69
|
<InputLabel class="text-capitalize" title="Unit" required />
|
|
68
|
-
<v-select v-model="vehicle.unit" :items="unitsArray" density="comfortable" :disabled="!vehicle.level"
|
|
70
|
+
<v-select v-model="vehicle.unit" :items="unitsArray" density="comfortable" :disabled="!vehicle.level || disablePrefilledInputs"
|
|
69
71
|
:rules="[requiredRule]" />
|
|
70
72
|
</v-col>
|
|
71
73
|
|
|
72
74
|
<v-col v-if="shouldShowField('plateNumber')" cols="12">
|
|
73
|
-
<InputLabel class="text-capitalize" title="Vehicle
|
|
75
|
+
<InputLabel class="text-capitalize" title="Vehicle Numbers" required />
|
|
74
76
|
<!-- <v-text-field v-model="vehicle.plateNumber" density="comfortable" :rules="[requiredRule]" /> -->
|
|
75
|
-
|
|
77
|
+
<template v-for="plate in vehicle.plates" :key="plate.plateNumber">
|
|
78
|
+
<v-text-field v-model="plate.plateNumber" density="comfortable" :rules="[requiredRule]" class="mb-2" read-only />
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<InputVehicleNumber v-model="newPlateNumber" density="comfortable" :rules="[requiredRule]" />
|
|
76
82
|
</v-col>
|
|
77
83
|
|
|
78
84
|
<v-col v-if="shouldShowField('remarks')" cols="12">
|
|
@@ -149,6 +155,44 @@
|
|
|
149
155
|
</v-col>
|
|
150
156
|
</v-row>
|
|
151
157
|
</v-toolbar>
|
|
158
|
+
|
|
159
|
+
<v-dialog v-model="showMatchingPeopleDialog" max-width="700">
|
|
160
|
+
<v-card>
|
|
161
|
+
<v-toolbar>
|
|
162
|
+
<v-toolbar-title>
|
|
163
|
+
Existing Records Found
|
|
164
|
+
</v-toolbar-title>
|
|
165
|
+
</v-toolbar>
|
|
166
|
+
|
|
167
|
+
<v-card-text>
|
|
168
|
+
|
|
169
|
+
<v-list lines="three">
|
|
170
|
+
<v-list-item v-for="v in matchingPeople" :key="v._id" class="cursor-pointer">
|
|
171
|
+
<v-list-item-title>
|
|
172
|
+
{{ v.name }}
|
|
173
|
+
</v-list-item-title>
|
|
174
|
+
|
|
175
|
+
<v-list-item-subtitle>
|
|
176
|
+
Block {{ v.block }} - {{ v.level }} - {{ v.unitName }}
|
|
177
|
+
</v-list-item-subtitle>
|
|
178
|
+
|
|
179
|
+
<div class="mt-1">
|
|
180
|
+
<v-chip v-for="p in v.plates" :key="p?.plateNumber" size="small" class="mr-1">
|
|
181
|
+
{{ p?.plateNumber }}
|
|
182
|
+
</v-chip>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<template #append>
|
|
186
|
+
<v-btn variant="flat" color="primary" @click="selectNRICRecord(v)">Select</v-btn>
|
|
187
|
+
</template>
|
|
188
|
+
|
|
189
|
+
</v-list-item>
|
|
190
|
+
</v-list>
|
|
191
|
+
|
|
192
|
+
</v-card-text>
|
|
193
|
+
</v-card>
|
|
194
|
+
</v-dialog>
|
|
195
|
+
|
|
152
196
|
</v-card>
|
|
153
197
|
</template>
|
|
154
198
|
|
|
@@ -182,12 +226,13 @@ const prop = defineProps({
|
|
|
182
226
|
const { requiredRule, formatDateISO8601, debounce } = useUtils();
|
|
183
227
|
const { addVehicle, getCustomSeasonPassTypes, updateVehicle, getVehicleByNRIC } = useVehicle();
|
|
184
228
|
const { getSiteById, getSiteLevels, getSiteUnits } = useSiteSettings();
|
|
229
|
+
const { findPersonByNRICMultipleResult } = usePeople();
|
|
185
230
|
|
|
186
231
|
const emit = defineEmits(['back', 'select', 'done', 'error', 'close', 'close:all']);
|
|
187
232
|
|
|
188
233
|
|
|
189
234
|
const vehicle = reactive<Partial<TVehicle>>({
|
|
190
|
-
|
|
235
|
+
plates: [],
|
|
191
236
|
type: prop.type,
|
|
192
237
|
category: "resident",
|
|
193
238
|
name: '',
|
|
@@ -205,15 +250,16 @@ const vehicle = reactive<Partial<TVehicle>>({
|
|
|
205
250
|
_id: '',
|
|
206
251
|
});
|
|
207
252
|
|
|
208
|
-
|
|
253
|
+
const newPlateNumber = ref('');
|
|
254
|
+
const disablePrefilledInputs = ref(true);
|
|
209
255
|
|
|
210
256
|
const blocksArray = ref<TDefaultOptionObj[]>([]);
|
|
211
257
|
const levelsArray = ref<TDefaultOptionObj[]>([]);
|
|
212
258
|
const unitsArray = ref<TDefaultOptionObj[]>([]);
|
|
213
259
|
const seasonPassTypeArray = ref<{ title: string, value: string }[]>([]);
|
|
214
260
|
|
|
215
|
-
const
|
|
216
|
-
const
|
|
261
|
+
const matchingPeople = ref<Partial<TPeople>[]>([]);
|
|
262
|
+
const showMatchingPeopleDialog = ref(false);
|
|
217
263
|
const checkingNRIC = ref(false);
|
|
218
264
|
|
|
219
265
|
const defaultSeasonPassTypeArray = computed(() => {
|
|
@@ -541,23 +587,42 @@ watch([() => vehicle.end, () => vehicle.start], () => {
|
|
|
541
587
|
});
|
|
542
588
|
|
|
543
589
|
|
|
590
|
+
function selectNRICRecord(record: TPeople) {
|
|
591
|
+
|
|
592
|
+
vehicle.name = record.name;
|
|
593
|
+
vehicle.phoneNumber = record.contact;
|
|
594
|
+
vehicle.block = Number(record.block);
|
|
595
|
+
vehicle.level = record.level;
|
|
596
|
+
vehicle.unit = record.unit;
|
|
597
|
+
|
|
598
|
+
vehicle.plates = record.plates || [];
|
|
599
|
+
|
|
600
|
+
disablePrefilledInputs.value = true;
|
|
601
|
+
showMatchingPeopleDialog.value = false;
|
|
602
|
+
|
|
603
|
+
refreshLevelsData();
|
|
604
|
+
refreshUnitsData();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
|
|
544
608
|
async function checkNRIC() {
|
|
545
609
|
if (!vehicle.nric || vehicle.nric.length < 5) return;
|
|
546
610
|
|
|
547
611
|
checkingNRIC.value = true;
|
|
612
|
+
|
|
548
613
|
try {
|
|
549
|
-
const res = await
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
614
|
+
const res = await findPersonByNRICMultipleResult(vehicle.nric, prop.site) as { items: TPeople[] } | null;
|
|
615
|
+
|
|
616
|
+
if (res?.items && res.items.length > 0) {
|
|
617
|
+
matchingPeople.value = res.items || []
|
|
618
|
+
showMatchingPeopleDialog.value = true;
|
|
553
619
|
} else {
|
|
554
|
-
|
|
555
|
-
|
|
620
|
+
matchingPeople.value = [];
|
|
621
|
+
showMatchingPeopleDialog.value = false;
|
|
556
622
|
}
|
|
623
|
+
|
|
557
624
|
} catch (error) {
|
|
558
|
-
console.error(
|
|
559
|
-
showVehicleMatchDialog.value = false;
|
|
560
|
-
matchingVehicles.value = [];
|
|
625
|
+
console.error("NRIC search failed:", error);
|
|
561
626
|
} finally {
|
|
562
627
|
checkingNRIC.value = false;
|
|
563
628
|
}
|
|
@@ -568,26 +633,24 @@ const debounceedCheckNRIC = debounce(checkNRIC, 500);
|
|
|
568
633
|
watch(
|
|
569
634
|
() => vehicle.nric,
|
|
570
635
|
async (newNRIC) => {
|
|
571
|
-
|
|
636
|
+
resetVehicleDetails();
|
|
637
|
+
if (!newNRIC || newNRIC.length < 3) return;
|
|
572
638
|
|
|
573
639
|
debounceedCheckNRIC();
|
|
574
640
|
}
|
|
575
641
|
);
|
|
576
642
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
errorMessage.value = '';
|
|
587
|
-
showVehicleMatchDialog.value = false;
|
|
588
|
-
}
|
|
643
|
+
|
|
644
|
+
const resetVehicleDetails = () => {
|
|
645
|
+
vehicle.name = '';
|
|
646
|
+
vehicle.phoneNumber = '';
|
|
647
|
+
vehicle.block = '';
|
|
648
|
+
vehicle.level = '';
|
|
649
|
+
vehicle.unit = '';
|
|
650
|
+
vehicle.plates = [];
|
|
651
|
+
disablePrefilledInputs.value = false;
|
|
589
652
|
}
|
|
590
|
-
|
|
653
|
+
|
|
591
654
|
|
|
592
655
|
|
|
593
656
|
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
<v-row no-gutters class="px-5 py-1 d-flex align-center justify-end ga-3">
|
|
10
10
|
<v-text-field v-model="searchInput" density="compact" placeholder="Search" clearable max-width="300"
|
|
11
11
|
append-inner-icon="mdi-magnify" hide-details />
|
|
12
|
-
<v-select v-model="
|
|
12
|
+
<v-select v-model="vehicleTypeFilter" density="compact" item-title="label" item-value="value"
|
|
13
13
|
placeholder="Filter by Type" clearable max-width="200" hide-details :items="typeOptions" />
|
|
14
14
|
</v-row>
|
|
15
15
|
</template>
|
|
16
16
|
|
|
17
17
|
<template #item.block="{ value }">
|
|
18
|
-
{{ value ? `
|
|
18
|
+
{{ value ? `Blk ${value}` : "" }}
|
|
19
19
|
</template>
|
|
20
20
|
<template #item.status="{ value }">
|
|
21
21
|
<v-chip :color="formatVehicleStatus(value).color" size="x-small" dark>
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
</template>
|
|
27
27
|
|
|
28
|
-
<template #item.
|
|
28
|
+
<template #item.plates="{ value, item }">
|
|
29
29
|
<PlateNumberDisplay :plate-numbers="value" :default-value="item.plateNumber" />
|
|
30
30
|
</template>
|
|
31
31
|
</TableMain>
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
<v-row no-gutters class="ga-1 mb-5">
|
|
56
56
|
|
|
57
57
|
<template v-for="(label, key) in formattedFields" :key="key">
|
|
58
|
-
<v-col v-if="key === '
|
|
58
|
+
<v-col v-if="key === 'plates'" class="d-flex ga-2">
|
|
59
59
|
<span class="d-flex ga-3 align-center"><strong>{{ label }}:</strong></span>
|
|
60
|
-
<PlateNumberDisplay :plate-numbers="selectedVehicleObject[key]" :default-value="selectedVehicleObject.plateNumber" />
|
|
60
|
+
<PlateNumberDisplay :plate-numbers="selectedVehicleObject[key]" show-all :default-value="selectedVehicleObject.plateNumber" />
|
|
61
61
|
</v-col>
|
|
62
62
|
|
|
63
63
|
<v-col v-else-if="selectedVehicleObject[key]" cols="12">
|
|
@@ -99,10 +99,11 @@ const props = defineProps({
|
|
|
99
99
|
const headers = [
|
|
100
100
|
{ title: "Name", value: "name" },
|
|
101
101
|
// { title: "Building", value: "buildingName" },
|
|
102
|
-
{ title: "Vehicle Numbers", value: "
|
|
102
|
+
{ title: "Vehicle Numbers", value: "plates" },
|
|
103
103
|
{ title: "NRIC", value: "nric" },
|
|
104
104
|
{ title: "Block", value: "block" },
|
|
105
105
|
{ title: "Floor", value: "level" },
|
|
106
|
+
{ title: "Unit", value: "unit" },
|
|
106
107
|
{ title: "Category", value: "category" },
|
|
107
108
|
{ title: "Type", value: "type" },
|
|
108
109
|
{ title: "Status", value: "status" },
|
|
@@ -123,6 +124,7 @@ const searchInput = ref("")
|
|
|
123
124
|
const loading = ref(false);
|
|
124
125
|
const selectedVehicleId = ref<string | null>(null)
|
|
125
126
|
const vehicleType = ref<TVehicleType | null>(null);
|
|
127
|
+
const vehicleTypeFilter = ref<TVehicleType | null>(null);
|
|
126
128
|
|
|
127
129
|
const message = ref("");
|
|
128
130
|
const messageColor = ref("");
|
|
@@ -151,7 +153,7 @@ const typeOptions = [
|
|
|
151
153
|
|
|
152
154
|
const formattedFields: Partial<Record<keyof TVehicle, string>> = {
|
|
153
155
|
name: "Name",
|
|
154
|
-
|
|
156
|
+
plates: "Vehicle Numbers",
|
|
155
157
|
phoneNumber: "Phone Number",
|
|
156
158
|
nric: "NRIC",
|
|
157
159
|
block: "Block",
|
|
@@ -201,7 +203,7 @@ const { data: getVehiclesReq, refresh: getVehiclesRefresh, pending: getVehiclesP
|
|
|
201
203
|
getVehicles({
|
|
202
204
|
page: page.value,
|
|
203
205
|
search: searchInput.value,
|
|
204
|
-
type:
|
|
206
|
+
type: vehicleTypeFilter.value ?? "",
|
|
205
207
|
}),
|
|
206
208
|
{
|
|
207
209
|
watch: [page],
|
|
@@ -289,7 +291,7 @@ async function submitDelete() {
|
|
|
289
291
|
}
|
|
290
292
|
|
|
291
293
|
const debouncedSearch = debounce(getVehiclesRefresh, 500);
|
|
292
|
-
watch([searchInput,
|
|
294
|
+
watch([searchInput, vehicleTypeFilter], () => {
|
|
293
295
|
page.value = 1;
|
|
294
296
|
debouncedSearch()
|
|
295
297
|
})
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
</template>
|
|
109
109
|
</TableMain>
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
<v-dialog v-model="dialog.showSelection" width="450" persistent>
|
|
112
112
|
<VisitorFormSelection @cancel="dialog.showSelection = false" @select="handleSelectVisitorType" />
|
|
113
113
|
</v-dialog>
|
|
114
114
|
|
|
@@ -437,7 +437,8 @@ async function handleFileAdded(file: File) {
|
|
|
437
437
|
const res = await addFile(file);
|
|
438
438
|
const uploadedId = res?.id;
|
|
439
439
|
if (uploadedId) {
|
|
440
|
-
const url = `${API_DO_STORAGE_ENDPOINT}/${uploadedId}`;
|
|
440
|
+
// const url = `${API_DO_STORAGE_ENDPOINT}/${uploadedId}`;
|
|
441
|
+
const url = `${uploadedId}`;
|
|
441
442
|
_workOrder.value.attachments = _workOrder.value.attachments ?? [];
|
|
442
443
|
_workOrder.value.attachments.push(url);
|
|
443
444
|
}
|
|
@@ -162,6 +162,16 @@ export default function useAccessManagement() {
|
|
|
162
162
|
);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
function getCardDetails(params: { siteId: string; cardId: string }) {
|
|
166
|
+
return useNuxtApp().$api<{ message: string; data: Record<string, any> }>(
|
|
167
|
+
`/api/access-management/card-details`,
|
|
168
|
+
{
|
|
169
|
+
method: "GET",
|
|
170
|
+
query: { siteId: params.siteId, cardId: params.cardId },
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
165
175
|
return {
|
|
166
176
|
getDoorAccessLevels,
|
|
167
177
|
getLiftAccessLevels,
|
|
@@ -175,5 +185,6 @@ export default function useAccessManagement() {
|
|
|
175
185
|
getAvailableAccessCards,
|
|
176
186
|
deleteCard,
|
|
177
187
|
getCardHistory,
|
|
188
|
+
getCardDetails,
|
|
178
189
|
};
|
|
179
190
|
}
|
|
@@ -75,9 +75,9 @@ export default function useFeedback() {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function deleteFeedback(id: string) {
|
|
78
|
-
return useNuxtApp().$api<Record<string, any>>(`/api/feedbacks/deleted/feedback`, {
|
|
78
|
+
return useNuxtApp().$api<Record<string, any>>(`/api/feedbacks/deleted/feedback/${id}`, {
|
|
79
79
|
method: "PUT",
|
|
80
|
-
query: { id },
|
|
80
|
+
// query: { id },
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
|
package/composables/usePeople.ts
CHANGED
|
@@ -25,6 +25,15 @@ export default function () {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
async function findPersonByNRICMultipleResult(
|
|
29
|
+
nric: string, site: string
|
|
30
|
+
){
|
|
31
|
+
return await $fetch<Record<any, any>>(`/api/people/all-nric`, {
|
|
32
|
+
method: "GET",
|
|
33
|
+
query: { nric, site }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
async function findPersonByContact(
|
|
29
38
|
contact: string
|
|
30
39
|
): Promise<null | Partial<TPeople>> {
|
|
@@ -95,6 +104,7 @@ export default function () {
|
|
|
95
104
|
updateById,
|
|
96
105
|
deleteById,
|
|
97
106
|
findPersonByNRIC,
|
|
107
|
+
findPersonByNRICMultipleResult,
|
|
98
108
|
findPersonByContact,
|
|
99
109
|
getPeopleByUnit,
|
|
100
110
|
searchCompanyList,
|
|
@@ -72,9 +72,9 @@ export default function useWorkOrder() {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function deleteWorkOrder(id: string) {
|
|
75
|
-
return useNuxtApp().$api<Record<string, any>>(`/api/work-orders/deleted/work-order`, {
|
|
75
|
+
return useNuxtApp().$api<Record<string, any>>(`/api/work-orders/deleted/work-order/${id}`, {
|
|
76
76
|
method: "PUT",
|
|
77
|
-
query: { id },
|
|
77
|
+
// query: { id },
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
|
package/package.json
CHANGED
package/types/people.d.ts
CHANGED
|
@@ -16,8 +16,11 @@ declare type TPeople = {
|
|
|
16
16
|
org?: string;
|
|
17
17
|
site?: string,
|
|
18
18
|
type?: TPeopleType;
|
|
19
|
+
plates?: TPlateNumber[]
|
|
19
20
|
};
|
|
20
21
|
|
|
22
|
+
declare type TPlateNumber = { plateNumber: string, recNo: string }
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
declare type TPeoplePayload = Pick<TGuest, "name" | "block" | "level" | "unit" | "unitName" | "contact" | "plateNumber" | "nric" | "contact" | "remarks" | "org" | "site" | "start" | "end" | "type">
|
|
23
26
|
|
package/types/vehicle.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
declare type TVehicle = {
|
|
2
2
|
plateNumber: string;
|
|
3
|
-
|
|
3
|
+
plates?: TPlateNumber[]; // For display purposes, the API will return an array of plate numbers if there are multiple associated with the same vehicle record
|
|
4
4
|
type: TVehicleType;
|
|
5
5
|
category: "resident" | "visitor";
|
|
6
6
|
direction: "entry" | "exit" | "both" | "none";
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<v-dialog :model-value="modelValue" width="480" persistent>
|
|
3
|
-
<v-card width="100%">
|
|
4
|
-
<v-toolbar density="compact" color="black">
|
|
5
|
-
<v-toolbar-title class="text-subtitle-1 font-weight-medium">
|
|
6
|
-
Card History — {{ card?.cardNo ?? "N/A" }}
|
|
7
|
-
</v-toolbar-title>
|
|
8
|
-
<v-btn icon @click="emit('update:modelValue', false)">
|
|
9
|
-
<v-icon>mdi-close</v-icon>
|
|
10
|
-
</v-btn>
|
|
11
|
-
</v-toolbar>
|
|
12
|
-
|
|
13
|
-
<v-card-text style="max-height: 70vh; overflow-y: auto" class="pa-4">
|
|
14
|
-
<div v-if="pending" class="d-flex justify-center align-center py-8">
|
|
15
|
-
<v-progress-circular indeterminate color="black" />
|
|
16
|
-
</div>
|
|
17
|
-
|
|
18
|
-
<div v-else-if="!history.length" class="text-center text-grey py-8">
|
|
19
|
-
No history available.
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
<v-timeline v-else side="end" density="compact" truncate-line="both">
|
|
23
|
-
<v-timeline-item
|
|
24
|
-
v-for="(item, index) in history"
|
|
25
|
-
:key="index"
|
|
26
|
-
:dot-color="actionColor(item.action)"
|
|
27
|
-
size="small"
|
|
28
|
-
>
|
|
29
|
-
<template #icon>
|
|
30
|
-
<v-icon size="14" color="white">{{ actionIcon(item.action) }}</v-icon>
|
|
31
|
-
</template>
|
|
32
|
-
|
|
33
|
-
<div class="mb-4">
|
|
34
|
-
<div class="d-flex align-center ga-2 mb-1">
|
|
35
|
-
<v-chip
|
|
36
|
-
:color="actionColor(item.action)"
|
|
37
|
-
variant="flat"
|
|
38
|
-
size="x-small"
|
|
39
|
-
class="text-capitalize font-weight-medium"
|
|
40
|
-
>
|
|
41
|
-
{{ actionLabel(item.action) }}
|
|
42
|
-
</v-chip>
|
|
43
|
-
</div>
|
|
44
|
-
<div class="text-body-2">{{ item.performedBy?.name ?? "N/A" }}</div>
|
|
45
|
-
<div class="text-caption text-grey">{{ formatDate(item.date) }}</div>
|
|
46
|
-
</div>
|
|
47
|
-
</v-timeline-item>
|
|
48
|
-
</v-timeline>
|
|
49
|
-
</v-card-text>
|
|
50
|
-
</v-card>
|
|
51
|
-
</v-dialog>
|
|
52
|
-
</template>
|
|
53
|
-
|
|
54
|
-
<script setup lang="ts">
|
|
55
|
-
const props = defineProps({
|
|
56
|
-
modelValue: {
|
|
57
|
-
type: Boolean,
|
|
58
|
-
default: false,
|
|
59
|
-
},
|
|
60
|
-
card: {
|
|
61
|
-
type: Object as PropType<Record<string, any> | null>,
|
|
62
|
-
default: null,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const emit = defineEmits<{
|
|
67
|
-
"update:modelValue": [value: boolean];
|
|
68
|
-
}>();
|
|
69
|
-
|
|
70
|
-
const { getCardHistory } = useAccessManagement();
|
|
71
|
-
|
|
72
|
-
const history = ref<Record<string, any>[]>([]);
|
|
73
|
-
const pending = ref(false);
|
|
74
|
-
|
|
75
|
-
watch(
|
|
76
|
-
() => props.modelValue,
|
|
77
|
-
async (val) => {
|
|
78
|
-
if (val && props.card?._id) {
|
|
79
|
-
pending.value = true;
|
|
80
|
-
try {
|
|
81
|
-
const data = await getCardHistory(props.card._id);
|
|
82
|
-
history.value = Array.isArray(data) ? data : [];
|
|
83
|
-
} finally {
|
|
84
|
-
pending.value = false;
|
|
85
|
-
}
|
|
86
|
-
} else {
|
|
87
|
-
history.value = [];
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
function actionLabel(action: string) {
|
|
93
|
-
const map: Record<string, string> = {
|
|
94
|
-
available: "Created",
|
|
95
|
-
assign: "Assigned",
|
|
96
|
-
replace: "Replaced",
|
|
97
|
-
deleted: "Deleted",
|
|
98
|
-
};
|
|
99
|
-
return map[action] ?? action;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function actionColor(action: string) {
|
|
103
|
-
const map: Record<string, string> = {
|
|
104
|
-
available: "success",
|
|
105
|
-
assign: "primary",
|
|
106
|
-
replace: "orange",
|
|
107
|
-
deleted: "error",
|
|
108
|
-
};
|
|
109
|
-
return map[action] ?? "grey";
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function actionIcon(action: string) {
|
|
113
|
-
const map: Record<string, string> = {
|
|
114
|
-
available: "mdi-plus",
|
|
115
|
-
assign: "mdi-account-check",
|
|
116
|
-
replace: "mdi-swap-horizontal",
|
|
117
|
-
deleted: "mdi-delete",
|
|
118
|
-
};
|
|
119
|
-
return map[action] ?? "mdi-circle-small";
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function formatDate(date: string) {
|
|
123
|
-
if (!date) return "N/A";
|
|
124
|
-
return new Intl.DateTimeFormat("en-GB", {
|
|
125
|
-
day: "2-digit",
|
|
126
|
-
month: "short",
|
|
127
|
-
year: "numeric",
|
|
128
|
-
hour: "2-digit",
|
|
129
|
-
minute: "2-digit",
|
|
130
|
-
hour12: true,
|
|
131
|
-
}).format(new Date(date));
|
|
132
|
-
}
|
|
133
|
-
</script>
|