@7365admin1/layer-common 4.118.10 → 4.120.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 +25 -4
- package/components/AccessCardDetailsDialog.vue +3 -2
- package/components/AddPassKeyToVisitor.vue +5 -0
- package/components/BuildingUnitFormEdit.vue +3 -1
- package/components/DocumentManagement.vue +56 -3
- package/components/FeedbackHandlingSettings.vue +385 -0
- package/components/HandoverChecklistRecords.vue +223 -0
- package/components/HandoverChecklistSetup.vue +224 -0
- package/components/IncidentReportEmailsSettings.vue +88 -0
- package/components/KeyPressFilter.vue +43 -0
- package/components/KeyPressName.vue +14 -0
- package/components/KeyPressSelect.vue +31 -0
- package/components/KeySettingsPanel.vue +156 -0
- package/components/ListCsvExport.vue +71 -0
- package/components/MaStaffRegistrationSettings.vue +61 -0
- package/components/MemberInformation.vue +2 -2
- package/components/PassInformation.vue +5 -0
- package/components/RolePermissionFormCreate.vue +33 -0
- package/components/SearchVehicleNumberUser.vue +3 -0
- package/components/SiteDataRetention.vue +286 -0
- package/components/SiteSettings.vue +48 -0
- package/components/SiteSettingsVisitorRegistration.vue +118 -0
- package/components/VehicleForm.vue +2 -2
- package/components/VisitorForm.vue +41 -6
- package/components/VisitorManagement.vue +3 -1
- package/components/VisitorSearchResultSummary.vue +2 -1
- package/composables/useCommonPermission.ts +10 -0
- package/composables/useDataRetention.ts +43 -0
- package/composables/useDocument.ts +13 -0
- package/composables/useFeedback.ts +12 -0
- package/composables/useHandoverChecklist.ts +111 -0
- package/composables/useKeyPresses.ts +57 -0
- package/composables/usePassKey.ts +4 -0
- package/composables/useSecurityPermission.ts +38 -0
- package/composables/useSettingsPermission.ts +16 -1
- package/composables/useSiteFeatureSettings.ts +37 -0
- package/composables/useVisitor.ts +22 -0
- package/constants/permissions.ts +3 -0
- package/package.json +2 -2
- package/types/key-log.d.ts +2 -0
- package/types/passKey.d.ts +2 -0
- package/utils/feedback-handling-settings.ts +139 -0
- package/utils/inventory-presets.ts +64 -0
- package/utils/key-presses.ts +55 -0
- package/utils/list-export.ts +39 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Passes & keys site switches (TLR B8 / D2). Saved as one value,
|
|
4
|
+
`metadata.keySettings`, through the shared one-switch save
|
|
5
|
+
(`useSiteFeatureSettings`).
|
|
6
|
+
Every switch starts off, so a site that never opens this behaves as before.
|
|
7
|
+
-->
|
|
8
|
+
<div class="key-settings" data-test="key-settings-panel">
|
|
9
|
+
<InputLabel title="Key presses" class="d-block mb-1" />
|
|
10
|
+
<p class="text-caption text-medium-emphasis mb-2">
|
|
11
|
+
Add one per key press, e.g. Management office and Guardhouse, so an unreturned key shows which press it came
|
|
12
|
+
from.
|
|
13
|
+
</p>
|
|
14
|
+
<div v-for="(row, i) in rows" :key="row.id || `new-${i}`" class="d-flex align-center ga-2 mb-2">
|
|
15
|
+
<v-text-field
|
|
16
|
+
v-model="row.name"
|
|
17
|
+
placeholder="Key press name"
|
|
18
|
+
density="compact"
|
|
19
|
+
hide-details
|
|
20
|
+
maxlength="60"
|
|
21
|
+
:readonly="readOnly"
|
|
22
|
+
:data-test="`key-press-name-${i}`"
|
|
23
|
+
/>
|
|
24
|
+
<v-btn
|
|
25
|
+
v-if="!readOnly"
|
|
26
|
+
icon="mdi-delete-outline"
|
|
27
|
+
variant="text"
|
|
28
|
+
density="comfortable"
|
|
29
|
+
:aria-label="`Remove ${row.name || 'key press'}`"
|
|
30
|
+
@click="rows.splice(i, 1)"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
<v-btn
|
|
34
|
+
v-if="!readOnly && rows.length < 20"
|
|
35
|
+
variant="text"
|
|
36
|
+
color="primary"
|
|
37
|
+
prepend-icon="mdi-plus"
|
|
38
|
+
class="mb-4"
|
|
39
|
+
data-test="key-press-add"
|
|
40
|
+
@click="rows.push({ name: '' })"
|
|
41
|
+
>
|
|
42
|
+
Add key press
|
|
43
|
+
</v-btn>
|
|
44
|
+
|
|
45
|
+
<v-divider class="my-3" />
|
|
46
|
+
|
|
47
|
+
<v-switch
|
|
48
|
+
v-model="dailyUnreturnedAlert"
|
|
49
|
+
label="Daily list of unreturned keys"
|
|
50
|
+
color="primary"
|
|
51
|
+
hide-details
|
|
52
|
+
:disabled="readOnly"
|
|
53
|
+
data-test="key-daily-alert-switch"
|
|
54
|
+
/>
|
|
55
|
+
<p class="text-caption text-medium-emphasis mb-2">
|
|
56
|
+
Once a day, people who look after Passes & Keys get a list of keys still out, by key press.
|
|
57
|
+
</p>
|
|
58
|
+
<v-text-field
|
|
59
|
+
v-if="dailyUnreturnedAlert"
|
|
60
|
+
v-model="alertTime"
|
|
61
|
+
type="time"
|
|
62
|
+
label="Send at (site time)"
|
|
63
|
+
density="compact"
|
|
64
|
+
hide-details
|
|
65
|
+
style="max-width: 200px"
|
|
66
|
+
:readonly="readOnly"
|
|
67
|
+
data-test="key-daily-alert-time"
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
<v-switch
|
|
71
|
+
v-model="blockCheckoutWithKeys"
|
|
72
|
+
label="Stop check-out while a key is still out"
|
|
73
|
+
color="primary"
|
|
74
|
+
hide-details
|
|
75
|
+
class="mt-3"
|
|
76
|
+
:disabled="readOnly"
|
|
77
|
+
data-test="key-block-checkout-switch"
|
|
78
|
+
/>
|
|
79
|
+
<p class="text-caption text-medium-emphasis mb-4">
|
|
80
|
+
The guard cannot check a visitor out until their key is returned or marked lost/damaged.
|
|
81
|
+
</p>
|
|
82
|
+
|
|
83
|
+
<v-btn
|
|
84
|
+
v-if="!readOnly"
|
|
85
|
+
color="primary"
|
|
86
|
+
:loading="saving"
|
|
87
|
+
:disabled="!siteData || !alertTimeValid"
|
|
88
|
+
data-test="key-settings-save"
|
|
89
|
+
@click="save"
|
|
90
|
+
>
|
|
91
|
+
Save
|
|
92
|
+
</v-btn>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<script setup lang="ts">
|
|
97
|
+
import useSiteFeatureSettings from "../composables/useSiteFeatureSettings";
|
|
98
|
+
import useKeyPresses from "../composables/useKeyPresses";
|
|
99
|
+
import { cleanKeyPresses, readKeySettings } from "../utils/key-presses";
|
|
100
|
+
|
|
101
|
+
const props = defineProps({
|
|
102
|
+
site: { type: String, required: true },
|
|
103
|
+
siteData: { type: Object, default: null },
|
|
104
|
+
readOnly: { type: Boolean, default: false },
|
|
105
|
+
});
|
|
106
|
+
const emit = defineEmits<{
|
|
107
|
+
(e: "saved"): void;
|
|
108
|
+
(e: "message", text: string, color: string): void;
|
|
109
|
+
}>();
|
|
110
|
+
|
|
111
|
+
const { saveSiteFeature } = useSiteFeatureSettings();
|
|
112
|
+
// Not `useKeyPresses(site)`: that would read the site again on open.
|
|
113
|
+
const { forget } = useKeyPresses(() => "");
|
|
114
|
+
|
|
115
|
+
const rows = ref<Array<{ id?: string; name: string }>>([]);
|
|
116
|
+
const dailyUnreturnedAlert = ref(false);
|
|
117
|
+
const alertTime = ref("18:00");
|
|
118
|
+
const blockCheckoutWithKeys = ref(false);
|
|
119
|
+
const saving = ref(false);
|
|
120
|
+
|
|
121
|
+
watch(
|
|
122
|
+
() => props.siteData,
|
|
123
|
+
(site) => {
|
|
124
|
+
const s = readKeySettings(site);
|
|
125
|
+
rows.value = s.cabinets.map((c) => ({ ...c }));
|
|
126
|
+
dailyUnreturnedAlert.value = s.dailyUnreturnedAlert;
|
|
127
|
+
alertTime.value = s.alertTime;
|
|
128
|
+
blockCheckoutWithKeys.value = s.blockCheckoutWithKeys;
|
|
129
|
+
},
|
|
130
|
+
{ immediate: true },
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const alertTimeValid = computed(() => /^([01]\d|2[0-3]):[0-5]\d$/.test(alertTime.value || ""));
|
|
134
|
+
|
|
135
|
+
async function save() {
|
|
136
|
+
if (!props.siteData) return;
|
|
137
|
+
saving.value = true;
|
|
138
|
+
try {
|
|
139
|
+
const cabinets = cleanKeyPresses(rows.value);
|
|
140
|
+
await saveSiteFeature(props.site, "keySettings", {
|
|
141
|
+
cabinets,
|
|
142
|
+
dailyUnreturnedAlert: dailyUnreturnedAlert.value,
|
|
143
|
+
alertTime: alertTime.value,
|
|
144
|
+
blockCheckoutWithKeys: blockCheckoutWithKeys.value,
|
|
145
|
+
});
|
|
146
|
+
rows.value = cabinets.map((c) => ({ ...c }));
|
|
147
|
+
forget(props.site);
|
|
148
|
+
emit("message", "Passes & keys settings saved", "success");
|
|
149
|
+
emit("saved");
|
|
150
|
+
} catch (e: any) {
|
|
151
|
+
emit("message", e?.data?.message || "Could not save Passes & keys settings", "error");
|
|
152
|
+
} finally {
|
|
153
|
+
saving.value = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
</script>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
"Download CSV" for a list screen: the same list API and filters the screen
|
|
4
|
+
uses, every page, as one CSV file. Reads only what the list already shows.
|
|
5
|
+
-->
|
|
6
|
+
<v-btn
|
|
7
|
+
variant="text"
|
|
8
|
+
size="small"
|
|
9
|
+
class="text-none"
|
|
10
|
+
prepend-icon="mdi-download"
|
|
11
|
+
:loading="busy"
|
|
12
|
+
data-test="list-csv-export"
|
|
13
|
+
@click="download"
|
|
14
|
+
>
|
|
15
|
+
{{ label }}
|
|
16
|
+
</v-btn>
|
|
17
|
+
<Snackbar v-model="toast.show" :text="toast.message" :color="toast.color" />
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
import { collectAllPages, EXPORT_MAX_ROWS, listCsv, type TExportColumn } from "../utils/list-export";
|
|
22
|
+
|
|
23
|
+
const props = defineProps({
|
|
24
|
+
/** `(page, limit) => list API result` with the screen's current filters. */
|
|
25
|
+
fetchPage: {
|
|
26
|
+
type: Function as PropType<(page: number, limit: number) => Promise<any>>,
|
|
27
|
+
required: true,
|
|
28
|
+
},
|
|
29
|
+
columns: { type: Array as PropType<TExportColumn[]>, required: true },
|
|
30
|
+
/** File name without the date or extension, e.g. "feedback". */
|
|
31
|
+
fileName: { type: String, required: true },
|
|
32
|
+
label: { type: String, default: "Download CSV" },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const busy = ref(false);
|
|
36
|
+
const toast = reactive({ show: false, message: "", color: "" });
|
|
37
|
+
|
|
38
|
+
async function download() {
|
|
39
|
+
busy.value = true;
|
|
40
|
+
try {
|
|
41
|
+
const { rows, truncated } = await collectAllPages(props.fetchPage);
|
|
42
|
+
if (!rows.length) {
|
|
43
|
+
Object.assign(toast, { show: true, message: "Nothing to download for these filters.", color: "info" });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const blob = new Blob([listCsv(props.columns, rows)], { type: "text/csv;charset=utf-8" });
|
|
47
|
+
const link = document.createElement("a");
|
|
48
|
+
link.href = URL.createObjectURL(blob);
|
|
49
|
+
link.download = `${props.fileName}-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
50
|
+
link.click();
|
|
51
|
+
// Revoked a moment later: some browsers start the download after click() returns.
|
|
52
|
+
const href = link.href;
|
|
53
|
+
setTimeout(() => URL.revokeObjectURL(href), 1000);
|
|
54
|
+
if (truncated) {
|
|
55
|
+
Object.assign(toast, {
|
|
56
|
+
show: true,
|
|
57
|
+
message: `Only the first ${EXPORT_MAX_ROWS} rows were downloaded. Narrow the dates to get the rest.`,
|
|
58
|
+
color: "warning",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch (error: any) {
|
|
62
|
+
Object.assign(toast, {
|
|
63
|
+
show: true,
|
|
64
|
+
message: error?.data?.message || "Could not download the list.",
|
|
65
|
+
color: "error",
|
|
66
|
+
});
|
|
67
|
+
} finally {
|
|
68
|
+
busy.value = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
</script>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div data-test="ma-staff-registration">
|
|
3
|
+
<v-switch v-model="enabled" :disabled="readOnly" label="Offer "Managing agent staff" at the guardhouse"
|
|
4
|
+
color="primary" hide-details />
|
|
5
|
+
<p class="text-caption text-medium-emphasis mt-1 mb-3">
|
|
6
|
+
The guardhouse can register your staff drawing keys as a contractor of type "Managing agent staff", with your company
|
|
7
|
+
name filled in.
|
|
8
|
+
</p>
|
|
9
|
+
<v-text-field v-if="enabled" v-model.trim="company" :readonly="readOnly" label="Company name to fill in"
|
|
10
|
+
placeholder="e.g. JLL" density="comfortable" variant="outlined" maxlength="120" />
|
|
11
|
+
<v-btn v-if="!readOnly" class="screen-btn-primary" color="primary-button" variant="flat" text="Save"
|
|
12
|
+
:loading="saving" :disabled="!loaded" @click="save" />
|
|
13
|
+
<Snackbar v-model="toast.show" :text="toast.message" :color="toast.color" />
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import useSiteFeatureSettings from "../composables/useSiteFeatureSettings";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Site setting `metadata.maStaffRegistration` (core site-feature-settings).
|
|
22
|
+
* Off = today: the contractor type list is unchanged.
|
|
23
|
+
*/
|
|
24
|
+
const props = defineProps({
|
|
25
|
+
siteId: { type: String, required: true },
|
|
26
|
+
saved: { type: Object as PropType<{ enabled?: boolean; company?: string } | null>, default: null },
|
|
27
|
+
readOnly: { type: Boolean, default: false },
|
|
28
|
+
/** The site has been read; Save stays off until then. */
|
|
29
|
+
loaded: { type: Boolean, default: false },
|
|
30
|
+
});
|
|
31
|
+
const emit = defineEmits(["saved"]);
|
|
32
|
+
|
|
33
|
+
const enabled = ref(false);
|
|
34
|
+
const company = ref("");
|
|
35
|
+
watch(
|
|
36
|
+
() => props.saved,
|
|
37
|
+
(v) => {
|
|
38
|
+
enabled.value = v?.enabled === true;
|
|
39
|
+
company.value = v?.company ?? "";
|
|
40
|
+
},
|
|
41
|
+
{ immediate: true },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const { saveSiteFeature } = useSiteFeatureSettings();
|
|
45
|
+
const saving = ref(false);
|
|
46
|
+
const toast = reactive({ show: false, message: "", color: "" });
|
|
47
|
+
|
|
48
|
+
async function save() {
|
|
49
|
+
if (!props.loaded) return;
|
|
50
|
+
saving.value = true;
|
|
51
|
+
try {
|
|
52
|
+
await saveSiteFeature(props.siteId, "maStaffRegistration", { enabled: enabled.value, company: company.value });
|
|
53
|
+
Object.assign(toast, { show: true, message: "Saved.", color: "success" });
|
|
54
|
+
emit("saved");
|
|
55
|
+
} catch (e: any) {
|
|
56
|
+
Object.assign(toast, { show: true, message: e?.data?.message || "Could not save.", color: "error" });
|
|
57
|
+
} finally {
|
|
58
|
+
saving.value = false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
<div class="nric-selected-person d-flex align-center justify-space-between ga-3 mb-5">
|
|
115
115
|
<span>{{ memberSelectedSearchResult.name || "Unknown" }}</span>
|
|
116
116
|
<span class="text-no-wrap">
|
|
117
|
-
NRIC: {{ memberSelectedSearchResult.nric || "N/A" }}
|
|
117
|
+
NRIC: {{ maskNRIC(memberSelectedSearchResult.nric) || "N/A" }}
|
|
118
118
|
</span>
|
|
119
119
|
</div>
|
|
120
120
|
|
|
@@ -214,7 +214,7 @@ const props = defineProps({
|
|
|
214
214
|
},
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
-
const { requiredRule, debounce } = useUtils();
|
|
217
|
+
const { requiredRule, debounce, maskNRIC } = useUtils();
|
|
218
218
|
const { getPassKeysByPageSearch } = usePassKey();
|
|
219
219
|
const {
|
|
220
220
|
findPersonByNRICMultipleResult,
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
:rules="props.passRules"
|
|
49
49
|
item-title="prefixAndName"
|
|
50
50
|
item-value="_id"
|
|
51
|
+
:item-props="keyItemProps"
|
|
51
52
|
label="Keys (Optional)"
|
|
52
53
|
multiple
|
|
53
54
|
variant="outlined"
|
|
@@ -97,6 +98,7 @@
|
|
|
97
98
|
import type { PropType } from "vue";
|
|
98
99
|
import type { ValidationRule } from "vuetify/lib/types.mjs";
|
|
99
100
|
import usePassKey from "../composables/usePassKey";
|
|
101
|
+
import useKeyPresses from "../composables/useKeyPresses";
|
|
100
102
|
import { errorConverter } from "../utils/data";
|
|
101
103
|
import VisitorPassKeyQRScanner from "./VisitorPassKeyQRScanner.vue";
|
|
102
104
|
|
|
@@ -125,6 +127,9 @@ const props = withDefaults(defineProps<PassInformationProps>(), {
|
|
|
125
127
|
});
|
|
126
128
|
|
|
127
129
|
const emit = defineEmits(["update:pass", "update:keys"]);
|
|
130
|
+
// The key's press (if the site has any) under each key in the picker. Read
|
|
131
|
+
// only when the key picker is shown.
|
|
132
|
+
const { keyItemProps } = useKeyPresses(() => (props.hideKeys ? "" : props.site));
|
|
128
133
|
const pass = ref<TPassKeyPayload[]>(props.pass);
|
|
129
134
|
const keys = ref<TPassKeyPayload[]>(props.keys);
|
|
130
135
|
const selectedPass = ref<string>("");
|
|
@@ -79,6 +79,27 @@
|
|
|
79
79
|
/>
|
|
80
80
|
</v-col>
|
|
81
81
|
|
|
82
|
+
<!--
|
|
83
|
+
Whole-role starting points (e.g. Guardhouse officer). Replaces the
|
|
84
|
+
ticks below; nothing is saved until Create. Only offered when this
|
|
85
|
+
catalogue has every permission the preset uses.
|
|
86
|
+
-->
|
|
87
|
+
<v-col v-if="rolePresets.length" cols="12" class="mt-3" data-test="role-presets">
|
|
88
|
+
<div class="text-caption font-weight-bold mb-1">Start from a ready-made role</div>
|
|
89
|
+
<div class="d-flex flex-wrap ga-2">
|
|
90
|
+
<v-tooltip v-for="p in rolePresets" :key="p.key" :text="p.description" location="top">
|
|
91
|
+
<template #activator="{ props: tip }">
|
|
92
|
+
<v-chip v-bind="tip" size="small" variant="outlined" class="text-none" @click="applyRoleTemplate(p)">
|
|
93
|
+
{{ p.label }}
|
|
94
|
+
</v-chip>
|
|
95
|
+
</template>
|
|
96
|
+
</v-tooltip>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="text-caption text-medium-emphasis mt-1">
|
|
99
|
+
Replaces the ticks below. Adjust them if needed, then create the role.
|
|
100
|
+
</div>
|
|
101
|
+
</v-col>
|
|
102
|
+
|
|
82
103
|
<v-col cols="12" class="mt-3">
|
|
83
104
|
<div class="role-create__list">
|
|
84
105
|
<div
|
|
@@ -262,6 +283,7 @@ import useRole from "../composables/useRole";
|
|
|
262
283
|
import useUtils from "../composables/useUtils";
|
|
263
284
|
import useSite from "../composables/useSite";
|
|
264
285
|
import { readApiError } from "../utils/subscription-form";
|
|
286
|
+
import { applyRolePreset, ROLE_PRESETS, type TRolePreset } from "../utils/inventory-presets";
|
|
265
287
|
|
|
266
288
|
/** Shown after "Couldn't save:" when the server gave no reason (network, timeout). */
|
|
267
289
|
const SAVE_FALLBACK =
|
|
@@ -406,6 +428,17 @@ function toggleCategory(resource: string, value: boolean | null) {
|
|
|
406
428
|
}
|
|
407
429
|
}
|
|
408
430
|
|
|
431
|
+
/** Whole-role presets this catalogue can express in full. */
|
|
432
|
+
const rolePresets = computed(() =>
|
|
433
|
+
ROLE_PRESETS.filter((p) => applyRolePreset(safePermissions.value as any, p)),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
function applyRoleTemplate(preset: TRolePreset) {
|
|
437
|
+
const state = applyRolePreset(safePermissions.value as any, preset);
|
|
438
|
+
// never an empty list: empty means "everything" on the server
|
|
439
|
+
if (state?.selected.length) permState.value = state;
|
|
440
|
+
}
|
|
441
|
+
|
|
409
442
|
/** A module preset replaced that module's ticks (see RolePermissionPresets). */
|
|
410
443
|
function applyPreset(selected: string[]) {
|
|
411
444
|
permState.value = { selected, enabled: permState.value.enabled };
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
</span>
|
|
20
20
|
</template>
|
|
21
21
|
|
|
22
|
+
<template #item.nric="{ value }">{{ maskNRIC(value) || "-" }}</template>
|
|
23
|
+
|
|
22
24
|
<template #item.actions="{ item }">
|
|
23
25
|
<AppButton variant="ghost" @click="handleSelectUser(item)">Select</AppButton>
|
|
24
26
|
</template>
|
|
@@ -37,6 +39,7 @@
|
|
|
37
39
|
</template>
|
|
38
40
|
|
|
39
41
|
<script setup lang="ts">
|
|
42
|
+
const { maskNRIC } = useUtils();
|
|
40
43
|
|
|
41
44
|
const prop = defineProps({
|
|
42
45
|
vehicleNumber: {
|