@7365admin1/layer-common 3.2.1-staging.74 → 3.2.1-staging.75

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.
@@ -0,0 +1,28 @@
1
+ ---
2
+ "@7365admin1/layer-common": minor
3
+ ---
4
+
5
+ Make the Pending tab's invitation actions reachable, and say which state an invitation is really in
6
+
7
+ The row menu in the service-provider list was drawn only for provider rows, so a
8
+ pending invitation offered no actions at all — including the cancel that was
9
+ already wired up behind it. Invitation rows now carry Cancel, Resend and Delete,
10
+ each behind a confirmation that names the company and the site, and Resend is
11
+ disabled with the reason while Seven365 has not approved the invitation yet.
12
+
13
+ Every invitation also used to read "Pending" whatever had happened to it. Rows
14
+ now show the state they are actually in — Waiting for Seven365, Waiting for the
15
+ provider, Not approved, Declined, Cancelled, Expired — in those words, with
16
+ Seven365's reason shown underneath a rejected one.
17
+
18
+ Also here: a new ServiceProviderInvitationPrompt, which is how an existing
19
+ provider answers an invitation from inside the app they already use, with no
20
+ sign-up and no one-time code.
21
+
22
+ Three contrast fixes found while rendering these screens: the list's tab strip
23
+ was a hardcoded light grey that turned into a white slab in dark mode and took
24
+ the tab labels with it; the service type column read the raw stored slug
25
+ ("security_agency") instead of "Security"; and `on-warning`, `on-info` and
26
+ dark's `on-error` are now set explicitly, because Vuetify derives white for them
27
+ and that measures 2.37:1, 3.12:1 and 3.60:1 on those fills — every filled
28
+ warning, info and error chip, alert and button in the product.
@@ -0,0 +1,150 @@
1
+ <template>
2
+ <v-card
3
+ v-if="invitation"
4
+ width="100%"
5
+ rounded="lg"
6
+ variant="outlined"
7
+ border="thin"
8
+ class="sp-invite-prompt"
9
+ >
10
+ <v-card-item>
11
+ <template #prepend>
12
+ <v-avatar color="primary" variant="tonal" size="44">
13
+ <v-icon>mdi-office-building-marker-outline</v-icon>
14
+ </v-avatar>
15
+ </template>
16
+ <v-card-title class="text-wrap sp-invite-prompt-title">
17
+ {{ invitation.orgName }} has invited you to {{ invitation.siteName }}
18
+ </v-card-title>
19
+ <v-card-subtitle class="text-wrap">
20
+ Approved by Seven365 · {{ expiryLine }}
21
+ </v-card-subtitle>
22
+ </v-card-item>
23
+
24
+ <v-card-text class="text-body-2">
25
+ {{ invitation.orgName }} would like your company to provide services at
26
+ <strong>{{ invitation.siteName }}</strong
27
+ >. If you accept, this site will be added to your list of sites.
28
+ </v-card-text>
29
+
30
+ <v-card-actions class="px-4 pb-4">
31
+ <v-btn
32
+ variant="flat"
33
+ color="primary"
34
+ class="text-none"
35
+ :loading="working === 'accept'"
36
+ :disabled="Boolean(working) || !open"
37
+ @click="accept"
38
+ >
39
+ Accept
40
+ </v-btn>
41
+ <v-btn
42
+ variant="outlined"
43
+ class="text-none ml-2"
44
+ :loading="working === 'decline'"
45
+ :disabled="Boolean(working) || !open"
46
+ @click="decline"
47
+ >
48
+ Decline
49
+ </v-btn>
50
+ <v-spacer />
51
+ <v-chip
52
+ v-if="!open"
53
+ size="small"
54
+ variant="tonal"
55
+ class="text-none"
56
+ :color="invitation.status === 'complete' ? 'success' : undefined"
57
+ >
58
+ {{ invitation.statusLabel }}
59
+ </v-chip>
60
+ </v-card-actions>
61
+ </v-card>
62
+
63
+ <v-card v-else-if="error" width="100%" rounded="lg" variant="outlined" border="thin">
64
+ <v-card-text class="text-body-2">{{ error }}</v-card-text>
65
+ </v-card>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ /**
70
+ * The invitation as the service provider sees it — inside the app they already
71
+ * use, from the session they already have.
72
+ *
73
+ * There is deliberately no sign-up and no one-time code anywhere on this
74
+ * screen: a company that already exists on iService365 never creates a second
75
+ * account. The email they receive only carries them here.
76
+ *
77
+ * An invitation that has not been approved by Seven365 is not readable at all —
78
+ * the server answers "not found" — so this component cannot show one.
79
+ */
80
+ const props = defineProps({
81
+ invitationId: { type: String, required: true },
82
+ });
83
+
84
+ const emit = defineEmits<{ (e: "answered", status: string): void }>();
85
+
86
+ const { getInvitation, declineInvitation } = useServiceProviderInvitation();
87
+ const { addViaInvite } = useCustomerSite();
88
+
89
+ const invitation = ref<any>(null);
90
+ const error = ref("");
91
+ const working = ref("");
92
+
93
+ const open = computed(() => invitation.value?.status === "pending");
94
+
95
+ const expiryLine = computed(() => {
96
+ const expires = invitation.value?.expireAt;
97
+ if (!expires) return "";
98
+ const days = Math.max(
99
+ 0,
100
+ Math.ceil((new Date(expires).getTime() - Date.now()) / 86400000),
101
+ );
102
+ if (!open.value) return "no longer waiting for an answer";
103
+ return days <= 1 ? "expires within a day" : `expires in ${days} days`;
104
+ });
105
+
106
+ async function load() {
107
+ try {
108
+ invitation.value = await getInvitation(props.invitationId);
109
+ } catch (err: any) {
110
+ error.value =
111
+ err?.data?.message || "This invitation is no longer available.";
112
+ }
113
+ }
114
+
115
+ async function accept() {
116
+ working.value = "accept";
117
+ try {
118
+ await addViaInvite(props.invitationId);
119
+ await load();
120
+ emit("answered", "complete");
121
+ } catch (err: any) {
122
+ error.value = err?.data?.message || "Could not accept this invitation.";
123
+ } finally {
124
+ working.value = "";
125
+ }
126
+ }
127
+
128
+ async function decline() {
129
+ working.value = "decline";
130
+ try {
131
+ await declineInvitation(props.invitationId);
132
+ await load();
133
+ emit("answered", "declined");
134
+ } catch (err: any) {
135
+ error.value = err?.data?.message || "Could not decline this invitation.";
136
+ } finally {
137
+ working.value = "";
138
+ }
139
+ }
140
+
141
+ await load();
142
+ </script>
143
+
144
+ <style scoped>
145
+ .sp-invite-prompt-title {
146
+ font-size: 1.05rem;
147
+ font-weight: 600;
148
+ line-height: 1.35;
149
+ }
150
+ </style>
@@ -65,9 +65,12 @@
65
65
  :loading="loading"
66
66
  >
67
67
  <div class="sp-table-header">
68
+ <!-- No fixed colour: `grey-lighten-4` is a light-mode grey in both
69
+ themes, which is what made this strip a white slab in dark
70
+ mode. `transparent` lets the themed .sp-table-header show. -->
68
71
  <v-toolbar
69
72
  density="compact"
70
- color="grey-lighten-4"
73
+ color="transparent"
71
74
  flat
72
75
  class="sp-toolbar-top px-2"
73
76
  >
@@ -137,17 +140,68 @@
137
140
  <span class="text-body-2">{{ item.members }}</span>
138
141
  </template>
139
142
  <template #item.status="{ item }">
143
+ <!--
144
+ Invitation rows use a FILLED chip, not a tonal one. A tonal chip
145
+ paints the status colour as TEXT, and measured on white the
146
+ warning and info tokens come out at 2.4:1 and 3.1:1 — under the
147
+ 4.5:1 a 12px label needs. Filled puts the colour in the
148
+ background and lets Vuetify pick a readable foreground for it.
149
+ -->
140
150
  <v-chip
141
- :color="statusChipColor(item.status)"
151
+ :color="statusChipColor(item.status, item.statusRaw)"
142
152
  size="small"
143
- variant="tonal"
153
+ :variant="item.source === 'invite' ? 'flat' : 'tonal'"
144
154
  class="text-none"
145
155
  >
146
156
  {{ item.status }}
147
157
  </v-chip>
158
+ <div
159
+ v-if="item.rejectionReason"
160
+ class="text-caption text-medium-emphasis mt-1 sp-reject-reason"
161
+ >
162
+ Seven365 said: {{ item.rejectionReason }}
163
+ </div>
148
164
  </template>
149
165
  <template #item.actions="{ item }">
150
- <v-menu v-if="item.source === 'provider'" location="bottom end">
166
+ <!--
167
+ Invitation rows. The menu used to be drawn only for
168
+ `source === 'provider'`, so a pending invitation showed no
169
+ actions at all and the cancel already wired up behind it was
170
+ unreachable.
171
+ -->
172
+ <v-menu v-if="item.source === 'invite'" location="bottom end">
173
+ <template #activator="{ props: menuProps }">
174
+ <v-btn
175
+ v-bind="menuProps"
176
+ icon
177
+ variant="text"
178
+ density="comfortable"
179
+ aria-label="Invitation actions"
180
+ >
181
+ <v-icon>mdi-dots-vertical</v-icon>
182
+ </v-btn>
183
+ </template>
184
+ <v-list density="compact" min-width="220">
185
+ <v-list-item
186
+ title="Cancel invitation"
187
+ :disabled="!canCancelInvite(item)"
188
+ :subtitle="canCancelInvite(item) ? undefined : 'Only while it is still waiting.'"
189
+ @click="openInviteAction(item, 'cancel')"
190
+ />
191
+ <v-list-item
192
+ title="Resend invitation"
193
+ :disabled="!canResendInvite(item)"
194
+ :subtitle="resendHint(item)"
195
+ @click="openInviteAction(item, 'resend')"
196
+ />
197
+ <v-list-item
198
+ title="Delete invitation"
199
+ base-color="error"
200
+ @click="openInviteAction(item, 'delete')"
201
+ />
202
+ </v-list>
203
+ </v-menu>
204
+ <v-menu v-else-if="item.source === 'provider'" location="bottom end">
151
205
  <template #activator="{ props: menuProps }">
152
206
  <v-btn
153
207
  v-bind="menuProps"
@@ -284,6 +338,56 @@
284
338
  </v-card>
285
339
  </v-dialog>
286
340
 
341
+ <!-- Cancel / Resend / Delete an invitation. Same shape as the provider
342
+ dialog above so the two read as one product. -->
343
+ <v-dialog v-model="inviteDialog" max-width="728" persistent>
344
+ <v-card rounded="lg" class="sp-status-card">
345
+ <v-card-title class="sp-status-title">
346
+ {{ inviteDialogTitle }}
347
+ </v-card-title>
348
+
349
+ <v-card-text class="sp-status-body">
350
+ <div class="sp-status-question">
351
+ {{ inviteDialogQuestion }}
352
+ </div>
353
+ <div class="sp-status-copy">
354
+ {{ inviteDialogCopy }}
355
+ </div>
356
+ </v-card-text>
357
+
358
+ <v-divider />
359
+
360
+ <v-card-actions class="pa-0">
361
+ <v-row no-gutters class="fill-height">
362
+ <v-col cols="6">
363
+ <v-btn
364
+ block
365
+ variant="flat"
366
+ height="82"
367
+ class="text-none sp-status-cancel"
368
+ :disabled="inviteWorking"
369
+ @click="closeInviteDialog"
370
+ >
371
+ Keep it
372
+ </v-btn>
373
+ </v-col>
374
+ <v-col cols="6">
375
+ <v-btn
376
+ block
377
+ variant="flat"
378
+ height="82"
379
+ class="text-none sp-status-confirm"
380
+ :loading="inviteWorking"
381
+ @click="confirmInviteAction"
382
+ >
383
+ {{ inviteConfirmLabel }}
384
+ </v-btn>
385
+ </v-col>
386
+ </v-row>
387
+ </v-card-actions>
388
+ </v-card>
389
+ </v-dialog>
390
+
287
391
  <v-dialog v-model="createDialog" width="500" persistent>
288
392
  <v-card width="100%">
289
393
  <v-toolbar>
@@ -582,6 +686,11 @@ type TableRow = {
582
686
  members: string;
583
687
  status: string;
584
688
  source: "provider" | "invite";
689
+ /** the stored status, for deciding which actions a row may offer */
690
+ statusRaw?: string;
691
+ /** shown under a "Not approved" row, in Seven365's own words */
692
+ rejectionReason?: string;
693
+ siteName?: string;
585
694
  };
586
695
 
587
696
  type ServiceProviderMemberRow = {
@@ -739,6 +848,13 @@ const { getAll: getAllMembers, getAllByUserId } = useMember();
739
848
  const { getVerifications, cancelUserInvitation, getVerificationByEmail } =
740
849
  useVerification();
741
850
 
851
+ const {
852
+ getInvitations,
853
+ cancelInvitation,
854
+ resendInvitation,
855
+ deleteInvitation,
856
+ } = useServiceProviderInvitation();
857
+
742
858
  const existingAccount = ref<any>(null);
743
859
  const checkingExistingAccount = ref(false);
744
860
  // const { getRoles } = useRole();
@@ -877,6 +993,109 @@ function canViewMembers(row: TableRow) {
877
993
  return row.source === "provider";
878
994
  }
879
995
 
996
+ // --- invitation actions (Cancel / Resend / Delete) ---------------------------
997
+ //
998
+ // Each one confirms first, and each one leaves an audit line on the server —
999
+ // who did it, when, and what. Delete is a SOFT delete: the row leaves this
1000
+ // list, Seven365 keeps the record and its history.
1001
+
1002
+ type InviteAction = "cancel" | "resend" | "delete";
1003
+
1004
+ const inviteDialog = ref(false);
1005
+ const inviteWorking = ref(false);
1006
+ const inviteActionRow = ref<TableRow | null>(null);
1007
+ const inviteAction = ref<InviteAction>("cancel");
1008
+
1009
+ function canCancelInvite(row: TableRow) {
1010
+ return ["awaiting-approval", "pending"].includes(row.statusRaw ?? "");
1011
+ }
1012
+
1013
+ function canResendInvite(row: TableRow) {
1014
+ return (row.statusRaw ?? "") === "pending";
1015
+ }
1016
+
1017
+ function resendHint(row: TableRow) {
1018
+ if (canResendInvite(row)) return undefined;
1019
+ return (row.statusRaw ?? "") === "awaiting-approval"
1020
+ ? "You can resend once Seven365 has approved it."
1021
+ : "Only an approved invitation can be sent again.";
1022
+ }
1023
+
1024
+ const inviteDialogTitle = computed(() => {
1025
+ if (inviteAction.value === "resend") return "Resend Invitation";
1026
+ if (inviteAction.value === "delete") return "Delete Invitation";
1027
+ return "Cancel Invitation";
1028
+ });
1029
+
1030
+ const inviteDialogQuestion = computed(() => {
1031
+ const row = inviteActionRow.value;
1032
+ const who = row?.companyName && row.companyName !== "-" ? row.companyName : row?.email;
1033
+ const site = row?.siteName ? ` for ${row.siteName}` : "";
1034
+
1035
+ if (inviteAction.value === "resend") {
1036
+ return `Send this invitation again to ${row?.email}?`;
1037
+ }
1038
+ if (inviteAction.value === "delete") {
1039
+ return "Delete this invitation permanently?";
1040
+ }
1041
+ return `Cancel this invitation to ${who}${site}?`;
1042
+ });
1043
+
1044
+ const inviteDialogCopy = computed(() => {
1045
+ if (inviteAction.value === "resend") {
1046
+ return "They will get a fresh message and another 3 days to accept. This does not create a second invitation.";
1047
+ }
1048
+ if (inviteAction.value === "delete") {
1049
+ return "It will disappear from this list. Seven365 keeps the record of who sent it and what happened to it.";
1050
+ }
1051
+ return "They will not be able to accept it. You can send a new one later.";
1052
+ });
1053
+
1054
+ const inviteConfirmLabel = computed(() => {
1055
+ if (inviteAction.value === "resend") return "Resend";
1056
+ if (inviteAction.value === "delete") return "Delete";
1057
+ return "Cancel invitation";
1058
+ });
1059
+
1060
+ function openInviteAction(row: TableRow, action: InviteAction) {
1061
+ if (action === "cancel" && !canCancelInvite(row)) return;
1062
+ if (action === "resend" && !canResendInvite(row)) return;
1063
+ inviteActionRow.value = row;
1064
+ inviteAction.value = action;
1065
+ inviteDialog.value = true;
1066
+ }
1067
+
1068
+ function closeInviteDialog() {
1069
+ if (inviteWorking.value) return;
1070
+ inviteDialog.value = false;
1071
+ inviteActionRow.value = null;
1072
+ }
1073
+
1074
+ async function confirmInviteAction() {
1075
+ const row = inviteActionRow.value;
1076
+ if (!row) return;
1077
+
1078
+ inviteWorking.value = true;
1079
+ try {
1080
+ const run =
1081
+ inviteAction.value === "resend"
1082
+ ? resendInvitation
1083
+ : inviteAction.value === "delete"
1084
+ ? deleteInvitation
1085
+ : cancelInvitation;
1086
+
1087
+ const result = await run(row._id);
1088
+ showMessage(result?.message ?? "Done.", "success");
1089
+ inviteDialog.value = false;
1090
+ inviteActionRow.value = null;
1091
+ await loadList();
1092
+ } catch (error: any) {
1093
+ showMessage(errorConverter(error), "error");
1094
+ } finally {
1095
+ inviteWorking.value = false;
1096
+ }
1097
+ }
1098
+
880
1099
  const statusDialog = ref(false);
881
1100
  const statusUpdating = ref(false);
882
1101
  const statusActionRow = ref<TableRow | null>(null);
@@ -908,7 +1127,11 @@ function rowActionLabel(row: TableRow) {
908
1127
 
909
1128
  function typeDisplay(type: string, nature?: string) {
910
1129
  if (nature && String(nature).trim()) return String(nature);
911
- const list = natureOfBusinessV3.value as Array<{
1130
+ // useLocal returns a plain array; reading `.value` off it was always
1131
+ // undefined, so every row fell through to the raw stored slug
1132
+ // ("security_agency") instead of "Security".
1133
+ const source = natureOfBusinessV3 as any;
1134
+ const list = (Array.isArray(source) ? source : source?.value) as Array<{
912
1135
  title?: string;
913
1136
  value?: string;
914
1137
  }>;
@@ -924,7 +1147,18 @@ function statusDisplay(raw: string, source: TableRow["source"]) {
924
1147
  return raw.charAt(0).toUpperCase() + raw.slice(1).toLowerCase();
925
1148
  }
926
1149
 
927
- function statusChipColor(statusLabel: string) {
1150
+ const INVITE_STATUS_COLOURS: Record<string, string> = {
1151
+ "awaiting-approval": "warning",
1152
+ pending: "info",
1153
+ complete: "success",
1154
+ rejected: "error",
1155
+ declined: "default",
1156
+ cancelled: "default",
1157
+ expired: "default",
1158
+ };
1159
+
1160
+ function statusChipColor(statusLabel: string, statusRaw?: string) {
1161
+ if (statusRaw) return INVITE_STATUS_COLOURS[statusRaw] ?? "primary";
928
1162
  const s = statusLabel.toLowerCase();
929
1163
  if (s === "active") return "success";
930
1164
  if (s === "pending") return "warning";
@@ -1006,7 +1240,12 @@ function mapInviteRows(raw: Record<string, any>[]): TableRow[] {
1006
1240
  ? typeDisplay(row.metadata.app)
1007
1241
  : inviteTypeLabel(row.type ?? ""),
1008
1242
  members: "-",
1009
- status: "Pending",
1243
+ // Every invitation used to read "Pending" whatever had happened to it. It
1244
+ // now says which of the seven states it is actually in, in words.
1245
+ status: row.statusLabel || serviceProviderInviteLabel(row.status),
1246
+ statusRaw: String(row.status ?? ""),
1247
+ rejectionReason: row.rejectionReason ?? "",
1248
+ siteName: row.metadata?.siteName ?? "",
1010
1249
  source: "invite",
1011
1250
  }));
1012
1251
  }
@@ -1022,13 +1261,14 @@ async function loadList() {
1022
1261
  loading.value = true;
1023
1262
  try {
1024
1263
  if (listTab.value === "pending") {
1025
- const data = await getVerifications({
1026
- status: "pending",
1027
- type: "service-provider-invite,service-provider-create-org",
1264
+ // Everything that is not a finished engagement, so a rejection or a
1265
+ // cancellation is visible here instead of the row simply vanishing.
1266
+ const data = await getInvitations({
1267
+ org: props.orgId,
1268
+ status:
1269
+ "awaiting-approval,pending,rejected,declined,cancelled,expired",
1028
1270
  page: page.value,
1029
1271
  search: searchText.value?.trim() || "",
1030
- siteId: props.siteId,
1031
- orgId: props.orgId,
1032
1272
  limit: pageSize,
1033
1273
  });
1034
1274
  items.value = mapInviteRows(data.items ?? []);
@@ -1282,8 +1522,12 @@ async function submitServiceProviderInvite() {
1282
1522
  }
1283
1523
 
1284
1524
  .sp-table-header {
1285
- background: #f5f5f5;
1286
- border-bottom: 1px solid rgba(0, 0, 0, 0.12);
1525
+ /* Was a hardcoded #f5f5f5 with a black border: in dark mode the tab strip
1526
+ became a white slab and the tab labels and the page range disappeared
1527
+ into it. These take the colour of whichever theme they are in. */
1528
+ background: rgb(var(--v-theme-surface-light, var(--v-theme-surface)));
1529
+ border-bottom: 1px solid
1530
+ rgba(var(--v-border-color), var(--v-border-opacity));
1287
1531
  }
1288
1532
 
1289
1533
  .sp-tabs {
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Service-provider invitations — the list a property manager sees and the
3
+ * things they can do to a row.
4
+ *
5
+ * Separate from `useVerification` because these rows are not generic
6
+ * verifications any more: they have their own states (waiting for Seven365,
7
+ * waiting for the provider, not approved, declined) and their own actions,
8
+ * and the server scopes them to the caller's own organisation.
9
+ */
10
+ export type TServiceProviderInvitation = {
11
+ _id: string;
12
+ email: string;
13
+ type: string;
14
+ status: string;
15
+ statusLabel: string;
16
+ rejectionReason?: string | null;
17
+ createdAt?: string;
18
+ expireAt?: string;
19
+ deletedAt?: string | null;
20
+ metadata?: Record<string, any>;
21
+ history?: Array<{
22
+ action: string;
23
+ at: string;
24
+ byName?: string;
25
+ reason?: string;
26
+ }>;
27
+ };
28
+
29
+ /** The words on screen. Kept beside the server's own list on purpose — a
30
+ * status the server invents must never render as a raw stored value. */
31
+ export const SERVICE_PROVIDER_INVITE_LABELS: Record<string, string> = {
32
+ "awaiting-approval": "Waiting for Seven365",
33
+ pending: "Waiting for the provider",
34
+ rejected: "Not approved",
35
+ complete: "Accepted",
36
+ declined: "Declined",
37
+ cancelled: "Cancelled",
38
+ expired: "Expired",
39
+ };
40
+
41
+ export function serviceProviderInviteLabel(status?: string) {
42
+ return SERVICE_PROVIDER_INVITE_LABELS[String(status ?? "")] ?? "Unknown";
43
+ }
44
+
45
+ export default function useServiceProviderInvitation() {
46
+ const base = "/api/service-provider-invitations";
47
+
48
+ function getInvitations({
49
+ org = "",
50
+ status = "",
51
+ page = 1,
52
+ limit = 20,
53
+ } = {}): Promise<{
54
+ items: TServiceProviderInvitation[];
55
+ pages: number;
56
+ pageRange: string;
57
+ }> {
58
+ return useNuxtApp().$api(base, {
59
+ method: "GET",
60
+ query: { org, status, page, limit },
61
+ });
62
+ }
63
+
64
+ function cancelInvitation(id: string) {
65
+ return useNuxtApp().$api<{ message: string }>(`${base}/${id}/cancel`, {
66
+ method: "PUT",
67
+ });
68
+ }
69
+
70
+ /** Sends the same invitation again. Never creates a second row. */
71
+ function resendInvitation(id: string) {
72
+ return useNuxtApp().$api<{ message: string }>(`${base}/${id}/resend`, {
73
+ method: "PUT",
74
+ });
75
+ }
76
+
77
+ /** Soft delete — the row leaves this list, Seven365 keeps the record. */
78
+ function deleteInvitation(id: string) {
79
+ return useNuxtApp().$api<{ message: string }>(`${base}/${id}`, {
80
+ method: "DELETE",
81
+ });
82
+ }
83
+
84
+ // ---- Seven365 super admin ------------------------------------------------
85
+
86
+ function getApprovals({
87
+ status = "awaiting-approval",
88
+ page = 1,
89
+ limit = 20,
90
+ includeDeleted = false,
91
+ } = {}): Promise<{
92
+ items: TServiceProviderInvitation[];
93
+ pages: number;
94
+ pageRange: string;
95
+ awaitingApprovalCount: number;
96
+ }> {
97
+ return useNuxtApp().$api(`${base}/approvals`, {
98
+ method: "GET",
99
+ query: { status, page, limit, includeDeleted },
100
+ });
101
+ }
102
+
103
+ function getApprovalCount(): Promise<{ count: number }> {
104
+ return useNuxtApp().$api(`${base}/approvals/count`, { method: "GET" });
105
+ }
106
+
107
+ function approveInvitation(id: string) {
108
+ return useNuxtApp().$api<{ message: string }>(`${base}/${id}/approve`, {
109
+ method: "PUT",
110
+ });
111
+ }
112
+
113
+ /** The reason is required — it is what the property manager is shown. */
114
+ function rejectInvitation(id: string, reason: string) {
115
+ return useNuxtApp().$api<{ message: string }>(`${base}/${id}/reject`, {
116
+ method: "PUT",
117
+ body: { reason },
118
+ });
119
+ }
120
+
121
+ // ---- the invited service provider ----------------------------------------
122
+
123
+ function getInvitation(id: string): Promise<TServiceProviderInvitation> {
124
+ return useNuxtApp().$api(`${base}/${id}`, { method: "GET" });
125
+ }
126
+
127
+ function declineInvitation(id: string) {
128
+ return useNuxtApp().$api<{ message: string }>(`${base}/${id}/decline`, {
129
+ method: "PUT",
130
+ });
131
+ }
132
+
133
+ return {
134
+ getInvitations,
135
+ cancelInvitation,
136
+ resendInvitation,
137
+ deleteInvitation,
138
+ getApprovals,
139
+ getApprovalCount,
140
+ approveInvitation,
141
+ rejectInvitation,
142
+ getInvitation,
143
+ declineInvitation,
144
+ };
145
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.2.1-staging.74",
5
+ "version": "3.2.1-staging.75",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
package/utils/theme.ts CHANGED
@@ -75,6 +75,17 @@ export const LIGHT_THEME = {
75
75
  "brand-surface": "#EDF1F7",
76
76
  /** The drawer's label colour. On the rail: 15.43:1. */
77
77
  "on-brand-surface": "#0B1B27",
78
+ /**
79
+ * Vuetify leaves `warning` (#FB8C00) and `info` (#2196F3) at its stock
80
+ * values and DERIVES their foreground, and — as with `on-primary` — the
81
+ * derivation picks WHITE. Measured, that is 2.37:1 on warning and 3.12:1
82
+ * on info: every filled warning/info chip, alert, button and badge in the
83
+ * product, including the invitation status chips. Setting the label to the
84
+ * dark page colour takes them to 8.9:1 and 5.3:1. Same value in both
85
+ * themes because the two fills are the same in both themes.
86
+ */
87
+ "on-warning": "#0E1319",
88
+ "on-info": "#0E1319",
78
89
  "primary-button": "#1867C0",
79
90
  "text-primary": "#052439",
80
91
  },
@@ -132,6 +143,14 @@ export const DARK_THEME = {
132
143
  "brand-surface": "#1B242F",
133
144
  /** The drawer's label colour. On the rail: 13.21:1. */
134
145
  "on-brand-surface": "#E8ECF1",
146
+ /** See LIGHT_THEME: Vuetify derives white on these fills, at 2.37:1. */
147
+ "on-warning": "#0E1319",
148
+ "on-info": "#0E1319",
149
+ /**
150
+ * Dark's stock `error` is the pale #CF6679, and white on it is 3.60:1 —
151
+ * the light theme's darker #B00020 carries white fine, this one does not.
152
+ */
153
+ "on-error": "#0E1319",
135
154
  /** Unchanged on purpose: it already passes in both themes. */
136
155
  "primary-button": "#1867C0",
137
156
  /** #052439 on a dark page measures 1.18:1. This is 15.72:1. */