@7365admin1/layer-common 4.2.2-staging.262 → 4.2.2-staging.264
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.
|
@@ -6,8 +6,15 @@
|
|
|
6
6
|
<!-- `text-error` is Vuetify's own class over `--v-theme-error`,
|
|
7
7
|
which is the design's `--err` in both themes - already a token,
|
|
8
8
|
so it stays. -->
|
|
9
|
+
<!-- `message` is a FAILURE the caller wants read before the
|
|
10
|
+
operator tries again. It used to append "Do you want to delete
|
|
11
|
+
anyway?", which promises a force this dialog cannot perform -
|
|
12
|
+
pressing Delete simply repeats the same request. No caller had
|
|
13
|
+
ever passed `message`, so the sentence had never rendered.
|
|
14
|
+
`VehicleManagement` is the first, and it passes the server's
|
|
15
|
+
refusal when an ANPR revoke could not be confirmed. -->
|
|
9
16
|
<div v-if="message" class="text-error mt-2">
|
|
10
|
-
{{ message }}
|
|
17
|
+
{{ message }}
|
|
11
18
|
</div>
|
|
12
19
|
</v-card-text>
|
|
13
20
|
|
|
@@ -327,7 +327,15 @@
|
|
|
327
327
|
Back to Selection
|
|
328
328
|
</AppButton>
|
|
329
329
|
<AppButton v-else variant="ghost" @click="close">Close</AppButton>
|
|
330
|
-
|
|
330
|
+
<!--
|
|
331
|
+
NOT disabled on `validForm`. It used to be, and on a record with no
|
|
332
|
+
Block/Level/Unit - which exist, e.g. BEN21 on staging - the Update
|
|
333
|
+
button was simply dead: Vuetify does not mark a field invalid until it
|
|
334
|
+
has been validated, so there was no red field, no message, and no way
|
|
335
|
+
to find out why. The click now runs the validation, which paints every
|
|
336
|
+
offending field and writes the reason above.
|
|
337
|
+
-->
|
|
338
|
+
<AppButton :disabled="processing" @click="submit">
|
|
331
339
|
{{ prop.mode == "add" ? "Submit" : "Update" }}
|
|
332
340
|
</AppButton>
|
|
333
341
|
</div>
|
|
@@ -751,6 +759,18 @@ function showMessage(msg: string, color: string) {
|
|
|
751
759
|
|
|
752
760
|
async function submit() {
|
|
753
761
|
errorMessage.value = "";
|
|
762
|
+
|
|
763
|
+
// Ask the form first. `validate()` marks each failing field, so the person
|
|
764
|
+
// filling it in can see WHICH details are missing rather than meeting a
|
|
765
|
+
// button that does nothing.
|
|
766
|
+
const result = await (formRef.value as any)?.validate?.();
|
|
767
|
+
if (result && result.valid === false) {
|
|
768
|
+
errorMessage.value =
|
|
769
|
+
"Some required details are missing or not valid. The fields marked in " +
|
|
770
|
+
"red above need to be filled in before this can be saved.";
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
754
774
|
processing.value = true;
|
|
755
775
|
try {
|
|
756
776
|
const SPTVal = vehicle?.seasonPassType as string;
|
|
@@ -828,19 +848,26 @@ async function submit() {
|
|
|
828
848
|
};
|
|
829
849
|
}
|
|
830
850
|
|
|
851
|
+
// Say what the SERVER says happened, not a hardcoded sentence. The server
|
|
852
|
+
// reports whether the plate actually reached an ANPR camera - a site whose
|
|
853
|
+
// camera is switched off saves the vehicle but registers nothing, and this
|
|
854
|
+
// used to announce "Vehicle added successfully." over the top of that.
|
|
831
855
|
if (prop.mode === "add") {
|
|
832
|
-
await addVehicle(payload);
|
|
833
|
-
showMessage("Vehicle added successfully.", "success");
|
|
856
|
+
const res: any = await addVehicle(payload);
|
|
857
|
+
showMessage(res?.data || res?.message || "Vehicle added successfully.", "success");
|
|
834
858
|
} else if (prop.mode === "edit") {
|
|
835
859
|
const plateNumberId = prop.plateNumberId as string;
|
|
836
|
-
await updateVehicle(plateNumberId, payload);
|
|
837
|
-
showMessage("Vehicle updated successfully.", "success");
|
|
860
|
+
const res: any = await updateVehicle(plateNumberId, payload);
|
|
861
|
+
showMessage(res?.data || res?.message || "Vehicle updated successfully.", "success");
|
|
838
862
|
}
|
|
839
863
|
emit("done");
|
|
840
864
|
} catch (error: any) {
|
|
841
|
-
|
|
865
|
+
// A refused create is fail-closed on the server and its message names the
|
|
866
|
+
// camera that would not take the plate. `$fetch` puts it on `data`, `ofetch`
|
|
867
|
+
// on `response._data`; reading only one of them dropped the reason.
|
|
842
868
|
errorMessage.value =
|
|
843
|
-
|
|
869
|
+
error?.data?.message ||
|
|
870
|
+
error?.response?._data?.message ||
|
|
844
871
|
`Failed to ${
|
|
845
872
|
prop.mode === "add" ? "add" : "update"
|
|
846
873
|
} vehicle. Please try again.`;
|
|
@@ -1023,7 +1050,6 @@ function handleCloseMatchDialog() {
|
|
|
1023
1050
|
|
|
1024
1051
|
onMounted(() => {
|
|
1025
1052
|
setTimeout(() => {
|
|
1026
|
-
console.log("VehicleForm mounted with props:", prop);
|
|
1027
1053
|
if (prop.mode === "edit" && prop.vehicleData) {
|
|
1028
1054
|
// In edit mode, we only want to check for matching people records if there is a pre-filled unit value. If there is no unit value, we can assume that this vehicle record is not linked to any existing people record and skip the check.
|
|
1029
1055
|
vehicle.seasonPassType = prop.vehicleData.seasonPassType || "";
|
|
@@ -36,6 +36,45 @@
|
|
|
36
36
|
<template #item.plates="{ value, item }">
|
|
37
37
|
<PlateNumberDisplay :plate-numbers="value" :default-value="item.plateNumber" />
|
|
38
38
|
</template>
|
|
39
|
+
|
|
40
|
+
<!--
|
|
41
|
+
The row kebab. Same activator every other table in this package draws
|
|
42
|
+
(`AppButton variant="row"` as a `v-menu` activator, see
|
|
43
|
+
`MemberMain.vue`). `@click.stop` because the row also carries
|
|
44
|
+
`@row-click`; without it the kebab would open the Preview dialog
|
|
45
|
+
underneath the menu.
|
|
46
|
+
|
|
47
|
+
A row can stand for MORE THAN ONE vehicle. The list groups sibling
|
|
48
|
+
plates by NRIC, so Test Res 3 on staging is one row holding TEST123 and
|
|
49
|
+
TEST124 - two separate documents. The menu used to resolve a single
|
|
50
|
+
plate and act on it silently; now every plate on the row is listed
|
|
51
|
+
under its own number and the operator picks the one they mean.
|
|
52
|
+
|
|
53
|
+
The items themselves come from `vehiclePlateActions`, the same function
|
|
54
|
+
the Preview dialog's menu uses, so the two can no longer disagree - and
|
|
55
|
+
the kebab's own `v-if` asks that function too, so an empty menu cannot
|
|
56
|
+
be drawn.
|
|
57
|
+
-->
|
|
58
|
+
<template #item.action="{ item }">
|
|
59
|
+
<v-menu v-if="rowHasVehicleActions(item, vehiclePermissions)" location="bottom end">
|
|
60
|
+
<template #activator="{ props: menuProps }">
|
|
61
|
+
<AppButton v-bind="menuProps" variant="row" icon="mdi-dots-vertical" aria-label="Vehicle actions"
|
|
62
|
+
@click.stop />
|
|
63
|
+
</template>
|
|
64
|
+
<v-list density="compact">
|
|
65
|
+
<template v-for="plate in rowPlates(item)" :key="plate._id ?? plate.plateNumber">
|
|
66
|
+
<template v-if="vehiclePlateActions(plate, vehiclePermissions).length">
|
|
67
|
+
<v-list-subheader v-if="rowPlates(item).length > 1">
|
|
68
|
+
{{ plate.plateNumber }}
|
|
69
|
+
</v-list-subheader>
|
|
70
|
+
<v-list-item v-for="action in vehiclePlateActions(plate, vehiclePermissions)" :key="action.key"
|
|
71
|
+
:title="action.title" :prepend-icon="action.icon" :base-color="action.color"
|
|
72
|
+
@click="runVehicleAction(action.key, plate as TPlateNumber)" />
|
|
73
|
+
</template>
|
|
74
|
+
</template>
|
|
75
|
+
</v-list>
|
|
76
|
+
</v-menu>
|
|
77
|
+
</template>
|
|
39
78
|
</TableMain>
|
|
40
79
|
|
|
41
80
|
<!-- <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" /> -->
|
|
@@ -91,33 +130,17 @@
|
|
|
91
130
|
{{ formatVehicleStatus(value).label }}
|
|
92
131
|
</v-chip>
|
|
93
132
|
</template>
|
|
94
|
-
<template #item.action="{ item: plateNumberItem
|
|
95
|
-
<v-menu v-if="
|
|
133
|
+
<template #item.action="{ item: plateNumberItem }">
|
|
134
|
+
<v-menu v-if="vehiclePlateActions(plateNumberItem, vehiclePermissions).length">
|
|
96
135
|
<template #activator="{ props: menuProps }">
|
|
97
136
|
<v-btn icon="mdi-dots-vertical" v-bind="menuProps" flat size="x-small" />
|
|
98
137
|
</template>
|
|
99
138
|
<v-list density="compact">
|
|
100
|
-
<v-list-item v-if="(plateNumberItem as TPlateNumber)?.status == 'active'"
|
|
101
|
-
title="Edit Vehicle" prepend-icon="mdi-pencil"
|
|
102
|
-
@click="handleEditVehicleAction(plateNumberItem as TPlateNumber)" />
|
|
103
|
-
<v-list-item
|
|
104
|
-
v-if="canApproveVehicle && (plateNumberItem as TPlateNumber)?.status == 'pending'"
|
|
105
|
-
title="Approve" prepend-icon="mdi-check-circle" base-color="success"
|
|
106
|
-
@click="handleApproveVehicle(plateNumberItem as TPlateNumber)" />
|
|
107
|
-
<v-list-item v-if="(plateNumberItem as TPlateNumber)?.status == 'deleted'" title="Restore"
|
|
108
|
-
prepend-icon="mdi-restore" base-color="warning"
|
|
109
|
-
@click="handleRestoreVehicle(plateNumberItem as TPlateNumber)" />
|
|
110
|
-
<v-list-item
|
|
111
|
-
v-if="((plateNumberItem as TPlateNumber)?.type === 'whitelist' || (plateNumberItem as TPlateNumber)?.type === 'blocklist') && plateNumberItem?.status == 'active'"
|
|
112
|
-
:title="(plateNumberItem as TPlateNumber)?.type === 'blocklist' ? 'Unblock' : 'Block'"
|
|
113
|
-
prepend-icon="mdi-swap-horizontal"
|
|
114
|
-
:base-color="(plateNumberItem as TPlateNumber)?.type === 'blocklist' ? 'primary' : 'error'"
|
|
115
|
-
@click="handleUpdateType(plateNumberItem as TPlateNumber)" />
|
|
116
|
-
|
|
117
139
|
<v-list-item
|
|
118
|
-
v-
|
|
119
|
-
|
|
120
|
-
|
|
140
|
+
v-for="action in vehiclePlateActions(plateNumberItem, vehiclePermissions)"
|
|
141
|
+
:key="action.key" :title="action.title" :prepend-icon="action.icon"
|
|
142
|
+
:base-color="action.color"
|
|
143
|
+
@click="runVehicleAction(action.key, plateNumberItem as TPlateNumber)" />
|
|
121
144
|
</v-list>
|
|
122
145
|
</v-menu>
|
|
123
146
|
</template>
|
|
@@ -140,24 +163,39 @@
|
|
|
140
163
|
</DialogUpdateMoreAction>
|
|
141
164
|
</v-dialog>
|
|
142
165
|
<v-dialog v-model="dialog.deleteVehicle" persistent width="540">
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
<!--
|
|
167
|
+
A row is ONE plate document, not a person: the list groups sibling
|
|
168
|
+
plates by NRIC and keeps the first document's `_id` as the row's `_id`.
|
|
169
|
+
So the copy names the plate it is about to remove, says what removing
|
|
170
|
+
it actually does (revokes ANPR access), and says plainly that nothing
|
|
171
|
+
else on the record is touched. `message` carries the server's refusal
|
|
172
|
+
when the ANPR revoke could not be confirmed - the dialog stays open on
|
|
173
|
+
that path so the operator can read it and retry.
|
|
174
|
+
-->
|
|
175
|
+
<DialogDeleteConfirmation :loading="deletingVehicle" :message="deleteError"
|
|
176
|
+
:prompt-title="`Remove vehicle number ${selectedPlateNumberObject?.plateNumber} from this record? This revokes its ANPR access - it will no longer open the barrier. The resident and any other vehicle numbers on this record are not affected.`"
|
|
145
177
|
@delete="submitDelete" @close="closeDeleteDialog" />
|
|
146
178
|
</v-dialog>
|
|
147
179
|
<v-dialog v-model="dialog.approveVehicle" persistent width="540">
|
|
148
|
-
|
|
180
|
+
<!--
|
|
181
|
+
`message` carries the server's refusal. Approve talks to the ANPR
|
|
182
|
+
cameras, so a failure here is a sentence about a camera that the
|
|
183
|
+
operator has to read and act on - it was going to a five-second
|
|
184
|
+
snackbar, along with the device's raw `{"ErrorCode":...}` reply.
|
|
185
|
+
-->
|
|
186
|
+
<DialogReusablePrompt :loading="approvingVehicle" :message="actionError"
|
|
149
187
|
:prompt-title="`Are you sure want to approve this vehicle - ${selectedPlateNumberObject?.plateNumber}?`"
|
|
150
|
-
@proceed="submitApprove" @close="
|
|
188
|
+
@proceed="submitApprove" @close="closeActionDialog('approveVehicle')" />
|
|
151
189
|
</v-dialog>
|
|
152
190
|
<v-dialog v-model="dialog.restoreVehicle" persistent width="540">
|
|
153
|
-
<DialogReusablePrompt :loading="restoringVehicle"
|
|
191
|
+
<DialogReusablePrompt :loading="restoringVehicle" :message="actionError"
|
|
154
192
|
:prompt-title="`Are you sure want to restore this vehicle - ${selectedPlateNumberObject?.plateNumber}?`"
|
|
155
|
-
@proceed="submitRestore" @close="
|
|
193
|
+
@proceed="submitRestore" @close="closeActionDialog('restoreVehicle')" />
|
|
156
194
|
</v-dialog>
|
|
157
195
|
<v-dialog v-model="dialog.updateVehicleType" persistent width="540">
|
|
158
|
-
<DialogReusablePrompt :loading="updatingType"
|
|
196
|
+
<DialogReusablePrompt :loading="updatingType" :message="actionError"
|
|
159
197
|
:prompt-title="`Are you sure want to update the type of this vehicle - ${selectedPlateNumberObject?.plateNumber}?`"
|
|
160
|
-
@proceed="submitUpdateType" @close="
|
|
198
|
+
@proceed="submitUpdateType" @close="closeActionDialog('updateVehicleType')" />
|
|
161
199
|
</v-dialog>
|
|
162
200
|
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
163
201
|
</v-row>
|
|
@@ -166,6 +204,12 @@
|
|
|
166
204
|
<script lang="ts" setup>
|
|
167
205
|
import useUtils from '../composables/useUtils';
|
|
168
206
|
import useVehicle from '../composables/useVehicle';
|
|
207
|
+
import {
|
|
208
|
+
rowHasVehicleActions,
|
|
209
|
+
rowPlates,
|
|
210
|
+
vehiclePlateActions,
|
|
211
|
+
type VehicleActionKey,
|
|
212
|
+
} from '../utils/vehicle-actions';
|
|
169
213
|
|
|
170
214
|
|
|
171
215
|
|
|
@@ -191,6 +235,7 @@ const headers = [
|
|
|
191
235
|
{ title: "Category", value: "category" },
|
|
192
236
|
// { title: "Type", value: "type" },
|
|
193
237
|
// { title: "Status", value: "status" },
|
|
238
|
+
{ title: "", value: "action" },
|
|
194
239
|
]
|
|
195
240
|
|
|
196
241
|
const plateHeaders = [
|
|
@@ -205,10 +250,6 @@ const plateHeaders = [
|
|
|
205
250
|
const { formatCamelCaseToWords, formatDate, debounce, maskNRIC } = useUtils();
|
|
206
251
|
const { getVehicles, deleteVehicle, formatVehicleStatus, approveVehicle, updateVehicle, getSpecificVehicleById } = useVehicle();
|
|
207
252
|
|
|
208
|
-
const canApproveVehicle = computed(() => {
|
|
209
|
-
return true;
|
|
210
|
-
})
|
|
211
|
-
|
|
212
253
|
const items = ref<Array<Record<string, any>>>([]);
|
|
213
254
|
const page = ref(1);
|
|
214
255
|
const pages = ref(0);
|
|
@@ -235,7 +276,33 @@ const selectedPlateNumberId = ref<string | null>(null); // individual plate numb
|
|
|
235
276
|
const message = ref("");
|
|
236
277
|
const messageColor = ref("");
|
|
237
278
|
const messageSnackbar = ref(false);
|
|
238
|
-
|
|
279
|
+
// The server's refusal when an ANPR revoke could not be confirmed. Rendered
|
|
280
|
+
// inside the delete dialog rather than a 5-second toast: it names the camera
|
|
281
|
+
// that failed and the operator needs it in front of them to retry.
|
|
282
|
+
const deleteError = ref("");
|
|
283
|
+
// The same thing for Approve / Restore / Block, which also talk to the cameras
|
|
284
|
+
// and whose refusals were being thrown at a snackbar that times out.
|
|
285
|
+
const actionError = ref("");
|
|
286
|
+
|
|
287
|
+
function closeActionDialog(key: "approveVehicle" | "restoreVehicle" | "updateVehicleType") {
|
|
288
|
+
dialog[key] = false;
|
|
289
|
+
actionError.value = "";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The server's own sentence, or a plain fallback.
|
|
294
|
+
*
|
|
295
|
+
* `error.response._data` is undefined on a network failure, so reading it
|
|
296
|
+
* unguarded threw out of the catch and the operator saw nothing at all.
|
|
297
|
+
*/
|
|
298
|
+
function readServerMessage(error: any, fallback: string) {
|
|
299
|
+
return (
|
|
300
|
+
error?.response?._data?.message ||
|
|
301
|
+
error?.data?.message ||
|
|
302
|
+
error?.message ||
|
|
303
|
+
fallback
|
|
304
|
+
);
|
|
305
|
+
}
|
|
239
306
|
|
|
240
307
|
function showMessage(msg: string, color: string) {
|
|
241
308
|
message.value = msg;
|
|
@@ -298,6 +365,7 @@ function formatValues(key: string, value: any) {
|
|
|
298
365
|
|
|
299
366
|
const closeDeleteDialog = () => {
|
|
300
367
|
dialog.deleteVehicle = false;
|
|
368
|
+
deleteError.value = "";
|
|
301
369
|
};
|
|
302
370
|
|
|
303
371
|
const handleCloseAll = () => {
|
|
@@ -385,31 +453,48 @@ function handleDeleteVehicleAction(item: TPlateNumber) {
|
|
|
385
453
|
|
|
386
454
|
function handleApproveVehicle(item: TPlateNumber) {
|
|
387
455
|
selectedPlateNumberObject.value = item || null;
|
|
456
|
+
actionError.value = "";
|
|
388
457
|
dialog.approveVehicle = true;
|
|
389
458
|
}
|
|
390
459
|
function handleRestoreVehicle(item: TPlateNumber) {
|
|
391
460
|
selectedPlateNumberObject.value = item || null;
|
|
461
|
+
actionError.value = "";
|
|
392
462
|
dialog.restoreVehicle = true;
|
|
393
463
|
}
|
|
394
464
|
|
|
395
465
|
function handleUpdateType(item: TPlateNumber) {
|
|
396
466
|
selectedPlateNumberObject.value = item || null;
|
|
467
|
+
actionError.value = "";
|
|
397
468
|
dialog.updateVehicleType = true;
|
|
398
469
|
}
|
|
399
470
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
471
|
+
/**
|
|
472
|
+
* The three gates the action list is decided by, passed straight through from
|
|
473
|
+
* the host app's `useLocalPermission()`.
|
|
474
|
+
*/
|
|
475
|
+
const vehiclePermissions = computed(() => ({
|
|
476
|
+
canUpdateVehicle: props.canUpdateVehicle,
|
|
477
|
+
canDeleteVehicle: props.canDeleteVehicle,
|
|
478
|
+
canApproveVehicle: props.canApproveVehicle,
|
|
479
|
+
}));
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* One entry point for every menu item, in both menus, so a new action can only
|
|
483
|
+
* be added in one place and cannot be forgotten in the other.
|
|
484
|
+
*/
|
|
485
|
+
function runVehicleAction(key: VehicleActionKey, plate: TPlateNumber) {
|
|
486
|
+
switch (key) {
|
|
487
|
+
case "edit":
|
|
488
|
+
return handleEditVehicleAction(plate);
|
|
489
|
+
case "approve":
|
|
490
|
+
return handleApproveVehicle(plate);
|
|
491
|
+
case "restore":
|
|
492
|
+
return handleRestoreVehicle(plate);
|
|
493
|
+
case "toggle-type":
|
|
494
|
+
return handleUpdateType(plate);
|
|
495
|
+
case "delete":
|
|
496
|
+
return handleDeleteVehicleAction(plate);
|
|
497
|
+
}
|
|
413
498
|
}
|
|
414
499
|
|
|
415
500
|
function handleSelectVehicleStatus(value: TVehicleType) {
|
|
@@ -469,14 +554,21 @@ async function submitDelete() {
|
|
|
469
554
|
|
|
470
555
|
try {
|
|
471
556
|
deletingVehicle.value = true;
|
|
557
|
+
deleteError.value = "";
|
|
472
558
|
const res = await deleteVehicle({ site: props.site, recno: recNo, id: plateNumberId as string, type }
|
|
473
559
|
);
|
|
474
560
|
dialog.deleteVehicle = false;
|
|
475
561
|
showMessage(res.message, "success");
|
|
476
562
|
await getVehiclesRefresh();
|
|
477
563
|
} catch (error: any) {
|
|
478
|
-
|
|
479
|
-
|
|
564
|
+
// The delete is fail-closed on the server: if the plate cannot be confirmed
|
|
565
|
+
// gone from every ANPR camera holding it, the record is left intact and the
|
|
566
|
+
// request is refused. Say so. This used to assign to `message` WITHOUT
|
|
567
|
+
// raising the snackbar - and `error.response._data` throws on a network
|
|
568
|
+
// error - so a refused delete showed the operator nothing at all.
|
|
569
|
+
deleteError.value = readServerMessage(error, "Failed to delete vehicle.");
|
|
570
|
+
// The dialog stays open and the row stays in the table - nothing is removed
|
|
571
|
+
// optimistically, so what is on screen still matches the database.
|
|
480
572
|
} finally {
|
|
481
573
|
deletingVehicle.value = false;
|
|
482
574
|
}
|
|
@@ -495,17 +587,15 @@ async function submitRestore() {
|
|
|
495
587
|
|
|
496
588
|
try {
|
|
497
589
|
restoringVehicle.value = true;
|
|
590
|
+
actionError.value = "";
|
|
498
591
|
// reactivate and restore will use the same endpoint, just with different payload
|
|
499
592
|
const res = await approveVehicle({ site: props.site, org: props.org, id: vehicleId as string }
|
|
500
593
|
);
|
|
501
|
-
|
|
594
|
+
closeActionDialog("restoreVehicle");
|
|
502
595
|
showMessage(res.message, "success");
|
|
503
596
|
await getVehiclesRefresh();
|
|
504
597
|
} catch (error: any) {
|
|
505
|
-
|
|
506
|
-
const errMessage = error?.response?._data?.message || "Failed to restore vehicle";
|
|
507
|
-
showMessage(errMessage, "error");
|
|
508
|
-
// message.value = error.response._data.message;
|
|
598
|
+
actionError.value = readServerMessage(error, "Failed to restore vehicle.");
|
|
509
599
|
} finally {
|
|
510
600
|
restoringVehicle.value = false;
|
|
511
601
|
}
|
|
@@ -546,15 +636,13 @@ async function submitUpdateType() {
|
|
|
546
636
|
|
|
547
637
|
try {
|
|
548
638
|
updatingType.value = true;
|
|
639
|
+
actionError.value = "";
|
|
549
640
|
const res = await updateVehicle(vehicleId as string, payload);
|
|
550
|
-
|
|
641
|
+
closeActionDialog("updateVehicleType");
|
|
551
642
|
showMessage(res.message, "success");
|
|
552
643
|
await getVehiclesRefresh();
|
|
553
644
|
} catch (error: any) {
|
|
554
|
-
|
|
555
|
-
const errMessage = error?.response?._data?.message || "Failed to update vehicle type";
|
|
556
|
-
showMessage(errMessage, "error");
|
|
557
|
-
// message.value = error.response._data.message;
|
|
645
|
+
actionError.value = readServerMessage(error, "Failed to update vehicle type.");
|
|
558
646
|
} finally {
|
|
559
647
|
updatingType.value = false;
|
|
560
648
|
}
|
|
@@ -571,16 +659,15 @@ async function submitApprove() {
|
|
|
571
659
|
|
|
572
660
|
try {
|
|
573
661
|
approvingVehicle.value = true;
|
|
662
|
+
actionError.value = "";
|
|
574
663
|
const res = await approveVehicle({ site: props.site, org: props.org, id: plateNumberId as string }
|
|
575
664
|
|
|
576
665
|
);
|
|
577
|
-
|
|
666
|
+
closeActionDialog("approveVehicle");
|
|
578
667
|
showMessage(res.message, "success");
|
|
579
668
|
await getVehiclesRefresh();
|
|
580
669
|
} catch (error: any) {
|
|
581
|
-
|
|
582
|
-
const errMessage = error?.response?._data?.message || "Failed to approve vehicle";
|
|
583
|
-
showMessage(errMessage, "error");
|
|
670
|
+
actionError.value = readServerMessage(error, "Failed to approve vehicle.");
|
|
584
671
|
} finally {
|
|
585
672
|
approvingVehicle.value = false;
|
|
586
673
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "4.2.2-staging.
|
|
5
|
+
"version": "4.2.2-staging.264",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which actions a vehicle plate offers, in one place.
|
|
3
|
+
*
|
|
4
|
+
* The reason this exists rather than a list of `v-if`s: the row menu and the
|
|
5
|
+
* Preview dialog's menu each had their OWN copy of the conditions, and they
|
|
6
|
+
* drifted. On staging, 1 Sep 2026:
|
|
7
|
+
*
|
|
8
|
+
* - a PENDING plate's row kebab opened an EMPTY menu - Approve was only ever
|
|
9
|
+
* written into the Preview copy;
|
|
10
|
+
* - an INACTIVE (blocked) plate had no kebab at all and no action in Preview
|
|
11
|
+
* either, because every condition in both copies tested for
|
|
12
|
+
* active / pending / deleted, so there was no way to restore it.
|
|
13
|
+
*
|
|
14
|
+
* With the decision in one function, "the kebab shows" and "the menu has
|
|
15
|
+
* something in it" are the same question, and an empty menu cannot be drawn.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type VehicleActionKey =
|
|
19
|
+
| "edit"
|
|
20
|
+
| "approve"
|
|
21
|
+
| "restore"
|
|
22
|
+
| "toggle-type"
|
|
23
|
+
| "delete";
|
|
24
|
+
|
|
25
|
+
export type VehicleActionPermissions = {
|
|
26
|
+
canUpdateVehicle?: boolean;
|
|
27
|
+
canDeleteVehicle?: boolean;
|
|
28
|
+
canApproveVehicle?: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type VehicleAction = {
|
|
32
|
+
key: VehicleActionKey;
|
|
33
|
+
title: string;
|
|
34
|
+
icon: string;
|
|
35
|
+
/** Vuetify `base-color`; undefined means the list's default. */
|
|
36
|
+
color?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** The shape a plate is read as. Only the three fields the decision uses. */
|
|
40
|
+
export type VehicleActionPlate = {
|
|
41
|
+
status?: string | null;
|
|
42
|
+
type?: string | null;
|
|
43
|
+
plateNumber?: string | null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `inactive` is the status a blocked vehicle carries, and `deleted` is a
|
|
48
|
+
* soft-deleted one. Both are "not currently working, and someone should be
|
|
49
|
+
* able to put it back".
|
|
50
|
+
*/
|
|
51
|
+
const RESTORABLE = new Set(["inactive", "deleted", "rejected"]);
|
|
52
|
+
|
|
53
|
+
export function vehiclePlateActions(
|
|
54
|
+
plate: VehicleActionPlate | null | undefined,
|
|
55
|
+
permissions: VehicleActionPermissions = {},
|
|
56
|
+
): VehicleAction[] {
|
|
57
|
+
if (!plate) return [];
|
|
58
|
+
|
|
59
|
+
const status = String(plate.status ?? "").toLowerCase();
|
|
60
|
+
const type = String(plate.type ?? "").toLowerCase();
|
|
61
|
+
|
|
62
|
+
const isActive = status === "active";
|
|
63
|
+
const isPending = status === "pending";
|
|
64
|
+
const isRestorable = RESTORABLE.has(status);
|
|
65
|
+
const isTogglableType = type === "whitelist" || type === "blocklist";
|
|
66
|
+
|
|
67
|
+
const actions: VehicleAction[] = [];
|
|
68
|
+
|
|
69
|
+
if (permissions.canUpdateVehicle && isActive) {
|
|
70
|
+
actions.push({ key: "edit", title: "Edit Vehicle", icon: "mdi-pencil" });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (permissions.canApproveVehicle && isPending) {
|
|
74
|
+
actions.push({
|
|
75
|
+
key: "approve",
|
|
76
|
+
title: "Approve",
|
|
77
|
+
icon: "mdi-check-circle",
|
|
78
|
+
color: "success",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Restore is deliberately NOT gated on `canUpdateVehicle`: it was ungated
|
|
83
|
+
// before this change and narrowing a gate is a separate, role-data decision.
|
|
84
|
+
// What changes is that a blocked vehicle now offers it at all.
|
|
85
|
+
if (isRestorable) {
|
|
86
|
+
actions.push({
|
|
87
|
+
key: "restore",
|
|
88
|
+
title: status === "inactive" ? "Unblock" : "Restore",
|
|
89
|
+
icon: "mdi-restore",
|
|
90
|
+
color: "warning",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (permissions.canUpdateVehicle && isTogglableType && isActive) {
|
|
95
|
+
actions.push({
|
|
96
|
+
key: "toggle-type",
|
|
97
|
+
title: type === "blocklist" ? "Unblock" : "Block",
|
|
98
|
+
icon: "mdi-swap-horizontal",
|
|
99
|
+
color: type === "blocklist" ? "primary" : "error",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (permissions.canDeleteVehicle && isActive) {
|
|
104
|
+
actions.push({
|
|
105
|
+
key: "delete",
|
|
106
|
+
title: "Delete",
|
|
107
|
+
icon: "mdi-delete",
|
|
108
|
+
color: "error",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return actions;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Every plate a MAIN-table row stands for.
|
|
117
|
+
*
|
|
118
|
+
* `vehicle.repo.ts` groups sibling plates by non-empty NRIC and keeps the FIRST
|
|
119
|
+
* document's `_id` as the row's `_id`, then collects the group into `plates`.
|
|
120
|
+
* Test Res 3 on staging is one row holding TEST123 (whitelist) and TEST124
|
|
121
|
+
* (blocklist) - two separate vehicle documents. The row menu used to resolve a
|
|
122
|
+
* single plate and silently act on TEST123, with no way to reach TEST124.
|
|
123
|
+
*/
|
|
124
|
+
export function rowPlates(row: any): any[] {
|
|
125
|
+
const plates = Array.isArray(row?.plates) ? row.plates : [];
|
|
126
|
+
if (plates.length) return plates;
|
|
127
|
+
|
|
128
|
+
// A row that carries its own plate fields and no group (the shape the older
|
|
129
|
+
// list endpoints return) is a single plate.
|
|
130
|
+
return row?.plateNumber ? [row] : [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** True when at least one plate on the row offers at least one action. */
|
|
134
|
+
export function rowHasVehicleActions(
|
|
135
|
+
row: any,
|
|
136
|
+
permissions: VehicleActionPermissions = {},
|
|
137
|
+
): boolean {
|
|
138
|
+
return rowPlates(row).some(
|
|
139
|
+
(plate) => vehiclePlateActions(plate, permissions).length > 0,
|
|
140
|
+
);
|
|
141
|
+
}
|