@7365admin1/layer-common 4.97.1-staging.601 → 4.97.1-staging.603

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.
@@ -158,14 +158,27 @@
158
158
  :title="`Card No (${cardNoMaxLength} digits)`"
159
159
  required
160
160
  />
161
+ <!--
162
+ `hide-details="auto"`, not `hide-details`: the bare prop hides the
163
+ details slot ENTIRELY, messages included, so the rules below ran
164
+ and refused the form while the field said nothing about why.
165
+ "auto" keeps the slot collapsed until there is a message.
166
+
167
+ `type="text"` with a numeric keypad, not `type="number"`:
168
+ `maxlength` is ignored on a number input (it applies to text,
169
+ search, url, tel, email and password only), and a card number is a
170
+ ZERO-PADDED fixed-length string - "0000065535" is the leading
171
+ zeros plus the serial, and a number input is free to drop them.
172
+ -->
161
173
  <v-text-field
162
174
  v-model="card.cardNumber"
163
175
  density="compact"
164
- hide-details
165
- :rules="[requiredRule]"
176
+ hide-details="auto"
177
+ :rules="[requiredRule, cardNoRule]"
166
178
  :maxlength="cardNoMaxLength"
167
- type="number"
168
- hide-spin-buttons
179
+ type="text"
180
+ inputmode="numeric"
181
+ autocomplete="off"
169
182
  :suffix="`${card.cardNumber?.toString().length || 0}/${cardNoMaxLength}`"
170
183
  @input="limitCardNumber"
171
184
  >
@@ -407,6 +420,10 @@
407
420
  </v-card>
408
421
  </template>
409
422
  <script setup lang="ts">
423
+ // Imported by name rather than left to auto-imports, the way
424
+ // `VehicleBulkAddForm` reaches for `~/utils/phoneMasks`.
425
+ import { normaliseCardNo, validateCardNo } from "~/utils/access-card-number";
426
+
410
427
  const prop = defineProps({
411
428
  mode: {
412
429
  type: String,
@@ -607,10 +624,12 @@ onMounted(async () => {
607
624
  }
608
625
  });
609
626
 
627
+ /** Mirrors what core accepts - see `utils/access-card-number.ts` for why. */
628
+ const cardNoRule = (value: string | number | null | undefined) =>
629
+ validateCardNo(value, cardNoMaxLength.value);
630
+
610
631
  function limitCardNumber() {
611
- if (card.value.cardNumber && card.value.cardNumber.toString().length > cardNoMaxLength.value) {
612
- card.value.cardNumber = card.value.cardNumber.toString().slice(0, cardNoMaxLength.value);
613
- }
632
+ card.value.cardNumber = normaliseCardNo(card.value.cardNumber, cardNoMaxLength.value);
614
633
  }
615
634
 
616
635
  function limitPinNo() {
@@ -327,6 +327,38 @@ export default function useSecurityPermission() {
327
327
  },
328
328
  },
329
329
 
330
+ // People Mgmt in the Security app (owner, 2026-10-27): the same six
331
+ // actions, spelling and wording as `usePropertyPermission`, because both
332
+ // apps open the same `/api/people` records.
333
+ "people-mgmt": {
334
+ "add-people": {
335
+ check: true,
336
+ description: "Allows the user to add a new person to the system.",
337
+ },
338
+ "see-all-people": {
339
+ check: true,
340
+ description: "Allows the user to view the list of all people.",
341
+ },
342
+ "see-people-details": {
343
+ check: true,
344
+ description:
345
+ "Allows the user to view the details of a specific person.",
346
+ },
347
+ "update-people": {
348
+ check: true,
349
+ description: "Allows the user to update person details.",
350
+ },
351
+ "delete-people": {
352
+ check: true,
353
+ description:
354
+ "Allows the user to remove a person from the system permanently.",
355
+ },
356
+ "see-people-nric": {
357
+ check: true,
358
+ description: "Allows the user to see a person nric from the system.",
359
+ },
360
+ },
361
+
330
362
  // "guest-mgmt": {
331
363
  // "add-guest": {
332
364
  // check: true,
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.97.1-staging.601",
5
+ "version": "4.97.1-staging.603",
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,86 @@
1
+ /**
2
+ * WHAT AN ACCESS CARD NUMBER IS.
3
+ *
4
+ * The server mints one as `randomInt(0, 65536).toString().padStart(10, "0")`
5
+ * (core `access-management.repo.ts generateCardNo`), so every number the system
6
+ * issues itself is TEN DIGITS, zero-padded, carrying a serial of at most 65535.
7
+ * A number typed in by hand has to be the same shape, because two places in
8
+ * core take it apart again and refuse anything else:
9
+ *
10
+ * createQrData `/^\d{10}$/`, then the LAST SIX digits as the
11
+ * (access-management.repo) serial (0-65535) and the FIRST FOUR as the
12
+ * facility code.
13
+ * cardNumberToHex `/^\d{10}$/`, then the whole value packed into
14
+ * (rsa-encryption) four bytes, so it cannot exceed 0xFFFFFFFF.
15
+ *
16
+ * Until now the Add Card form mirrored neither: the field carried a "Required"
17
+ * rule alone, so "7" passed the form and failed at the server - or was stored
18
+ * and only failed later, when a QR code had to be built from it.
19
+ *
20
+ * Both server rules are mirrored here so the form refuses exactly what the
21
+ * server would, and says so while the number is still on screen.
22
+ *
23
+ * The two ceilings are NOT redundant. `createQrData` bounds only the last six
24
+ * digits, which leaves the first four free up to 9999; `cardNumberToHex` bounds
25
+ * the whole ten-digit value at 4294967295, which caps those first four at 4294.
26
+ * A number such as "9999065535" satisfies the first and fails the second, so a
27
+ * form that checked only the serial would still hand the server a number it
28
+ * refuses.
29
+ */
30
+
31
+ export const CARD_NO_DIGITS = 10;
32
+ export const CARD_NO_SERIAL_DIGITS = 6;
33
+
34
+ /** The 16-bit serial `generateCardNo` draws from, and `createQrData` bounds. */
35
+ export const CARD_NO_MAX_SERIAL = 65535;
36
+
37
+ /** Four bytes: `cardNumberToHex` packs the whole number into a uint32. */
38
+ export const CARD_NO_MAX_VALUE = 0xffffffff;
39
+
40
+ /**
41
+ * Digits only, clipped to length.
42
+ *
43
+ * The field is a text input so that leading zeros survive and `maxlength`
44
+ * applies (it is ignored on `type="number"`), which also lets letters through -
45
+ * so they are stripped here rather than left for the rule to reject something
46
+ * the user could not see was wrong.
47
+ */
48
+ export function normaliseCardNo(
49
+ value: string | number | null | undefined,
50
+ digits: number = CARD_NO_DIGITS,
51
+ ): string {
52
+ return (value ?? "")
53
+ .toString()
54
+ .replace(/\D/g, "")
55
+ .slice(0, digits);
56
+ }
57
+
58
+ /**
59
+ * `true` when the number is one the server will accept, otherwise the message
60
+ * to show under the field.
61
+ *
62
+ * An EMPTY value passes: the field's own "Required" rule already speaks for it,
63
+ * and returning a message here too stacks two errors under one input.
64
+ */
65
+ export function validateCardNo(
66
+ value: string | number | null | undefined,
67
+ digits: number = CARD_NO_DIGITS,
68
+ ): true | string {
69
+ const text = (value ?? "").toString();
70
+ if (!text) return true;
71
+
72
+ if (!new RegExp(`^\\d{${digits}}$`).test(text)) {
73
+ return `Card No must be exactly ${digits} digits`;
74
+ }
75
+
76
+ const serial = Number(text.slice(-CARD_NO_SERIAL_DIGITS));
77
+ if (serial > CARD_NO_MAX_SERIAL) {
78
+ return `The last ${CARD_NO_SERIAL_DIGITS} digits must be ${CARD_NO_MAX_SERIAL} or lower`;
79
+ }
80
+
81
+ if (Number(text) > CARD_NO_MAX_VALUE) {
82
+ return `Card No must be ${CARD_NO_MAX_VALUE} or lower`;
83
+ }
84
+
85
+ return true;
86
+ }