@7365admin1/layer-common 4.2.2-staging.261 → 4.2.2-staging.263
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
|
|
|
@@ -36,6 +36,35 @@
|
|
|
36
36
|
<template #item.plates="{ value, item }">
|
|
37
37
|
<PlateNumberDisplay :plate-numbers="value" :default-value="item.plateNumber" />
|
|
38
38
|
</template>
|
|
39
|
+
|
|
40
|
+
<!--
|
|
41
|
+
Edit and Delete were already built and already shipping, but only two
|
|
42
|
+
clicks deep: click the row -> the read-only "Preview" dialog -> the
|
|
43
|
+
kebab on the NESTED plate table. Nothing on the row itself said the
|
|
44
|
+
actions existed. This is the same kebab every other table in this
|
|
45
|
+
package draws (`AppButton variant="row"` as a `v-menu` activator, see
|
|
46
|
+
`MemberMain.vue`), and it calls the SAME handlers the nested menu calls,
|
|
47
|
+
so there is exactly one code path per operation.
|
|
48
|
+
|
|
49
|
+
`@click.stop` because the row carries `@row-click`; without it the
|
|
50
|
+
kebab would open the Preview dialog underneath the menu.
|
|
51
|
+
-->
|
|
52
|
+
<template #item.action="{ item }">
|
|
53
|
+
<v-menu v-if="rowPlate(item) && hasVehicleActions(rowPlate(item) as TPlateNumber)"
|
|
54
|
+
location="bottom end">
|
|
55
|
+
<template #activator="{ props: menuProps }">
|
|
56
|
+
<AppButton v-bind="menuProps" variant="row" icon="mdi-dots-vertical" aria-label="Vehicle actions"
|
|
57
|
+
@click.stop />
|
|
58
|
+
</template>
|
|
59
|
+
<v-list density="compact">
|
|
60
|
+
<v-list-item v-if="canUpdateVehicle && rowPlate(item)?.status === 'active'" title="Edit Vehicle"
|
|
61
|
+
prepend-icon="mdi-pencil" @click="handleEditVehicleAction(rowPlate(item) as TPlateNumber)" />
|
|
62
|
+
<v-list-item v-if="canDeleteVehicle && rowPlate(item)?.status === 'active'" title="Delete"
|
|
63
|
+
prepend-icon="mdi-delete" base-color="error"
|
|
64
|
+
@click="handleDeleteVehicleAction(rowPlate(item) as TPlateNumber)" />
|
|
65
|
+
</v-list>
|
|
66
|
+
</v-menu>
|
|
67
|
+
</template>
|
|
39
68
|
</TableMain>
|
|
40
69
|
|
|
41
70
|
<!-- <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" /> -->
|
|
@@ -97,7 +126,8 @@
|
|
|
97
126
|
<v-btn icon="mdi-dots-vertical" v-bind="menuProps" flat size="x-small" />
|
|
98
127
|
</template>
|
|
99
128
|
<v-list density="compact">
|
|
100
|
-
<v-list-item
|
|
129
|
+
<v-list-item
|
|
130
|
+
v-if="canUpdateVehicle && (plateNumberItem as TPlateNumber)?.status == 'active'"
|
|
101
131
|
title="Edit Vehicle" prepend-icon="mdi-pencil"
|
|
102
132
|
@click="handleEditVehicleAction(plateNumberItem as TPlateNumber)" />
|
|
103
133
|
<v-list-item
|
|
@@ -108,7 +138,7 @@
|
|
|
108
138
|
prepend-icon="mdi-restore" base-color="warning"
|
|
109
139
|
@click="handleRestoreVehicle(plateNumberItem as TPlateNumber)" />
|
|
110
140
|
<v-list-item
|
|
111
|
-
v-if="((plateNumberItem as TPlateNumber)?.type === 'whitelist' || (plateNumberItem as TPlateNumber)?.type === 'blocklist') && plateNumberItem?.status == 'active'"
|
|
141
|
+
v-if="canUpdateVehicle && ((plateNumberItem as TPlateNumber)?.type === 'whitelist' || (plateNumberItem as TPlateNumber)?.type === 'blocklist') && plateNumberItem?.status == 'active'"
|
|
112
142
|
:title="(plateNumberItem as TPlateNumber)?.type === 'blocklist' ? 'Unblock' : 'Block'"
|
|
113
143
|
prepend-icon="mdi-swap-horizontal"
|
|
114
144
|
:base-color="(plateNumberItem as TPlateNumber)?.type === 'blocklist' ? 'primary' : 'error'"
|
|
@@ -140,8 +170,17 @@
|
|
|
140
170
|
</DialogUpdateMoreAction>
|
|
141
171
|
</v-dialog>
|
|
142
172
|
<v-dialog v-model="dialog.deleteVehicle" persistent width="540">
|
|
143
|
-
|
|
144
|
-
|
|
173
|
+
<!--
|
|
174
|
+
A row is ONE plate document, not a person: the list groups sibling
|
|
175
|
+
plates by NRIC and keeps the first document's `_id` as the row's `_id`.
|
|
176
|
+
So the copy names the plate it is about to remove, says what removing
|
|
177
|
+
it actually does (revokes ANPR access), and says plainly that nothing
|
|
178
|
+
else on the record is touched. `message` carries the server's refusal
|
|
179
|
+
when the ANPR revoke could not be confirmed - the dialog stays open on
|
|
180
|
+
that path so the operator can read it and retry.
|
|
181
|
+
-->
|
|
182
|
+
<DialogDeleteConfirmation :loading="deletingVehicle" :message="deleteError"
|
|
183
|
+
: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
184
|
@delete="submitDelete" @close="closeDeleteDialog" />
|
|
146
185
|
</v-dialog>
|
|
147
186
|
<v-dialog v-model="dialog.approveVehicle" persistent width="540">
|
|
@@ -191,6 +230,7 @@ const headers = [
|
|
|
191
230
|
{ title: "Category", value: "category" },
|
|
192
231
|
// { title: "Type", value: "type" },
|
|
193
232
|
// { title: "Status", value: "status" },
|
|
233
|
+
{ title: "", value: "action" },
|
|
194
234
|
]
|
|
195
235
|
|
|
196
236
|
const plateHeaders = [
|
|
@@ -205,10 +245,6 @@ const plateHeaders = [
|
|
|
205
245
|
const { formatCamelCaseToWords, formatDate, debounce, maskNRIC } = useUtils();
|
|
206
246
|
const { getVehicles, deleteVehicle, formatVehicleStatus, approveVehicle, updateVehicle, getSpecificVehicleById } = useVehicle();
|
|
207
247
|
|
|
208
|
-
const canApproveVehicle = computed(() => {
|
|
209
|
-
return true;
|
|
210
|
-
})
|
|
211
|
-
|
|
212
248
|
const items = ref<Array<Record<string, any>>>([]);
|
|
213
249
|
const page = ref(1);
|
|
214
250
|
const pages = ref(0);
|
|
@@ -235,7 +271,10 @@ const selectedPlateNumberId = ref<string | null>(null); // individual plate numb
|
|
|
235
271
|
const message = ref("");
|
|
236
272
|
const messageColor = ref("");
|
|
237
273
|
const messageSnackbar = ref(false);
|
|
238
|
-
|
|
274
|
+
// The server's refusal when an ANPR revoke could not be confirmed. Rendered
|
|
275
|
+
// inside the delete dialog rather than a 5-second toast: it names the camera
|
|
276
|
+
// that failed and the operator needs it in front of them to retry.
|
|
277
|
+
const deleteError = ref("");
|
|
239
278
|
|
|
240
279
|
function showMessage(msg: string, color: string) {
|
|
241
280
|
message.value = msg;
|
|
@@ -298,6 +337,7 @@ function formatValues(key: string, value: any) {
|
|
|
298
337
|
|
|
299
338
|
const closeDeleteDialog = () => {
|
|
300
339
|
dialog.deleteVehicle = false;
|
|
340
|
+
deleteError.value = "";
|
|
301
341
|
};
|
|
302
342
|
|
|
303
343
|
const handleCloseAll = () => {
|
|
@@ -397,6 +437,34 @@ function handleUpdateType(item: TPlateNumber) {
|
|
|
397
437
|
dialog.updateVehicleType = true;
|
|
398
438
|
}
|
|
399
439
|
|
|
440
|
+
/**
|
|
441
|
+
* The plate document a MAIN-table row actually is.
|
|
442
|
+
*
|
|
443
|
+
* `vehicle.repo.ts` groups sibling plates by non-empty NRIC and keeps the FIRST
|
|
444
|
+
* document's `_id` as the row's `_id` (`vehicleId: { $first: "$_id" }`), then
|
|
445
|
+
* `$addToSet`s the group into `plates`. The row itself carries no
|
|
446
|
+
* `plateNumber`, `type`, `recNo` or `status` - only `plates[]` does - so a row
|
|
447
|
+
* action has to resolve its own plate out of that array before it can name it
|
|
448
|
+
* or delete it.
|
|
449
|
+
*/
|
|
450
|
+
function rowPlate(item: any): TPlateNumber | null {
|
|
451
|
+
const plates = (item?.plates ?? []) as Array<TPlateNumber>;
|
|
452
|
+
return (
|
|
453
|
+
plates.find((plate) => String(plate?._id) === String(item?._id)) ??
|
|
454
|
+
plates[0] ??
|
|
455
|
+
null
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* True when at least one menu item would render. It has to be the exact union
|
|
461
|
+
* of the five `v-if`s below it, or the kebab appears over an empty menu.
|
|
462
|
+
*
|
|
463
|
+
* `isActive` used to stand alone here, which was the Edit item's test - but
|
|
464
|
+
* Edit is now gated on `canUpdateVehicle`, and Block/Unblock only offers itself
|
|
465
|
+
* for a whitelist/blocklist plate, so a read-only role or a plate of another
|
|
466
|
+
* type would have opened nothing.
|
|
467
|
+
*/
|
|
400
468
|
function hasVehicleActions(item: TPlateNumber) {
|
|
401
469
|
const isActive = item?.status === "active";
|
|
402
470
|
const isPending = item?.status === "pending";
|
|
@@ -404,10 +472,10 @@ function hasVehicleActions(item: TPlateNumber) {
|
|
|
404
472
|
const canUpdateType = item?.type === "whitelist" || item?.type === "blocklist";
|
|
405
473
|
|
|
406
474
|
return (
|
|
407
|
-
isActive ||
|
|
475
|
+
(props.canUpdateVehicle && isActive) ||
|
|
408
476
|
(props.canApproveVehicle && isPending) ||
|
|
409
477
|
isDeleted ||
|
|
410
|
-
(canUpdateType && isActive) ||
|
|
478
|
+
(props.canUpdateVehicle && canUpdateType && isActive) ||
|
|
411
479
|
(props.canDeleteVehicle && isActive)
|
|
412
480
|
);
|
|
413
481
|
}
|
|
@@ -469,14 +537,26 @@ async function submitDelete() {
|
|
|
469
537
|
|
|
470
538
|
try {
|
|
471
539
|
deletingVehicle.value = true;
|
|
540
|
+
deleteError.value = "";
|
|
472
541
|
const res = await deleteVehicle({ site: props.site, recno: recNo, id: plateNumberId as string, type }
|
|
473
542
|
);
|
|
474
543
|
dialog.deleteVehicle = false;
|
|
475
544
|
showMessage(res.message, "success");
|
|
476
545
|
await getVehiclesRefresh();
|
|
477
546
|
} catch (error: any) {
|
|
547
|
+
// The delete is fail-closed on the server: if the plate cannot be confirmed
|
|
548
|
+
// gone from every ANPR camera holding it, the record is left intact and the
|
|
549
|
+
// request is refused. Say so. This used to assign to `message` WITHOUT
|
|
550
|
+
// raising the snackbar - and `error.response._data` throws on a network
|
|
551
|
+
// error - so a refused delete showed the operator nothing at all.
|
|
478
552
|
console.error("Error deleting vehicle:", error);
|
|
479
|
-
|
|
553
|
+
deleteError.value =
|
|
554
|
+
error?.response?._data?.message ||
|
|
555
|
+
error?.data?.message ||
|
|
556
|
+
error?.message ||
|
|
557
|
+
"Failed to delete vehicle.";
|
|
558
|
+
// The dialog stays open and the row stays in the table - nothing is removed
|
|
559
|
+
// optimistically, so what is on screen still matches the database.
|
|
480
560
|
} finally {
|
|
481
561
|
deletingVehicle.value = false;
|
|
482
562
|
}
|
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.263",
|
|
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.",
|