@7365admin1/layer-common 4.39.1 → 4.40.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,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.40.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 783ffe2: Edit Camera: the device password is write-only. On edit the Password field is optional, starts blank with the placeholder "Saved · leave blank to keep", and is sent only when a new one is typed, so an admin can switch a camera's Status without re-typing its login. A status-only change now enables Submit. Password stays required when adding a camera.
8
+
9
+ ## 4.40.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 3a96438: Client invitations: an accepted invitation whose site never reached the company's Sites list now says so in plain words and offers "Add <site> to your Sites" (`linkInvitationSite`, `PUT /service-provider-invitations/:id/link-site`). Needs core 3.100.0.
14
+
3
15
  ## 4.39.1
4
16
 
5
17
  ### Patch Changes
@@ -1453,11 +1465,11 @@
1453
1465
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
1454
1466
  own rule from two endpoints the console already calls, unprojected:
1455
1467
 
1456
- GET /api/members/user/:user/app/admin the Seven365 staff membership
1457
- GET /api/roles/id/:role that membership's role document
1468
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
1469
+ GET /api/roles/id/:role that membership's role document
1458
1470
 
1459
- owner = member.type === "admin" && role.type === "admin" && role.default === true
1460
- staff = member.type === "admin" && role.type === "admin"
1471
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
1472
+ staff = member.type === "admin" && role.type === "admin"
1461
1473
 
1462
1474
  `role.default` is the marker because it is the only property of a platform
1463
1475
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -161,14 +161,21 @@
161
161
  </v-col>
162
162
 
163
163
  <v-col v-if="isANPR" cols="12">
164
- <InputLabel class="text-capitalize" title="Password" required />
164
+ <!-- Write-only: the saved password is never sent to the browser, so
165
+ on edit a blank field keeps it. Required only for a new camera. -->
166
+ <InputLabel
167
+ class="text-capitalize"
168
+ title="Password"
169
+ :required="!isEdit"
170
+ />
165
171
  <v-text-field
166
172
  v-model.trim="camera.password"
167
173
  type="password"
174
+ autocomplete="new-password"
168
175
  class="filter-field"
169
176
  density="comfortable"
170
- :rules="[requiredRule]"
171
- placeholder="Enter password"
177
+ :rules="isEdit ? [] : [requiredRule]"
178
+ :placeholder="isEdit ? 'Saved · leave blank to keep' : 'Enter password'"
172
179
  />
173
180
  </v-col>
174
181
 
@@ -419,8 +426,12 @@ const camera = ref<TSiteCamera>({
419
426
 
420
427
  camera.value.site = prop.site;
421
428
 
422
- if (prop.mode === "edit") {
429
+ const isEdit = prop.mode === "edit";
430
+
431
+ if (isEdit) {
423
432
  camera.value = JSON.parse(JSON.stringify(prop.camera));
433
+ // Never pre-filled: blank means "keep the saved password".
434
+ camera.value.password = "";
424
435
  }
425
436
  camera.value.site = prop.site;
426
437
  camera.value.type = prop.type;
@@ -576,7 +587,9 @@ const hasChanges = computed(() => {
576
587
  return (
577
588
  camera.value.host !== prop.camera.host ||
578
589
  camera.value.username !== prop.camera.username ||
579
- camera.value.password !== prop.camera.password ||
590
+ // A typed password is a change; blank keeps the saved one.
591
+ !!camera.value.password ||
592
+ camera.value.status !== prop.camera.status ||
580
593
  camera.value.category !== prop.camera.category ||
581
594
  camera.value.guardPost !== prop.camera.guardPost ||
582
595
  camera.value.direction !== prop.camera.direction ||
@@ -895,7 +908,8 @@ async function save(
895
908
  const res = await updateSiteCamera(camera.value._id ?? "", {
896
909
  ...addressPayload(),
897
910
  username: camera.value.username,
898
- password: camera.value.password,
911
+ // Sent only when typed; left out, the server keeps the saved one.
912
+ ...(camera.value.password ? { password: camera.value.password } : {}),
899
913
  category: camera.value.category,
900
914
  guardPost: camera.value.guardPost,
901
915
  direction: camera.value.direction,
@@ -78,7 +78,17 @@
78
78
  </div>
79
79
 
80
80
  <div v-else-if="item.status === 'complete'" class="px-4 pb-4">
81
- <NuxtLink v-if="item.onThisOrg" :to="`/org/${org}/sites`">Open Sites</NuxtLink>
81
+ <v-btn
82
+ v-if="item.needsSiteLink"
83
+ variant="flat"
84
+ class="screen-btn-primary"
85
+ :loading="working[item._id] === 'link'"
86
+ :disabled="Boolean(working[item._id])"
87
+ @click="addToSites(item)"
88
+ >
89
+ Add {{ item.siteName }} to your Sites
90
+ </v-btn>
91
+ <NuxtLink v-else-if="item.onThisOrg" :to="`/org/${org}/sites`">Open Sites</NuxtLink>
82
92
  <NuxtLink
83
93
  v-else-if="item.providerOrgId && item.providerOrgId !== org"
84
94
  :to="`/org/${item.providerOrgId}/sites`"
@@ -124,8 +134,13 @@ const tabs: Array<{ value: Tab; label: string }> = [
124
134
  ];
125
135
 
126
136
  const route = useRoute();
127
- const { getReceivedInvitations, getInvitation, acceptInvitation, declineInvitation } =
128
- useServiceProviderInvitation();
137
+ const {
138
+ getReceivedInvitations,
139
+ getInvitation,
140
+ acceptInvitation,
141
+ declineInvitation,
142
+ linkInvitationSite,
143
+ } = useServiceProviderInvitation();
129
144
 
130
145
  const tab = ref<Tab>("pending");
131
146
  const items = ref<TReceivedServiceProviderInvitation[]>([]);
@@ -180,6 +195,9 @@ function stateLine(item: TReceivedServiceProviderInvitation) {
180
195
  if (item.onThisOrg) {
181
196
  return `Accepted${on(item.answeredAt)} — ${item.siteName} is on your Sites list as a site you serve for ${item.orgName}.`;
182
197
  }
198
+ if (item.needsSiteLink) {
199
+ return "This invitation was accepted, but the site wasn't added to your Sites. Add it now.";
200
+ }
183
201
  if (!item.providerOrgId || item.providerOrgId === props.org) {
184
202
  return `Accepted${on(item.answeredAt)}, but ${item.siteName} is not on this company's Sites list. Please contact Seven365 support so we can add it.`;
185
203
  }
@@ -228,6 +246,20 @@ async function answer(item: TReceivedServiceProviderInvitation, action: "accept"
228
246
  }
229
247
  }
230
248
 
249
+ /** Accepted but never added to the Sites list: add it now, then show it. */
250
+ async function addToSites(item: TReceivedServiceProviderInvitation) {
251
+ errors[item._id] = "";
252
+ working[item._id] = "link";
253
+ try {
254
+ await linkInvitationSite(item._id, props.org);
255
+ await load();
256
+ } catch (err: any) {
257
+ errors[item._id] = err?.data?.message || "Could not add this site to your Sites.";
258
+ } finally {
259
+ working[item._id] = "";
260
+ }
261
+ }
262
+
231
263
  // Opened from the bell: start on the tab the linked invitation is in.
232
264
  if (linked.value) {
233
265
  try {
@@ -48,6 +48,8 @@ export type TReceivedServiceProviderInvitation = {
48
48
  providerOrgName?: string;
49
49
  /** Accepted AND on the Sites list of the company on screen. */
50
50
  onThisOrg?: boolean;
51
+ /** Accepted, but the site is on no company's Sites list: offer to add it. */
52
+ needsSiteLink?: boolean;
51
53
  };
52
54
 
53
55
  /** The words on screen. Kept beside the server's own list on purpose — a
@@ -171,6 +173,17 @@ export default function useServiceProviderInvitation() {
171
173
  });
172
174
  }
173
175
 
176
+ /**
177
+ * An accepted invitation whose site never reached this company's Sites
178
+ * list: add it now. Safe to repeat - the server adds it once.
179
+ */
180
+ function linkInvitationSite(id: string, org: string) {
181
+ return useNuxtApp().$api<{ message: string }>(`${base}/${id}/link-site`, {
182
+ method: "PUT",
183
+ body: { org },
184
+ });
185
+ }
186
+
174
187
  /**
175
188
  * "Client invitations": one tab of the invitations sent to the signed-in
176
189
  * account, plus the count in every tab. `org` is the company on screen.
@@ -200,6 +213,7 @@ export default function useServiceProviderInvitation() {
200
213
  getInvitation,
201
214
  declineInvitation,
202
215
  acceptInvitation,
216
+ linkInvitationSite,
203
217
  getReceivedInvitations,
204
218
  };
205
219
  }
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.39.1",
5
+ "version": "4.40.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.",