@7365admin1/layer-common 4.110.0 → 4.110.2

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.110.2
4
+
5
+ ### Patch Changes
6
+
7
+ - aa41164: Staging QA 2026-10-08: comboboxes are outlined like other fields; Edit Unit saves a unit whose level is a bare id (message says unit, not building); Tenancies period buttons fit a phone and the renew message shows a readable date; confirm prompts read "Are you sure you want to".
8
+
9
+ ## 4.110.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 7876496: Top bar shows the renamed modules ("Occupancy Mgmt", "Task Calendar") instead of the old route names; the Provider changes / Change provider windows wrap long titles and keep the close button inside the window on phones.
14
+
3
15
  ## 4.110.0
4
16
 
5
17
  ### Minor Changes
@@ -24,6 +36,7 @@ The report PDF is laid out as a document rather than a screenshot cut into pages
24
36
  The report CSV is built from the report's data instead of read off the first table on screen (`buildReportCsv` in `utils/report-export.ts`, `patrolReportCsv` / `dailyReportCsv` / `monthlyReportCsv` in `utils/nfc-patrol-report.ts`; each tab's `export` event now carries its tables): the report, site, route and date on the first lines, then every table under its title. The Patrol CSV now has every checkpoint, with each skip's remarks and photo link on its row (it used to carry only schedule against actual); the Daily CSV adds a Skipped Checkpoints table, one row per skip naming its run. UTF-8 with a byte order mark so Excel reads it, cells a spreadsheet would run as a formula kept as text, and named like the PDF. Signatures load eagerly so ones below the fold are no longer missing from the PDF.
25
37
 
26
38
  Monthly report: a month that had patrols opens the list of that month's patrol runs (new `PatrolMonthDialog`) - by day then scheduled start, with the month's totals in its title, ten runs a page ("1-10 of 124" and the arrows, like the app's tables) - and each run opens the same Patrol Route Details dialog, checkpoints and skip photos included. Built from the year of logs the report already loads (`mapLogsToMonthRuns`, sharing `monthIndexOf` with the yearly table so a month's list adds up to its row), so opening a month asks the server nothing. The summary-row mapping moved from the daily composable to `mapLogToSummaryRow()` and now carries the run's `date`, which the details dialog shows.
39
+
27
40
  - 8d3d430: NFC Routes: the Security Check Schedule reads at a glance. Each check window is a pill that never breaks across lines ("09:01 – 14:40"), earliest first instead of in the order the times were typed, with "+1" on a window that ends the next day (new `NFCRouteScheduleTimes`, `routeWindows` in `utils/nfc-patrol-schedule.ts`). The table shows the first three and folds the rest into "+N more" (all of them on hover), so a busy route keeps a normal row; the Route Details dialog shows them all under "Security Check Schedule · 9 checks a day". Route days read Monday first with runs as ranges - "Mon – Sat", "Every day" - instead of "Mon, Wed, Fri, Tue, Thu, Sat" (`formatRouteDays`).
28
41
 
29
42
  ## 4.108.0
@@ -3061,11 +3074,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
3061
3074
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
3062
3075
  own rule from two endpoints the console already calls, unprojected:
3063
3076
 
3064
- GET /api/members/user/:user/app/admin the Seven365 staff membership
3065
- GET /api/roles/id/:role that membership's role document
3077
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
3078
+ GET /api/roles/id/:role that membership's role document
3066
3079
 
3067
- owner = member.type === "admin" && role.type === "admin" && role.default === true
3068
- staff = member.type === "admin" && role.type === "admin"
3080
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
3081
+ staff = member.type === "admin" && role.type === "admin"
3069
3082
 
3070
3083
  `role.default` is the marker because it is the only property of a platform
3071
3084
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -387,7 +387,7 @@ async function submit() {
387
387
  emit("success");
388
388
  } catch (error: any) {
389
389
  message.value =
390
- error.response?._data?.message || "Failed to create building";
390
+ error.response?._data?.message || "Could not add the unit. Please try again.";
391
391
  showMessage(message.value, "error");
392
392
  } finally {
393
393
  disable.value = false;
@@ -536,7 +536,8 @@ async function submit() {
536
536
 
537
537
  await updateById(buildingUnit.value._id ?? "", {
538
538
  name: buildingUnit.value.name,
539
- level: buildingUnit.value.level._id,
539
+ // The level arrives as a populated object, or as a bare id when it could not be looked up.
540
+ level: (buildingUnit.value.level as any)?._id ?? buildingUnit.value.level ?? "",
540
541
  // category: buildingUnit.value.category,
541
542
  buildingUnitFiles: (buildingUnit.value.buildingUnitFiles as string[] || []).map((id) => ({ id, name: buildingUnitFilesNames.value[id] ?? "" })),
542
543
  companyName: buildingUnit.value.companyName,
@@ -550,7 +551,7 @@ async function submit() {
550
551
  emit("success");
551
552
  } catch (error: any) {
552
553
  message.value =
553
- error.response?._data?.message || "Failed to create building";
554
+ error.response?._data?.message || "Could not save the unit. Please try again.";
554
555
  } finally {
555
556
  disable.value = false;
556
557
  }
@@ -75,7 +75,7 @@
75
75
  </v-dialog>
76
76
 
77
77
  <v-dialog v-model="dialog.deletePrompt" width="450" persistent>
78
- <CardDeleteConfirmation prompt-title="Are you sure want to delete this announcement?"
78
+ <CardDeleteConfirmation prompt-title="Are you sure you want to delete this announcement?"
79
79
  :loading="loading.deletingAnnouncement" @close="dialog.deletePrompt = false"
80
80
  @delete="handleProceedDeleteAnnouncement" />
81
81
  </v-dialog>
@@ -288,7 +288,7 @@
288
288
  style="max-height: 100vh; overflow-y: auto"
289
289
  class="pa-5 my-5 px-7 text-center"
290
290
  >
291
- Are you sure want to delete this {{ camera.type }} camera?
291
+ Are you sure you want to delete this {{ camera.type }} camera?
292
292
  </v-card-text>
293
293
 
294
294
  <v-toolbar class="pa-0" density="compact">
@@ -9,9 +9,8 @@
9
9
  -->
10
10
  <v-dialog v-model="changeOpen" max-width="620" persistent>
11
11
  <v-card class="screen-modal" data-test="spc-dialog">
12
- <v-card-title class="d-flex align-center">
13
- {{ kind === "owner" ? "Change site owner" : `Change or end ${shownFrom}` }}
14
- <v-spacer />
12
+ <v-card-title class="spc-head">
13
+ <span class="spc-head__text">{{ kind === "owner" ? "Change site owner" : `Change or end ${shownFrom}` }}</span>
15
14
  <v-btn variant="text" icon="mdi-close" class="screen-modal__close" aria-label="Close" :disabled="saving" @click="changeOpen = false" />
16
15
  </v-card-title>
17
16
  <v-card-text class="screen-modal__body">
@@ -92,9 +91,8 @@
92
91
 
93
92
  <v-dialog v-model="historyOpen" max-width="720">
94
93
  <v-card class="screen-modal" data-test="spc-history">
95
- <v-card-title class="d-flex align-center">
96
- Provider changes at {{ siteName || "this site" }}
97
- <v-spacer />
94
+ <v-card-title class="spc-head">
95
+ <span class="spc-head__text">Provider changes at {{ siteName || "this site" }}</span>
98
96
  <v-btn
99
97
  v-if="ownerOrgId && isStaff"
100
98
  variant="outlined"
@@ -394,4 +392,8 @@ defineExpose({ openChange, openOwnerChange, openHistory });
394
392
  .spc-item__title { font-weight: 600; }
395
393
  .spc-chip { font-size: 12px; padding: 2px 10px; border-radius: 999px; border: 1px solid currentColor; }
396
394
  .spc-chip--failed { color: var(--err, #b00020); }
395
+
396
+ /* Long site / company names wrap; the close button always stays inside the window (phones). */
397
+ .spc-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; white-space: normal; }
398
+ .spc-head__text { flex: 1 1 0; min-width: 0; overflow-wrap: anywhere; }
397
399
  </style>
@@ -70,7 +70,7 @@
70
70
 
71
71
  <v-dialog v-model="dialog.showPrompt" persistent width="540">
72
72
  <DialogReusablePrompt :loading="loadingOwnerProcessing"
73
- :prompt-title="`Are you sure want to ${person.isOwner ? 'remove' : 'add'} (${person.name}) as owner ?`"
73
+ :prompt-title="`Are you sure you want to ${person.isOwner ? 'remove' : 'add'} (${person.name}) as owner ?`"
74
74
  @proceed="handleApprove(person)" @close="dialog.showPrompt = false" />
75
75
  </v-dialog>
76
76
  </v-card-text>
@@ -22,10 +22,11 @@
22
22
  <v-col cols="12">
23
23
  <AppCard class="pa-4">
24
24
  <div class="d-flex align-center flex-wrap ga-2 mb-3">
25
+ <span class="text-body-2">Ending in the next</span>
25
26
  <v-btn-toggle v-model="days" mandatory density="comfortable" variant="outlined" divided data-test="tn-days" @update:model-value="load">
26
- <v-btn :value="30" class="text-none">Next 30 days</v-btn>
27
- <v-btn :value="60" class="text-none">Next 60 days</v-btn>
28
- <v-btn :value="90" class="text-none">Next 90 days</v-btn>
27
+ <v-btn :value="30" class="text-none">30 days</v-btn>
28
+ <v-btn :value="60" class="text-none">60 days</v-btn>
29
+ <v-btn :value="90" class="text-none">90 days</v-btn>
29
30
  </v-btn-toggle>
30
31
  <v-spacer />
31
32
  <v-btn variant="text" icon="mdi-refresh" aria-label="Refresh" :loading="loading" @click="load" />
@@ -217,7 +218,7 @@ async function confirmRenew() {
217
218
  leaseEnd: `${renewEnd.value}T23:59`,
218
219
  });
219
220
  renewOpen.value = false;
220
- say(`Tenancy for ${current.value.unit} renewed to ${renewEnd.value}.`);
221
+ say(`Tenancy for ${current.value.unit} renewed to ${new Date(`${renewEnd.value}T00:00`).toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" })}.`);
221
222
  await load();
222
223
  } catch (e: any) {
223
224
  renewError.value = message(e, "The tenancy could not be renewed. Please try again.");
@@ -276,7 +276,7 @@
276
276
  snackbar, along with the device's raw `{"ErrorCode":...}` reply.
277
277
  -->
278
278
  <DialogReusablePrompt :loading="approvingVehicle" :message="actionError" message-error :no-permission="revoked.approve"
279
- :prompt-title="`Are you sure want to approve this vehicle - ${selectedPlateNumberObject?.plateNumber}?`"
279
+ :prompt-title="`Are you sure you want to approve this vehicle - ${selectedPlateNumberObject?.plateNumber}?`"
280
280
  @proceed="submitApprove" @close="closeActionDialog('approveVehicle')" />
281
281
  </v-dialog>
282
282
  <!-- Like Delete, Restore says what it does at the barrier: a whitelist
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.110.0",
5
+ "version": "4.110.2",
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.",
@@ -129,6 +129,12 @@ export default defineNuxtPlugin((app) => {
129
129
  density: "comfortable",
130
130
  menuProps: { attach: document.body, zIndex: 3000 },
131
131
  },
132
+ // Same look as the other fields: without this every combobox drew grey and filled.
133
+ VCombobox: {
134
+ variant: "outlined",
135
+ density: "comfortable",
136
+ menuProps: { attach: document.body, zIndex: 3000 },
137
+ },
132
138
  VSelect: {
133
139
  variant: "outlined",
134
140
  density: "comfortable",
@@ -22,6 +22,11 @@ const ACRONYMS: Record<string, string> = {
22
22
  id: "ID",
23
23
  };
24
24
 
25
+ const RENAMED: [string, string][] = [
26
+ ["Building Mgmt", "Occupancy Mgmt"],
27
+ ["Event Mgmt", "Task Calendar"],
28
+ ];
29
+
25
30
  export function moduleNameFromRoute(route: {
26
31
  name?: unknown;
27
32
  path?: unknown;
@@ -76,7 +81,10 @@ export function moduleNameFromRoute(route: {
76
81
  const words: string[] = [];
77
82
  while (i < segments.length && !params.has(segments[i])) words.push(segments[i++]);
78
83
 
79
- return words
84
+ const name = words
80
85
  .map((w) => ACRONYMS[w] ?? w.charAt(0).toUpperCase() + w.slice(1))
81
86
  .join(" ");
87
+ // Modules renamed on screen (owner 2026-10-07, 1PAM) keep their old route folder.
88
+ for (const [from, to] of RENAMED) if (name === from || name.startsWith(`${from} `)) return to + name.slice(from.length);
89
+ return name;
82
90
  }