@7365admin1/layer-common 4.97.1-staging.581 → 4.97.1-staging.583

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.
Files changed (39) hide show
  1. package/assets/css/primitives.css +54 -11
  2. package/assets/css/screens.css +3 -3
  3. package/assets/css/tokens.css +17 -12
  4. package/components/AnprCameraBadge.vue +1 -1
  5. package/components/AreaMain.vue +11 -5
  6. package/components/BillingMain.vue +12 -53
  7. package/components/BulletinBoardManagement.vue +9 -13
  8. package/components/CameraWall.vue +7 -7
  9. package/components/CameraWallTile.vue +8 -8
  10. package/components/Chat/ListCard.vue +1 -1
  11. package/components/CleaningScheduleMain.vue +56 -20
  12. package/components/ClientMain.vue +1 -1
  13. package/components/ClientSubscriptionForm.vue +1 -1
  14. package/components/DashboardMain.vue +13 -13
  15. package/components/HidAccessLogDashboard.vue +3 -3
  16. package/components/HidIntercomManagement.vue +14 -14
  17. package/components/HidQrCodeConfiguration.vue +3 -3
  18. package/components/HidReaderManagement.vue +1 -1
  19. package/components/InventoryPhotoThumbs.vue +1 -1
  20. package/components/InvitationMain.vue +8 -0
  21. package/components/Layout/Header.vue +2 -2
  22. package/components/Layout/NavigationDrawer.vue +1 -1
  23. package/components/MemberMain.vue +8 -0
  24. package/components/Nfc/NFCRouteScheduleTimes.vue +1 -1
  25. package/components/PlateNumberDisplay.vue +1 -1
  26. package/components/RolePermissionMain.vue +13 -0
  27. package/components/ScheduleAreaMain.vue +6 -9
  28. package/components/ScheduleGuide.vue +101 -0
  29. package/components/ScheduleTaskForm.vue +88 -7
  30. package/components/ScheduleTaskMain.vue +163 -56
  31. package/components/SiteSettings.vue +5 -5
  32. package/components/StatusChip.vue +1 -1
  33. package/components/VisitorsReportPreview.vue +2 -2
  34. package/composables/useKey.ts +20 -16
  35. package/package.json +1 -1
  36. package/pages/unauthorized.vue +4 -3
  37. package/utils/breadcrumb.ts +7 -0
  38. package/utils/nav-sections.ts +2 -0
  39. package/utils/schedule-task.ts +106 -23
@@ -59,8 +59,10 @@
59
59
  variant="outlined"
60
60
  density="comfortable"
61
61
  class="filter-field mb-4"
62
- placeholder="For example: Midday toilet round"
62
+ placeholder="For example: Lunch toilet check"
63
63
  :error-messages="fieldError('title')"
64
+ hint="Staff see this name on their phone, on the extra round."
65
+ persistent-hint
64
66
  hide-details="auto"
65
67
  />
66
68
  <InputLabel for="description" title="Instructions" />
@@ -72,6 +74,8 @@
72
74
  class="filter-field"
73
75
  rows="3"
74
76
  placeholder="Optional. Anything staff should know for this visit."
77
+ hint="Optional. For example: refill soap and tissue as well."
78
+ persistent-hint
75
79
  hide-details="auto"
76
80
  />
77
81
  </div>
@@ -81,8 +85,9 @@
81
85
  <v-stepper-window-item :value="2">
82
86
  <div class="st-step">
83
87
  <p class="st-step__help">
84
- Pick the areas that get the extra visit. Every unit in an
85
- area is added to the checklist.
88
+ Pick the areas that get the extra visit. Every unit in a
89
+ picked area gets one more tick box on the Daily Checklist
90
+ for this visit.
86
91
  </p>
87
92
  <InputLabel for="selected-areas" title="Areas" required />
88
93
  <v-select
@@ -165,7 +170,8 @@
165
170
  <div class="st-step">
166
171
  <p class="st-step__help">
167
172
  Choose the time, and whether it happens once or repeats.
168
- Times are Singapore time.
173
+ Times are Singapore time. The extra round appears on staff's
174
+ checklist at this time.
169
175
  </p>
170
176
  <v-row dense>
171
177
  <v-col cols="12" sm="6">
@@ -193,6 +199,8 @@
193
199
  variant="outlined"
194
200
  density="comfortable"
195
201
  class="filter-field"
202
+ :hint="REPEAT_HINTS[repeat]"
203
+ persistent-hint
196
204
  hide-details="auto"
197
205
  />
198
206
  </v-col>
@@ -200,7 +208,7 @@
200
208
 
201
209
  <!-- Once: pick the day(s) -->
202
210
  <div v-if="repeat === 'none'" class="mt-4">
203
- <InputLabel title="Day" required />
211
+ <InputLabel title="Day(s)" required />
204
212
  <div class="st-panel">
205
213
  <v-date-picker
206
214
  v-model="pickerDates"
@@ -213,7 +221,8 @@
213
221
  class="ma-0"
214
222
  />
215
223
  <div class="st-step__help mt-2">
216
- {{ selectedDates.length }} day(s) selected.
224
+ {{ selectedDates.length }} day(s) selected. Click a day
225
+ again to remove it.
217
226
  </div>
218
227
  </div>
219
228
  <div v-if="fieldError('dates').length" class="st-error">
@@ -304,6 +313,32 @@
304
313
  <div class="st-summary__label">This will:</div>
305
314
  <div class="st-summary__text">{{ summary }}</div>
306
315
  </div>
316
+
317
+ <!-- What staff will see: the same card shape as the mobile checklist. -->
318
+ <div class="st-summary__label mt-4">
319
+ What staff will see on their phone:
320
+ </div>
321
+ <div class="st-preview">
322
+ <div class="st-preview__head">
323
+ <v-icon size="18" class="mr-1">mdi-clock-outline</v-icon>
324
+ {{ previewTime }} · Extra visit
325
+ </div>
326
+ <div class="st-preview__title">
327
+ {{ subject.trim() || "(your title)" }}
328
+ </div>
329
+ <div v-if="description.trim()" class="st-preview__desc">
330
+ {{ description.trim() }}
331
+ </div>
332
+ <div
333
+ v-for="area in selectedAreas"
334
+ :key="areaKey(area)"
335
+ class="st-preview__row"
336
+ >
337
+ <v-icon size="18" class="mr-1">mdi-checkbox-blank-outline</v-icon>
338
+ {{ area.name }} -
339
+ {{ unitCountOf(area) }} unit{{ unitCountOf(area) === 1 ? "" : "s" }} to tick
340
+ </div>
341
+ </div>
307
342
  <div v-if="fieldError('never').length" class="st-error mt-2">
308
343
  {{ fieldError("never")[0] }}
309
344
  </div>
@@ -372,6 +407,7 @@ import {
372
407
  REPEAT_OPTIONS,
373
408
  WEEK_DAYS,
374
409
  buildSummary,
410
+ fmtTime,
375
411
  sgtToday,
376
412
  validateSchedule,
377
413
  type TRepeat,
@@ -395,6 +431,11 @@ const props = defineProps({
395
431
  type: String,
396
432
  default: "",
397
433
  },
434
+ /** The step the dialog opens on (3 = When, used by "Extend or repeat"). */
435
+ startStep: {
436
+ type: Number,
437
+ default: 1,
438
+ },
398
439
  });
399
440
 
400
441
  const show = defineModel("show", { type: Boolean, default: false });
@@ -476,6 +517,15 @@ const formState = computed(() => ({
476
517
  const errors = computed<TScheduleErrors>(() => validateSchedule(formState.value));
477
518
  const summary = computed(() => buildSummary(formState.value, totalUnits.value));
478
519
 
520
+ const REPEAT_HINTS: Record<TRepeat, string> = {
521
+ none: "Only on the day(s) you pick below.",
522
+ daily: "Every day, weekends included.",
523
+ weekdays: "Monday to Friday.",
524
+ weekly: "On the weekdays you pick below.",
525
+ monthly: "Once a month, on the start date's day.",
526
+ };
527
+ const previewTime = computed(() => fmtTime(scheduleTime.value) || "--");
528
+
479
529
  const STEP_FIELDS: Record<number, (keyof TScheduleErrors)[]> = {
480
530
  1: ["title"],
481
531
  2: ["areas"],
@@ -694,6 +744,7 @@ const close = () => {
694
744
  watch(
695
745
  [show, () => props.mode, () => props.taskData],
696
746
  async ([isOpen, mode]) => {
747
+ if (isOpen) step.value = props.startStep || 1;
697
748
  if (isOpen && mode === "edit") {
698
749
  await loadTaskData();
699
750
  } else if (!isOpen) {
@@ -710,7 +761,7 @@ watch(
710
761
  }
711
762
  .st-step__help {
712
763
  margin: 0 0 12px;
713
- font-size: var(--fs-chip-lg);
764
+ font-size: var(--fs-cell);
714
765
  font-weight: 600;
715
766
  color: var(--text2);
716
767
  }
@@ -748,6 +799,36 @@ watch(
748
799
  font-size: 15px;
749
800
  font-weight: 600;
750
801
  }
802
+ .st-preview {
803
+ margin-top: 6px;
804
+ padding: 14px 16px;
805
+ border: 1px solid var(--border);
806
+ border-left: 4px solid rgb(var(--v-theme-primary));
807
+ border-radius: var(--r-inner);
808
+ background: var(--card);
809
+ font-size: var(--fs-cell);
810
+ }
811
+ .st-preview__head {
812
+ display: flex;
813
+ align-items: center;
814
+ font-size: var(--fs-chip-lg);
815
+ font-weight: 700;
816
+ color: var(--text2);
817
+ }
818
+ .st-preview__title {
819
+ margin: 4px 0;
820
+ font-size: var(--fs-card-title);
821
+ font-weight: 800;
822
+ }
823
+ .st-preview__desc {
824
+ margin-bottom: 6px;
825
+ color: var(--text2);
826
+ }
827
+ .st-preview__row {
828
+ display: flex;
829
+ align-items: center;
830
+ padding: 4px 0;
831
+ }
751
832
  .st-stepper,
752
833
  .st-stepper__header {
753
834
  background: transparent;
@@ -5,10 +5,52 @@
5
5
  :org-id="orgId"
6
6
  :site="site"
7
7
  :service-type="serviceType"
8
- text="An extra visit adds one more round to your daily schedule at the time you choose, once or on a repeat. Staff on this site get a push notification when it is added."
9
- />
8
+ text="An extra visit adds one more round to the Daily Checklist at a time you choose - once, or on a repeat."
9
+ how-title="How extra visits work"
10
+ how-key="extra-visits"
11
+ >
12
+ <ol class="sg-steps">
13
+ <li>
14
+ Every night the app makes the <strong>Daily Checklist</strong>
15
+ for every area on this site. You do not need to do anything for
16
+ that.
17
+ </li>
18
+ <li>
19
+ An <strong>extra visit</strong> adds one more round to that
20
+ checklist, in the areas you pick, at the time you set - once, or
21
+ repeating (every day, weekdays, chosen days, or monthly).
22
+ </li>
23
+ <li>
24
+ At that time, staff see the extra round in their mobile app's
25
+ daily checklist (with a push notification) and tick each unit as
26
+ they finish it.
27
+ </li>
28
+ <li>
29
+ Here and in the Daily Checklist you see how it went:
30
+ <strong>Done</strong>, <strong>In progress</strong>,
31
+ <strong>Needs redo</strong> or <strong>Missed</strong>.
32
+ </li>
33
+ </ol>
34
+ <div class="sg-example">
35
+ <strong>Example:</strong> the lobby toilets get busy at lunch. Add
36
+ an extra visit called "Lunch toilet check", pick the area "Lobby
37
+ Toilets", set 2:00 pm, every weekday. From the next weekday, staff
38
+ get one more toilet round at 2:00 pm, on top of their normal ones.
39
+ </div>
40
+ <div class="sg-h">What the words mean</div>
41
+ <dl class="sg-legend">
42
+ <template v-for="h in STATUS_HELP" :key="h.word">
43
+ <dt><StatusChip :status="h.word" :label="h.word" /></dt>
44
+ <dd>{{ h.meaning }}</dd>
45
+ </template>
46
+ <template v-for="h in RESULT_HELP" :key="'r' + h.word">
47
+ <dt><StatusChip :status="h.word" :label="h.word" /></dt>
48
+ <dd>Last visit: {{ h.meaning }}</dd>
49
+ </template>
50
+ </dl>
51
+ </ScheduleGuide>
10
52
  <TableMain
11
- :title="'Extra Visits (Scheduled)'"
53
+ :title="'Extra Visits'"
12
54
  :items="items"
13
55
  :headers="headers"
14
56
  :loading="loading"
@@ -26,9 +68,14 @@
26
68
  <div class="st-empty">
27
69
  <strong>No extra visits yet.</strong>
28
70
  <span>
29
- An extra visit adds one more round to your daily
30
- {{ scheduleName }} at a time you choose. Set up your Areas and
31
- Units first, then add one.
71
+ Your Daily Checklist already covers the normal rounds. Add an
72
+ extra visit when an area needs one more round at a set time -
73
+ for example "Lunch toilet check, Lobby Toilets, 2:00 pm every
74
+ weekday".
75
+ </span>
76
+ <span v-if="canCreateScheduleTask">
77
+ Press <strong>Add Extra Visit</strong> above to start. Your
78
+ Areas and Units must be set up first.
32
79
  </span>
33
80
  </div>
34
81
  </template>
@@ -61,27 +108,6 @@
61
108
  </v-row>
62
109
  </template>
63
110
 
64
- <template #item.areas="{ value }">
65
- <div v-if="value && value.length > 0">
66
- <StatusChip
67
- v-for="(area, idx) in value.slice(0, 2)"
68
- :key="idx"
69
- class="mr-1 mb-1"
70
- :dot="false"
71
- tone="neutral"
72
- :label="area.name"
73
- />
74
- <StatusChip
75
- v-if="value.length > 2"
76
- class="mb-1"
77
- :dot="false"
78
- tone="neutral"
79
- :label="`+${value.length - 2} more`"
80
- />
81
- </div>
82
- <span v-else>N/A</span>
83
- </template>
84
-
85
111
  <template #item.when="{ item }">
86
112
  <span class="st-when">{{ item.whenText }}</span>
87
113
  </template>
@@ -91,24 +117,34 @@
91
117
  </template>
92
118
 
93
119
  <template #item.lastRun="{ item }">
94
- <StatusChip
95
- v-if="item.lastRun && item.lastRunStatus === 'done'"
96
- status="Done"
97
- :label="item.lastRunText"
98
- />
99
- <StatusChip
100
- v-else-if="item.lastRun && item.lastRunStatus === 'missed'"
101
- status="Missed"
102
- :label="item.lastRunText"
103
- />
104
- <span v-else>{{ item.lastRunText }}</span>
120
+ <div class="st-cell">
121
+ <StatusChip
122
+ v-if="item.lastRun && item.lastRunWord"
123
+ :status="item.lastRunWord"
124
+ :label="item.lastRunText"
125
+ />
126
+ <span v-else>{{ item.lastRunText }}</span>
127
+ <a
128
+ v-if="item.lastRun"
129
+ href="#"
130
+ class="st-link"
131
+ @click.prevent.stop="openChecklist(item)"
132
+ >
133
+ See that day's checklist
134
+ </a>
135
+ </div>
105
136
  </template>
106
137
 
107
138
  <template #item.status="{ item }">
108
- <StatusChip
109
- :status="item.stateLabel"
110
- :label="item.stateLabel"
111
- />
139
+ <div class="st-cell">
140
+ <StatusChip
141
+ :status="item.stateLabel"
142
+ :label="item.stateLabel"
143
+ />
144
+ <span v-if="item.stateKey === 'ended'" class="st-note">
145
+ {{ item.finishedText }}
146
+ </span>
147
+ </div>
112
148
  </template>
113
149
 
114
150
  <template #item.actions="{ item }">
@@ -123,6 +159,17 @@
123
159
  >
124
160
  Edit
125
161
  </v-btn>
162
+ <v-btn
163
+ v-if="canUpdateScheduleTask && item.stateKey === 'ended'"
164
+ size="small"
165
+ variant="tonal"
166
+ color="primary"
167
+ class="text-none"
168
+ prepend-icon="mdi-calendar-refresh"
169
+ @click.stop="onExtendRow(item)"
170
+ >
171
+ Extend or repeat
172
+ </v-btn>
126
173
  <v-btn
127
174
  v-if="canUpdateScheduleTask && item.stateKey !== 'ended'"
128
175
  size="small"
@@ -155,6 +202,7 @@
155
202
  v-model:show="showForm"
156
203
  :mode="formMode"
157
204
  :taskData="selectedTask"
205
+ :start-step="formStartStep"
158
206
  :site="props.site"
159
207
  :serviceType="props.serviceType"
160
208
  @refresh="getTaskRefresh"
@@ -194,7 +242,7 @@
194
242
 
195
243
  <v-row no-gutters class="mb-2">
196
244
  <v-col cols="5" class="text-subtitle-2 font-weight-bold">
197
- Next run:
245
+ Next visit:
198
246
  </v-col>
199
247
  <v-col cols="7" class="text-subtitle-2">
200
248
  {{ selectedNext || "-" }}
@@ -203,10 +251,18 @@
203
251
 
204
252
  <v-row no-gutters class="mb-2">
205
253
  <v-col cols="5" class="text-subtitle-2 font-weight-bold">
206
- Last run:
254
+ Last visit:
207
255
  </v-col>
208
256
  <v-col cols="7" class="text-subtitle-2">
209
257
  {{ lastRunLabel(selectedTask) }}
258
+ <a
259
+ v-if="selectedTask.lastRun"
260
+ href="#"
261
+ class="st-link d-block"
262
+ @click.prevent="openChecklist(selectedTask)"
263
+ >
264
+ See that day's checklist
265
+ </a>
210
266
  </v-col>
211
267
  </v-row>
212
268
 
@@ -319,7 +375,7 @@
319
375
  </div>
320
376
  <span v-else class="text-caption text-disabled"
321
377
  >No units in this area, so it is skipped when
322
- this visit runs.</span
378
+ this visit is added.</span
323
379
  >
324
380
  </v-card-text>
325
381
  </div>
@@ -348,6 +404,12 @@
348
404
  :status="stateOf(selectedTask).label"
349
405
  :label="stateOf(selectedTask).label"
350
406
  />
407
+ <div
408
+ v-if="stateOf(selectedTask).key === 'ended'"
409
+ class="st-note mt-1"
410
+ >
411
+ {{ finishedReason(selectedTask as any) }}
412
+ </div>
351
413
  </v-col>
352
414
  </v-row>
353
415
 
@@ -383,8 +445,8 @@
383
445
  </v-card-title>
384
446
 
385
447
  <v-card-text class="screen-modal__body">
386
- This stops all future runs of this extra visit. Visits that already
387
- happened are kept. Delete it?
448
+ This stops all future extra visits from it. Visits that already
449
+ happened stay on their Daily Checklists. Delete it?
388
450
  </v-card-text>
389
451
 
390
452
  <div class="screen-modal__footer">
@@ -422,8 +484,12 @@ import useScheduleTask from "../composables/useScheduleTask";
422
484
  import useUtils from "../composables/useUtils";
423
485
  import useAreas from "../composables/useAreas";
424
486
  import {
487
+ RESULT_HELP,
488
+ STATUS_HELP,
425
489
  STATUS_LABELS,
490
+ describeRow,
426
491
  describeWhen,
492
+ finishedReason,
427
493
  fmtMoment,
428
494
  lastRunLabel,
429
495
  scheduleNameFor,
@@ -458,7 +524,16 @@ const canViewScheduleTaskDetails = computed(
458
524
  () => props.canViewScheduleTaskDetails
459
525
  );
460
526
 
461
- const scheduleName = scheduleNameFor(props.serviceType).title;
527
+ const checklistRoute = scheduleNameFor(props.serviceType).route;
528
+
529
+ /** Opens the Daily Checklist filtered to the day this extra visit was last added. */
530
+ function openChecklist(row: Record<string, any>) {
531
+ const day = String(row?.lastRun || "").slice(0, 10);
532
+ navigateTo({
533
+ path: `/${props.orgId}/${props.site}/${checklistRoute}`,
534
+ query: /^\d{4}-\d{2}-\d{2}$/.test(day) ? { date: day } : {},
535
+ });
536
+ }
462
537
 
463
538
  const page = ref(1);
464
539
  const pages = ref(0);
@@ -468,6 +543,7 @@ const searchInput = ref("");
468
543
  const showForm = ref(false);
469
544
  const selectedTask = ref<Record<string, any>>({});
470
545
  const formMode = ref<"add" | "edit">("add");
546
+ const formStartStep = ref(1);
471
547
  const submitting = ref(false);
472
548
  const dialogShowMoreActions = ref(false);
473
549
  const dialogDeleteTask = ref(false);
@@ -517,11 +593,10 @@ watch(dialogShowMoreActions, async (isOpen) => {
517
593
  const items = ref<any[]>([]);
518
594
 
519
595
  const headers = [
520
- { title: "Title", value: "title" },
521
- { title: "Areas", value: "areas" },
522
- { title: "When", value: "when", sortable: false },
523
- { title: "Next run", value: "nextRun", sortable: false },
524
- { title: "Last run", value: "lastRun", sortable: false },
596
+ { title: "Extra visit", value: "title" },
597
+ { title: "What will happen", value: "when", sortable: false },
598
+ { title: "Next visit", value: "nextRun", sortable: false },
599
+ { title: "Last visit", value: "lastRun", sortable: false },
525
600
  { title: "Status", value: "status", sortable: false },
526
601
  { title: "", value: "actions", sortable: false, align: "end" as const },
527
602
  ];
@@ -566,7 +641,9 @@ watchEffect(() => {
566
641
  const st = stateOf(row);
567
642
  return {
568
643
  ...row,
569
- whenText: describeWhen(row),
644
+ whenText: describeRow(row),
645
+ finishedText: finishedReason(row),
646
+ lastRunWord: ({ done: "Done", missed: "Missed", pending: "In progress" } as Record<string, string>)[row.lastRunStatus] || "",
570
647
  nextRunText: fmtMoment(row.nextRun),
571
648
  lastRunText: lastRunLabel(row),
572
649
  stateKey: st.key,
@@ -590,6 +667,7 @@ watch(
590
667
 
591
668
  function openFormDialog() {
592
669
  formMode.value = "add";
670
+ formStartStep.value = 1;
593
671
  selectedTask.value = {};
594
672
  showForm.value = true;
595
673
  }
@@ -597,6 +675,15 @@ function openFormDialog() {
597
675
  function onEditRow(item: Record<string, any>) {
598
676
  selectedTask.value = item;
599
677
  formMode.value = "edit";
678
+ formStartStep.value = 1;
679
+ showForm.value = true;
680
+ }
681
+
682
+ // A finished extra visit: open its "When" step so a new end date or repeat is one change away.
683
+ function onExtendRow(item: Record<string, any>) {
684
+ selectedTask.value = item;
685
+ formMode.value = "edit";
686
+ formStartStep.value = 3;
600
687
  showForm.value = true;
601
688
  }
602
689
 
@@ -612,8 +699,8 @@ async function togglePause(item: Record<string, any>) {
612
699
  await setScheduleTaskPaused(item._id, !resuming);
613
700
  showMessage(
614
701
  resuming
615
- ? "Resumed. It will run again from its next date."
616
- : "Paused. It will not run until you resume it.",
702
+ ? "Resumed. Extra visits are added again from its next date."
703
+ : "Paused. No extra visits are added until you resume it.",
617
704
  "success",
618
705
  );
619
706
  await getTaskRefresh();
@@ -649,6 +736,7 @@ async function onRowClick(data: any) {
649
736
  const onEditFromMoreAction = async () => {
650
737
  dialogShowMoreActions.value = false;
651
738
  formMode.value = "edit";
739
+ formStartStep.value = stateOf(selectedTask.value).key === "ended" ? 3 : 1;
652
740
  showForm.value = true;
653
741
  };
654
742
 
@@ -693,8 +781,27 @@ async function _deleteScheduleTask() {
693
781
  color: var(--text2);
694
782
  }
695
783
  .st-when {
784
+ display: inline-block;
785
+ min-width: 220px;
696
786
  white-space: normal;
697
787
  }
788
+ .st-cell {
789
+ display: flex;
790
+ flex-direction: column;
791
+ align-items: flex-start;
792
+ gap: 4px;
793
+ }
794
+ .st-note {
795
+ font-size: var(--fs-chip-lg);
796
+ color: var(--text2);
797
+ white-space: normal;
798
+ }
799
+ .st-link {
800
+ font-size: var(--fs-chip-lg);
801
+ font-weight: 600;
802
+ color: rgb(var(--v-theme-primary));
803
+ text-decoration: underline;
804
+ }
698
805
  .st-actions {
699
806
  display: flex;
700
807
  justify-content: flex-end;
@@ -544,11 +544,6 @@ const saveANPRSettings = async () => {
544
544
  try {
545
545
  isSaving.value = true;
546
546
 
547
- // Replace this with your actual API endpoint / store action
548
- // await $fetch(`/api/sites/${siteId.value}/camera-settings`, {
549
- // method: 'POST',
550
- // body: cameraSettings.value
551
- // })
552
547
  const data = {
553
548
  site: props.siteId,
554
549
  ANPRSwitches: ANPRSwitches.value,
@@ -559,6 +554,11 @@ const saveANPRSettings = async () => {
559
554
  show("Saved successfully", "success");
560
555
  } catch (error) {
561
556
  console.error("Failed to save settings:", error);
557
+ show(
558
+ (error as any)?.data?.message ||
559
+ "Could not save the camera (ANPR) settings. Please try again.",
560
+ "error"
561
+ );
562
562
  } finally {
563
563
  isSaving.value = false;
564
564
  }
@@ -104,7 +104,7 @@ const toneClass = computed(() =>
104
104
  font-size: var(--fs-chip);
105
105
  }
106
106
 
107
- /* The role tag: `padding:3px 10px;border-radius:999px;font-size:11.5px`. */
107
+ /* The role tag: `padding:3px 10px;border-radius:999px;font-size:12.5px`. */
108
108
  .status-chip--pill {
109
109
  padding: 3px 10px;
110
110
  font-size: var(--fs-chip);
@@ -397,12 +397,12 @@ onMounted(() => {
397
397
 
398
398
  :deep(.v-data-table thead tr th) {
399
399
  padding: 6px !important;
400
- font-size: 11px !important;
400
+ font-size: 12px !important;
401
401
  }
402
402
 
403
403
  :deep(.v-data-table tbody tr td) {
404
404
  padding: 4px !important;
405
- font-size: 10px !important;
405
+ font-size: 12px !important;
406
406
  }
407
407
 
408
408
  :deep(.v-img) {
@@ -132,21 +132,25 @@ export default function useKey() {
132
132
  keyLog.value = data || new MKeyLog();
133
133
  }
134
134
 
135
- function addKeyLog(_keyLog: TKeyLog) {
135
+ // Await each request so a failed save reaches the caller's catch instead of showing "success".
136
+ async function addKeyLog(_keyLog: TKeyLog) {
136
137
  isAdding.value = true;
137
138
  delete _keyLog._id;
138
- const data = useNuxtApp().$api<{
139
- message: string | undefined;
140
- }>(keyLogApiUrl, {
141
- method: "POST",
142
- body: JSON.stringify(_keyLog),
143
- });
144
- isAdding.value = false;
145
- return data.message;
139
+ try {
140
+ const data = await useNuxtApp().$api<{
141
+ message: string | undefined;
142
+ }>(keyLogApiUrl, {
143
+ method: "POST",
144
+ body: JSON.stringify(_keyLog),
145
+ });
146
+ return data?.message;
147
+ } finally {
148
+ isAdding.value = false;
149
+ }
146
150
  }
147
151
 
148
- function getKeyById(id: string) {
149
- const data = useNuxtApp().$api<TKeyLog | undefined>(
152
+ async function getKeyById(id: string) {
153
+ const data = await useNuxtApp().$api<TKeyLog | undefined>(
150
154
  `${keyLogApiUrl}/${id}`,
151
155
  {
152
156
  method: "GET",
@@ -158,22 +162,22 @@ export default function useKey() {
158
162
  return data;
159
163
  }
160
164
 
161
- function updateKeyById(id: string, payload: TKeyLog) {
165
+ async function updateKeyById(id: string, payload: TKeyLog) {
162
166
  payload.codes =
163
167
  payload.codes instanceof Array && payload.codes.length > 0
164
168
  ? payload.codes
165
169
  : [];
166
- const data = useNuxtApp().$api<{
170
+ const data = await useNuxtApp().$api<{
167
171
  message: { acknowledged: boolean };
168
172
  }>(`${keyLogApiUrl}/${id}`, {
169
173
  method: "PUT",
170
174
  body: payload,
171
175
  });
172
- return data.message;
176
+ return data?.message;
173
177
  }
174
178
 
175
- function deleteKeyById(id: string) {
176
- const data = useNuxtApp().$api<{ message: TKeyLog }>(
179
+ async function deleteKeyById(id: string) {
180
+ const data = await useNuxtApp().$api<{ message: TKeyLog }>(
177
181
  `${keyLogApiUrl}/${id}`,
178
182
  {
179
183
  method: "DELETE",