@7365admin1/layer-common 4.95.0 → 4.97.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,21 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.97.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e3ade83: Vehicle QR stickers: nine cut-out shapes (circle, heart, star badge, hexagon, octagon, shield, speech bubble, rounded square, car) with a live picture of each, a "Show cut lines" switch, "Scan me" wording, and a cutting layout - 12 shapes or 16 cards per A4 sheet with 4 / 3 mm cutting gaps, + cut marks and a 5 mm printer margin. The QR inside every shape is unchanged (same content, 32 mm, error correction Q, 4-module quiet zone); the rest of the shape is a dot pattern that is decode-tested not to confuse scanners. Round-dot codes now draw the small alignment square solid and rounded-square codes join neighbouring modules, which makes both easier to scan.
8
+
9
+ ## 4.96.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 8fae702: Inventory: requested stock is set aside at once (Available / set aside for requests), Received / Not received after approval, write-offs (used up, spoilt, damaged, lost, other), holder-started returns, draft take-over and "Also attended", low-stock acknowledge with PO number and reminder setting, Reconciliation report with Excel export, Written off columns, and a "Switch property" hint for people who work at several sites. Screens hide the new actions when the API does not have them yet. Technicians see colleagues' open jobs in My Stock with Take over, and approved / confirm-received notifications open that exact request.
14
+
15
+ ### Patch Changes
16
+
17
+ - 7b70ce1: Inventory wording and setup fixes from the store keeper walk: store setup names the property instead of its id, quantities read "1 pc" / "2 pcs", "(asked N)" only when less was handed over than asked, a shelf write-off starts with no reason chosen, form errors only after a field is left or the button is pressed, no empty box under "Give to", Receive dialog titled by type (PO / DO / cash purchase), and the reminder setting says "0 = no reminders".
18
+
3
19
  ## 4.95.0
4
20
 
5
21
  ### Minor Changes
@@ -2066,11 +2082,11 @@
2066
2082
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
2067
2083
  own rule from two endpoints the console already calls, unprojected:
2068
2084
 
2069
- GET /api/members/user/:user/app/admin the Seven365 staff membership
2070
- GET /api/roles/id/:role that membership's role document
2085
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
2086
+ GET /api/roles/id/:role that membership's role document
2071
2087
 
2072
- owner = member.type === "admin" && role.type === "admin" && role.default === true
2073
- staff = member.type === "admin" && role.type === "admin"
2088
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
2089
+ staff = member.type === "admin" && role.type === "admin"
2074
2090
 
2075
2091
  `role.default` is the marker because it is the only property of a platform
2076
2092
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -24,7 +24,7 @@
24
24
  <tbody>
25
25
  <tr v-for="h in holdings" :key="h._id">
26
26
  <td>{{ h.itemName }}</td>
27
- <td class="text-right">{{ h.qty }} {{ h.unit }}</td>
27
+ <td class="text-right">{{ qtyUnit(h.qty, h.unit) }}</td>
28
28
  <td class="text-right">{{ h.inDrafts || "—" }}</td>
29
29
  <td class="text-right">{{ h.usedThisMonth ?? 0 }}</td>
30
30
  </tr>
@@ -58,13 +58,18 @@
58
58
  <template #item.date="{ value }">{{ standardFormatDateTime(value) }}</template>
59
59
  <template #item.what="{ item }">{{ historyText(item) }}</template>
60
60
  <template #item.person="{ item }">{{ item.holderLabel || item.holderName || "—" }}</template>
61
- <template #item.qty="{ item }">{{ item.qty }} {{ item.unit }}</template>
61
+ <template #item.qty="{ item }">{{ qtyUnit(item.qty, item.unit) }}</template>
62
62
  <template #item.where="{ item }">{{ whereText(item.location) || (item.type === "handover" ? item.remarks : "") || "—" }}</template>
63
63
  <template #item.reference="{ item }">{{ item.type === "handover" ? "—" : item.reference || "—" }}</template>
64
64
  <template #item.photos="{ item }">
65
65
  <InventoryPhotoThumbs :photos="item.photos" :before-at="item.photosBeforeAt" :after-at="item.photosAfterAt" />
66
66
  </template>
67
- <template #item.by="{ item }">{{ item.createdByLabel || item.createdByName }}</template>
67
+ <template #item.by="{ item }">
68
+ {{ item.createdByLabel || item.createdByName }}
69
+ <div v-if="item.attendedWith" class="text-caption text-medium-emphasis">Also attended: {{ item.attendedWith }}</div>
70
+ <div v-if="item.startedByName" class="text-caption text-medium-emphasis">Started by {{ item.startedByName }}</div>
71
+ <div v-if="item.type === 'write_off' && item.remarks" class="text-caption text-medium-emphasis">"{{ item.remarks }}"</div>
72
+ </template>
68
73
  <template #item.holderBalance="{ value }">{{ value ?? "—" }}</template>
69
74
  </v-data-table-server>
70
75
  </v-card-text>
@@ -77,6 +82,7 @@ import useInventory from "../composables/useInventory";
77
82
  import useUtils from "../composables/useUtils";
78
83
  import useLocalAuth from "../composables/useLocalAuth";
79
84
  import { historyText, whereText } from "../utils/inventory-usage";
85
+ import { inventoryStepsLive, qtyUnit } from "../utils/inventory-ack";
80
86
 
81
87
  /**
82
88
  * History of stock (owner 2026-09-29, "where they used their stock"): one person's (`holder`, "me" =
@@ -116,6 +122,7 @@ const typeChoices = [
116
122
  { value: "issue", title: "Taken from store" },
117
123
  { value: "handover", title: "Hand-overs" },
118
124
  { value: "return", title: "Given back" },
125
+ ...(inventoryStepsLive(props.store) ? [{ value: "write_off", title: "Written off" }] : []),
119
126
  ];
120
127
 
121
128
  const intro = computed(() =>
@@ -10,7 +10,9 @@
10
10
  <div class="d-flex flex-wrap ga-4 mb-4">
11
11
  <v-img v-if="item.photo" :src="getFileUrl(item.photo)" width="120" height="120" cover class="rounded flex-grow-0" />
12
12
  <div class="flex-grow-1">
13
- <div class="text-h5 mb-1" :class="item.lowStock ? 'text-error' : ''">{{ item.onHand }} {{ item.unit }} in store</div>
13
+ <div class="text-h5 mb-1" :class="item.lowStock ? 'text-error' : ''">{{ qtyUnit(item.onHand, item.unit) }} in store</div>
14
+ <div v-if="item.reserved" class="text-body-2 mb-1">{{ availableText(item) }} (the rest is set aside for requests waiting for approval)</div>
15
+ <div v-if="item.lowStockAlert" class="text-body-2 text-error mb-1">{{ lowStockAlertText(item.lowStockAlert, standardFormatDate) }}</div>
14
16
  <div class="text-body-2 text-medium-emphasis">
15
17
  {{ [inventoryItemMeta(item), item.reorderLevel ? `Alert at ${item.reorderLevel}` : "", item.unitCost !== undefined ? `$${Number(item.unitCost).toFixed(2)} each` : ""].filter(Boolean).join(" · ") }}
16
18
  </div>
@@ -21,7 +23,18 @@
21
23
  <div class="d-flex flex-wrap ga-2 mb-4" v-if="ctl.editItem || ctl.count">
22
24
  <v-btn v-if="ctl.editItem" class="text-none screen-btn-ghost" variant="outlined" prepend-icon="mdi-pencil-outline" @click="emit('edit', item)">Edit item</v-btn>
23
25
  <v-btn v-if="ctl.count" class="text-none screen-btn-ghost" variant="outlined" prepend-icon="mdi-counter" @click="counting = !counting">Stock count</v-btn>
26
+ <v-btn v-if="ctl.count && stepsLive && item.onHand > 0" class="text-none screen-btn-ghost" variant="outlined" prepend-icon="mdi-delete-alert-outline" @click="writingOff = true">Write off</v-btn>
24
27
  </div>
28
+ <InventoryWriteOffDialog
29
+ v-model="writingOff"
30
+ :store="store"
31
+ from="store"
32
+ :item="item._id"
33
+ :item-name="item.name"
34
+ :unit="item.unit"
35
+ :max="item.onHand"
36
+ @done="(show = false), emit('done')"
37
+ />
25
38
 
26
39
  <v-form v-if="counting" ref="countForm" class="mb-4">
27
40
  <AppCard class="pa-4">
@@ -80,6 +93,7 @@ import useUtils from "../composables/useUtils";
80
93
  import { inventoryItemMeta } from "../utils/inventory-store-label";
81
94
  import { inventoryControls } from "../utils/inventory-presets";
82
95
  import { receiptRefText } from "../utils/inventory-receiving";
96
+ import { availableText, inventoryStepsLive, lowStockAlertText, qtyUnit, writeOffLabel } from "../utils/inventory-ack";
83
97
 
84
98
  const show = defineModel<boolean>({ default: false });
85
99
  const props = defineProps({
@@ -103,6 +117,8 @@ const counted = ref<number | null>(null);
103
117
  const reason = ref("");
104
118
  const saving = ref(false);
105
119
  const countForm = ref();
120
+ const writingOff = ref(false);
121
+ const stepsLive = computed(() => inventoryStepsLive(props.store));
106
122
 
107
123
  const formatDate = (d: string) => standardFormatDate(d);
108
124
 
@@ -121,6 +137,7 @@ function describe(t: TInventoryTransaction) {
121
137
  t.reference || "",
122
138
  );
123
139
  if (t.type === "adjust") parts.push(t.remarks || "");
140
+ if (t.type === "write_off") parts.push(`${t.qty} ${writeOffLabel(t.writeOffReason).toLowerCase()}`, (t.holderChange ?? 0) < 0 ? `by ${who}` : "from the shelf", t.remarks || "");
124
141
  if (t.type === "opening") parts.push(t.supplier || "", t.reference ? `PO ${t.reference}` : "", t.receivedBy ? `received by ${t.receivedBy}` : "", t.remarks || "");
125
142
  return parts.filter(Boolean).join(" · ");
126
143
  }
@@ -7,7 +7,7 @@
7
7
  <v-btn icon="mdi-close" variant="text" class="screen-modal__close" aria-label="Close" @click="show = false" />
8
8
  </v-card-title>
9
9
  <v-card-text class="screen-modal__body">
10
- <v-form ref="form">
10
+ <v-form ref="form" validate-on="blur invalid-input">
11
11
  <InputLabel title="Item name" required class="mb-1" />
12
12
  <v-text-field v-model="f.name" :rules="[requiredRule]" density="comfortable" placeholder="e.g. Philips T8 2ft 9W LED Tube" />
13
13
  <v-row dense>
@@ -9,7 +9,8 @@
9
9
 
10
10
  <v-card-text class="screen-modal__body">
11
11
  <p class="text-body-2 text-medium-emphasis mb-4">{{ copy.help }}</p>
12
- <v-form ref="form">
12
+ <!-- Errors once a field is left or the button is pressed, never from opening or resetting the form. -->
13
+ <v-form ref="form" validate-on="blur invalid-input">
13
14
  <template v-if="mode === 'receive'">
14
15
  <InputLabel title="How did this stock come in?" required class="mb-1" />
15
16
  <v-btn-toggle v-model="receivingType" mandatory divided variant="outlined" density="comfortable" class="mb-1 receiving-type">
@@ -40,7 +41,7 @@
40
41
  </template>
41
42
  </v-autocomplete>
42
43
  <v-alert v-if="usedByWarning" type="warning" variant="tonal" density="compact" class="mt-2">{{ usedByWarning }}</v-alert>
43
- <v-text-field v-else :model-value="holderName" density="comfortable" readonly />
44
+ <v-text-field v-if="mode !== 'issue'" :model-value="holderName" density="comfortable" readonly />
44
45
  </template>
45
46
 
46
47
  <div class="text-subtitle-2 mb-2">Items</div>
@@ -170,6 +171,7 @@
170
171
  import useInventory, { pickedDate } from "../composables/useInventory";
171
172
  import useUtils from "../composables/useUtils";
172
173
  import { normaliseUsedBy, usedByChips, usedByText } from "../utils/inventory-used-by";
174
+ import { availableQty, availableText as availableTextOf, qtyUnit } from "../utils/inventory-ack";
173
175
  import { RECEIVING_TYPES, receivingFields, receivingProblem, type TReceivingType } from "../utils/inventory-receiving";
174
176
 
175
177
  type TMode = "receive" | "issue" | "request" | "return";
@@ -194,7 +196,7 @@ const notify = inject<(text: string, color?: string) => void>("inventoryNotify",
194
196
  const COPY: Record<TMode, { title: string; help: string; button: string; success: string }> = {
195
197
  receive: {
196
198
  title: "Receive Stock",
197
- help: "Record a delivery from a supplier. The quantities are added to the store straight away.",
199
+ help: "The quantities are added to the store straight away.",
198
200
  button: "Add to store",
199
201
  success: "Stock received.",
200
202
  },
@@ -217,7 +219,27 @@ const COPY: Record<TMode, { title: string; help: string; button: string; success
217
219
  success: "Stock returned to the store.",
218
220
  },
219
221
  };
220
- const copy = computed(() => COPY[props.mode]);
222
+ // The API sets requested stock aside at once (owner 2026-10-03) when its items carry `available`.
223
+ const reserving = computed(() => itemChoices.value.some((i) => i.available !== undefined));
224
+ const RECEIVE_TITLES: Record<TReceivingType, string> = { PO: "Record a delivery for a PO", DO: "Record a delivery order (DO)", CASH: "Record a cash purchase" };
225
+ const copy = computed(() => {
226
+ const c = COPY[props.mode];
227
+ if (props.mode === "receive") return { ...c, title: RECEIVE_TITLES[receivingType.value] };
228
+ if (!reserving.value) return c;
229
+ if (props.mode === "request")
230
+ return {
231
+ ...c,
232
+ help: "Ask the store for what you need, up to what is available (shown under each item). The quantity is set aside for you as soon as you send it, so nobody else can take it. If the request is rejected or cancelled, it goes back to the store.",
233
+ success: "Request sent. The items are set aside for you until the store keeper approves. Then open My Requests and tap Received once you have them.",
234
+ };
235
+ if (props.mode === "issue")
236
+ return {
237
+ ...c,
238
+ help: "Hand stock to someone at the store counter. It leaves the store at once. They tap Received in their app to confirm they got it, and can use it after that.",
239
+ success: "Stock issued. The person is asked to tap Received to confirm they got it.",
240
+ };
241
+ return c;
242
+ });
221
243
 
222
244
  const form = ref();
223
245
  const saving = ref(false);
@@ -254,12 +276,15 @@ const choices = computed<any[]>(() =>
254
276
  const siteChoices = computed(() => (props.sites.length ? props.sites : props.store.sites.map((_id) => ({ _id, name: _id === props.siteId ? "This property" : "Other property" }))));
255
277
 
256
278
  const unitOf = (id: string) => choices.value.find((c) => c._id === id)?.unit ?? "";
257
- const availableOf = (id: string) => choices.value.find((c) => c._id === id)?.onHand;
279
+ const availableOf = (id: string) => {
280
+ const c = choices.value.find((x) => x._id === id);
281
+ return c && (props.mode === "request" || props.mode === "issue") ? availableQty(c) : c?.onHand;
282
+ };
258
283
 
259
284
  function availableText(item: any) {
260
- if (props.mode === "return") return `In hand: ${item.onHand} ${item.unit}`;
261
- if (props.mode === "receive") return `In store: ${item.onHand} ${item.unit}`;
262
- return `Available: ${item.onHand} ${item.unit}`;
285
+ if (props.mode === "return") return `In hand: ${qtyUnit(item.onHand, item.unit)}`;
286
+ if (props.mode === "receive") return `In store: ${qtyUnit(item.onHand, item.unit)}`;
287
+ return availableTextOf(item);
263
288
  }
264
289
 
265
290
  function qtyRule(line: TRow) {
@@ -270,7 +295,7 @@ function qtyRule(line: TRow) {
270
295
  const total = rows.value.filter((r) => r.item === line.item).reduce((s, r) => s + Number(r.qty || 0), 0);
271
296
  // Requests too (client 2026-10-02): nobody can ask for more than the store has; the server refuses it as well.
272
297
  if (available !== undefined && total > available)
273
- return available > 0 ? `Only ${available} ${unitOf(line.item)} in the store — ask for ${available} or fewer` : "None left in the store";
298
+ return available > 0 ? `Only ${qtyUnit(available, unitOf(line.item))} available — ${props.mode === "request" ? "ask for" : "give"} ${available} or fewer` : "None available right now";
274
299
  return true;
275
300
  };
276
301
  }
@@ -364,6 +389,7 @@ async function submit() {
364
389
 
365
390
  watch(show, (open) => {
366
391
  if (!open) return;
392
+ form.value?.resetValidation();
367
393
  rows.value = [blank()];
368
394
  supplier.value = "";
369
395
  reference.value = "";
@@ -34,6 +34,22 @@
34
34
  Equipment is now in Inventory.
35
35
  </v-alert>
36
36
 
37
+ <!-- Client 2026-10-02 (Winsland): each property keeps its own stock, so say plainly whose stock this is. -->
38
+ <v-alert v-if="mySites.length > 1" type="info" variant="tonal" density="compact" class="mb-4" icon="mdi-office-building-marker-outline">
39
+ <div class="d-flex flex-wrap align-center ga-2">
40
+ <span>You are viewing <strong>{{ currentSiteName }}</strong>'s inventory.</span>
41
+ <v-spacer />
42
+ <v-menu>
43
+ <template #activator="{ props: menu }">
44
+ <v-btn v-bind="menu" size="small" variant="outlined" class="text-none" append-icon="mdi-chevron-down">Switch property</v-btn>
45
+ </template>
46
+ <v-list density="compact" max-height="320">
47
+ <v-list-item v-for="s in mySites" :key="s._id" :title="s.name" :active="s._id === siteId" @click="switchSite(s._id)" />
48
+ </v-list>
49
+ </v-menu>
50
+ </div>
51
+ </v-alert>
52
+
37
53
  <v-progress-linear v-if="loading" indeterminate color="primary" class="mb-4" />
38
54
 
39
55
  <AppCard v-else-if="loadError" class="pa-6">
@@ -62,7 +78,7 @@
62
78
  <InputLabel title="Properties this store supplies" required class="mb-1" />
63
79
  <v-select
64
80
  v-model="setup.sites"
65
- :items="orgSites"
81
+ :items="setupSiteOptions"
66
82
  item-title="name"
67
83
  item-value="_id"
68
84
  multiple
@@ -99,6 +115,12 @@
99
115
  <v-btn size="small" variant="flat" color="warning" class="text-none" @click="tab = 'requests'">Review requests</v-btn>
100
116
  </div>
101
117
  </v-alert>
118
+ <v-alert v-if="toConfirm > 0 && tab !== 'mine'" type="warning" variant="tonal" density="compact" class="mb-4" icon="mdi-package-variant-closed-check">
119
+ <div class="d-flex align-center flex-wrap ga-2">
120
+ <span>{{ toConfirm === 1 ? "1 delivery is" : `${toConfirm} deliveries are` }} waiting for you to tap Received. You can use the items after that.</span>
121
+ <v-btn size="small" variant="flat" color="warning" class="text-none" @click="tab = 'mine'">Open My Stock</v-btn>
122
+ </div>
123
+ </v-alert>
102
124
  <v-tabs v-model="tab" color="primary" class="mb-4" show-arrows>
103
125
  <v-tab v-for="t in tabs" :key="t.value" :value="t.value" class="text-none">
104
126
  {{ t.label }}
@@ -114,7 +136,7 @@
114
136
  <InventoryMyStockTab :store="store" :site-id="siteId" :sites="storeSites" @changed="refreshCounts" />
115
137
  </v-window-item>
116
138
  <v-window-item v-if="ctl.tabs.requests" value="requests">
117
- <InventoryRequestsTab :store="store" :site-id="siteId" @changed="refreshCounts" />
139
+ <InventoryRequestsTab :store="store" :site-id="siteId" :focus-request="focusRequest" @changed="refreshCounts" />
118
140
  </v-window-item>
119
141
  <v-window-item v-if="ctl.tabs.people" value="technicians">
120
142
  <InventoryTechniciansTab :store="store" @changed="refreshCounts" />
@@ -139,9 +161,12 @@
139
161
  import useInventory from "../composables/useInventory";
140
162
  import useWorkerTerm from "../composables/useWorkerTerm";
141
163
  import useSite from "../composables/useSite";
164
+ import useCustomerSite from "../composables/useCustomerSite";
142
165
  import useUtils from "../composables/useUtils";
143
- import { inventoryManagedBy, inventoryStoreLabel, inventorySuppliesText } from "../utils/inventory-store-label";
166
+ import { inventoryManagedBy, inventorySiteOptions, inventoryStoreLabel, inventorySuppliesText } from "../utils/inventory-store-label";
144
167
  import { inventoryControls } from "../utils/inventory-presets";
168
+ import { inventoryStepsLive } from "../utils/inventory-ack";
169
+ import useLocalAuth from "../composables/useLocalAuth";
145
170
 
146
171
  const props = defineProps({
147
172
  orgId: { type: String, required: true },
@@ -152,8 +177,10 @@ const movedFromEquipment = useRoute().query.moved === "equipment";
152
177
  // A notification link (`?storeId=&tab=`, NotificationBell): open that store on that tab, once.
153
178
  const linkQuery = useRoute().query;
154
179
  const linked = { storeId: String(linkQuery.storeId ?? ""), tab: String(linkQuery.tab ?? "") };
180
+ // `?requestId=` (approved / confirm-received alerts): the Requests tab opens on that request.
181
+ const focusRequest = String(linkQuery.requestId ?? "");
155
182
 
156
- const { getStores, createStore: apiCreateStore, getRequests, getReport } = useInventory();
183
+ const { getHandovers, getStores, getStore, createStore: apiCreateStore, getRequests, getReport } = useInventory();
157
184
  const { getSites } = useSite();
158
185
  const { requiredRule } = useUtils();
159
186
 
@@ -217,7 +244,7 @@ const tabs = computed(() => {
217
244
  // Hide, never grey: each tab only for the job that uses it (API-core decides `access`).
218
245
  ...(ctl.value.tabs.mine ? [{ value: "mine", label: "My Stock" }] : []),
219
246
  ...(ctl.value.tabs.requests ? [{ value: "requests", label: a.canApprove ? "Requests" : "My Requests", badge: a.canApprove ? pendingCount.value : 0 }] : []),
220
- ...(ctl.value.tabs.people ? [{ value: "technicians", label: worker.many }] : []),
247
+ ...(ctl.value.tabs.people ? [{ value: "technicians", label: worker.many, badge: returnsWaiting.value }] : []),
221
248
  ...(ctl.value.tabs.reports ? [{ value: "reports", label: a.canReports ? "Reports" : "Usage check" }] : []),
222
249
  ...(!ctl.value.tabs.reports && companyUsage.value ? [{ value: "reports", label: "Company usage" }] : []),
223
250
  ...(ctl.value.tabs.suppliers ? [{ value: "suppliers", label: "Suppliers" }] : []),
@@ -244,6 +271,7 @@ async function loadStores() {
244
271
  loading.value = false;
245
272
  }
246
273
  refreshCounts();
274
+ loadStoreDetail();
247
275
  }
248
276
 
249
277
  /**
@@ -265,7 +293,38 @@ async function probeCompanyUsage() {
265
293
  }
266
294
  }
267
295
 
296
+ /** Stock issued to me that I have not confirmed yet (only where the API has the Received step). */
297
+ const toConfirm = ref(0);
298
+ const { currentUser } = useLocalAuth();
299
+ async function refreshToConfirm() {
300
+ const s = store.value;
301
+ toConfirm.value = 0;
302
+ if (!s || !ctl.value.tabs.mine || !inventoryStepsLive(s)) return;
303
+ try {
304
+ const me = String(currentUser.value?._id ?? "");
305
+ const res = await getRequests(s._id, { status: "issued", limit: 100 });
306
+ toConfirm.value = res.items.filter((r) => String(r.requestedBy) === me && (r.ackStatus === "awaiting" || r.ackStatus === "not_received")).length;
307
+ } catch {
308
+ toConfirm.value = 0;
309
+ }
310
+ }
311
+
312
+ /** Returns started by holders, waiting for a store keeper (badge on the staff tab). */
313
+ const returnsWaiting = ref(0);
314
+ async function refreshReturns() {
315
+ const s = store.value;
316
+ returnsWaiting.value = 0;
317
+ if (!s || !ctl.value.takeReturn || !inventoryStepsLive(s)) return;
318
+ try {
319
+ returnsWaiting.value = (await getHandovers(s._id, { status: "pending", limit: 1 }))?.returnsWaiting ?? 0;
320
+ } catch {
321
+ returnsWaiting.value = 0;
322
+ }
323
+ }
324
+
268
325
  async function refreshCounts() {
326
+ refreshToConfirm();
327
+ refreshReturns();
269
328
  if (!store.value?.access.canApprove) return (pendingCount.value = 0);
270
329
  try {
271
330
  pendingCount.value = (await getRequests(store.value._id, { status: "pending", limit: 1 }))?.pendingCount ?? 0;
@@ -274,6 +333,47 @@ async function refreshCounts() {
274
333
  }
275
334
  }
276
335
 
336
+ /** The properties this person works at in this organisation (the same list as the site switcher). */
337
+ const { getAll: getCustomerSites } = useCustomerSite();
338
+ const mySites = ref<{ _id: string; name: string }[]>([]);
339
+ // The property being viewed may be one the organisation serves, not owns: name it, never show its id.
340
+ const setupSiteOptions = computed(() =>
341
+ inventorySiteOptions(orgSites.value, { sites: setup.sites, siteNames: [...mySites.value, { _id: props.siteId, name: currentSiteName.value }] }),
342
+ );
343
+ const currentSiteName = computed(() => mySites.value.find((s) => s._id === props.siteId)?.name || storeSites.value.find((s) => s._id === props.siteId)?.name || "this property");
344
+ async function loadMySites() {
345
+ try {
346
+ const res = await getCustomerSites({ org: props.orgId, limit: 100 });
347
+ const seen = new Set<string>();
348
+ mySites.value = (res?.items ?? [])
349
+ .map((i: any) => ({ _id: String(i.site ?? ""), name: String(i.name ?? "") }))
350
+ .filter((s: { _id: string }) => s._id && !seen.has(s._id) && seen.add(s._id));
351
+ } catch {
352
+ mySites.value = [];
353
+ }
354
+ }
355
+ const route = useRoute();
356
+ function switchSite(site: string) {
357
+ if (site === props.siteId) return;
358
+ navigateTo({ name: route.name as string, params: { ...route.params, site } });
359
+ }
360
+
361
+ /** The store's own settings (the reminder interval also tells the screens the new steps exist on the server). */
362
+ async function loadStoreDetail() {
363
+ const s = store.value;
364
+ if (!s) return;
365
+ try {
366
+ const full = await getStore(s._id);
367
+ if (typeof full?.lowStockReminderDays === "number") {
368
+ s.lowStockReminderDays = full.lowStockReminderDays;
369
+ refreshToConfirm();
370
+ refreshReturns();
371
+ }
372
+ } catch {
373
+ /* the list's copy is enough to work with */
374
+ }
375
+ }
376
+
277
377
  async function loadOrgSites() {
278
378
  try {
279
379
  const res = await getSites({ orgId: props.orgId, limit: 100 });
@@ -307,11 +407,13 @@ watch(storeId, () => {
307
407
  tab.value = wanted && tabs.value.some((t) => t.value === wanted) ? wanted : "stock";
308
408
  refreshCounts();
309
409
  probeCompanyUsage();
410
+ loadStoreDetail();
310
411
  });
311
412
  watch(() => props.siteId, loadStores);
312
413
 
313
414
  onMounted(() => {
314
415
  loadStores();
315
416
  loadOrgSites();
417
+ loadMySites();
316
418
  });
317
419
  </script>