@7365admin1/layer-common 4.97.1-staging.602 → 4.97.1-staging.604
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="
|
|
168
|
-
|
|
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
|
-
|
|
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() {
|
|
@@ -226,7 +226,7 @@
|
|
|
226
226
|
<td class="td-muted" data-label="Email">{{ item.email }}</td>
|
|
227
227
|
<td data-label="App">{{ item.app }}</td>
|
|
228
228
|
<td class="td-muted" data-label="Role">{{ item.role }}</td>
|
|
229
|
-
<td data-label="Status"><span class="status-chip pending">{{ item.status }}</span></td>
|
|
229
|
+
<td data-label="Status"><span class="status-chip pending text-capitalize">{{ item.status }}</span></td>
|
|
230
230
|
<td data-label="Action">
|
|
231
231
|
<button
|
|
232
232
|
class="cancel-btn"
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
<v-menu v-model="filterMenu" offset-y>
|
|
35
35
|
<template #activator="{ props }">
|
|
36
36
|
<button v-bind="props" type="button" class="doc-filters__type">
|
|
37
|
-
<span>{{ selectedFileType
|
|
37
|
+
<span>{{ selectedFileType ? fileTypeLabel(selectedFileType) : "Filter by File Type" }}</span>
|
|
38
38
|
<v-icon size="18">mdi-chevron-down</v-icon>
|
|
39
39
|
</button>
|
|
40
40
|
</template>
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
class="filtertype-item"
|
|
49
49
|
>
|
|
50
50
|
<v-list-item-title class="text-subtitle-2">
|
|
51
|
-
{{ type }}
|
|
51
|
+
{{ fileTypeLabel(type) }}
|
|
52
52
|
</v-list-item-title>
|
|
53
53
|
</v-list-item>
|
|
54
54
|
</v-list>
|
|
@@ -859,6 +859,9 @@ const currentFolderPath = computed(() => {
|
|
|
859
859
|
});
|
|
860
860
|
|
|
861
861
|
const fileTypes = ref(["all", "pdf", "csv", "doc"]);
|
|
862
|
+
/** What each filter value reads as on screen; the value sent stays as is. */
|
|
863
|
+
const FILE_TYPE_LABELS: Record<string, string> = { all: "All file types", pdf: "PDF", csv: "CSV", doc: "Word (DOC)" };
|
|
864
|
+
const fileTypeLabel = (type: string) => FILE_TYPE_LABELS[type] ?? type.toUpperCase();
|
|
862
865
|
const selectedFileType = ref("all");
|
|
863
866
|
|
|
864
867
|
/**
|
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.
|
|
5
|
+
"version": "4.97.1-staging.604",
|
|
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
|
+
}
|