@7365admin1/layer-common 4.118.0 → 4.118.2
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
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.118.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
## 4.118.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 83b3d6d: Incident Report PDF: the header now carries the report status (new optional
|
|
14
|
+
`status` prop on `IncidentInformationDownload`), cells use one consistent 1px
|
|
15
|
+
border with no half-row page splits, and the dates print as DD/MM/YYYY in
|
|
16
|
+
Singapore time. They printed as MM/DD/YYYY, and from a UTC server an 8 Oct date
|
|
17
|
+
showed as 07/10. No field removed; NRIC masking unchanged.
|
|
18
|
+
|
|
3
19
|
## 4.118.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
|
@@ -3123,11 +3139,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
|
|
|
3123
3139
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
3124
3140
|
own rule from two endpoints the console already calls, unprojected:
|
|
3125
3141
|
|
|
3126
|
-
|
|
3127
|
-
|
|
3142
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
3143
|
+
GET /api/roles/id/:role that membership's role document
|
|
3128
3144
|
|
|
3129
|
-
|
|
3130
|
-
|
|
3145
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
3146
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
3131
3147
|
|
|
3132
3148
|
`role.default` is the marker because it is the only property of a platform
|
|
3133
3149
|
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>
|
|
@@ -1,42 +1,48 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<!--
|
|
4
|
-
<
|
|
5
|
-
class="text-uppercase text-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
<div class="ir-doc px-5 pt-4">
|
|
3
|
+
<!-- Report header: title with the status inside it, so the two never float apart. -->
|
|
4
|
+
<div class="ir-title bg-error text-on-error">
|
|
5
|
+
<p class="text-uppercase text-h6 ma-0">Incident Report</p>
|
|
6
|
+
<p v-if="status" class="ir-status ma-0 mt-2" :class="`ir-status--${statusKey}`">
|
|
7
|
+
Status: {{ String(status).toUpperCase() }}
|
|
8
|
+
</p>
|
|
9
|
+
<p v-if="companyName" class="ma-0 mt-2 ir-company">
|
|
10
|
+
Reported by: {{ companyName }}
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
<v-row no-gutters class="ir-box mt-5">
|
|
14
|
+
<p class="ir-section w-100 text-h6 pa-2 text-center bg-warning text-on-warning">
|
|
14
15
|
Incident Information
|
|
15
16
|
</p>
|
|
16
17
|
|
|
17
|
-
<v-col cols="6" class="pa-3
|
|
18
|
+
<v-col cols="6" class="pa-3 ir-cell ir-e">
|
|
18
19
|
<p class="font-weight-bold">Site</p>
|
|
19
20
|
<p>{{ siteName || "-" }}</p>
|
|
20
21
|
</v-col>
|
|
21
|
-
<v-col cols="6" class="pa-3
|
|
22
|
+
<v-col cols="6" class="pa-3 ir-cell">
|
|
22
23
|
<p class="font-weight-bold">Reference</p>
|
|
23
24
|
<p>{{ incidentInformation?.siteInfo?.irNumber || "-" }}</p>
|
|
24
25
|
</v-col>
|
|
25
26
|
|
|
26
|
-
<v-col cols="
|
|
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
|
+
|
|
32
|
+
<v-col cols="6" class="pa-3 ir-cell ir-e">
|
|
27
33
|
<p class="font-weight-bold">Place of Incident</p>
|
|
28
34
|
<p>
|
|
29
35
|
{{ incidentInformation?.placeOfIncident?.incidentLocation || "-" }}
|
|
30
36
|
</p>
|
|
31
37
|
</v-col>
|
|
32
|
-
<v-col cols="6" class="pa-3
|
|
38
|
+
<v-col cols="6" class="pa-3 ir-cell">
|
|
33
39
|
<p class="font-weight-bold">Type of Incident</p>
|
|
34
40
|
<p>
|
|
35
41
|
{{ incidentInformation?.incidentTypeAndTime?.typeOfIncident || "-" }}
|
|
36
42
|
</p>
|
|
37
43
|
</v-col>
|
|
38
44
|
|
|
39
|
-
<v-col cols="6" class="pa-3
|
|
45
|
+
<v-col cols="6" class="pa-3 ir-cell ir-e">
|
|
40
46
|
<p class="font-weight-bold">Date of Incident</p>
|
|
41
47
|
<p>
|
|
42
48
|
{{
|
|
@@ -46,55 +52,58 @@
|
|
|
46
52
|
}}
|
|
47
53
|
</p>
|
|
48
54
|
</v-col>
|
|
49
|
-
<v-col cols="6" class="pa-3
|
|
55
|
+
<v-col cols="6" class="pa-3 ir-cell">
|
|
50
56
|
<p class="font-weight-bold">Incident Start Time</p>
|
|
51
57
|
<p>
|
|
52
58
|
{{ incidentInformation?.incidentTypeAndTime?.incidentStart || "-" }}
|
|
53
59
|
</p>
|
|
54
60
|
</v-col>
|
|
55
61
|
|
|
56
|
-
<v-col cols="6" class="pa-3
|
|
62
|
+
<v-col cols="6" class="pa-3 ir-cell ir-e">
|
|
57
63
|
<p class="font-weight-bold">Time of Response (From 1st Contact)</p>
|
|
58
64
|
<p>
|
|
59
65
|
{{ incidentInformation?.incidentTypeAndTime?.timeOfResponse || "-" }}
|
|
60
66
|
</p>
|
|
61
67
|
</v-col>
|
|
62
|
-
<v-col cols="6" class="pa-3
|
|
68
|
+
<v-col cols="6" class="pa-3 ir-cell">
|
|
63
69
|
<p class="font-weight-bold">Incident End Time</p>
|
|
64
70
|
<p>
|
|
65
71
|
{{ incidentInformation?.incidentTypeAndTime?.incidentEnd || "-" }}
|
|
66
72
|
</p>
|
|
67
73
|
</v-col>
|
|
68
74
|
|
|
69
|
-
<v-col cols="6" class="pa-3
|
|
75
|
+
<v-col cols="6" class="pa-3 ir-cell ir-e">
|
|
70
76
|
<p class="font-weight-bold">Date of Report Submission</p>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
<p>
|
|
78
|
+
{{
|
|
79
|
+
toLocalDate(incidentInformation?.submissionForm?.dateOfReport) ||
|
|
80
|
+
"NA"
|
|
81
|
+
}}
|
|
82
|
+
</p>
|
|
74
83
|
</v-col>
|
|
75
|
-
<v-col cols="6" class="pa-3
|
|
84
|
+
<v-col cols="6" class="pa-3 ir-cell">
|
|
76
85
|
<p class="font-weight-bold">Time of Report Submission</p>
|
|
77
86
|
<p>{{ incidentInformation?.submissionForm?.time || "-" }}</p>
|
|
78
87
|
</v-col>
|
|
79
88
|
|
|
80
|
-
<v-col cols="6" class="pa-3
|
|
89
|
+
<v-col cols="6" class="pa-3 ir-cell ir-e">
|
|
81
90
|
<p class="font-weight-bold">Report Submitted By</p>
|
|
82
91
|
<p>{{ incidentInformation?.siteInfo?.submittedBy || "-" }}</p>
|
|
83
92
|
</v-col>
|
|
84
|
-
<v-col cols="6" class="pa-3
|
|
93
|
+
<v-col cols="6" class="pa-3 ir-cell">
|
|
85
94
|
<p class="font-weight-bold">Designation</p>
|
|
86
95
|
<p>{{ incidentInformation?.siteInfo?.designation || "-" }}</p>
|
|
87
96
|
</v-col>
|
|
88
97
|
|
|
89
|
-
<v-col cols="4" class="pa-3
|
|
98
|
+
<v-col cols="4" class="pa-3 ir-cell ir-e">
|
|
90
99
|
<p class="font-weight-bold">Name of Informant/Complainant</p>
|
|
91
100
|
<p>{{ incidentInformation?.complaintInfo?.complainant || "-" }}</p>
|
|
92
101
|
</v-col>
|
|
93
|
-
<v-col cols="4" class="pa-3
|
|
102
|
+
<v-col cols="4" class="pa-3 ir-cell ir-e">
|
|
94
103
|
<p class="font-weight-bold">Contact No.</p>
|
|
95
104
|
<p>{{ incidentInformation?.complaintInfo?.contact || "-" }}</p>
|
|
96
105
|
</v-col>
|
|
97
|
-
<v-col cols="4" class="pa-3
|
|
106
|
+
<v-col cols="4" class="pa-3 ir-cell">
|
|
98
107
|
<p class="font-weight-bold">NRIC/WP No.</p>
|
|
99
108
|
<p>
|
|
100
109
|
{{
|
|
@@ -105,17 +114,17 @@
|
|
|
105
114
|
</p>
|
|
106
115
|
</v-col>
|
|
107
116
|
|
|
108
|
-
<v-col cols="4" class="pa-3
|
|
117
|
+
<v-col cols="4" class="pa-3 ir-cell ir-e">
|
|
109
118
|
<p class="font-weight-bold">
|
|
110
119
|
Name of Recipient of Complaint (1st Contact)
|
|
111
120
|
</p>
|
|
112
121
|
<p>{{ incidentInformation?.recipientOfComplaint?.recipient || "-" }}</p>
|
|
113
122
|
</v-col>
|
|
114
|
-
<v-col cols="4" class="pa-3
|
|
123
|
+
<v-col cols="4" class="pa-3 ir-cell ir-e">
|
|
115
124
|
<p class="font-weight-bold">Contact No.</p>
|
|
116
125
|
<p>{{ incidentInformation?.recipientOfComplaint?.contact || "-" }}</p>
|
|
117
126
|
</v-col>
|
|
118
|
-
<v-col cols="4" class="pa-3
|
|
127
|
+
<v-col cols="4" class="pa-3 ir-cell">
|
|
119
128
|
<p class="font-weight-bold">NRIC/WP No.</p>
|
|
120
129
|
<p>
|
|
121
130
|
{{
|
|
@@ -126,7 +135,7 @@
|
|
|
126
135
|
</p>
|
|
127
136
|
</v-col>
|
|
128
137
|
|
|
129
|
-
<v-col cols="12" class="pa-3
|
|
138
|
+
<v-col cols="12" class="pa-3 ir-cell">
|
|
130
139
|
<p class="font-weight-bold">Complaint Received through</p>
|
|
131
140
|
<p
|
|
132
141
|
v-if="
|
|
@@ -142,7 +151,7 @@
|
|
|
142
151
|
}}
|
|
143
152
|
</p>
|
|
144
153
|
</v-col>
|
|
145
|
-
<v-col cols="12" class="pa-3">
|
|
154
|
+
<v-col cols="12" class="pa-3 ir-cell ir-last">
|
|
146
155
|
<p class="font-weight-bold">Brief Description</p>
|
|
147
156
|
<p>
|
|
148
157
|
{{
|
|
@@ -151,7 +160,7 @@
|
|
|
151
160
|
</p>
|
|
152
161
|
</v-col>
|
|
153
162
|
</v-row>
|
|
154
|
-
</
|
|
163
|
+
</div>
|
|
155
164
|
</template>
|
|
156
165
|
|
|
157
166
|
<script lang="ts" setup>
|
|
@@ -162,8 +171,36 @@ const props = defineProps({
|
|
|
162
171
|
type: Object as PropType<Record<string, any> | null>,
|
|
163
172
|
required: true,
|
|
164
173
|
},
|
|
174
|
+
// Optional report status (pending/approved/rejected), printed inside the header.
|
|
175
|
+
status: {
|
|
176
|
+
type: String,
|
|
177
|
+
default: "",
|
|
178
|
+
},
|
|
179
|
+
// The report's `organization` id: the company that filed it.
|
|
180
|
+
organizationId: {
|
|
181
|
+
type: String,
|
|
182
|
+
default: "",
|
|
183
|
+
},
|
|
165
184
|
});
|
|
166
185
|
|
|
186
|
+
const { getById: getOrgById } = useOrg();
|
|
187
|
+
const companyName = ref("");
|
|
188
|
+
watch(
|
|
189
|
+
() => props.organizationId,
|
|
190
|
+
async (id) => {
|
|
191
|
+
companyName.value = "";
|
|
192
|
+
if (!id) return;
|
|
193
|
+
try {
|
|
194
|
+
companyName.value = incidentCompanyName(await getOrgById(id));
|
|
195
|
+
} catch {
|
|
196
|
+
// Not readable by this viewer: print nothing rather than a wrong name.
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{ immediate: true }
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const statusKey = computed(() => String(props.status || "").toLowerCase());
|
|
203
|
+
|
|
167
204
|
// utilities
|
|
168
205
|
const { getSiteById } = useSiteSettings();
|
|
169
206
|
const route = useRoute();
|
|
@@ -204,11 +241,16 @@ watch(
|
|
|
204
241
|
{ immediate: true }
|
|
205
242
|
);
|
|
206
243
|
|
|
244
|
+
// DD/MM/YYYY in Singapore time. The PDF is rendered on a server whose clock is
|
|
245
|
+
// UTC, so without the zone an 8 Oct (SGT) date prints as 07/10.
|
|
207
246
|
function toLocalDate(utcString: string) {
|
|
208
|
-
|
|
247
|
+
const d = new Date(utcString);
|
|
248
|
+
if (!utcString || isNaN(d.getTime())) return "";
|
|
249
|
+
return d.toLocaleDateString("en-GB", {
|
|
209
250
|
year: "numeric",
|
|
210
251
|
month: "2-digit",
|
|
211
252
|
day: "2-digit",
|
|
253
|
+
timeZone: "Asia/Singapore",
|
|
212
254
|
});
|
|
213
255
|
}
|
|
214
256
|
|
|
@@ -218,3 +260,52 @@ const maskNRIC = (value: any) => {
|
|
|
218
260
|
return value.replace(/.(?=.{3})/g, "*");
|
|
219
261
|
};
|
|
220
262
|
</script>
|
|
263
|
+
|
|
264
|
+
<style scoped>
|
|
265
|
+
/* One consistent 1px grey rule for every cell edge; rows never split across pages. */
|
|
266
|
+
.ir-title {
|
|
267
|
+
text-align: center;
|
|
268
|
+
padding: 10px;
|
|
269
|
+
}
|
|
270
|
+
.ir-status {
|
|
271
|
+
display: inline-block;
|
|
272
|
+
background: #fff;
|
|
273
|
+
color: #b26a00;
|
|
274
|
+
border: 1px solid currentColor;
|
|
275
|
+
border-radius: 4px;
|
|
276
|
+
padding: 2px 14px;
|
|
277
|
+
font-size: 0.95rem;
|
|
278
|
+
font-weight: 700;
|
|
279
|
+
letter-spacing: 0.03em;
|
|
280
|
+
}
|
|
281
|
+
.ir-company {
|
|
282
|
+
font-size: 1rem;
|
|
283
|
+
font-weight: 600;
|
|
284
|
+
}
|
|
285
|
+
.ir-status--approved { color: #2e7d32; }
|
|
286
|
+
.ir-status--rejected { color: #c62828; }
|
|
287
|
+
.ir-box {
|
|
288
|
+
border: 1px solid #8a8a8a;
|
|
289
|
+
}
|
|
290
|
+
.ir-cell {
|
|
291
|
+
border-bottom: 1px solid #8a8a8a;
|
|
292
|
+
break-inside: avoid;
|
|
293
|
+
page-break-inside: avoid;
|
|
294
|
+
}
|
|
295
|
+
.ir-e {
|
|
296
|
+
border-right: 1px solid #8a8a8a;
|
|
297
|
+
}
|
|
298
|
+
.ir-last {
|
|
299
|
+
border-bottom: 0;
|
|
300
|
+
/* The long free-text answer may flow onto the next page, but never leave
|
|
301
|
+
one or two stray lines behind. */
|
|
302
|
+
break-inside: auto;
|
|
303
|
+
page-break-inside: auto;
|
|
304
|
+
orphans: 3;
|
|
305
|
+
widows: 3;
|
|
306
|
+
}
|
|
307
|
+
.ir-section {
|
|
308
|
+
break-after: avoid;
|
|
309
|
+
border-bottom: 1px solid #8a8a8a;
|
|
310
|
+
}
|
|
311
|
+
</style>
|
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.2",
|
|
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
|
+
}
|