@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 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
- GET /api/members/user/:user/app/admin the Seven365 staff membership
3137
- GET /api/roles/id/:role that membership's role document
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
- owner = member.type === "admin" && role.type === "admin" && role.default === true
3140
- staff = member.type === "admin" && role.type === "admin"
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
- /* The long free-text answer may flow onto the next page, but never leave
268
- one or two stray lines behind. */
269
- break-inside: auto;
270
- page-break-inside: auto;
271
- orphans: 3;
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.1",
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
+ }