@7365admin1/layer-common 4.119.0 → 4.121.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 +28 -4
- package/components/AccessCardDetailsDialog.vue +3 -2
- package/components/AddPassKeyToVisitor.vue +5 -0
- package/components/BuildingUnitFormEdit.vue +3 -1
- package/components/DataRetentionBanner.vue +73 -0
- 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/MemberSiteAccess.vue +51 -2
- package/components/PassInformation.vue +5 -0
- package/components/RolePermissionFormCreate.vue +33 -0
- package/components/SearchVehicleNumberUser.vue +3 -0
- package/components/SiteDataRetention.vue +153 -0
- package/components/SiteSettings.vue +48 -0
- package/components/SiteSettingsVisitorRegistration.vue +131 -0
- package/components/VehicleForm.vue +2 -2
- package/components/VisitorForm.vue +45 -6
- package/components/VisitorManagement.vue +3 -1
- package/components/VisitorSearchResultSummary.vue +2 -1
- package/composables/useDataRetention.ts +49 -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/useMember.ts +6 -0
- package/composables/usePassKey.ts +4 -0
- package/composables/useSiteFeatureSettings.ts +37 -0
- package/composables/useVisitor.ts +25 -0
- package/constants/permissions.ts +12 -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 +108 -2
- package/utils/key-presses.ts +55 -0
- package/utils/list-export.ts +39 -0
- package/utils/notification-target.ts +10 -0
- package/utils/site-access.ts +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.121.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 19485b6: Data retention is managed by Seven365 Admin: `SiteDataRetention` becomes a read-only summary (rules, dates, removal certificates still download), and the new `DataRetentionBanner` shows staff of the selected site a scheduled permanent deletion (`useDataRetention().getDataRetentionBanner`).
|
|
8
|
+
- 495ae9c: Site Settings > Visitor registration: the NRIC / FIN setting is now a choice of "Ask and keep in full (as usual)", "Ask, but keep only the last 4 characters" or "Don't ask", with help text for each. On a last-4 site the visitor form tells staff that only the last 4 characters will be kept. Needs @7365admin1/core with visitorFields.nric = "last4".
|
|
9
|
+
- 42cdb65: The Security Ops permissions are registered in the canonical vocabulary, so a misspelling is caught rather than silently false. The role editor offers two one-click sets, Security Ops officer and Head of security, and a Security Ops notification now opens the Ops Review page instead of nothing.
|
|
10
|
+
- f663200: Site access has a switch marking a person as part of Security Ops: the officers who go out, walk a site and fill in the visit checklists. It applies to every site ticked in the dialog, shows a chip on each one, and reads the result back in a sentence. It grants nothing on its own, so the Role column still decides what a member may do.
|
|
11
|
+
|
|
12
|
+
## 4.120.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- bccdbb9: Data retention (PDPA) per site: `SiteDataRetention` settings card (switch with confirmation, keep-for months per record type, notice days, preview, removal records with certificate download), `SiteDataRemovalDialog` for the staff contract-end removal, and `useDataRetention`.
|
|
17
|
+
- bccdbb9: Site Settings "Feedback handling" card (`FeedbackHandlingSettings`): office hours, public holidays, who receives new feedback per category, escalation steps and auto-close categories, saved as `site.metadata.feedbackSettings`. `useFeedback().sendToProvider` for "Send to another provider".
|
|
18
|
+
- bccdbb9: Passes & keys site settings (key presses, daily unreturned-key list, stop check-out while a key is out), `KeyPressSelect` and `useKeyPresses`, and the key press shown under each key in the visitor key pickers. Nothing changes for a site until it adds a key press or turns a switch on.
|
|
19
|
+
- bccdbb9: Room handover / takeover checklists (setup with ready-made starters, records with PDF) and an optional "Managing agent staff" contractor type behind a site switch.
|
|
20
|
+
- bccdbb9: Site Settings: Incident report emails. Documents: Archived tab with archive/restore. Create role: Guardhouse officer preset. New ListCsvExport component for list CSV downloads.
|
|
21
|
+
- bccdbb9: Site Settings gains a "Visitor registration" panel (Ask for NRIC / FIN, Contact number, Vehicle number), and the visitor form follows it. Sites that never saved it keep today's form.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- d4ae8bb: Show NRIC as last 4 characters only (e.g. **\***567A) in the remaining places that printed it in full: visitor autofill search result and pick dialog, the non-checked-out / Skip NRIC prompt, member autofill, vehicle owner list, unit vehicles table, vehicle-number user picker and access card details. Display only — the value typed into or saved by the forms is unchanged.
|
|
26
|
+
|
|
3
27
|
## 4.119.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
|
@@ -3194,11 +3218,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
|
|
|
3194
3218
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
3195
3219
|
own rule from two endpoints the console already calls, unprojected:
|
|
3196
3220
|
|
|
3197
|
-
|
|
3198
|
-
|
|
3221
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
3222
|
+
GET /api/roles/id/:role that membership's role document
|
|
3199
3223
|
|
|
3200
|
-
|
|
3201
|
-
|
|
3224
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
3225
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
3202
3226
|
|
|
3203
3227
|
`role.default` is the marker because it is the only property of a platform
|
|
3204
3228
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
|
|
80
80
|
<div v-if="details.userCred.nric">
|
|
81
81
|
<div class="card-details__label">NRIC</div>
|
|
82
|
-
<div class="card-details__value">{{ details.userCred.nric }}</div>
|
|
82
|
+
<div class="card-details__value">{{ maskNRIC(details.userCred.nric) }}</div>
|
|
83
83
|
</div>
|
|
84
84
|
|
|
85
85
|
<div v-if="details.userCred.contact">
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
<template v-if="entry.type === 'assigned' || entry.visitorId">
|
|
163
163
|
<div class="card-details__value">{{ entry.name ?? "N/A" }}</div>
|
|
164
164
|
<div class="card-details__sub">
|
|
165
|
-
<span v-if="entry.nric">NRIC: {{ entry.nric }}</span>
|
|
165
|
+
<span v-if="entry.nric">NRIC: {{ maskNRIC(entry.nric) }}</span>
|
|
166
166
|
<span v-if="entry.contact" class="ml-2">· {{ entry.contact }}</span>
|
|
167
167
|
<span v-if="entry.email && !entry.nric && !entry.contact">{{ entry.email }}</span>
|
|
168
168
|
</div>
|
|
@@ -193,6 +193,7 @@
|
|
|
193
193
|
</template>
|
|
194
194
|
|
|
195
195
|
<script setup lang="ts">
|
|
196
|
+
const { maskNRIC } = useUtils();
|
|
196
197
|
const props = defineProps({
|
|
197
198
|
modelValue: {
|
|
198
199
|
type: Boolean,
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
:items="keyItems"
|
|
103
103
|
item-title="prefixAndName"
|
|
104
104
|
item-value="_id"
|
|
105
|
+
:item-props="keyItemProps"
|
|
105
106
|
:label="existingKeys.length > 0 ? 'Add more keys' : undefined"
|
|
106
107
|
multiple
|
|
107
108
|
variant="outlined"
|
|
@@ -147,6 +148,7 @@
|
|
|
147
148
|
import type { PropType } from "vue";
|
|
148
149
|
import usePassKey from "../composables/usePassKey";
|
|
149
150
|
import useVisitor from "../composables/useVisitor";
|
|
151
|
+
import useKeyPresses from "../composables/useKeyPresses";
|
|
150
152
|
|
|
151
153
|
const prop = defineProps({
|
|
152
154
|
visitor: {
|
|
@@ -195,6 +197,9 @@ const existingKeys = ref<
|
|
|
195
197
|
|
|
196
198
|
const isEditMode = computed(() => prop.mode === "edit");
|
|
197
199
|
const showKeys = computed(() => prop.visitor.type === "contractor");
|
|
200
|
+
// The key's press (if the site has any) under each key in the picker. Read
|
|
201
|
+
// only when the key picker is shown.
|
|
202
|
+
const { keyItemProps } = useKeyPresses(() => (showKeys.value ? prop.site : ""));
|
|
198
203
|
|
|
199
204
|
const passTypesComputed = computed(() => {
|
|
200
205
|
const type = prop.visitor.type;
|
|
@@ -163,6 +163,8 @@
|
|
|
163
163
|
<TableMain :headers="vehicleHeaders" :show-header="true" :page="vehiclePage" :pages="vehicleTotalPages"
|
|
164
164
|
:pageRange="vehiclePageRange" :items="unitVehicleItems" @refresh="getUnitVehiclesRefresh" @update:page="handleUpdateVehiclePage">
|
|
165
165
|
|
|
166
|
+
<template #item.nric="{ value }">{{ maskNRIC(value) || "-" }}</template>
|
|
167
|
+
|
|
166
168
|
<template #item.status="{value}">
|
|
167
169
|
<v-chip :color="formatVehicleStatus(value).color" class="ma-0" small>
|
|
168
170
|
{{ formatVehicleStatus(value).label }}
|
|
@@ -404,7 +406,7 @@ const validForm = ref(false);
|
|
|
404
406
|
|
|
405
407
|
const { getAll } = useBuilding();
|
|
406
408
|
const { getPeopleByUnit, updateById: updatePeopleById } = usePeople();
|
|
407
|
-
const { debounce } = useUtils();
|
|
409
|
+
const { debounce, maskNRIC } = useUtils();
|
|
408
410
|
const { searchMultipleVehiclesByUnitId, formatVehicleStatus } = useVehicle();
|
|
409
411
|
|
|
410
412
|
const loading = reactive({
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Scheduled data removal at the selected site (owner 2026-10-14). Shown to
|
|
4
|
+
the people who work the site, never residents (the server answers nothing
|
|
5
|
+
to them), and only for a removal that will really happen: Seven365 Admin
|
|
6
|
+
allowed deletion at this site and the site's admins were already told.
|
|
7
|
+
A site with no data retention never shows it - the request answers
|
|
8
|
+
`{ banner: null }`.
|
|
9
|
+
-->
|
|
10
|
+
<v-alert
|
|
11
|
+
v-if="banner"
|
|
12
|
+
type="warning"
|
|
13
|
+
variant="tonal"
|
|
14
|
+
density="comfortable"
|
|
15
|
+
class="drb mb-3"
|
|
16
|
+
icon="mdi-delete-clock-outline"
|
|
17
|
+
data-test="drb"
|
|
18
|
+
>
|
|
19
|
+
<div class="drb__text">{{ banner.message }}</div>
|
|
20
|
+
<button type="button" class="drb__toggle" :aria-expanded="open" data-test="drb-details" @click="open = !open">
|
|
21
|
+
{{ open ? "Hide details" : "Details" }}
|
|
22
|
+
</button>
|
|
23
|
+
<div v-if="open" class="drb__details" data-test="drb-details-body">
|
|
24
|
+
<ul class="drb__list">
|
|
25
|
+
<li v-for="i in banner.items" :key="i.dataType">
|
|
26
|
+
{{ i.label }}: {{ Number(i.count).toLocaleString("en-SG") }} created before {{ day(i.cutoff) }}
|
|
27
|
+
</li>
|
|
28
|
+
</ul>
|
|
29
|
+
<div>
|
|
30
|
+
Only finished records are deleted (closed feedback and work orders, visitors who have checked out, decided
|
|
31
|
+
incident reports, past days' occurrence books). Use each list's export to keep a copy. To postpone this, contact
|
|
32
|
+
Seven365.
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</v-alert>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script lang="ts" setup>
|
|
39
|
+
const props = defineProps({
|
|
40
|
+
/** The selected site. Anything that is not a site id shows nothing. */
|
|
41
|
+
siteId: { type: String, default: "" },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const { getDataRetentionBanner } = useDataRetention();
|
|
45
|
+
const banner = ref<Record<string, any> | null>(null);
|
|
46
|
+
const open = ref(false);
|
|
47
|
+
|
|
48
|
+
const day = (d: string) =>
|
|
49
|
+
new Date(d).toLocaleDateString("en-SG", { day: "numeric", month: "short", year: "numeric", timeZone: "Asia/Singapore" });
|
|
50
|
+
|
|
51
|
+
async function load(site: string) {
|
|
52
|
+
banner.value = null;
|
|
53
|
+
open.value = false;
|
|
54
|
+
if (!/^[a-f0-9]{24}$/i.test(site)) return;
|
|
55
|
+
try {
|
|
56
|
+
const res = await getDataRetentionBanner(site);
|
|
57
|
+
// A late answer for a site the user already left is dropped.
|
|
58
|
+
if (props.siteId === site) banner.value = res?.banner ?? null;
|
|
59
|
+
} catch {
|
|
60
|
+
// Not reachable, a resident, or an older server: no banner.
|
|
61
|
+
banner.value = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
watch(() => props.siteId, (s) => load(s), { immediate: true });
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<style scoped>
|
|
69
|
+
.drb__text { word-break: break-word; }
|
|
70
|
+
.drb__toggle { text-decoration: underline; font-weight: 600; margin-top: 4px; background: none; border: 0; padding: 0; color: inherit; cursor: pointer; }
|
|
71
|
+
.drb__details { margin-top: 8px; font-size: 13px; }
|
|
72
|
+
.drb__list { padding-left: 18px; margin-bottom: 6px; }
|
|
73
|
+
</style>
|
|
@@ -66,7 +66,12 @@
|
|
|
66
66
|
>
|
|
67
67
|
<v-tab value="active" class="text-none">Documents</v-tab>
|
|
68
68
|
<v-tab value="expired" class="text-none">Document (Expired)</v-tab>
|
|
69
|
+
<v-tab value="archived" class="text-none" data-test="doc-tab-archived">Archived</v-tab>
|
|
69
70
|
</v-tabs>
|
|
71
|
+
<p v-if="isArchivedTab" class="text-caption text-medium-emphasis mt-2 mb-0">
|
|
72
|
+
Archived documents are kept but hidden from the main list. Archiving a folder archives everything in it, and
|
|
73
|
+
restoring the folder brings it all back.
|
|
74
|
+
</p>
|
|
70
75
|
</v-col>
|
|
71
76
|
|
|
72
77
|
<!--
|
|
@@ -74,7 +79,7 @@
|
|
|
74
79
|
place in the folder tree, so a breadcrumb there would name a folder whose
|
|
75
80
|
contents are not what is on screen.
|
|
76
81
|
-->
|
|
77
|
-
<v-col cols="12" class="mb-2" v-if="isInsideFolder && !isExpiredTab">
|
|
82
|
+
<v-col cols="12" class="mb-2" v-if="isInsideFolder && !isExpiredTab && !isArchivedTab">
|
|
78
83
|
<div class="folder-path-bar d-flex align-center ga-2 px-3 py-2">
|
|
79
84
|
<v-btn
|
|
80
85
|
icon="mdi-arrow-left"
|
|
@@ -227,6 +232,15 @@
|
|
|
227
232
|
>
|
|
228
233
|
<v-list-item-title class="text-body-2">Edit</v-list-item-title>
|
|
229
234
|
</v-list-item>
|
|
235
|
+
<v-list-item
|
|
236
|
+
v-if="canUpdateDocument"
|
|
237
|
+
class="folder-action-item"
|
|
238
|
+
@click.stop="archiveItem(item._raw, !isArchivedTab)"
|
|
239
|
+
>
|
|
240
|
+
<v-list-item-title class="text-body-2">
|
|
241
|
+
{{ isArchivedTab ? "Restore" : "Archive" }}
|
|
242
|
+
</v-list-item-title>
|
|
243
|
+
</v-list-item>
|
|
230
244
|
<v-list-item
|
|
231
245
|
v-if="canDeleteDocument"
|
|
232
246
|
class="folder-action-item"
|
|
@@ -265,6 +279,15 @@
|
|
|
265
279
|
>
|
|
266
280
|
<v-list-item-title class="text-body-2">Edit</v-list-item-title>
|
|
267
281
|
</v-list-item>
|
|
282
|
+
<v-list-item
|
|
283
|
+
v-if="canUpdateDocument"
|
|
284
|
+
class="folder-action-item"
|
|
285
|
+
@click.stop="archiveItem(item._raw, !isArchivedTab)"
|
|
286
|
+
>
|
|
287
|
+
<v-list-item-title class="text-body-2">
|
|
288
|
+
{{ isArchivedTab ? "Restore" : "Archive" }}
|
|
289
|
+
</v-list-item-title>
|
|
290
|
+
</v-list-item>
|
|
268
291
|
<v-list-item
|
|
269
292
|
class="folder-action-item"
|
|
270
293
|
@click.stop="openDeleteFolderDialog(item._raw)"
|
|
@@ -776,6 +799,7 @@ const documentService = useDocument();
|
|
|
776
799
|
const {
|
|
777
800
|
getAll: _getAllDocuments,
|
|
778
801
|
deleteById,
|
|
802
|
+
setArchived,
|
|
779
803
|
deleteFolderById,
|
|
780
804
|
updateFolderById,
|
|
781
805
|
createFolder,
|
|
@@ -841,8 +865,10 @@ const selectedFileType = ref("all");
|
|
|
841
865
|
* Which half of the list is on screen. The value is sent straight to the API as
|
|
842
866
|
* its `expiry` filter, so the tab and the query cannot drift apart.
|
|
843
867
|
*/
|
|
844
|
-
const activeTab = ref<"active" | "expired">("active");
|
|
868
|
+
const activeTab = ref<"active" | "expired" | "archived">("active");
|
|
845
869
|
const isExpiredTab = computed(() => activeTab.value === "expired");
|
|
870
|
+
// Archived: every archived item at the site, wherever it was filed (like Expired).
|
|
871
|
+
const isArchivedTab = computed(() => activeTab.value === "archived");
|
|
846
872
|
|
|
847
873
|
const {
|
|
848
874
|
data: getDocumentReq,
|
|
@@ -859,7 +885,8 @@ const {
|
|
|
859
885
|
parentId: activeParentId.value,
|
|
860
886
|
type:
|
|
861
887
|
selectedFileType.value === "all" ? undefined : selectedFileType.value,
|
|
862
|
-
expiry: activeTab.value,
|
|
888
|
+
expiry: isArchivedTab.value ? "all" : activeTab.value,
|
|
889
|
+
archived: isArchivedTab.value,
|
|
863
890
|
}),
|
|
864
891
|
{
|
|
865
892
|
watch: [page, headerSearch, activeParentId, selectedFileType, activeTab],
|
|
@@ -1043,6 +1070,8 @@ function tableRowClickHandler(data: any) {
|
|
|
1043
1070
|
const rowData = (data?._raw || data) as TDocument;
|
|
1044
1071
|
|
|
1045
1072
|
if (isFolderItem(rowData)) {
|
|
1073
|
+
// An archived folder is restored, not browsed: the Archived tab is a flat list.
|
|
1074
|
+
if (isArchivedTab.value) return;
|
|
1046
1075
|
openFolder(rowData);
|
|
1047
1076
|
return;
|
|
1048
1077
|
}
|
|
@@ -1142,6 +1171,30 @@ function setDocument({
|
|
|
1142
1171
|
}
|
|
1143
1172
|
}
|
|
1144
1173
|
|
|
1174
|
+
/** Archive / restore one row (document or folder). Same right as editing it. */
|
|
1175
|
+
async function archiveItem(item: TDocument, archive: boolean) {
|
|
1176
|
+
const id = String((item as any)?._id || "");
|
|
1177
|
+
if (!id) return;
|
|
1178
|
+
documentActionMenu.value[id] = false;
|
|
1179
|
+
folderActionMenu.value[id] = false;
|
|
1180
|
+
try {
|
|
1181
|
+
await setArchived(id, archive);
|
|
1182
|
+
showMessage(
|
|
1183
|
+
archive
|
|
1184
|
+
? "Archived. Find it under the Archived tab."
|
|
1185
|
+
: "Restored to the main list.",
|
|
1186
|
+
"success"
|
|
1187
|
+
);
|
|
1188
|
+
await getDocuments();
|
|
1189
|
+
} catch (error: any) {
|
|
1190
|
+
showMessage(
|
|
1191
|
+
error?.data?.message ||
|
|
1192
|
+
(archive ? "Could not archive it." : "Could not restore it."),
|
|
1193
|
+
"error"
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1145
1198
|
function showMessage(msg: string, color: string) {
|
|
1146
1199
|
message.value = msg;
|
|
1147
1200
|
messageColor.value = color;
|