@7365admin1/layer-common 4.71.2 → 4.73.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,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.73.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cd5501e: Inventory: usage drafts (save the before photo first, add the after photo and submit later; edit or discard with a confirm; My drafts cards with an old-draft warning; "in drafts / free" on holdings); History per item and "My usage" with date, item and place filters; Usage report as the store owner's usage tracking (status incl. in-progress drafts, person, item, place, work order, flags, search; totals by item / person / company; photo thumbnails with times; draft and submit times; how the stock reached the person; Excel with the same filters and photo links; click a person or an item for its full history); a provider's supervisor gets "Company usage" for its own people at the main store.
8
+
9
+ ## 4.72.0
10
+
11
+ ### Minor Changes
12
+
13
+ - eb2e98f: Inventory: "Main store · <company>" with who manages it, a request-first view for other companies, pending-approval banner for the owner; Used by on items (form, chips, column, filter, bulk edit, report filter); Give to colleague (My Stock) with confirm/decline; Hand-overs report; name · company · service on requests, holdings, history, reports; Issue Stock "Give to" grouped by company with a Used-by warning.
14
+
3
15
  ## 4.71.2
4
16
 
5
17
  ### Patch Changes
@@ -1873,11 +1885,11 @@
1873
1885
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
1874
1886
  own rule from two endpoints the console already calls, unprojected:
1875
1887
 
1876
- GET /api/members/user/:user/app/admin the Seven365 staff membership
1877
- GET /api/roles/id/:role that membership's role document
1888
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
1889
+ GET /api/roles/id/:role that membership's role document
1878
1890
 
1879
- owner = member.type === "admin" && role.type === "admin" && role.default === true
1880
- staff = member.type === "admin" && role.type === "admin"
1891
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
1892
+ staff = member.type === "admin" && role.type === "admin"
1881
1893
 
1882
1894
  `role.default` is the marker because it is the only property of a platform
1883
1895
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -0,0 +1,205 @@
1
+ <template>
2
+ <v-dialog v-model="show" max-width="1100" scrollable>
3
+ <v-card class="screen-modal">
4
+ <v-card-title class="d-flex align-center">
5
+ {{ title }}
6
+ <v-spacer />
7
+ <v-btn icon="mdi-close" variant="text" class="screen-modal__close" aria-label="Close" @click="show = false" />
8
+ </v-card-title>
9
+ <v-card-text class="screen-modal__body">
10
+ <p class="text-body-2 text-medium-emphasis mb-3">{{ intro }}</p>
11
+
12
+ <!-- A supervisor opening a person: what they hold now, then everything that happened to their stock. -->
13
+ <template v-if="holder && holder !== 'me' && holder !== selfId && (store.access.canApprove || store.access.canHandle)">
14
+ <div class="text-subtitle-2 mb-1">Holding now</div>
15
+ <v-table density="compact" class="mb-4 border rounded">
16
+ <thead>
17
+ <tr>
18
+ <th>Item</th>
19
+ <th class="text-right">In hand</th>
20
+ <th class="text-right">In drafts</th>
21
+ <th class="text-right">Used this month</th>
22
+ </tr>
23
+ </thead>
24
+ <tbody>
25
+ <tr v-for="h in holdings" :key="h._id">
26
+ <td>{{ h.itemName }}</td>
27
+ <td class="text-right">{{ h.qty }} {{ h.unit }}</td>
28
+ <td class="text-right">{{ h.inDrafts || "—" }}</td>
29
+ <td class="text-right">{{ h.usedThisMonth ?? 0 }}</td>
30
+ </tr>
31
+ <tr v-if="!holdings.length">
32
+ <td colspan="4" class="text-center text-medium-emphasis py-3">Not holding any stock now.</td>
33
+ </tr>
34
+ </tbody>
35
+ </v-table>
36
+ </template>
37
+
38
+ <div class="d-flex flex-wrap ga-2 mb-2">
39
+ <v-text-field v-model="from" type="date" label="From" density="compact" hide-details style="max-width: 170px" />
40
+ <v-text-field v-model="to" type="date" label="To" density="compact" hide-details style="max-width: 170px" />
41
+ <v-select v-model="type" :items="typeChoices" label="What" density="compact" hide-details style="max-width: 200px" />
42
+ <v-select v-if="!item" v-model="itemPick" :items="[{ value: '', title: 'All items' }, ...itemChoices]" label="Item" density="compact" hide-details style="max-width: 240px" />
43
+ <v-text-field v-model="location" label="Where (unit, tenant or area)" density="compact" hide-details clearable style="max-width: 240px" />
44
+ </div>
45
+
46
+ <v-data-table-server
47
+ :headers="headers"
48
+ :items="rows"
49
+ :items-length="total"
50
+ :loading="loading"
51
+ :items-per-page="limit"
52
+ :page="page"
53
+ density="compact"
54
+ class="border rounded"
55
+ :no-data-text="emptyText"
56
+ @update:page="(p: number) => ((page = p), load())"
57
+ >
58
+ <template #item.date="{ value }">{{ standardFormatDateTime(value) }}</template>
59
+ <template #item.what="{ item }">{{ historyText(item) }}</template>
60
+ <template #item.person="{ item }">{{ item.holderLabel || item.holderName || "—" }}</template>
61
+ <template #item.qty="{ item }">{{ item.qty }} {{ item.unit }}</template>
62
+ <template #item.where="{ item }">{{ whereText(item.location) || (item.type === "handover" ? item.remarks : "") || "—" }}</template>
63
+ <template #item.reference="{ item }">{{ item.type === "handover" ? "—" : item.reference || "—" }}</template>
64
+ <template #item.photos="{ item }">
65
+ <InventoryPhotoThumbs :photos="item.photos" :before-at="item.photosBeforeAt" :after-at="item.photosAfterAt" />
66
+ </template>
67
+ <template #item.by="{ item }">{{ item.createdByLabel || item.createdByName }}</template>
68
+ <template #item.holderBalance="{ value }">{{ value ?? "—" }}</template>
69
+ </v-data-table-server>
70
+ </v-card-text>
71
+ </v-card>
72
+ </v-dialog>
73
+ </template>
74
+
75
+ <script setup lang="ts">
76
+ import useInventory from "../composables/useInventory";
77
+ import useUtils from "../composables/useUtils";
78
+ import useLocalAuth from "../composables/useLocalAuth";
79
+ import { historyText, whereText } from "../utils/inventory-usage";
80
+
81
+ /**
82
+ * History of stock (owner 2026-09-29, "where they used their stock"): one person's (`holder`, "me" =
83
+ * the signed-in person), one item's (`item`: its whole chain across people), or both. Received from the
84
+ * store, hand-overs in and out, usage with where + work order + photos, and returns. Drafts are not history.
85
+ */
86
+ const show = defineModel<boolean>({ default: false });
87
+ const props = defineProps({
88
+ store: { type: Object as PropType<TInventoryStore>, required: true },
89
+ holder: { type: String, default: "" },
90
+ item: { type: String, default: "" },
91
+ title: { type: String, default: "History" },
92
+ /** Open on "Used" only (My usage). */
93
+ usageOnly: { type: Boolean, default: false },
94
+ });
95
+
96
+ const api = useInventory();
97
+ const { standardFormatDateTime } = useUtils();
98
+ const { currentUser } = useLocalAuth();
99
+ const selfId = computed(() => String(currentUser.value?._id ?? ""));
100
+ const rows = ref<TInventoryTransaction[]>([]);
101
+ const holdings = ref<TInventoryHolding[]>([]);
102
+ const total = ref(0);
103
+ const page = ref(1);
104
+ const limit = 50;
105
+ const loading = ref(false);
106
+ const from = ref("");
107
+ const to = ref("");
108
+ const type = ref("");
109
+ const location = ref("");
110
+ const itemPick = ref("");
111
+ const itemChoices = ref<{ value: string; title: string }[]>([]);
112
+
113
+ const typeChoices = [
114
+ { value: "", title: "Everything" },
115
+ { value: "use", title: "Used" },
116
+ { value: "issue", title: "Taken from store" },
117
+ { value: "handover", title: "Hand-overs" },
118
+ { value: "return", title: "Given back" },
119
+ ];
120
+
121
+ const intro = computed(() =>
122
+ props.usageOnly
123
+ ? "Where the stock was used: date, place, work order and the before / after photos. Filter by date, item or place."
124
+ : props.item && !props.holder
125
+ ? "Every movement of this item: into the store, to each person, between colleagues, where it was used and what came back."
126
+ : "Everything that happened to this stock: taken from the store, hand-overs, where it was used (with the work order and photos) and returns.",
127
+ );
128
+ const emptyText = computed(() => (from.value || to.value || type.value || location.value || itemPick.value ? "Nothing matches these filters." : "Nothing recorded yet."));
129
+ const headers = computed(() => [
130
+ { title: "Date", key: "date", sortable: false },
131
+ { title: "What happened", key: "what", sortable: false },
132
+ ...(props.item ? [] : [{ title: "Item", key: "itemName", sortable: false }]),
133
+ ...(props.holder ? [] : [{ title: "Person", key: "person", sortable: false }]),
134
+ { title: "Qty", key: "qty", sortable: false },
135
+ { title: "Where", key: "where", sortable: false },
136
+ { title: "Work order / ref", key: "reference", sortable: false },
137
+ { title: "Photos", key: "photos", sortable: false },
138
+ { title: "Recorded by", key: "by", sortable: false },
139
+ ...(props.holder ? [{ title: "In hand after", key: "holderBalance", sortable: false }] : []),
140
+ ]);
141
+
142
+ async function load() {
143
+ loading.value = true;
144
+ try {
145
+ const res = await api.getTransactions(props.store._id, {
146
+ page: page.value,
147
+ limit,
148
+ ...(props.holder ? { holder: props.holder } : {}),
149
+ ...(props.item || itemPick.value ? { item: props.item || itemPick.value } : {}),
150
+ ...(type.value ? { type: type.value } : {}),
151
+ ...(location.value ? { location: location.value } : {}),
152
+ ...(from.value ? { from: new Date(`${from.value}T00:00:00`).toISOString() } : {}),
153
+ ...(to.value ? { to: new Date(`${to.value}T23:59:59.999`).toISOString() } : {}),
154
+ });
155
+ rows.value = res.items;
156
+ total.value = res.total;
157
+ } catch (error: any) {
158
+ rows.value = [];
159
+ total.value = 0;
160
+ notify(error?.data?.message || "Could not load the history.", "error");
161
+ } finally {
162
+ loading.value = false;
163
+ }
164
+ }
165
+ const notify = inject<(text: string, color?: string) => void>("inventoryNotify", () => {});
166
+
167
+ async function loadHoldings() {
168
+ holdings.value = [];
169
+ // The API gives another person's holdings to the store's approvers / storekeepers only.
170
+ if (!props.holder || props.holder === "me" || props.holder === selfId.value || !(props.store.access.canApprove || props.store.access.canHandle)) return;
171
+ try {
172
+ holdings.value = await api.getHoldings(props.store._id, props.holder);
173
+ } catch {
174
+ holdings.value = [];
175
+ }
176
+ }
177
+
178
+ let resetting = false;
179
+ watch([from, to, type, location, itemPick], () => {
180
+ if (resetting) return;
181
+ page.value = 1;
182
+ load();
183
+ });
184
+ watch(show, async (open) => {
185
+ if (!open) return;
186
+ resetting = true;
187
+ page.value = 1;
188
+ from.value = "";
189
+ to.value = "";
190
+ location.value = "";
191
+ itemPick.value = "";
192
+ type.value = props.usageOnly ? "use" : "";
193
+ await nextTick();
194
+ resetting = false;
195
+ load();
196
+ loadHoldings();
197
+ if (!props.item) {
198
+ try {
199
+ itemChoices.value = (await api.getItems(props.store._id, { limit: 200 })).items.map((i) => ({ value: i._id, title: i.name }));
200
+ } catch {
201
+ itemChoices.value = [];
202
+ }
203
+ }
204
+ });
205
+ </script>
@@ -58,7 +58,7 @@
58
58
  </td>
59
59
  <td class="text-right">{{ t.type === "use" ? "" : t.storeBalance }}</td>
60
60
  <td class="text-caption">{{ describe(t) }}</td>
61
- <td class="text-caption">{{ t.createdByName }}</td>
61
+ <td class="text-caption">{{ (t as any).createdByLabel || t.createdByName }}</td>
62
62
  </tr>
63
63
  <tr v-if="!history.length && !loading">
64
64
  <td colspan="6" class="text-center text-medium-emphasis py-4">Nothing recorded yet.</td>
@@ -108,11 +108,14 @@ const formatDate = (d: string) => standardFormatDate(d);
108
108
  function describe(t: TInventoryTransaction) {
109
109
  const parts: string[] = [];
110
110
  if (t.type === "receive") parts.push(t.supplier || "", t.reference ? `PO ${t.reference}` : "");
111
- if (t.type === "issue") parts.push(`${t.qty} to ${t.holderName}`, t.reference || "");
112
- if (t.type === "return") parts.push(`${t.qty} from ${t.holderName}`);
111
+ const who = (t as any).holderLabel || t.holderName;
112
+ if (t.type === "issue") parts.push(`${t.qty} to ${who}`, t.reference || "");
113
+ if (t.type === "return") parts.push(`${t.qty} from ${who}`);
114
+ // Give to colleague: one row per holder.
115
+ if (t.type === "handover") parts.push(who || "", t.remarks || "");
113
116
  if (t.type === "use")
114
117
  parts.push(
115
- `${t.holderName} used ${t.qty}`,
118
+ `${who} used ${t.qty}`,
116
119
  t.location?.type === "tenant_unit" ? `${t.location.unitName} ${t.location.tenantName || ""}` : t.location?.area || "",
117
120
  t.reference || "",
118
121
  );
@@ -36,6 +36,21 @@
36
36
  <v-text-field v-model.number="f.unitCost" type="number" min="0" step="0.01" prefix="$" density="comfortable" :rules="[moneyRule]" hint="Price of one unit" persistent-hint />
37
37
  </v-col>
38
38
  </v-row>
39
+ <!-- Main store only: which service providers see and may request it (owner 2026-09-29). -->
40
+ <template v-if="store.ownerType === 'property'">
41
+ <InputLabel title="Used by" class="mb-1 mt-2" />
42
+ <v-select
43
+ :model-value="f.usedBy"
44
+ :items="usedByItems"
45
+ multiple
46
+ chips
47
+ closable-chips
48
+ density="comfortable"
49
+ :hint="USED_BY_HINT"
50
+ persistent-hint
51
+ @update:model-value="(v: string[]) => (f.usedBy = nextUsedBy(f.usedBy, v))"
52
+ />
53
+ </template>
39
54
  <!-- Stock already on the shelf, recorded like a delivery (one "Opening stock" entry in the item's history). -->
40
55
  <template v-if="!item">
41
56
  <div class="text-subtitle-2 mt-4">Opening stock</div>
@@ -93,6 +108,9 @@
93
108
  <script setup lang="ts">
94
109
  import useInventory, { pickedDate } from "../composables/useInventory";
95
110
  import useUtils from "../composables/useUtils";
111
+ import { normaliseUsedBy, nextUsedBy, USED_BY_ALL, USED_BY_ALL_LABEL, USED_BY_HINT, USED_BY_OPTIONS } from "../utils/inventory-used-by";
112
+
113
+ const usedByItems = [{ value: USED_BY_ALL, title: USED_BY_ALL_LABEL }, ...USED_BY_OPTIONS];
96
114
 
97
115
  const show = defineModel<boolean>({ default: false });
98
116
  const props = defineProps({
@@ -126,6 +144,7 @@ const blank = () => ({
126
144
  openingReceivedBy: "",
127
145
  openingRemarks: "",
128
146
  description: "",
147
+ usedBy: [USED_BY_ALL] as string[],
129
148
  });
130
149
  const today = new Date().toLocaleDateString("en-CA");
131
150
  const f = reactive(blank());
@@ -135,7 +154,7 @@ const inUse = ref(true);
135
154
  watch(show, (open) => {
136
155
  if (!open) return;
137
156
  const i = props.item;
138
- Object.assign(f, blank(), i ? { name: i.name, unit: i.unit, packing: i.packing ?? "", code: i.code, category: i.category, reorderLevel: i.reorderLevel, unitCost: i.unitCost ?? 0, description: i.description } : {});
157
+ Object.assign(f, blank(), i ? { name: i.name, unit: i.unit, packing: i.packing ?? "", code: i.code, category: i.category, reorderLevel: i.reorderLevel, unitCost: i.unitCost ?? 0, description: i.description, usedBy: normaliseUsedBy(i.usedBy) } : {});
139
158
  photo.value = i?.photo ? [i.photo] : [];
140
159
  inUse.value = i ? i.status === "active" : true;
141
160
  });
@@ -154,6 +173,7 @@ async function save() {
154
173
  unitCost: Number(f.unitCost || 0),
155
174
  description: f.description,
156
175
  photo: photo.value[0] ?? "",
176
+ ...(props.store.ownerType === "property" ? { usedBy: normaliseUsedBy(f.usedBy) } : {}),
157
177
  };
158
178
  try {
159
179
  if (props.item) {
@@ -16,19 +16,23 @@
16
16
  v-if="mode === 'issue'"
17
17
  v-model="holder"
18
18
  :items="people"
19
- item-title="name"
19
+ :item-title="(p: any) => p.label || p.name"
20
20
  item-value="_id"
21
21
  :loading="peopleLoading"
22
22
  placeholder="Search a name"
23
23
  density="comfortable"
24
24
  :rules="[requiredRule]"
25
25
  no-filter
26
+ hint="Everyone at this property who can take stock: your staff and the service providers' people."
27
+ persistent-hint
26
28
  @update:search="searchPeople"
27
29
  >
28
30
  <template #item="{ props: p, item }">
29
- <v-list-item v-bind="p" :subtitle="item.raw.orgName" />
31
+ <v-list-subheader v-if="groupStarts(item.raw)">{{ item.raw.company || "Other" }}</v-list-subheader>
32
+ <v-list-item v-bind="p" :title="item.raw.name" :subtitle="[item.raw.serviceLabel, item.raw.email].filter(Boolean).join(' · ')" />
30
33
  </template>
31
34
  </v-autocomplete>
35
+ <v-alert v-if="usedByWarning" type="warning" variant="tonal" density="compact" class="mt-2">{{ usedByWarning }}</v-alert>
32
36
  <v-text-field v-else :model-value="holderName" density="comfortable" readonly />
33
37
  </template>
34
38
 
@@ -129,6 +133,7 @@
129
133
  <script setup lang="ts">
130
134
  import useInventory, { pickedDate } from "../composables/useInventory";
131
135
  import useUtils from "../composables/useUtils";
136
+ import { normaliseUsedBy, usedByChips, usedByText } from "../utils/inventory-used-by";
132
137
 
133
138
  type TMode = "receive" | "issue" | "request" | "return";
134
139
  type TRow = { item: string; qty: number | null; unitCost?: number | null };
@@ -230,7 +235,24 @@ async function loadItems(search = "") {
230
235
  }
231
236
  const searchItems = debounce((q: string) => loadItems(q || ""), 300);
232
237
 
233
- const people = ref<{ _id: string; name: string; orgName: string }[]>([]);
238
+ type TPerson = { _id: string; name: string; orgName: string; email?: string; company?: string; service?: string; serviceLabel?: string; label?: string };
239
+ const people = ref<TPerson[]>([]);
240
+ /** A company heading above the first person of each company (the server sorts by company). */
241
+ const groupStarts = (p: TPerson) => {
242
+ const i = people.value.indexOf(p);
243
+ return i === 0 || (i > 0 && people.value[i - 1].company !== p.company);
244
+ };
245
+ /** Used by: warn before the server refuses an item meant for another service. */
246
+ const usedByWarning = computed(() => {
247
+ if (props.mode !== "issue" || !holder.value) return "";
248
+ const person = people.value.find((p) => p._id === holder.value);
249
+ if (!person?.service || person.service === "property") return "";
250
+ const blocked = rows.value
251
+ .map((r) => itemChoices.value.find((c) => c._id === r.item))
252
+ .filter((i): i is TInventoryItem => Boolean(i) && usedByChips(i!.usedBy).length > 0 && !normaliseUsedBy(i!.usedBy).includes(person.service!));
253
+ if (!blocked.length) return "";
254
+ return `${blocked.map((i) => i.name).join(", ")} ${blocked.length === 1 ? "is" : "are"} for ${usedByText(blocked[0].usedBy)} only — ${person.name} works for ${person.serviceLabel || "another service"}.`;
255
+ });
234
256
  const peopleLoading = ref(false);
235
257
  const searchPeople = debounce(async (q: string) => {
236
258
  peopleLoading.value = true;
@@ -82,6 +82,23 @@
82
82
  </AppCard>
83
83
 
84
84
  <template v-else>
85
+ <!-- Owner rule 2026-09-29: another organisation's store is for requesting only. -->
86
+ <v-alert v-if="requestOnly" type="info" variant="tonal" density="compact" class="mb-4" icon="mdi-cart-arrow-down">
87
+ <div class="font-weight-medium">Request items from the main store</div>
88
+ <div class="text-body-2">Pick the items you need and press Request Stock. You are told when the store keeper approves it.</div>
89
+ </v-alert>
90
+ <v-alert
91
+ v-else-if="store.access.canApprove && pendingCount > 0 && tab !== 'requests'"
92
+ type="warning"
93
+ variant="tonal"
94
+ density="compact"
95
+ class="mb-4"
96
+ >
97
+ <div class="d-flex align-center flex-wrap ga-2">
98
+ <span>{{ pendingCount === 1 ? "1 request is" : `${pendingCount} requests are` }} waiting for your approval.</span>
99
+ <v-btn size="small" variant="flat" color="warning" class="text-none" @click="tab = 'requests'">Review requests</v-btn>
100
+ </div>
101
+ </v-alert>
85
102
  <v-tabs v-model="tab" color="primary" class="mb-4" show-arrows>
86
103
  <v-tab v-for="t in tabs" :key="t.value" :value="t.value" class="text-none">
87
104
  {{ t.label }}
@@ -102,7 +119,7 @@
102
119
  <v-window-item v-if="ctl.tabs.people" value="technicians">
103
120
  <InventoryTechniciansTab :store="store" @changed="refreshCounts" />
104
121
  </v-window-item>
105
- <v-window-item v-if="ctl.tabs.reports" value="reports">
122
+ <v-window-item v-if="ctl.tabs.reports || companyUsage" value="reports">
106
123
  <InventoryReportsTab :store="store" :sites="storeSites" />
107
124
  </v-window-item>
108
125
  <v-window-item v-if="ctl.tabs.suppliers" value="suppliers">
@@ -123,7 +140,7 @@ import useInventory from "../composables/useInventory";
123
140
  import useWorkerTerm from "../composables/useWorkerTerm";
124
141
  import useSite from "../composables/useSite";
125
142
  import useUtils from "../composables/useUtils";
126
- import { inventoryStoreLabel, inventorySuppliesText } from "../utils/inventory-store-label";
143
+ import { inventoryManagedBy, inventoryStoreLabel, inventorySuppliesText } from "../utils/inventory-store-label";
127
144
  import { inventoryControls } from "../utils/inventory-presets";
128
145
 
129
146
  const props = defineProps({
@@ -133,7 +150,7 @@ const props = defineProps({
133
150
 
134
151
  const movedFromEquipment = useRoute().query.moved === "equipment";
135
152
 
136
- const { getStores, createStore: apiCreateStore, getRequests } = useInventory();
153
+ const { getStores, createStore: apiCreateStore, getRequests, getReport } = useInventory();
137
154
  const { getSites } = useSite();
138
155
  const { requiredRule } = useUtils();
139
156
 
@@ -167,8 +184,16 @@ const storeSites = computed(() =>
167
184
  })),
168
185
  );
169
186
 
187
+ // Another organisation's (main) store: the person requests from it and manages nothing there.
188
+ const requestOnly = computed(() => Boolean(store.value && store.value.owner === false));
189
+ const managedBy = computed(() => (store.value ? inventoryManagedBy(store.value) : ""));
190
+
170
191
  const subtitle = computed(() => {
171
192
  if (!store.value) return "";
193
+ if (store.value.main && store.value.owner === false) {
194
+ // "Main store · managed by Management Pte Ltd (Property management)"
195
+ return managedBy.value ? `Main store · ${managedBy.value.replace(/^Managed/, "managed")}` : "Main store";
196
+ }
172
197
  if (storeSites.value.length > 1) {
173
198
  const supplies = inventorySuppliesText(storeSites.value.map((s) => s.name));
174
199
  return stores.value.length > 1 ? `${inventoryStoreLabel(store.value, stores.value)} · ${supplies}` : supplies;
@@ -191,6 +216,7 @@ const tabs = computed(() => {
191
216
  ...(ctl.value.tabs.requests ? [{ value: "requests", label: a.canApprove ? "Requests" : "My Requests", badge: a.canApprove ? pendingCount.value : 0 }] : []),
192
217
  ...(ctl.value.tabs.people ? [{ value: "technicians", label: worker.many }] : []),
193
218
  ...(ctl.value.tabs.reports ? [{ value: "reports", label: a.canReports ? "Reports" : "Usage check" }] : []),
219
+ ...(!ctl.value.tabs.reports && companyUsage.value ? [{ value: "reports", label: "Company usage" }] : []),
194
220
  ...(ctl.value.tabs.suppliers ? [{ value: "suppliers", label: "Suppliers" }] : []),
195
221
  ...(ctl.value.tabs.settings ? [{ value: "settings", label: "Settings" }] : []),
196
222
  ];
@@ -214,6 +240,25 @@ async function loadStores() {
214
240
  refreshCounts();
215
241
  }
216
242
 
243
+ /**
244
+ * Another company's supervisor at the main store may track its OWN people's usage (the API answers the usage
245
+ * report for them, scoped). The store's access flags do not say so, so ask once per store.
246
+ * ponytail: one probe request per store switch; add a flag to the store's access if the API grows one.
247
+ */
248
+ const companyUsage = ref(false);
249
+ async function probeCompanyUsage() {
250
+ companyUsage.value = false;
251
+ const s = store.value;
252
+ if (!s || ctl.value.tabs.reports || !s.main || s.owner !== false) return;
253
+ const now = new Date().toISOString();
254
+ try {
255
+ await getReport(s._id, "usage", { from: now, to: now });
256
+ companyUsage.value = true;
257
+ } catch {
258
+ companyUsage.value = false;
259
+ }
260
+ }
261
+
217
262
  async function refreshCounts() {
218
263
  if (!store.value?.access.canApprove) return (pendingCount.value = 0);
219
264
  try {
@@ -252,6 +297,7 @@ async function createStore() {
252
297
  watch(storeId, () => {
253
298
  tab.value = "stock";
254
299
  refreshCounts();
300
+ probeCompanyUsage();
255
301
  });
256
302
  watch(() => props.siteId, loadStores);
257
303