@7365admin1/layer-common 4.104.0 → 4.106.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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.106.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4740604: Online forms review: remarks are optional when approving and still required to reject or request a resubmission (same rule as iMA365).
8
+
9
+ ## 4.105.0
10
+
11
+ ### Minor Changes
12
+
13
+ - c43bf75: Site role pickers (Assign Role, Invite member, Reassign on delete) label organisation-wide roles "Name (organisation)"; site roles keep their plain name. Display only - the same role id is sent.
14
+
15
+ ## 4.104.1
16
+
17
+ ### Patch Changes
18
+
19
+ - 0112ec7: Service Providers: an unknown staff count (older row with no linked company) shows "-" instead of 0; View Members lists the company's active people on this site (same as Members > Service Providers) instead of one person's memberships.
20
+
3
21
  ## 4.104.0
4
22
 
5
23
  ### Minor Changes
@@ -374,7 +374,7 @@
374
374
  v-model="selectedRole"
375
375
  :items="pickerRoles"
376
376
  item-value="_id"
377
- :item-title="roleLabel"
377
+ :item-title="props.siteId ? sitePickerRoleLabel : roleLabel"
378
378
  density="comfortable"
379
379
  :rules="[requiredRule]"
380
380
  :error-messages="
@@ -428,6 +428,7 @@ import {
428
428
  limitedSites,
429
429
  withCurrentRole,
430
430
  roleLabel,
431
+ sitePickerRoleLabel,
431
432
  } from "../utils/role";
432
433
  import { formatLastActive } from "../utils/last-active";
433
434
  import { formatMemberDate, memberAccess, memberPerson } from "../utils/member-row";
@@ -593,11 +593,12 @@ watch(() => props.initialManagementValues, (saved) => {
593
593
  managementValues.value = { ...loaded, ...managementValues.value };
594
594
  }, { immediate: true });
595
595
 
596
- const remarksRequired = computed(() => !!selectedAction.value);
596
+ // Owner 2026-10-06: remarks are optional for Approve, required to reject or send back.
597
+ const remarksRequired = computed(() => !!selectedAction.value && selectedAction.value !== "approved");
597
598
 
598
599
  // Same wording as the iMA365 review screen, so office and phone read alike.
599
600
  const REVIEW_TEXT: Record<string, { remarks: string; confirm: string }> = {
600
- approved: { remarks: "Remarks (the resident sees this)", confirm: "Confirm approval" },
601
+ approved: { remarks: "Remarks - optional (the resident sees this)", confirm: "Confirm approval" },
601
602
  rejected: { remarks: "Reason for rejection (the resident sees this)", confirm: "Confirm rejection" },
602
603
  resubmission: { remarks: "Reason for resubmission (the resident sees this)", confirm: "Send back to resident" },
603
604
  };
@@ -605,8 +606,8 @@ const remarksLabel = computed(() => REVIEW_TEXT[selectedAction.value]?.remarks ?
605
606
  const confirmLabel = computed(() => REVIEW_TEXT[selectedAction.value]?.confirm ?? "Confirm");
606
607
  const reviewHint = computed(() =>
607
608
  selectedAction.value === "approved" && managementUseItems.value.length
608
- ? "Fill in the For Management Use Only box and your remarks, then confirm."
609
- : "Choose Approve, Request resubmission or Reject, add your remarks, then confirm.",
609
+ ? "Fill in the For Management Use Only box, add remarks if you wish, then confirm."
610
+ : "Choose Approve, Request resubmission or Reject. Remarks are needed to reject or send back. Then confirm.",
610
611
  );
611
612
 
612
613
  const managementComplete = computed(() => {
@@ -616,7 +617,7 @@ const managementComplete = computed(() => {
616
617
 
617
618
  const canConfirm = computed(() =>
618
619
  !!selectedAction.value &&
619
- adminRemarks.value.trim().length > 0 &&
620
+ (!remarksRequired.value || adminRemarks.value.trim().length > 0) &&
620
621
  managementComplete.value
621
622
  );
622
623
 
@@ -166,7 +166,7 @@
166
166
  <v-select
167
167
  v-model="memberReassignments[member._id]"
168
168
  :items="deletionPreview.replacementRoles"
169
- :item-title="roleLabel"
169
+ :item-title="props.siteId ? sitePickerRoleLabel : roleLabel"
170
170
  item-value="_id"
171
171
  label="Reassign role"
172
172
  density="comfortable"
@@ -206,7 +206,7 @@
206
206
 
207
207
  <script setup lang="ts">
208
208
  // Relative import: this layer's `utils/` is not auto-imported into consumers.
209
- import { roleLabel, safeReturnTo, showSiteField } from "../utils/role";
209
+ import { roleLabel, safeReturnTo, showSiteField, sitePickerRoleLabel } from "../utils/role";
210
210
  import { holdsPermission } from "../utils/permission-spellings";
211
211
  import {
212
212
  catalogueForOrg,
@@ -320,7 +320,7 @@
320
320
 
321
321
  <v-dialog v-model="viewDialog" max-width="514">
322
322
  <v-card class="screen-modal">
323
- <v-card-title>Members ({{ memberDialogCount }})</v-card-title>
323
+ <v-card-title>Members ({{ viewRow?.serviceProviderOrgId ? memberDialogCount : "-" }})</v-card-title>
324
324
 
325
325
  <div class="screen-modal__body sp-members-scroll">
326
326
  <v-table class="sp-members-table">
@@ -336,6 +336,11 @@
336
336
  Loading...
337
337
  </td>
338
338
  </tr>
339
+ <tr v-else-if="!viewRow?.serviceProviderOrgId">
340
+ <td colspan="2" class="text-center sp-members-note">
341
+ This provider was added without a linked company account, so its staff cannot be listed.
342
+ </td>
343
+ </tr>
339
344
  <tr v-else-if="!serviceProviderMembers.length">
340
345
  <td colspan="2" class="text-center sp-members-note">
341
346
  No members found.
@@ -1006,6 +1011,7 @@ const {
1006
1011
  createServiceProviderInvite,
1007
1012
  updateStatus: updateServiceProviderStatus,
1008
1013
  invite,
1014
+ getSiteMembers,
1009
1015
  } = useServiceProvider();
1010
1016
  const { getAll: getAllMembers, getAllByUserId } = useMember();
1011
1017
 
@@ -1377,7 +1383,8 @@ function setInviteSiteName(siteId: string) {
1377
1383
  }
1378
1384
 
1379
1385
  function formatMembersCell(value: unknown): string {
1380
- if (value == null || value === "") return "0";
1386
+ // null = an older row with no company recorded: unknown, not 0.
1387
+ if (value == null || value === "") return "-";
1381
1388
  const n = Number(value);
1382
1389
  if (!Number.isFinite(n)) return "0";
1383
1390
  return String(n);
@@ -1522,13 +1529,17 @@ async function openView(row: TableRow) {
1522
1529
 
1523
1530
  loadingMembers.value = true;
1524
1531
  try {
1525
- const data = await getAllByUserId(row.userId);
1526
-
1527
- serviceProviderMembers.value = (data.items ?? []).map((member: any) => ({
1528
- _id: member._id,
1529
- name: member.name || "-",
1530
- role: member.role || "-",
1531
- }));
1532
+ // The company's people on THIS site - the same list as Members > Service
1533
+ // Providers. It used to read one person's memberships (`row.userId`),
1534
+ // which a stored provider row does not even carry (owner report 2026-10-06).
1535
+ const data = await getSiteMembers(props.siteId);
1536
+ serviceProviderMembers.value = (data.items ?? [])
1537
+ .filter((member) => member.companyId === row.serviceProviderOrgId && member.status === "active")
1538
+ .map((member) => ({
1539
+ _id: String(member._id),
1540
+ name: member.name || member.email || "-",
1541
+ role: member.role || "-",
1542
+ }));
1532
1543
  } catch (error: any) {
1533
1544
  showMessage(errorConverter(error), "error");
1534
1545
  } finally {
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.104.0",
5
+ "version": "4.106.0",
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.",
@@ -16,7 +16,8 @@ export function inviteRoleOptions(
16
16
  .filter((role) => role && role._id)
17
17
  .map((role) => ({
18
18
  _id: String(role._id),
19
- title: String(role.name ?? ""),
19
+ // Organisation-wide roles read "Administrator (organisation)" (owner, 2026-10-06).
20
+ title: role.site ? String(role.name ?? "") : `${String(role.name ?? "")} (organisation)`,
20
21
  subtitle: role.site ? SITE_ROLE_LABEL : ORG_ROLE_LABEL,
21
22
  }));
22
23
  }
package/utils/role.ts CHANGED
@@ -22,6 +22,19 @@ export function roleLabel(role: { name?: unknown; siteName?: unknown; site?: unk
22
22
  return role?.site && site ? `${name} · ${site}` : name;
23
23
  }
24
24
 
25
+ /**
26
+ * A site-level role picker's label (owner, 2026-10-06): organisation-wide roles
27
+ * stay offered at a site but read "Administrator (organisation)", so nobody
28
+ * mistakes them for a role of this site. Site roles keep `roleLabel`. Display
29
+ * only - the picker still sends the same `_id`. A row the server did not send
30
+ * (`withCurrentRole`'s placeholder has no `type`) is left plain: we do not know
31
+ * whether it is organisation-wide.
32
+ */
33
+ export function sitePickerRoleLabel(role: { name?: unknown; siteName?: unknown; site?: unknown; type?: unknown } | null | undefined): string {
34
+ if (!role || role.site || role.type === undefined) return roleLabel(role);
35
+ return `${roleLabel(role)} (organisation)`;
36
+ }
37
+
25
38
  /**
26
39
  * The Assign Role picker's list, with the member's CURRENT role always in it.
27
40
  *