@7365admin1/layer-common 4.118.1 → 4.118.3
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/IncidentReport/Attachments.vue +112 -0
- package/components/IncidentReport/IncidentInformation.vue +11 -0
- package/components/IncidentReport/IncidentInformationDownload.vue +24 -6
- package/composables/useIncidentCompany.ts +20 -0
- package/package.json +1 -1
- package/utils/incident-company.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.118.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 90727b3: Incident report PDF: a field label stays with its value across a page break. Incident report dialog: shows "Reported by" (the report's organisation) when given `organizationId`.
|
|
8
|
+
|
|
9
|
+
## 4.118.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 7beb1db: Incident Report PDF: attachments in a 2-up grid with Download links, sections flow without big gaps, and "Reported by" company name in the header and information table.
|
|
14
|
+
|
|
3
15
|
## 4.118.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -3133,11 +3145,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
|
|
|
3133
3145
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
3134
3146
|
own rule from two endpoints the console already calls, unprojected:
|
|
3135
3147
|
|
|
3136
|
-
|
|
3137
|
-
|
|
3148
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
3149
|
+
GET /api/roles/id/:role that membership's role document
|
|
3138
3150
|
|
|
3139
|
-
|
|
3140
|
-
|
|
3151
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
3152
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
3141
3153
|
|
|
3142
3154
|
`role.default` is the marker because it is the only property of a platform
|
|
3143
3155
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="ir-att">
|
|
3
|
+
<p class="ir-att-title">Attachments</p>
|
|
4
|
+
<div v-if="items.length" class="ir-att-grid">
|
|
5
|
+
<div v-for="item in items" :key="item.id" class="ir-att-card">
|
|
6
|
+
<div class="ir-att-frame">
|
|
7
|
+
<img v-if="item.isImage" :src="item.url" :alt="item.name" />
|
|
8
|
+
<div v-else class="ir-att-icon">
|
|
9
|
+
<v-icon size="48">{{ item.icon }}</v-icon>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
<p class="ir-att-name">{{ item.name }}</p>
|
|
13
|
+
<a class="ir-att-link" :href="item.url" target="_blank" rel="noopener">Download</a>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<p v-else class="mt-2 text-h6">No Attachments Available</p>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script lang="ts" setup>
|
|
21
|
+
// Print/PDF attachments: 2 per row in fixed frames so tall phone screenshots and
|
|
22
|
+
// wide photos line up. The Download link is a plain <a href>, which the PDF
|
|
23
|
+
// renderer keeps as a live link. Files live in the public file store, so the
|
|
24
|
+
// link works for anyone holding the PDF.
|
|
25
|
+
const props = defineProps({
|
|
26
|
+
photos: {
|
|
27
|
+
type: Array as PropType<string[]>,
|
|
28
|
+
default: () => [],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const { getFileUrl, getFileById } = useFile();
|
|
33
|
+
|
|
34
|
+
const IMAGE = /\.(jpe?g|png|gif|webp|bmp|heic|svg)$/i;
|
|
35
|
+
const VIDEO = /\.(mp4|mov|avi|mkv|webm|3gp)$/i;
|
|
36
|
+
|
|
37
|
+
type Item = { id: string; name: string; url: string; isImage: boolean; icon: string };
|
|
38
|
+
const items = ref<Item[]>([]);
|
|
39
|
+
|
|
40
|
+
watch(
|
|
41
|
+
() => props.photos,
|
|
42
|
+
async (ids) => {
|
|
43
|
+
items.value = await Promise.all(
|
|
44
|
+
(ids ?? []).map(async (id) => {
|
|
45
|
+
let name = "";
|
|
46
|
+
try {
|
|
47
|
+
const res: any = await getFileById(id);
|
|
48
|
+
name = res?.data?.name ?? res?.name ?? "";
|
|
49
|
+
} catch {
|
|
50
|
+
// keep the link even if the name lookup fails
|
|
51
|
+
}
|
|
52
|
+
// No name known: treat as an image (that is what reports carry).
|
|
53
|
+
const isImage = !name || IMAGE.test(name);
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
name: name || "Attachment",
|
|
57
|
+
url: getFileUrl(id),
|
|
58
|
+
isImage,
|
|
59
|
+
icon: VIDEO.test(name) ? "mdi-file-video-outline" : "mdi-file-document-outline",
|
|
60
|
+
};
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
{ immediate: true }
|
|
65
|
+
);
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<style scoped>
|
|
69
|
+
.ir-att-title {
|
|
70
|
+
font-size: 17px;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
margin: 0 0 12px;
|
|
73
|
+
break-after: avoid;
|
|
74
|
+
page-break-after: avoid;
|
|
75
|
+
}
|
|
76
|
+
.ir-att-grid {
|
|
77
|
+
display: grid;
|
|
78
|
+
grid-template-columns: 1fr 1fr;
|
|
79
|
+
gap: 16px;
|
|
80
|
+
}
|
|
81
|
+
.ir-att-card {
|
|
82
|
+
break-inside: avoid;
|
|
83
|
+
page-break-inside: avoid;
|
|
84
|
+
border: 1px solid #8a8a8a;
|
|
85
|
+
padding: 8px;
|
|
86
|
+
min-width: 0;
|
|
87
|
+
}
|
|
88
|
+
.ir-att-frame {
|
|
89
|
+
height: 260px;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
background: #f5f5f5;
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
}
|
|
96
|
+
.ir-att-frame img {
|
|
97
|
+
width: 100%;
|
|
98
|
+
height: 100%;
|
|
99
|
+
object-fit: contain;
|
|
100
|
+
}
|
|
101
|
+
.ir-att-name {
|
|
102
|
+
margin: 6px 0 2px;
|
|
103
|
+
font-size: 12px;
|
|
104
|
+
overflow-wrap: anywhere;
|
|
105
|
+
}
|
|
106
|
+
.ir-att-link {
|
|
107
|
+
font-size: 12px;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
color: #1565c0;
|
|
110
|
+
text-decoration: underline;
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
{{ incidentInformation?.siteInfo?.designation || "NA" }}
|
|
32
32
|
</p>
|
|
33
33
|
</v-col>
|
|
34
|
+
<v-col v-if="companyName" cols="12" sm="6" md="3">
|
|
35
|
+
<InputLabel class="text-capitalize" title="Reported by" />
|
|
36
|
+
<p class="my-1 text-h6">{{ companyName }}</p>
|
|
37
|
+
</v-col>
|
|
34
38
|
</v-row>
|
|
35
39
|
</v-col>
|
|
36
40
|
|
|
@@ -256,8 +260,15 @@ const props = defineProps({
|
|
|
256
260
|
type: Object as PropType<Record<string, any> | null>,
|
|
257
261
|
required: true,
|
|
258
262
|
},
|
|
263
|
+
// The report's `organization` id: the company that filed it.
|
|
264
|
+
organizationId: {
|
|
265
|
+
type: String,
|
|
266
|
+
default: "",
|
|
267
|
+
},
|
|
259
268
|
});
|
|
260
269
|
|
|
270
|
+
const companyName = useIncidentCompany(() => props.organizationId);
|
|
271
|
+
|
|
261
272
|
const emit = defineEmits(["showNRICComplainant", "showNRICRecipient"]);
|
|
262
273
|
|
|
263
274
|
// utilities
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
<p v-if="status" class="ir-status ma-0 mt-2" :class="`ir-status--${statusKey}`">
|
|
7
7
|
Status: {{ String(status).toUpperCase() }}
|
|
8
8
|
</p>
|
|
9
|
+
<p v-if="companyName" class="ma-0 mt-2 ir-company">
|
|
10
|
+
Reported by: {{ companyName }}
|
|
11
|
+
</p>
|
|
9
12
|
</div>
|
|
10
13
|
<v-row no-gutters class="ir-box mt-5">
|
|
11
14
|
<p class="ir-section w-100 text-h6 pa-2 text-center bg-warning text-on-warning">
|
|
@@ -21,6 +24,11 @@
|
|
|
21
24
|
<p>{{ incidentInformation?.siteInfo?.irNumber || "-" }}</p>
|
|
22
25
|
</v-col>
|
|
23
26
|
|
|
27
|
+
<v-col v-if="companyName" cols="12" class="pa-3 ir-cell">
|
|
28
|
+
<p class="font-weight-bold">Reported by (Company)</p>
|
|
29
|
+
<p>{{ companyName }}</p>
|
|
30
|
+
</v-col>
|
|
31
|
+
|
|
24
32
|
<v-col cols="6" class="pa-3 ir-cell ir-e">
|
|
25
33
|
<p class="font-weight-bold">Place of Incident</p>
|
|
26
34
|
<p>
|
|
@@ -168,8 +176,15 @@ const props = defineProps({
|
|
|
168
176
|
type: String,
|
|
169
177
|
default: "",
|
|
170
178
|
},
|
|
179
|
+
// The report's `organization` id: the company that filed it.
|
|
180
|
+
organizationId: {
|
|
181
|
+
type: String,
|
|
182
|
+
default: "",
|
|
183
|
+
},
|
|
171
184
|
});
|
|
172
185
|
|
|
186
|
+
const companyName = useIncidentCompany(() => props.organizationId);
|
|
187
|
+
|
|
173
188
|
const statusKey = computed(() => String(props.status || "").toLowerCase());
|
|
174
189
|
|
|
175
190
|
// utilities
|
|
@@ -249,6 +264,10 @@ const maskNRIC = (value: any) => {
|
|
|
249
264
|
font-weight: 700;
|
|
250
265
|
letter-spacing: 0.03em;
|
|
251
266
|
}
|
|
267
|
+
.ir-company {
|
|
268
|
+
font-size: 1rem;
|
|
269
|
+
font-weight: 600;
|
|
270
|
+
}
|
|
252
271
|
.ir-status--approved { color: #2e7d32; }
|
|
253
272
|
.ir-status--rejected { color: #c62828; }
|
|
254
273
|
.ir-box {
|
|
@@ -264,12 +283,11 @@ const maskNRIC = (value: any) => {
|
|
|
264
283
|
}
|
|
265
284
|
.ir-last {
|
|
266
285
|
border-bottom: 0;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
widows: 3;
|
|
286
|
+
}
|
|
287
|
+
/* A label always travels with its value. */
|
|
288
|
+
.ir-cell p:first-child {
|
|
289
|
+
break-after: avoid;
|
|
290
|
+
page-break-after: avoid;
|
|
273
291
|
}
|
|
274
292
|
.ir-section {
|
|
275
293
|
break-after: avoid;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// "Reported by" company for an incident report: the report's `organization`
|
|
2
|
+
// id looked up through the existing org endpoint. Blank when unreadable.
|
|
3
|
+
export function useIncidentCompany(getId: () => string | undefined) {
|
|
4
|
+
const { getById: getOrgById } = useOrg();
|
|
5
|
+
const companyName = ref("");
|
|
6
|
+
watch(
|
|
7
|
+
getId,
|
|
8
|
+
async (id) => {
|
|
9
|
+
companyName.value = "";
|
|
10
|
+
if (!id) return;
|
|
11
|
+
try {
|
|
12
|
+
companyName.value = incidentCompanyName(await getOrgById(id));
|
|
13
|
+
} catch {
|
|
14
|
+
// Not readable by this viewer: show nothing rather than a wrong name.
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{ immediate: true }
|
|
18
|
+
);
|
|
19
|
+
return companyName;
|
|
20
|
+
}
|
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.118.
|
|
5
|
+
"version": "4.118.3",
|
|
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,7 @@
|
|
|
1
|
+
// The company that filed an incident report = the organisation stored on the
|
|
2
|
+
// report (the filer's own membership org: security agency, or the client's own
|
|
3
|
+
// staff org). Returns "" when it cannot be resolved so nothing wrong is printed.
|
|
4
|
+
export function incidentCompanyName(org: unknown): string {
|
|
5
|
+
const name = (org as { name?: unknown } | null | undefined)?.name;
|
|
6
|
+
return typeof name === "string" ? name.trim() : "";
|
|
7
|
+
}
|