@7365admin1/layer-common 4.125.1 → 4.127.0
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 +16 -4
- package/components/SiteSettings.vue +3 -0
- package/components/TaskCalendarReadOnly.vue +115 -0
- package/components/VisitorManagement.vue +82 -0
- package/components/VisitorPassSmsSettings.vue +57 -0
- package/composables/useSiteFeatureSettings.ts +1 -0
- package/composables/useVisitor.ts +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.127.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e57229d: TaskCalendarReadOnly: a view-only Task Calendar list for the contractor apps (open / overdue / done tabs, task details on click). The server limits it to the caller's own company's tasks.
|
|
8
|
+
|
|
9
|
+
## 4.126.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 5e36856: Visitors: "Send pass by SMS" on the Visitors page (Guests tab, approved and not checked out, only when the site switch is on and the user may add visitors), and a "Send visitor pass by SMS" switch in Site Settings > Visitor registration (PM and Security). Shows the server's own words, e.g. "Sent to +65 ••••4567" or "SMS is not set up yet."
|
|
14
|
+
|
|
3
15
|
## 4.125.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -3294,11 +3306,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
|
|
|
3294
3306
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
3295
3307
|
own rule from two endpoints the console already calls, unprojected:
|
|
3296
3308
|
|
|
3297
|
-
|
|
3298
|
-
|
|
3309
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
3310
|
+
GET /api/roles/id/:role that membership's role document
|
|
3299
3311
|
|
|
3300
|
-
|
|
3301
|
-
|
|
3312
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
3313
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
3302
3314
|
|
|
3303
3315
|
`role.default` is the marker because it is the only property of a platform
|
|
3304
3316
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -230,6 +230,9 @@
|
|
|
230
230
|
<v-expansion-panel-text>
|
|
231
231
|
<SiteSettingsVisitorRegistration :site-id="siteId" :site-data="siteData"
|
|
232
232
|
:can-manage="canManageSiteSettings" @saved="refreshSiteData" @message="show" />
|
|
233
|
+
<v-divider class="my-4" />
|
|
234
|
+
<VisitorPassSmsSettings :site-id="siteId" :saved="(siteData as any)?.metadata?.visitorPassSms"
|
|
235
|
+
:loaded="!!siteData" :read-only="!canManageSiteSettings" @saved="refreshSiteData" />
|
|
233
236
|
</v-expansion-panel-text>
|
|
234
237
|
</v-expansion-panel>
|
|
235
238
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row no-gutters>
|
|
3
|
+
<v-col cols="12">
|
|
4
|
+
<PageHeader title="Task Calendar"
|
|
5
|
+
subtitle="Tasks the managing agent assigned to your company at this site. View only - the managing agent marks them done." />
|
|
6
|
+
</v-col>
|
|
7
|
+
|
|
8
|
+
<TableMain :headers="headers" :items="items" :loading="pending" :page="page" :pages="pages"
|
|
9
|
+
:pageRange="pageRange" :items-per-page="LIMIT" @refresh="refresh" @update:page="(p: number) => (page = p)"
|
|
10
|
+
@row-click="(row: any) => (selected = row?.item ?? row)">
|
|
11
|
+
<template #tabs>
|
|
12
|
+
<button v-for="tab in TABS" :key="tab.status" type="button" role="tab" class="app-tab"
|
|
13
|
+
:class="{ 'app-tab--active': status === tab.status }" :aria-selected="status === tab.status"
|
|
14
|
+
@click="status = tab.status; page = 1">
|
|
15
|
+
{{ tab.name }}
|
|
16
|
+
</button>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<template v-if="error" #no-data>
|
|
20
|
+
<DashboardEmptyState error />
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<template v-slot:item.dateTime="{ item }">{{ dueDate(item.dateTime) }}</template>
|
|
24
|
+
<template v-slot:item.category="{ item }">{{ item.category || "-" }}</template>
|
|
25
|
+
<template v-slot:item.repeat="{ item }">{{ repeatText(item) }}</template>
|
|
26
|
+
<template v-slot:item.status="{ item }">
|
|
27
|
+
<v-chip size="small" :color="taskState(item).color" variant="tonal">{{ taskState(item).label }}</v-chip>
|
|
28
|
+
</template>
|
|
29
|
+
</TableMain>
|
|
30
|
+
|
|
31
|
+
<v-dialog :model-value="!!selected" width="520" @update:model-value="selected = null">
|
|
32
|
+
<v-card v-if="selected" :title="selected.title">
|
|
33
|
+
<v-card-text>
|
|
34
|
+
<p><strong>Due:</strong> {{ dueDate(selected.dateTime) }}</p>
|
|
35
|
+
<p><strong>Status:</strong> {{ taskState(selected).label }}</p>
|
|
36
|
+
<p v-if="selected.category"><strong>Category:</strong> {{ selected.category }}</p>
|
|
37
|
+
<p><strong>Repeats:</strong> {{ repeatText(selected) }}</p>
|
|
38
|
+
<p v-if="selected.description" class="mt-2" style="white-space: pre-line">{{ selected.description }}</p>
|
|
39
|
+
</v-card-text>
|
|
40
|
+
<v-card-actions>
|
|
41
|
+
<v-spacer />
|
|
42
|
+
<v-btn variant="text" @click="selected = null">Close</v-btn>
|
|
43
|
+
</v-card-actions>
|
|
44
|
+
</v-card>
|
|
45
|
+
</v-dialog>
|
|
46
|
+
</v-row>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script setup lang="ts">
|
|
50
|
+
// Task Calendar for contractor companies (1PAM, owner 2026-10-17): READ-ONLY.
|
|
51
|
+
// The server limits the list to tasks whose contractor is the caller's own
|
|
52
|
+
// company; this screen adds nothing to that rule and offers no write.
|
|
53
|
+
import { readListPage } from '../utils/list-page';
|
|
54
|
+
|
|
55
|
+
const props = defineProps({
|
|
56
|
+
siteId: { type: String, required: true },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const LIMIT = 20;
|
|
60
|
+
const TABS = [
|
|
61
|
+
{ name: "Open", status: "planned" },
|
|
62
|
+
{ name: "Overdue", status: "overdue" },
|
|
63
|
+
{ name: "Done", status: "completed" },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const headers = [
|
|
67
|
+
{ title: "Due", value: "dateTime" },
|
|
68
|
+
{ title: "Task", value: "title" },
|
|
69
|
+
{ title: "Category", value: "category" },
|
|
70
|
+
{ title: "Repeats", value: "repeat" },
|
|
71
|
+
{ title: "Status", value: "status" },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const page = ref(1);
|
|
75
|
+
const status = ref("planned");
|
|
76
|
+
const selected = ref<Record<string, any> | null>(null);
|
|
77
|
+
const items = ref<Array<Record<string, any>>>([]);
|
|
78
|
+
const pages = ref(0);
|
|
79
|
+
const pageRange = ref("-- - -- of --");
|
|
80
|
+
|
|
81
|
+
const { data, pending, error, refresh } = await useLazyAsyncData(
|
|
82
|
+
`contractor-task-calendar-${props.siteId}`,
|
|
83
|
+
() =>
|
|
84
|
+
useNuxtApp().$api<Record<string, any>>("/api/event-management", {
|
|
85
|
+
method: "GET",
|
|
86
|
+
query: { site: props.siteId, page: page.value, limit: LIMIT, status: status.value },
|
|
87
|
+
}),
|
|
88
|
+
{ watch: [page, status] },
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
watch(data, (reply) => {
|
|
92
|
+
const listPage = readListPage<Record<string, any>>(reply as any);
|
|
93
|
+
items.value = listPage.items;
|
|
94
|
+
pages.value = listPage.pages;
|
|
95
|
+
pageRange.value = listPage.pageRange;
|
|
96
|
+
}, { immediate: true });
|
|
97
|
+
|
|
98
|
+
function dueDate(value: string) {
|
|
99
|
+
if (!value) return "-";
|
|
100
|
+
return new Date(value).toLocaleDateString("en-SG", { day: "numeric", month: "short", year: "numeric", timeZone: "Asia/Singapore" });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function repeatText(task: Record<string, any>) {
|
|
104
|
+
const every = Number(task?.repeatInterval) || 1;
|
|
105
|
+
if (task?.repeat === "monthly") return every === 1 ? "Every month" : `Every ${every} months`;
|
|
106
|
+
if (task?.repeat === "yearly") return every === 1 ? "Every year" : `Every ${every} years`;
|
|
107
|
+
return "Once";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function taskState(task: Record<string, any>) {
|
|
111
|
+
if (task?.status === "completed") return { label: "Done", color: "success" };
|
|
112
|
+
if (task?.dateTime && new Date(task.dateTime) < new Date()) return { label: "Overdue", color: "error" };
|
|
113
|
+
return { label: "Upcoming", color: "primary" };
|
|
114
|
+
}
|
|
115
|
+
</script>
|
|
@@ -570,6 +570,24 @@
|
|
|
570
570
|
</v-list-item>
|
|
571
571
|
</v-card>
|
|
572
572
|
</v-menu>
|
|
573
|
+
<!-- Walk-in pass by SMS: only when the site switched it on and this user may add visitors. -->
|
|
574
|
+
<v-menu
|
|
575
|
+
v-else-if="activeTab === 'guests' && item?.status === 'approved' && !item?.checkOut && canAddVisitor && passSmsEnabled"
|
|
576
|
+
>
|
|
577
|
+
<template #activator="{ props }">
|
|
578
|
+
<v-avatar v-bind="props" class="rounded-xl border-md" data-test="visitor-pass-sms-menu">
|
|
579
|
+
<v-icon icon="mdi-dots-vertical" />
|
|
580
|
+
</v-avatar>
|
|
581
|
+
</template>
|
|
582
|
+
|
|
583
|
+
<v-card>
|
|
584
|
+
<v-list-item data-test="visitor-pass-sms-open" @click.stop="openPassSms(item)">
|
|
585
|
+
<template #title>
|
|
586
|
+
<span class="text-caption">Send pass by SMS</span>
|
|
587
|
+
</template>
|
|
588
|
+
</v-list-item>
|
|
589
|
+
</v-card>
|
|
590
|
+
</v-menu>
|
|
573
591
|
</template>
|
|
574
592
|
|
|
575
593
|
<template v-slot:item.checkInRemarks="{ item }">
|
|
@@ -1560,6 +1578,35 @@
|
|
|
1560
1578
|
</v-card>
|
|
1561
1579
|
</v-dialog>
|
|
1562
1580
|
|
|
1581
|
+
<v-dialog v-model="passSms.open" max-width="420">
|
|
1582
|
+
<v-card data-test="visitor-pass-sms-dialog">
|
|
1583
|
+
<v-toolbar color="transparent" density="comfortable" class="px-2">
|
|
1584
|
+
<v-toolbar-title class="text-subtitle-1 font-weight-bold">Send pass by SMS</v-toolbar-title>
|
|
1585
|
+
<v-btn icon="mdi-close" variant="text" title="Close" @click="passSms.open = false" />
|
|
1586
|
+
</v-toolbar>
|
|
1587
|
+
<v-card-text>
|
|
1588
|
+
<p class="text-body-2 mb-3">
|
|
1589
|
+
{{ passSms.name }} gets a text with the site name, the visit date and their pass link.
|
|
1590
|
+
</p>
|
|
1591
|
+
<v-text-field v-model.trim="passSms.phone" label="Mobile number" placeholder="e.g. 9123 4567"
|
|
1592
|
+
density="comfortable" variant="outlined" maxlength="30" :disabled="passSms.sending"
|
|
1593
|
+
data-test="visitor-pass-sms-phone" @keyup.enter="sendPassSmsNow" />
|
|
1594
|
+
<p v-if="passSms.result" class="text-body-2 mt-1" :class="passSms.ok ? 'text-success' : 'text-error'"
|
|
1595
|
+
data-test="visitor-pass-sms-result">
|
|
1596
|
+
{{ passSms.result }}
|
|
1597
|
+
</p>
|
|
1598
|
+
</v-card-text>
|
|
1599
|
+
<v-card-actions class="px-6 pb-4">
|
|
1600
|
+
<v-spacer />
|
|
1601
|
+
<v-btn variant="text" @click="passSms.open = false">Close</v-btn>
|
|
1602
|
+
<v-btn class="screen-btn-primary" color="primary-button" variant="flat" :loading="passSms.sending"
|
|
1603
|
+
:disabled="!passSms.phone" data-test="visitor-pass-sms-send" @click="sendPassSmsNow">
|
|
1604
|
+
Send
|
|
1605
|
+
</v-btn>
|
|
1606
|
+
</v-card-actions>
|
|
1607
|
+
</v-card>
|
|
1608
|
+
</v-dialog>
|
|
1609
|
+
|
|
1563
1610
|
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
1564
1611
|
</v-row>
|
|
1565
1612
|
</template>
|
|
@@ -1567,6 +1614,7 @@
|
|
|
1567
1614
|
<script setup lang="ts">
|
|
1568
1615
|
import moment from "moment-timezone";
|
|
1569
1616
|
import QRCode from "qrcode";
|
|
1617
|
+
import { isVisitorPassSmsOn } from "../composables/useVisitor";
|
|
1570
1618
|
import {
|
|
1571
1619
|
VISITOR_SEARCH_DEBOUNCE_MS,
|
|
1572
1620
|
visitorSearchEmptyText,
|
|
@@ -1637,6 +1685,7 @@ const {
|
|
|
1637
1685
|
typeFieldMap,
|
|
1638
1686
|
updateVisitor,
|
|
1639
1687
|
visitorDataFromScannedQRCodeCheckIn,
|
|
1688
|
+
sendPassSms,
|
|
1640
1689
|
} = useVisitor();
|
|
1641
1690
|
const { visitorCheckout } = useAccessManagement();
|
|
1642
1691
|
const {
|
|
@@ -1673,6 +1722,37 @@ const hidQrVisitor = ref<Record<string, any> | null>(null);
|
|
|
1673
1722
|
const hidQrConfig = ref({ enabled: false, readerId: "", validityMinutes: 60 });
|
|
1674
1723
|
let hidQrTimer: ReturnType<typeof setInterval> | null = null;
|
|
1675
1724
|
const hidQrEnabled = computed(() => hidQrConfig.value.enabled && Boolean(hidQrConfig.value.readerId));
|
|
1725
|
+
// Walk-in pass by SMS: read with the HID settings (same site document).
|
|
1726
|
+
const passSmsEnabled = ref(false);
|
|
1727
|
+
const passSms = reactive({ open: false, id: "", name: "", phone: "", sending: false, result: "", ok: false });
|
|
1728
|
+
|
|
1729
|
+
function openPassSms(item: Record<string, any>) {
|
|
1730
|
+
Object.assign(passSms, {
|
|
1731
|
+
open: true,
|
|
1732
|
+
id: String(item?._id ?? ""),
|
|
1733
|
+
name: item?.name || "The visitor",
|
|
1734
|
+
phone: String(item?.contact ?? ""),
|
|
1735
|
+
sending: false,
|
|
1736
|
+
result: "",
|
|
1737
|
+
ok: false,
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
async function sendPassSmsNow() {
|
|
1742
|
+
if (!passSms.id || !passSms.phone || passSms.sending) return;
|
|
1743
|
+
passSms.sending = true;
|
|
1744
|
+
passSms.result = "";
|
|
1745
|
+
try {
|
|
1746
|
+
const res: any = await sendPassSms(passSms.id, passSms.phone);
|
|
1747
|
+
passSms.ok = true;
|
|
1748
|
+
passSms.result = res?.message || "Sent.";
|
|
1749
|
+
} catch (error: any) {
|
|
1750
|
+
passSms.ok = false;
|
|
1751
|
+
passSms.result = error?.data?.message || "The SMS could not be sent. Please try again in a minute.";
|
|
1752
|
+
} finally {
|
|
1753
|
+
passSms.sending = false;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1676
1756
|
const hidQrRemainingSeconds = computed(() => {
|
|
1677
1757
|
const expiry = new Date(hidQrExpiresAt.value).getTime();
|
|
1678
1758
|
return Number.isFinite(expiry) ? Math.max(0, Math.ceil((expiry - hidQrNow.value) / 1000)) : 0;
|
|
@@ -1683,6 +1763,7 @@ async function loadHidQrConfig() {
|
|
|
1683
1763
|
try {
|
|
1684
1764
|
const response: any = await getSiteById(siteId);
|
|
1685
1765
|
const site = response?.data ?? response;
|
|
1766
|
+
passSmsEnabled.value = isVisitorPassSmsOn(site);
|
|
1686
1767
|
const config = site?.metadata?.hidQrCodePass ?? {};
|
|
1687
1768
|
hidQrConfig.value = {
|
|
1688
1769
|
enabled: Boolean(config.enabled && config.onlineMode !== false),
|
|
@@ -1690,6 +1771,7 @@ async function loadHidQrConfig() {
|
|
|
1690
1771
|
validityMinutes: Number(config.validityMinutes) > 0 ? Number(config.validityMinutes) : 60,
|
|
1691
1772
|
};
|
|
1692
1773
|
} catch {
|
|
1774
|
+
passSmsEnabled.value = false;
|
|
1693
1775
|
hidQrConfig.value = { enabled: false, readerId: "", validityMinutes: 60 };
|
|
1694
1776
|
}
|
|
1695
1777
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div data-test="visitor-pass-sms-settings">
|
|
3
|
+
<v-switch v-model="enabled" :disabled="readOnly" label="Send visitor pass by SMS" color="primary" hide-details
|
|
4
|
+
data-test="visitor-pass-sms-switch" />
|
|
5
|
+
<p class="text-caption text-medium-emphasis mt-1 mb-3">
|
|
6
|
+
Staff can text an approved visitor their pass link (the QR they show at the guardhouse). The text has the site
|
|
7
|
+
name, the visit date and the link only.
|
|
8
|
+
</p>
|
|
9
|
+
<v-btn v-if="!readOnly" class="screen-btn-primary" color="primary-button" variant="flat" text="Save"
|
|
10
|
+
:loading="saving" :disabled="!loaded" data-test="visitor-pass-sms-save" @click="save" />
|
|
11
|
+
<Snackbar v-model="toast.show" :text="toast.message" :color="toast.color" />
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import useSiteFeatureSettings from "../composables/useSiteFeatureSettings";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Site setting `metadata.visitorPassSms` (core site-feature-settings).
|
|
20
|
+
* Off = today: no "Send pass by SMS" action anywhere.
|
|
21
|
+
*/
|
|
22
|
+
const props = defineProps({
|
|
23
|
+
siteId: { type: String, required: true },
|
|
24
|
+
saved: { type: Object as PropType<{ enabled?: boolean } | null>, default: null },
|
|
25
|
+
readOnly: { type: Boolean, default: false },
|
|
26
|
+
/** The site has been read; Save stays off until then. */
|
|
27
|
+
loaded: { type: Boolean, default: false },
|
|
28
|
+
});
|
|
29
|
+
const emit = defineEmits(["saved"]);
|
|
30
|
+
|
|
31
|
+
const enabled = ref(false);
|
|
32
|
+
watch(
|
|
33
|
+
() => props.saved,
|
|
34
|
+
(v) => {
|
|
35
|
+
enabled.value = v?.enabled === true;
|
|
36
|
+
},
|
|
37
|
+
{ immediate: true },
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const { saveSiteFeature } = useSiteFeatureSettings();
|
|
41
|
+
const saving = ref(false);
|
|
42
|
+
const toast = reactive({ show: false, message: "", color: "" });
|
|
43
|
+
|
|
44
|
+
async function save() {
|
|
45
|
+
if (!props.loaded) return;
|
|
46
|
+
saving.value = true;
|
|
47
|
+
try {
|
|
48
|
+
await saveSiteFeature(props.siteId, "visitorPassSms", { enabled: enabled.value });
|
|
49
|
+
Object.assign(toast, { show: true, message: "Saved.", color: "success" });
|
|
50
|
+
emit("saved");
|
|
51
|
+
} catch (e: any) {
|
|
52
|
+
Object.assign(toast, { show: true, message: e?.data?.message || "Could not save.", color: "error" });
|
|
53
|
+
} finally {
|
|
54
|
+
saving.value = false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
@@ -173,6 +173,14 @@ export default function () {
|
|
|
173
173
|
);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/** Text the visit's pass link (walk-in pass by SMS). `phone` empty = the visit's contact. */
|
|
177
|
+
async function sendPassSms(_id: string, phone?: string) {
|
|
178
|
+
return await useNuxtApp().$api<{ message: string; to?: string }>(
|
|
179
|
+
`/api/visitor-transactions/${_id}/send-pass-sms`,
|
|
180
|
+
{ method: "POST", body: phone ? { phone } : {} }
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
176
184
|
async function updateVisitor(_id: string, payload: Partial<TVisitorPayload>) {
|
|
177
185
|
return await useNuxtApp().$api<Record<string, any>>(
|
|
178
186
|
`/api/visitor-transactions/id/${_id}`,
|
|
@@ -271,6 +279,7 @@ export default function () {
|
|
|
271
279
|
validateVisitorQrCode,
|
|
272
280
|
updateVisitorPassKey,
|
|
273
281
|
visitorDataFromScannedQRCodeCheckIn,
|
|
282
|
+
sendPassSms,
|
|
274
283
|
};
|
|
275
284
|
}
|
|
276
285
|
|
|
@@ -293,6 +302,11 @@ export function getVisitorFieldSwitches(site: any): TVisitorFieldSwitches {
|
|
|
293
302
|
};
|
|
294
303
|
}
|
|
295
304
|
|
|
305
|
+
/** "Send visitor pass by SMS" (`site.metadata.visitorPassSms`): off unless the site saved it on. */
|
|
306
|
+
export function isVisitorPassSmsOn(site: any): boolean {
|
|
307
|
+
return site?.metadata?.visitorPassSms?.enabled === true;
|
|
308
|
+
}
|
|
309
|
+
|
|
296
310
|
/** Shown under the NRIC field on a "keep only the last 4" site. */
|
|
297
311
|
export const NRIC_LAST4_HINT = "Only the last 4 characters will be kept.";
|
|
298
312
|
|
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.
|
|
5
|
+
"version": "4.127.0",
|
|
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.",
|