@7365admin1/layer-common 1.10.4 → 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 +12 -0
- package/components/AccessCardDeleteDialog.vue +109 -0
- package/components/AccessCardDetailsDialog.vue +144 -0
- package/components/AccessCardPreviewDialog.vue +18 -2
- package/components/AccessCardQrTagging.vue +183 -0
- package/components/AccessManagement.vue +22 -57
- package/components/BulletinBoardManagement.vue +6 -1
- package/components/CleaningScheduleMain.vue +18 -7
- package/components/IncidentReport/affectedEntities.vue +29 -0
- package/components/Input/InputPhoneNumberV2.vue +3 -0
- package/components/PlateNumberDisplay.vue +53 -0
- package/components/ScheduleAreaMain.vue +3 -0
- package/components/TableHygiene.vue +238 -113
- package/components/VehicleAddSelection.vue +58 -0
- package/components/VehicleForm.vue +663 -0
- package/components/VehicleManagement.vue +300 -0
- package/components/VisitorManagement.vue +1 -1
- package/components/WorkOrder/Main.vue +2 -1
- package/composables/useAccessManagement.ts +20 -1
- package/composables/useBulletin.ts +6 -4
- package/composables/useFeedback.ts +2 -2
- package/composables/usePeople.ts +10 -0
- package/composables/useVehicle.ts +114 -0
- package/composables/useWorkOrder.ts +2 -2
- package/package.json +1 -1
- package/types/people.d.ts +3 -0
- package/types/vehicle.d.ts +43 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card width="100%" :loading="processing">
|
|
3
|
+
<v-toolbar>
|
|
4
|
+
<v-row no-gutters class="fill-height px-6 d-flex ga-2 justify-space-between" align="center">
|
|
5
|
+
<span class="font-weight-bold text-h5 text-capitalize">
|
|
6
|
+
{{ prop.mode }} Vehicle <span>({{ formatVehicleType(type) }})</span>
|
|
7
|
+
</span>
|
|
8
|
+
<span>
|
|
9
|
+
<ButtonClose @click="emit('close:all')" icon-only />
|
|
10
|
+
</span>
|
|
11
|
+
</v-row>
|
|
12
|
+
</v-toolbar>
|
|
13
|
+
|
|
14
|
+
<v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-3">
|
|
15
|
+
<span class="text-subtitle-1 w-100 font-weight-bold mb-3">General Information</span>
|
|
16
|
+
<v-form ref="formRef" v-model="validForm" :disabled="processing" @click="errorMessage = ''">
|
|
17
|
+
<v-row no-gutters class="pt-4">
|
|
18
|
+
<v-col v-if="shouldShowField('seasonPassType')" cols="12">
|
|
19
|
+
<InputLabel class="text-capitalize" title="Season Pass Type" required />
|
|
20
|
+
<v-combobox v-model.trim="vehicle.seasonPassType" v-model:search="seasonPassTypeInput" :hide-no-data="false"
|
|
21
|
+
@update:modelValue="onSeasonPassTypeSelected" :items="seasonPassTypeArray" :rules="[requiredRule]"
|
|
22
|
+
item-title="title" item-value="value" variant="outlined" density="comfortable" persistent-hint
|
|
23
|
+
small-chips>
|
|
24
|
+
<template v-slot:no-data>
|
|
25
|
+
<v-list-item>
|
|
26
|
+
<v-list-item-title>
|
|
27
|
+
No results matching "<strong>{{ seasonPassTypeInput }}</strong>". This value will be added as new
|
|
28
|
+
option.
|
|
29
|
+
</v-list-item-title>
|
|
30
|
+
</v-list-item>
|
|
31
|
+
</template>
|
|
32
|
+
</v-combobox>
|
|
33
|
+
</v-col>
|
|
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
|
+
|
|
40
|
+
<v-col v-if="shouldShowField('name')" cols="12">
|
|
41
|
+
<v-row>
|
|
42
|
+
<v-col cols="12">
|
|
43
|
+
<InputLabel class="text-capitalize" title="Full Name" required />
|
|
44
|
+
<v-text-field v-model.trim="vehicle.name" density="comfortable" :rules="[requiredRule]" :disabled="disablePrefilledInputs" />
|
|
45
|
+
</v-col>
|
|
46
|
+
</v-row>
|
|
47
|
+
</v-col>
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
<v-col v-if="shouldShowField('phone')" cols="12">
|
|
52
|
+
<InputLabel class="text-capitalize" title="Phone Number" required />
|
|
53
|
+
<InputPhoneNumberV2 v-model="vehicle.phoneNumber" density="comfortable" :rules="[requiredRule]" :disabled="disablePrefilledInputs" />
|
|
54
|
+
</v-col>
|
|
55
|
+
|
|
56
|
+
<v-col v-if="shouldShowField('block')" cols="12">
|
|
57
|
+
<InputLabel class="text-capitalize" title="Block" required />
|
|
58
|
+
<v-select v-model="vehicle.block" :items="blocksArray" item-value="value" item-title="title"
|
|
59
|
+
@update:model-value="handleChangeBlock" density="comfortable" :rules="[requiredRule]" :disabled="disablePrefilledInputs" />
|
|
60
|
+
</v-col>
|
|
61
|
+
|
|
62
|
+
<v-col v-if="shouldShowField('level')" cols="12">
|
|
63
|
+
<InputLabel class="text-capitalize" title="Level" required />
|
|
64
|
+
<v-select v-model="vehicle.level" :items="levelsArray" density="comfortable" :disabled="!vehicle.block || disablePrefilledInputs"
|
|
65
|
+
@update:model-value="handleChangeLevel" :rules="[requiredRule]" />
|
|
66
|
+
</v-col>
|
|
67
|
+
|
|
68
|
+
<v-col v-if="shouldShowField('unit')" cols="12">
|
|
69
|
+
<InputLabel class="text-capitalize" title="Unit" required />
|
|
70
|
+
<v-select v-model="vehicle.unit" :items="unitsArray" density="comfortable" :disabled="!vehicle.level || disablePrefilledInputs"
|
|
71
|
+
:rules="[requiredRule]" />
|
|
72
|
+
</v-col>
|
|
73
|
+
|
|
74
|
+
<v-col v-if="shouldShowField('plateNumber')" cols="12">
|
|
75
|
+
<InputLabel class="text-capitalize" title="Vehicle Numbers" required />
|
|
76
|
+
<!-- <v-text-field v-model="vehicle.plateNumber" density="comfortable" :rules="[requiredRule]" /> -->
|
|
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]" />
|
|
82
|
+
</v-col>
|
|
83
|
+
|
|
84
|
+
<v-col v-if="shouldShowField('remarks')" cols="12">
|
|
85
|
+
<InputLabel class="text-capitalize" title="Remarks" required />
|
|
86
|
+
<v-textarea v-model="vehicle.remarks" density="comfortable" :rows="3" no-resize :rules="[requiredRule]" />
|
|
87
|
+
</v-col>
|
|
88
|
+
|
|
89
|
+
<v-col v-if="shouldShowField('start')" cols="12">
|
|
90
|
+
<v-expand-transition v-if="showSubscriptionDateOptions">
|
|
91
|
+
<v-row no-gutters>
|
|
92
|
+
<v-row dense justify="space-between">
|
|
93
|
+
<template v-for="option in subscriptionOptions" :key="option.value">
|
|
94
|
+
<v-col cols="4">
|
|
95
|
+
<v-btn :text="option.label" class="text-capitalize" min-width="120" :ripple="false"
|
|
96
|
+
:class="[option.value === selectedSubscriptionDuration ? 'button-outline-class' : '']"
|
|
97
|
+
@click="selectedSubscriptionDuration = option.value" />
|
|
98
|
+
</v-col>
|
|
99
|
+
|
|
100
|
+
</template>
|
|
101
|
+
</v-row>
|
|
102
|
+
|
|
103
|
+
<v-row no-gutters class="mt-5">
|
|
104
|
+
<v-col cols="12">
|
|
105
|
+
<InputLabel class="text-capitalize" title="Start Date" required />
|
|
106
|
+
<InputDateTimePicker ref="startDateRef" v-model="vehicle.start" :rules="[validStartDateRule]" />
|
|
107
|
+
</v-col>
|
|
108
|
+
|
|
109
|
+
<v-col cols="12">
|
|
110
|
+
<InputLabel class="text-capitalize" title="Expiry Date" required />
|
|
111
|
+
<InputDateTimePicker ref="expiryDateRef" v-model="vehicle.end" :rules="[validExpiryDateRule]" />
|
|
112
|
+
</v-col>
|
|
113
|
+
</v-row>
|
|
114
|
+
</v-row>
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
</v-expand-transition>
|
|
118
|
+
|
|
119
|
+
<v-col cols="12" :class="[showSubscriptionDateOptions && 'mt-5']">
|
|
120
|
+
<v-row justify="center">
|
|
121
|
+
<v-col cols="6">
|
|
122
|
+
<v-btn block color="primary" variant="text" class="text-none font-weight-bold"
|
|
123
|
+
:text="showSubscriptionDateOptions ? 'Hide Subscription Options' : 'Show Subscription Options'"
|
|
124
|
+
@click="showSubscriptionDateOptions = !showSubscriptionDateOptions"></v-btn>
|
|
125
|
+
</v-col>
|
|
126
|
+
</v-row>
|
|
127
|
+
</v-col>
|
|
128
|
+
</v-col>
|
|
129
|
+
|
|
130
|
+
<v-col cols="12">
|
|
131
|
+
<v-row no-gutters>
|
|
132
|
+
<v-col cols="12" class="text-center">
|
|
133
|
+
<span class="text-none text-subtitle-2 font-weight-medium text-error">
|
|
134
|
+
{{ message }}
|
|
135
|
+
</span>
|
|
136
|
+
</v-col>
|
|
137
|
+
</v-row>
|
|
138
|
+
</v-col>
|
|
139
|
+
</v-row>
|
|
140
|
+
</v-form>
|
|
141
|
+
</v-card-text>
|
|
142
|
+
<v-row no-gutters class="w-100" v-if="errorMessage">
|
|
143
|
+
<p class="text-error w-100 text-center text-subtitle-2">{{ errorMessage }}</p>
|
|
144
|
+
</v-row>
|
|
145
|
+
<v-toolbar density="compact">
|
|
146
|
+
<v-row no-gutters>
|
|
147
|
+
<v-col cols="6">
|
|
148
|
+
<v-btn v-if="prop.mode === 'add'" tile block variant="text" class="text-none" size="48" @click="back"
|
|
149
|
+
text="Back to Selection" />
|
|
150
|
+
<v-btn v-else tile block variant="text" class="text-none" size="48" @click="close" text="Close" />
|
|
151
|
+
</v-col>
|
|
152
|
+
<v-col cols="6">
|
|
153
|
+
<v-btn tile block variant="flat" color="black" class="text-none" size="48"
|
|
154
|
+
:disabled="!validForm || processing" @click="submit" :text="prop.mode == 'add' ? 'Submit' : 'Update'" />
|
|
155
|
+
</v-col>
|
|
156
|
+
</v-row>
|
|
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
|
+
|
|
196
|
+
</v-card>
|
|
197
|
+
</template>
|
|
198
|
+
|
|
199
|
+
<script setup lang="ts">
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
const prop = defineProps({
|
|
204
|
+
type: {
|
|
205
|
+
type: String as PropType<TVehicleType>,
|
|
206
|
+
required: true
|
|
207
|
+
},
|
|
208
|
+
org: {
|
|
209
|
+
type: String,
|
|
210
|
+
required: true
|
|
211
|
+
},
|
|
212
|
+
site: {
|
|
213
|
+
type: String,
|
|
214
|
+
required: true
|
|
215
|
+
},
|
|
216
|
+
mode: {
|
|
217
|
+
type: String as PropType<'add' | 'edit'>,
|
|
218
|
+
default: 'add'
|
|
219
|
+
},
|
|
220
|
+
vehicleData: {
|
|
221
|
+
type: Object as PropType<Partial<TVehicle> | null>,
|
|
222
|
+
default: null
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const { requiredRule, formatDateISO8601, debounce } = useUtils();
|
|
227
|
+
const { addVehicle, getCustomSeasonPassTypes, updateVehicle, getVehicleByNRIC } = useVehicle();
|
|
228
|
+
const { getSiteById, getSiteLevels, getSiteUnits } = useSiteSettings();
|
|
229
|
+
const { findPersonByNRICMultipleResult } = usePeople();
|
|
230
|
+
|
|
231
|
+
const emit = defineEmits(['back', 'select', 'done', 'error', 'close', 'close:all']);
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
const vehicle = reactive<Partial<TVehicle>>({
|
|
235
|
+
plates: [],
|
|
236
|
+
type: prop.type,
|
|
237
|
+
category: "resident",
|
|
238
|
+
name: '',
|
|
239
|
+
phoneNumber: '',
|
|
240
|
+
block: '',
|
|
241
|
+
level: '',
|
|
242
|
+
unit: '',
|
|
243
|
+
nric: '',
|
|
244
|
+
remarks: '',
|
|
245
|
+
seasonPassType: '',
|
|
246
|
+
start: '',
|
|
247
|
+
end: '',
|
|
248
|
+
site: prop.site,
|
|
249
|
+
org: prop.org,
|
|
250
|
+
_id: '',
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const newPlateNumber = ref('');
|
|
254
|
+
const disablePrefilledInputs = ref(true);
|
|
255
|
+
|
|
256
|
+
const blocksArray = ref<TDefaultOptionObj[]>([]);
|
|
257
|
+
const levelsArray = ref<TDefaultOptionObj[]>([]);
|
|
258
|
+
const unitsArray = ref<TDefaultOptionObj[]>([]);
|
|
259
|
+
const seasonPassTypeArray = ref<{ title: string, value: string }[]>([]);
|
|
260
|
+
|
|
261
|
+
const matchingPeople = ref<Partial<TPeople>[]>([]);
|
|
262
|
+
const showMatchingPeopleDialog = ref(false);
|
|
263
|
+
const checkingNRIC = ref(false);
|
|
264
|
+
|
|
265
|
+
const defaultSeasonPassTypeArray = computed(() => {
|
|
266
|
+
return [
|
|
267
|
+
'Resident Season Pass',
|
|
268
|
+
'Tenant/Leaseholder Pass',
|
|
269
|
+
'VIP/Preferred Parking Pass',
|
|
270
|
+
'Visitor Season Pass',
|
|
271
|
+
'Staff & Service Provider Pass',
|
|
272
|
+
'Business Tenant Pass',
|
|
273
|
+
].map((item) => ({
|
|
274
|
+
title: item,
|
|
275
|
+
value: item
|
|
276
|
+
}))
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
const typeFieldMap: Record<TVehicleType, string[]> = {
|
|
281
|
+
seasonpass: ['seasonPassType', 'name', 'phone', 'plateNumber', 'block', 'level', 'unit', 'start', 'end'],
|
|
282
|
+
blocklist: ['name', 'nric', 'phone', 'plateNumber', 'remarks'],
|
|
283
|
+
whitelist: ['name', 'nric', 'phone', 'plateNumber', 'block', 'level', 'unit']
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const shouldShowField = (fieldKey: string): boolean => {
|
|
287
|
+
const visibleFields = typeFieldMap[prop.type as TVehicleType];
|
|
288
|
+
return visibleFields?.includes(fieldKey);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
const validForm = ref(false);
|
|
293
|
+
const seasonPassTypeInput = ref('')
|
|
294
|
+
const formRef = ref<HTMLFormElement | null>(null);
|
|
295
|
+
const startDateRef = ref<HTMLInputElement | null>(null)
|
|
296
|
+
const expiryDateRef = ref<HTMLInputElement | null>(null)
|
|
297
|
+
const processing = ref(false);
|
|
298
|
+
const message = ref('');
|
|
299
|
+
const selectedSubscriptionDuration = ref<string | null>('custom')
|
|
300
|
+
const showSubscriptionDateOptions = ref(false)
|
|
301
|
+
const errorMessage = ref('');
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
// fetch existing vehicle data if in edit mode
|
|
305
|
+
if (prop.mode === 'edit') {
|
|
306
|
+
const existingVehicleData = JSON.parse(JSON.stringify(prop.vehicleData || {}));
|
|
307
|
+
Object.assign(vehicle, existingVehicleData);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
const { data: siteData, refresh: refreshSiteData } = useLazyAsyncData(
|
|
312
|
+
`fetch-site-data-${prop.site}`,
|
|
313
|
+
async () => await getSiteById(prop.site));
|
|
314
|
+
|
|
315
|
+
const { data: levelsData, refresh: refreshLevelsData } = useLazyAsyncData(
|
|
316
|
+
`fetch-levels-data-${prop.site}-${vehicle.block}`,
|
|
317
|
+
async () => {
|
|
318
|
+
if (!vehicle.block) return Promise.resolve(null);;
|
|
319
|
+
return await getSiteLevels(prop.site, { block: Number(vehicle.block) })
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const { data: unitsData, refresh: refreshUnitsData } = useLazyAsyncData(
|
|
323
|
+
`fetch-units-data-${prop.site}-${vehicle.level}`,
|
|
324
|
+
async () => {
|
|
325
|
+
if (!vehicle.level) return Promise.resolve(null);;
|
|
326
|
+
return await getSiteUnits(prop.site, Number(vehicle.block), vehicle.level)
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
const { data: seasonPassTypeData, refresh: refreshSeasonPassTypeData } = useLazyAsyncData(
|
|
331
|
+
`fetch-season-pass-type-data-${prop.site}`,
|
|
332
|
+
async () => await getCustomSeasonPassTypes(prop.site));
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
watch(
|
|
336
|
+
siteData,
|
|
337
|
+
(newVal) => {
|
|
338
|
+
const siteDataValue = newVal as any;
|
|
339
|
+
if (siteDataValue) {
|
|
340
|
+
const numberOfBlocks = siteDataValue.metadata?.block || 0;
|
|
341
|
+
for (let i = 1; i <= numberOfBlocks; i++) {
|
|
342
|
+
blocksArray.value.push({
|
|
343
|
+
title: `Block ${i}`,
|
|
344
|
+
value: i
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
} else {
|
|
349
|
+
blocksArray.value = [];
|
|
350
|
+
}
|
|
351
|
+
}, { immediate: true });
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
watch(
|
|
355
|
+
levelsData,
|
|
356
|
+
(newVal: any) => {
|
|
357
|
+
if (newVal) {
|
|
358
|
+
const arr = newVal.levels || [];
|
|
359
|
+
levelsArray.value = arr?.map((level: any) => ({
|
|
360
|
+
title: level,
|
|
361
|
+
value: level
|
|
362
|
+
}));
|
|
363
|
+
} else {
|
|
364
|
+
levelsArray.value = [];
|
|
365
|
+
}
|
|
366
|
+
}, { immediate: true });
|
|
367
|
+
|
|
368
|
+
watch(
|
|
369
|
+
unitsData,
|
|
370
|
+
(newVal: any) => {
|
|
371
|
+
if (newVal && Array.isArray(newVal)) {
|
|
372
|
+
const arr = newVal || [];
|
|
373
|
+
unitsArray.value = arr?.map((unit: any) => ({
|
|
374
|
+
title: unit?.name,
|
|
375
|
+
value: unit?._id
|
|
376
|
+
}));
|
|
377
|
+
} else {
|
|
378
|
+
unitsArray.value = [];
|
|
379
|
+
}
|
|
380
|
+
}, { immediate: true });
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
watch(
|
|
384
|
+
seasonPassTypeData,
|
|
385
|
+
(newVal) => {
|
|
386
|
+
if (Array.isArray(newVal)) {
|
|
387
|
+
const filteredArr = newVal.filter((item: any) => item.title && item.value);
|
|
388
|
+
seasonPassTypeArray.value = [...defaultSeasonPassTypeArray.value, ...filteredArr];
|
|
389
|
+
} else {
|
|
390
|
+
seasonPassTypeArray.value = defaultSeasonPassTypeArray.value;
|
|
391
|
+
}
|
|
392
|
+
}, { immediate: true });
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
const subscriptionOptions = computed(() => {
|
|
398
|
+
return [
|
|
399
|
+
{ label: 'Custom', value: 'custom' },
|
|
400
|
+
{ label: '1 Week', value: '1week' },
|
|
401
|
+
{ label: '2 Weeks', value: '2weeks' },
|
|
402
|
+
{ label: '3 Weeks', value: '3weeks' },
|
|
403
|
+
{ label: '4 Weeks', value: '4weeks' },
|
|
404
|
+
{ label: '5 Weeks', value: '5weeks' },
|
|
405
|
+
];
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
function handleChangeBlock(value: any) {
|
|
409
|
+
vehicle.level = '';
|
|
410
|
+
vehicle.unit = '';
|
|
411
|
+
refreshLevelsData();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function handleChangeLevel(value: any) {
|
|
415
|
+
vehicle.unit = '';
|
|
416
|
+
refreshUnitsData();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
function back() {
|
|
422
|
+
emit("back");
|
|
423
|
+
message.value = '';
|
|
424
|
+
showSubscriptionDateOptions.value = false;
|
|
425
|
+
selectedSubscriptionDuration.value = null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function close() {
|
|
429
|
+
emit("close");
|
|
430
|
+
message.value = '';
|
|
431
|
+
showSubscriptionDateOptions.value = false;
|
|
432
|
+
selectedSubscriptionDuration.value = null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function formatVehicleType(type: TVehicleType): string {
|
|
436
|
+
switch (type) {
|
|
437
|
+
case 'whitelist':
|
|
438
|
+
return 'Whitelist';
|
|
439
|
+
case 'blocklist':
|
|
440
|
+
return 'Blocklist';
|
|
441
|
+
case 'seasonpass':
|
|
442
|
+
return 'Season Pass';
|
|
443
|
+
default:
|
|
444
|
+
return '';
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function onSeasonPassTypeSelected(val: string | { title: string, value: string }) {
|
|
449
|
+
vehicle.seasonPassType = typeof val === 'object' ? val?.value : val;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
async function submit() {
|
|
454
|
+
errorMessage.value = '';
|
|
455
|
+
processing.value = true;
|
|
456
|
+
try {
|
|
457
|
+
const SPTVal = vehicle?.seasonPassType as string;
|
|
458
|
+
if (SPTVal) {
|
|
459
|
+
vehicle.seasonPassType = SPTVal?.charAt(0)?.toUpperCase() + SPTVal?.slice(1)?.toLowerCase();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
const { plateNumber, type, category, name, phoneNumber, block, level, unit, nric, remarks, seasonPassType, start, end, site, org } = vehicle
|
|
464
|
+
|
|
465
|
+
let payload: Partial<TVehiclePayload> = {
|
|
466
|
+
plateNumber,
|
|
467
|
+
name,
|
|
468
|
+
phoneNumber,
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
if (prop.mode === 'add') {
|
|
472
|
+
payload = {
|
|
473
|
+
...payload,
|
|
474
|
+
type,
|
|
475
|
+
category,
|
|
476
|
+
site,
|
|
477
|
+
org
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (vehicle.type === 'whitelist') {
|
|
482
|
+
payload = {
|
|
483
|
+
...payload,
|
|
484
|
+
block,
|
|
485
|
+
level,
|
|
486
|
+
unit,
|
|
487
|
+
};
|
|
488
|
+
} else if (vehicle.type === 'seasonpass') {
|
|
489
|
+
payload = {
|
|
490
|
+
...payload,
|
|
491
|
+
seasonPassType,
|
|
492
|
+
block,
|
|
493
|
+
level,
|
|
494
|
+
unit,
|
|
495
|
+
start,
|
|
496
|
+
end
|
|
497
|
+
};
|
|
498
|
+
} else if (vehicle.type === 'blocklist') {
|
|
499
|
+
payload = {
|
|
500
|
+
...payload,
|
|
501
|
+
nric,
|
|
502
|
+
remarks,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (prop.mode === 'add') {
|
|
507
|
+
await addVehicle(payload);
|
|
508
|
+
} else if (prop.mode === 'edit') {
|
|
509
|
+
const vehicleId = vehicle?._id as string;
|
|
510
|
+
await updateVehicle(vehicleId, payload);
|
|
511
|
+
}
|
|
512
|
+
emit("done");
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
} catch (error: any) {
|
|
516
|
+
const err = error?.data?.message
|
|
517
|
+
errorMessage.value = err || `Failed to ${prop.mode === 'add' ? 'add' : 'update'} vehicle. Please try again.`;
|
|
518
|
+
} finally {
|
|
519
|
+
processing.value = false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// const showComponent = (value: TVehicleType[]) => {
|
|
524
|
+
// return value.includes(prop.type);
|
|
525
|
+
|
|
526
|
+
// }
|
|
527
|
+
|
|
528
|
+
watch(selectedSubscriptionDuration, (duration) => {
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
const dateNowISO = formatDateISO8601(new Date())
|
|
532
|
+
const weeksLaterISO = (week: number) => {
|
|
533
|
+
const now = new Date()
|
|
534
|
+
const dateWeekLater = new Date(now.getTime() + (week * 7) * 24 * 60 * 60 * 1000)
|
|
535
|
+
return formatDateISO8601(dateWeekLater)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (duration === 'custom') {
|
|
539
|
+
vehicle.start = '';
|
|
540
|
+
vehicle.end = '';
|
|
541
|
+
} else if (duration === '1week') {
|
|
542
|
+
vehicle.start = dateNowISO
|
|
543
|
+
vehicle.end = weeksLaterISO(1)
|
|
544
|
+
} else if (duration === '2weeks') {
|
|
545
|
+
vehicle.start = dateNowISO
|
|
546
|
+
vehicle.end = weeksLaterISO(2)
|
|
547
|
+
} else if (duration === '3weeks') {
|
|
548
|
+
vehicle.start = dateNowISO
|
|
549
|
+
vehicle.end = weeksLaterISO(3)
|
|
550
|
+
} else if (duration === '4weeks') {
|
|
551
|
+
vehicle.start = dateNowISO
|
|
552
|
+
vehicle.end = weeksLaterISO(4)
|
|
553
|
+
} else if (duration === '5weeks') {
|
|
554
|
+
vehicle.start = dateNowISO
|
|
555
|
+
vehicle.end = weeksLaterISO(5)
|
|
556
|
+
}
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
function validStartDateRule(value: string) {
|
|
560
|
+
const expiryDateISO = vehicle.end;
|
|
561
|
+
if (!value && expiryDateISO) {
|
|
562
|
+
return 'Start Date is required';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function validExpiryDateRule(value: string) {
|
|
569
|
+
const startDateISO = vehicle.start;
|
|
570
|
+
|
|
571
|
+
if (!value && startDateISO) {
|
|
572
|
+
return 'Expiry Date is required';
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (value && startDateISO) {
|
|
576
|
+
const expiry = new Date(value);
|
|
577
|
+
const start = new Date(startDateISO as string);
|
|
578
|
+
return expiry > start || 'Expiry date must be later than start date';
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
watch([() => vehicle.end, () => vehicle.start], () => {
|
|
585
|
+
(expiryDateRef.value as any)?.validate();
|
|
586
|
+
(startDateRef.value as any)?.validate();
|
|
587
|
+
});
|
|
588
|
+
|
|
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
|
+
|
|
608
|
+
async function checkNRIC() {
|
|
609
|
+
if (!vehicle.nric || vehicle.nric.length < 5) return;
|
|
610
|
+
|
|
611
|
+
checkingNRIC.value = true;
|
|
612
|
+
|
|
613
|
+
try {
|
|
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;
|
|
619
|
+
} else {
|
|
620
|
+
matchingPeople.value = [];
|
|
621
|
+
showMatchingPeopleDialog.value = false;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
} catch (error) {
|
|
625
|
+
console.error("NRIC search failed:", error);
|
|
626
|
+
} finally {
|
|
627
|
+
checkingNRIC.value = false;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const debounceedCheckNRIC = debounce(checkNRIC, 500);
|
|
632
|
+
|
|
633
|
+
watch(
|
|
634
|
+
() => vehicle.nric,
|
|
635
|
+
async (newNRIC) => {
|
|
636
|
+
resetVehicleDetails();
|
|
637
|
+
if (!newNRIC || newNRIC.length < 3) return;
|
|
638
|
+
|
|
639
|
+
debounceedCheckNRIC();
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
|
|
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;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
</script>
|
|
659
|
+
<style scoped>
|
|
660
|
+
.button-outline-class {
|
|
661
|
+
border: 1px solid rgba(var(--v-theme-primary));
|
|
662
|
+
}
|
|
663
|
+
</style>
|