@7365admin1/layer-common 4.2.2-staging.262 → 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 }} Do you want to delete anyway?
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 v-if="(plateNumberItem as TPlateNumber)?.status == 'active'"
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
- <DialogDeleteConfirmation :loading="deletingVehicle"
144
- :prompt-title="`Are you sure want to delete this vehicle - ${selectedPlateNumberObject?.plateNumber}?`"
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
- const bypass = ref(false);
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
- message.value = error.response._data.message;
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.262",
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.",