@7365admin1/layer-common 4.117.0 → 4.118.1

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.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 83b3d6d: Incident Report PDF: the header now carries the report status (new optional
8
+ `status` prop on `IncidentInformationDownload`), cells use one consistent 1px
9
+ border with no half-row page splits, and the dates print as DD/MM/YYYY in
10
+ Singapore time. They printed as MM/DD/YYYY, and from a UTC server an 8 Oct date
11
+ showed as 07/10. No field removed; NRIC masking unchanged.
12
+
13
+ ## 4.118.0
14
+
15
+ ### Minor Changes
16
+
17
+ - 42930fa: Robot screens: Add robot, Edit, Remove and the building-map editor show only for Seven365 Super Admin. Commands on a HERO Fleet Core robot show only for Seven365 Super Admin; other users see status and position (view only). Matches the server rule that Fleet Core and robot setup are Super Admin only.
18
+
3
19
  ## 4.117.0
4
20
 
5
21
  ### Minor Changes
@@ -3117,11 +3133,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
3117
3133
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
3118
3134
  own rule from two endpoints the console already calls, unprojected:
3119
3135
 
3120
- GET /api/members/user/:user/app/admin the Seven365 staff membership
3121
- GET /api/roles/id/:role that membership's role document
3136
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
3137
+ GET /api/roles/id/:role that membership's role document
3122
3138
 
3123
- owner = member.type === "admin" && role.type === "admin" && role.default === true
3124
- staff = member.type === "admin" && role.type === "admin"
3139
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
3140
+ staff = member.type === "admin" && role.type === "admin"
3125
3141
 
3126
3142
  `role.default` is the marker because it is the only property of a platform
3127
3143
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -1,42 +1,40 @@
1
1
  <template>
2
- <v-row no-gutters class="px-5 pt-4">
3
- <!-- Site Information -->
4
- <p
5
- class="text-uppercase text-center w-100 text-h6 bg-error text-on-error"
6
- style="padding: 10px"
7
- >
8
- Incident Report
9
- </p>
10
- <v-row no-gutters class="mt-5 border pb-5">
11
- <p
12
- class="w-100 text-h6 pa-2 text-center bg-warning text-on-warning"
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
+ </div>
10
+ <v-row no-gutters class="ir-box mt-5">
11
+ <p class="ir-section w-100 text-h6 pa-2 text-center bg-warning text-on-warning">
14
12
  Incident Information
15
13
  </p>
16
14
 
17
- <v-col cols="6" class="pa-3 border-b border-e">
15
+ <v-col cols="6" class="pa-3 ir-cell ir-e">
18
16
  <p class="font-weight-bold">Site</p>
19
17
  <p>{{ siteName || "-" }}</p>
20
18
  </v-col>
21
- <v-col cols="6" class="pa-3 border-b">
19
+ <v-col cols="6" class="pa-3 ir-cell">
22
20
  <p class="font-weight-bold">Reference</p>
23
21
  <p>{{ incidentInformation?.siteInfo?.irNumber || "-" }}</p>
24
22
  </v-col>
25
23
 
26
- <v-col cols="6" class="pa-3 border-b border-e">
24
+ <v-col cols="6" class="pa-3 ir-cell ir-e">
27
25
  <p class="font-weight-bold">Place of Incident</p>
28
26
  <p>
29
27
  {{ incidentInformation?.placeOfIncident?.incidentLocation || "-" }}
30
28
  </p>
31
29
  </v-col>
32
- <v-col cols="6" class="pa-3 border-b">
30
+ <v-col cols="6" class="pa-3 ir-cell">
33
31
  <p class="font-weight-bold">Type of Incident</p>
34
32
  <p>
35
33
  {{ incidentInformation?.incidentTypeAndTime?.typeOfIncident || "-" }}
36
34
  </p>
37
35
  </v-col>
38
36
 
39
- <v-col cols="6" class="pa-3 border-b border-e">
37
+ <v-col cols="6" class="pa-3 ir-cell ir-e">
40
38
  <p class="font-weight-bold">Date of Incident</p>
41
39
  <p>
42
40
  {{
@@ -46,55 +44,58 @@
46
44
  }}
47
45
  </p>
48
46
  </v-col>
49
- <v-col cols="6" class="pa-3 border-b">
47
+ <v-col cols="6" class="pa-3 ir-cell">
50
48
  <p class="font-weight-bold">Incident Start Time</p>
51
49
  <p>
52
50
  {{ incidentInformation?.incidentTypeAndTime?.incidentStart || "-" }}
53
51
  </p>
54
52
  </v-col>
55
53
 
56
- <v-col cols="6" class="pa-3 border-b border-e">
54
+ <v-col cols="6" class="pa-3 ir-cell ir-e">
57
55
  <p class="font-weight-bold">Time of Response (From 1st Contact)</p>
58
56
  <p>
59
57
  {{ incidentInformation?.incidentTypeAndTime?.timeOfResponse || "-" }}
60
58
  </p>
61
59
  </v-col>
62
- <v-col cols="6" class="pa-3 border-b">
60
+ <v-col cols="6" class="pa-3 ir-cell">
63
61
  <p class="font-weight-bold">Incident End Time</p>
64
62
  <p>
65
63
  {{ incidentInformation?.incidentTypeAndTime?.incidentEnd || "-" }}
66
64
  </p>
67
65
  </v-col>
68
66
 
69
- <v-col cols="6" class="pa-3 border-b border-e">
67
+ <v-col cols="6" class="pa-3 ir-cell ir-e">
70
68
  <p class="font-weight-bold">Date of Report Submission</p>
71
- {{
72
- toLocalDate(incidentInformation?.submissionForm?.dateOfReport) || "NA"
73
- }}
69
+ <p>
70
+ {{
71
+ toLocalDate(incidentInformation?.submissionForm?.dateOfReport) ||
72
+ "NA"
73
+ }}
74
+ </p>
74
75
  </v-col>
75
- <v-col cols="6" class="pa-3 border-b">
76
+ <v-col cols="6" class="pa-3 ir-cell">
76
77
  <p class="font-weight-bold">Time of Report Submission</p>
77
78
  <p>{{ incidentInformation?.submissionForm?.time || "-" }}</p>
78
79
  </v-col>
79
80
 
80
- <v-col cols="6" class="pa-3 border-b border-e">
81
+ <v-col cols="6" class="pa-3 ir-cell ir-e">
81
82
  <p class="font-weight-bold">Report Submitted By</p>
82
83
  <p>{{ incidentInformation?.siteInfo?.submittedBy || "-" }}</p>
83
84
  </v-col>
84
- <v-col cols="6" class="pa-3 border-b">
85
+ <v-col cols="6" class="pa-3 ir-cell">
85
86
  <p class="font-weight-bold">Designation</p>
86
87
  <p>{{ incidentInformation?.siteInfo?.designation || "-" }}</p>
87
88
  </v-col>
88
89
 
89
- <v-col cols="4" class="pa-3 border-b border-e">
90
+ <v-col cols="4" class="pa-3 ir-cell ir-e">
90
91
  <p class="font-weight-bold">Name of Informant/Complainant</p>
91
92
  <p>{{ incidentInformation?.complaintInfo?.complainant || "-" }}</p>
92
93
  </v-col>
93
- <v-col cols="4" class="pa-3 border-b border-e">
94
+ <v-col cols="4" class="pa-3 ir-cell ir-e">
94
95
  <p class="font-weight-bold">Contact No.</p>
95
96
  <p>{{ incidentInformation?.complaintInfo?.contact || "-" }}</p>
96
97
  </v-col>
97
- <v-col cols="4" class="pa-3 border-b">
98
+ <v-col cols="4" class="pa-3 ir-cell">
98
99
  <p class="font-weight-bold">NRIC/WP No.</p>
99
100
  <p>
100
101
  {{
@@ -105,17 +106,17 @@
105
106
  </p>
106
107
  </v-col>
107
108
 
108
- <v-col cols="4" class="pa-3 border-b border-e">
109
+ <v-col cols="4" class="pa-3 ir-cell ir-e">
109
110
  <p class="font-weight-bold">
110
111
  Name of Recipient of Complaint (1st Contact)
111
112
  </p>
112
113
  <p>{{ incidentInformation?.recipientOfComplaint?.recipient || "-" }}</p>
113
114
  </v-col>
114
- <v-col cols="4" class="pa-3 border-b border-e">
115
+ <v-col cols="4" class="pa-3 ir-cell ir-e">
115
116
  <p class="font-weight-bold">Contact No.</p>
116
117
  <p>{{ incidentInformation?.recipientOfComplaint?.contact || "-" }}</p>
117
118
  </v-col>
118
- <v-col cols="4" class="pa-3 border-b">
119
+ <v-col cols="4" class="pa-3 ir-cell">
119
120
  <p class="font-weight-bold">NRIC/WP No.</p>
120
121
  <p>
121
122
  {{
@@ -126,7 +127,7 @@
126
127
  </p>
127
128
  </v-col>
128
129
 
129
- <v-col cols="12" class="pa-3 border-b border-e">
130
+ <v-col cols="12" class="pa-3 ir-cell">
130
131
  <p class="font-weight-bold">Complaint Received through</p>
131
132
  <p
132
133
  v-if="
@@ -142,7 +143,7 @@
142
143
  }}
143
144
  </p>
144
145
  </v-col>
145
- <v-col cols="12" class="pa-3">
146
+ <v-col cols="12" class="pa-3 ir-cell ir-last">
146
147
  <p class="font-weight-bold">Brief Description</p>
147
148
  <p>
148
149
  {{
@@ -151,7 +152,7 @@
151
152
  </p>
152
153
  </v-col>
153
154
  </v-row>
154
- </v-row>
155
+ </div>
155
156
  </template>
156
157
 
157
158
  <script lang="ts" setup>
@@ -162,8 +163,15 @@ const props = defineProps({
162
163
  type: Object as PropType<Record<string, any> | null>,
163
164
  required: true,
164
165
  },
166
+ // Optional report status (pending/approved/rejected), printed inside the header.
167
+ status: {
168
+ type: String,
169
+ default: "",
170
+ },
165
171
  });
166
172
 
173
+ const statusKey = computed(() => String(props.status || "").toLowerCase());
174
+
167
175
  // utilities
168
176
  const { getSiteById } = useSiteSettings();
169
177
  const route = useRoute();
@@ -204,11 +212,16 @@ watch(
204
212
  { immediate: true }
205
213
  );
206
214
 
215
+ // DD/MM/YYYY in Singapore time. The PDF is rendered on a server whose clock is
216
+ // UTC, so without the zone an 8 Oct (SGT) date prints as 07/10.
207
217
  function toLocalDate(utcString: string) {
208
- return new Date(utcString).toLocaleString("en-US", {
218
+ const d = new Date(utcString);
219
+ if (!utcString || isNaN(d.getTime())) return "";
220
+ return d.toLocaleDateString("en-GB", {
209
221
  year: "numeric",
210
222
  month: "2-digit",
211
223
  day: "2-digit",
224
+ timeZone: "Asia/Singapore",
212
225
  });
213
226
  }
214
227
 
@@ -218,3 +231,48 @@ const maskNRIC = (value: any) => {
218
231
  return value.replace(/.(?=.{3})/g, "*");
219
232
  };
220
233
  </script>
234
+
235
+ <style scoped>
236
+ /* One consistent 1px grey rule for every cell edge; rows never split across pages. */
237
+ .ir-title {
238
+ text-align: center;
239
+ padding: 10px;
240
+ }
241
+ .ir-status {
242
+ display: inline-block;
243
+ background: #fff;
244
+ color: #b26a00;
245
+ border: 1px solid currentColor;
246
+ border-radius: 4px;
247
+ padding: 2px 14px;
248
+ font-size: 0.95rem;
249
+ font-weight: 700;
250
+ letter-spacing: 0.03em;
251
+ }
252
+ .ir-status--approved { color: #2e7d32; }
253
+ .ir-status--rejected { color: #c62828; }
254
+ .ir-box {
255
+ border: 1px solid #8a8a8a;
256
+ }
257
+ .ir-cell {
258
+ border-bottom: 1px solid #8a8a8a;
259
+ break-inside: avoid;
260
+ page-break-inside: avoid;
261
+ }
262
+ .ir-e {
263
+ border-right: 1px solid #8a8a8a;
264
+ }
265
+ .ir-last {
266
+ 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;
273
+ }
274
+ .ir-section {
275
+ break-after: avoid;
276
+ border-bottom: 1px solid #8a8a8a;
277
+ }
278
+ </style>
@@ -138,7 +138,7 @@
138
138
  <div v-if="commands.length" class="hero-detail__commands">
139
139
  <RobotCommandBar
140
140
  :capabilities="caps"
141
- :can-command="canCommand"
141
+ :can-command="cmdOk"
142
142
  :online="live?.ok === true && live.online === true"
143
143
  :mode="statusMode"
144
144
  :type="robotType"
@@ -151,7 +151,7 @@
151
151
  {{ commandResult.text }}
152
152
  </div>
153
153
  </div>
154
- <RobotViewOnlyNotice v-if="!canCommand" />
154
+ <RobotViewOnlyNotice v-if="!cmdOk" />
155
155
  </section>
156
156
  </div>
157
157
 
@@ -369,11 +369,13 @@ const props = withDefaults(
369
369
  canEdit?: boolean;
370
370
  canCommand?: boolean;
371
371
  canDelete?: boolean;
372
+ /** Seven365 Super Admin: the only one who may command a HERO Fleet Core robot (owner, 12 Oct 2026). */
373
+ superAdmin?: boolean;
372
374
  /** The robot types this app presents - the edit dialog shows Type only with both. */
373
375
  types?: Array<"cleaning" | "security">;
374
376
  tickMs?: number;
375
377
  }>(),
376
- { backLabel: "", canEdit: false, canCommand: false, canDelete: false, types: () => [], tickMs: 1200 },
378
+ { backLabel: "", canEdit: false, canCommand: false, canDelete: false, superAdmin: false, types: () => [], tickMs: 1200 },
377
379
  );
378
380
  const emit = defineEmits<{
379
381
  back: [];
@@ -398,6 +400,7 @@ const {
398
400
  } = useRobots();
399
401
 
400
402
  const record = ref<Record<string, any> | null>(null);
403
+ const cmdOk = computed(() => props.canCommand && (props.superAdmin || record.value?.brand !== "fleet-core"));
401
404
  const topEl = ref<HTMLElement | null>(null);
402
405
  const brands = ref<Array<Record<string, any>>>([]);
403
406
  const error = ref("");
@@ -478,7 +481,7 @@ const makerApp = computed(() => (live.value?.ok ? live.value.profile?.makerApp :
478
481
 
479
482
  const online = computed(() => live.value?.ok === true && live.value.online === true);
480
483
  const commands = computed(() =>
481
- visibleCommands({ capabilities: caps.value, canCommand: props.canCommand, online: online.value, mode: statusMode.value, type: robotType.value, resumable: resumable.value }),
484
+ visibleCommands({ capabilities: caps.value, canCommand: cmdOk.value, online: online.value, mode: statusMode.value, type: robotType.value, resumable: resumable.value }),
482
485
  );
483
486
 
484
487
  /* ---- the plan ---- */
@@ -506,7 +509,7 @@ watch(
506
509
  const readout = computed(() => `${positionText(live.value, caps.value, now.value)} · ${speedText(prevPose.value, nextPose.value, statusMode.value)}`);
507
510
 
508
511
  const spot = ref<Array<{ x: number; y: number }>>([]);
509
- const spotOn = computed(() => showSpotClean({ canCommand: props.canCommand, capabilities: caps.value, online: online.value, type: robotType.value }));
512
+ const spotOn = computed(() => showSpotClean({ canCommand: cmdOk.value, capabilities: caps.value, online: online.value, type: robotType.value }));
510
513
 
511
514
  // The route drawn on the plan: the shown map's first route, else the first with placed checkpoints (the rest are listed by name).
512
515
  const route = computed<TPatrolRouteView | null>(() =>
@@ -839,9 +842,9 @@ function onPick(p: TSavedPoint) {
839
842
  const livePanel = ref<{ show(): void } | null>(null);
840
843
  const hasCamera = computed(() => caps.value?.liveVideo === true || canDrive.value);
841
844
  const driveOpen = ref(false);
842
- const canDrive = computed(() => props.canCommand && caps.value?.manualDrive === true && online.value);
845
+ const canDrive = computed(() => cmdOk.value && caps.value?.manualDrive === true && online.value);
843
846
  const canResolve = computed(
844
- () => props.canCommand && caps.value?.resolveAlerts === true && faultBadge(fault.value) === "ALARM" && typeof fault.value?.code === "string",
847
+ () => cmdOk.value && caps.value?.resolveAlerts === true && faultBadge(fault.value) === "ALARM" && typeof fault.value?.code === "string",
845
848
  );
846
849
  const resolving = ref(false);
847
850
  async function resolveFault() {
@@ -26,9 +26,10 @@
26
26
  :key="robotId"
27
27
  :robot-id="robotId"
28
28
  :back-label="siteName || names.siteName ? `${siteName || names.siteName} fleet` : ''"
29
- :can-edit="canEdit"
29
+ :can-edit="canEdit && superAdmin"
30
30
  :can-command="canCommand"
31
- :can-delete="canDelete"
31
+ :super-admin="superAdmin"
32
+ :can-delete="canDelete && superAdmin"
32
33
  :types="types"
33
34
  @back="emit('back')"
34
35
  @removed="onRemoved"
@@ -42,7 +43,7 @@
42
43
  :site-name="siteName"
43
44
  :client-name="clientName"
44
45
  :type="type"
45
- :can-edit-map="canSetup && canEdit"
46
+ :can-edit-map="canSetup && canEdit && superAdmin"
46
47
  :back-to-network="backToNetwork"
47
48
  @open="(id) => emit('open', id)"
48
49
  @network="emit('network')"
@@ -57,7 +58,7 @@
57
58
  :client-name="clientName"
58
59
  :type="type"
59
60
  :can-command="canCommand"
60
- :can-edit-map="canSetup && canEdit"
61
+ :can-edit-map="canSetup && canEdit && superAdmin"
61
62
  :back-to-network="backToNetwork"
62
63
  @open="(id) => emit('open', id)"
63
64
  @network="emit('network')"
@@ -90,7 +91,7 @@
90
91
  :site-name="siteName"
91
92
  :client-name="clientName"
92
93
  :type="type"
93
- :can-setup="canSetup"
94
+ :can-setup="canSetup && superAdmin"
94
95
  :back-to-network="backToNetwork"
95
96
  :reload-key="reloadKey"
96
97
  @open="(r) => emit('open', String(r._id))"
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.117.0",
5
+ "version": "4.118.1",
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.",