@7365admin1/layer-common 4.90.2-staging.493 → 4.90.2-staging.494
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.
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
<InventoryMyStockTab :store="store" :site-id="siteId" :sites="storeSites" @changed="refreshCounts" />
|
|
137
137
|
</v-window-item>
|
|
138
138
|
<v-window-item v-if="ctl.tabs.requests" value="requests">
|
|
139
|
-
<InventoryRequestsTab :store="store" :site-id="siteId" @changed="refreshCounts" />
|
|
139
|
+
<InventoryRequestsTab :store="store" :site-id="siteId" :focus-request="focusRequest" @changed="refreshCounts" />
|
|
140
140
|
</v-window-item>
|
|
141
141
|
<v-window-item v-if="ctl.tabs.people" value="technicians">
|
|
142
142
|
<InventoryTechniciansTab :store="store" @changed="refreshCounts" />
|
|
@@ -177,6 +177,8 @@ const movedFromEquipment = useRoute().query.moved === "equipment";
|
|
|
177
177
|
// A notification link (`?storeId=&tab=`, NotificationBell): open that store on that tab, once.
|
|
178
178
|
const linkQuery = useRoute().query;
|
|
179
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 ?? "");
|
|
180
182
|
|
|
181
183
|
const { getHandovers, getStores, getStore, createStore: apiCreateStore, getRequests, getReport } = useInventory();
|
|
182
184
|
const { getSites } = useSite();
|
|
@@ -59,6 +59,19 @@
|
|
|
59
59
|
</AppCard>
|
|
60
60
|
</template>
|
|
61
61
|
|
|
62
|
+
<!-- Colleagues' open drafts (same company): any technician can take one over and finish it from their own stock. -->
|
|
63
|
+
<template v-if="colleagueDrafts.length">
|
|
64
|
+
<div class="text-subtitle-2 mb-2">Colleagues' jobs in progress ({{ colleagueDrafts.length }})</div>
|
|
65
|
+
<AppCard v-for="d in colleagueDrafts" :key="d._id" class="pa-3 mb-2 d-flex flex-wrap align-center ga-3" style="border-left: 4px solid rgb(var(--v-theme-secondary))">
|
|
66
|
+
<InventoryPhotoThumbs :photos="{ before: d.photos?.before ?? [] }" :before-at="d.photosBeforeAt" :size="44" />
|
|
67
|
+
<div class="flex-grow-1" style="min-width: 200px">
|
|
68
|
+
<div class="font-weight-medium">{{ d.itemName }} × {{ d.qty }} {{ d.unit }}</div>
|
|
69
|
+
<div class="text-body-2">{{ colleagueDraftText(d, standardFormatDateTime) }}</div>
|
|
70
|
+
</div>
|
|
71
|
+
<v-btn class="text-none screen-btn-primary" variant="flat" size="small" :loading="acting === d._id + 't'" @click="takeOver(d)">Take over</v-btn>
|
|
72
|
+
</AppCard>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
62
75
|
<v-progress-linear v-if="loading" indeterminate color="primary" />
|
|
63
76
|
<AppCard v-else-if="!holdings.length" class="pa-6 text-center">
|
|
64
77
|
<div class="text-body-1 mb-1">You are not holding any stock.</div>
|
|
@@ -221,7 +234,7 @@ import useFile from "../composables/useFile";
|
|
|
221
234
|
import useUtils from "../composables/useUtils";
|
|
222
235
|
import useLocalAuth from "../composables/useLocalAuth";
|
|
223
236
|
import { inventoryControls } from "../utils/inventory-presets";
|
|
224
|
-
import { draftCardText } from "../utils/inventory-usage";
|
|
237
|
+
import { colleagueDraftText, draftCardText } from "../utils/inventory-usage";
|
|
225
238
|
import { ackText, inventoryStepsLive } from "../utils/inventory-ack";
|
|
226
239
|
|
|
227
240
|
const props = defineProps({
|
|
@@ -320,6 +333,31 @@ async function loadDrafts() {
|
|
|
320
333
|
drafts.value = [];
|
|
321
334
|
}
|
|
322
335
|
}
|
|
336
|
+
const colleagueDrafts = ref<TInventoryUsageDraft[]>([]);
|
|
337
|
+
async function loadColleagueDrafts() {
|
|
338
|
+
// stepsLive = the API knows ?colleagues (an older API ignores it and returns my own, hence the mine/holder filter too).
|
|
339
|
+
if (!ctl.value.recordUsage || !stepsLive.value) return (colleagueDrafts.value = []);
|
|
340
|
+
try {
|
|
341
|
+
const res = await api.getUsageDrafts(props.store._id, "colleagues");
|
|
342
|
+
colleagueDrafts.value = res.items.filter((d) => !d.mine && String(d.holder) !== me.value);
|
|
343
|
+
} catch {
|
|
344
|
+
colleagueDrafts.value = []; // 403/404: not offered, nothing to show
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async function takeOver(d: TInventoryUsageDraft) {
|
|
348
|
+
const who = d.holderName || d.startedByName || "your colleague";
|
|
349
|
+
if (!confirm(`Take over ${who}'s job (${d.qty} ${d.unit ?? ""} ${d.itemName})? You finish it (after photo, submit), and the stock comes from YOUR own stock, not theirs.`)) return;
|
|
350
|
+
acting.value = d._id + "t";
|
|
351
|
+
try {
|
|
352
|
+
await api.takeOverDraft(String(d._id));
|
|
353
|
+
notify("The job is yours now. Finish it under My drafts.");
|
|
354
|
+
changed();
|
|
355
|
+
} catch (error: any) {
|
|
356
|
+
notify(error?.data?.message || "Could not take over the job.", "error");
|
|
357
|
+
} finally {
|
|
358
|
+
acting.value = "";
|
|
359
|
+
}
|
|
360
|
+
}
|
|
323
361
|
const notify = inject<(text: string, color?: string) => void>("inventoryNotify", () => {});
|
|
324
362
|
|
|
325
363
|
const handovers = ref<TInventoryHandover[]>([]);
|
|
@@ -436,11 +474,12 @@ function changed() {
|
|
|
436
474
|
load();
|
|
437
475
|
loadHandovers();
|
|
438
476
|
loadDrafts();
|
|
477
|
+
loadColleagueDrafts();
|
|
439
478
|
loadToReceive();
|
|
440
479
|
emit("changed");
|
|
441
480
|
}
|
|
442
481
|
|
|
443
|
-
watch(() => props.store._id, () => (load(), loadHandovers(), loadDrafts(), loadToReceive()));
|
|
444
|
-
watch(stepsLive, loadToReceive);
|
|
445
|
-
onMounted(() => (load(), loadHandovers(), loadDrafts(), loadToReceive()));
|
|
482
|
+
watch(() => props.store._id, () => (load(), loadHandovers(), loadDrafts(), loadColleagueDrafts(), loadToReceive()));
|
|
483
|
+
watch(stepsLive, () => (loadToReceive(), loadColleagueDrafts()));
|
|
484
|
+
onMounted(() => (load(), loadHandovers(), loadDrafts(), loadColleagueDrafts(), loadToReceive()));
|
|
446
485
|
</script>
|
|
@@ -28,7 +28,12 @@
|
|
|
28
28
|
{{ search ? `No requests match "${search}". Try another word, or pick "All" to look through every status.` : status === "confirm" ? "Nothing is waiting for anyone to tap Received." : status === "pending" ? (store.access.canApprove ? "No requests waiting for approval." : "You have no requests waiting. Use Request Stock to ask the store for items.") : "No requests to show." }}
|
|
29
29
|
</div>
|
|
30
30
|
|
|
31
|
-
<AppCard
|
|
31
|
+
<AppCard
|
|
32
|
+
v-for="r in requests"
|
|
33
|
+
:key="r._id"
|
|
34
|
+
class="pa-4 mb-3"
|
|
35
|
+
:style="r._id === focusRequest ? { borderLeft: '4px solid rgb(var(--v-theme-primary))' } : undefined"
|
|
36
|
+
>
|
|
32
37
|
<div class="d-flex flex-wrap align-center ga-2 mb-2">
|
|
33
38
|
<div class="font-weight-medium">{{ r.requestedByLabel || r.requestedByName || "Someone" }}</div>
|
|
34
39
|
<div class="text-caption text-medium-emphasis">{{ standardFormatDateTime(r.createdAt) }}</div>
|
|
@@ -111,6 +116,8 @@ import { ACK_CHIP, ackText, approveCap, inventoryStepsLive } from "../utils/inve
|
|
|
111
116
|
const props = defineProps({
|
|
112
117
|
store: { type: Object as PropType<TInventoryStore>, required: true },
|
|
113
118
|
siteId: { type: String, required: true },
|
|
119
|
+
/** A request to open first (notification link). Shown on top, marked, in the "All" list. */
|
|
120
|
+
focusRequest: { type: String, default: "" },
|
|
114
121
|
});
|
|
115
122
|
const ctl = computed(() => inventoryControls(props.store.access));
|
|
116
123
|
const emit = defineEmits(["changed"]);
|
|
@@ -123,7 +130,7 @@ const notify = inject<(text: string, color?: string) => void>("inventoryNotify",
|
|
|
123
130
|
const chip = REQUEST_STATUS;
|
|
124
131
|
const stepsLive = computed(() => inventoryStepsLive(props.store));
|
|
125
132
|
|
|
126
|
-
const status = ref("pending");
|
|
133
|
+
const status = ref(props.focusRequest ? "" : "pending");
|
|
127
134
|
const search = ref("");
|
|
128
135
|
const requests = ref<TInventoryRequest[]>([]);
|
|
129
136
|
const page = ref(1);
|
|
@@ -141,7 +148,10 @@ async function load(p = 1) {
|
|
|
141
148
|
const confirm = status.value === "confirm";
|
|
142
149
|
const res = await api.getRequests(props.store._id, { page: p, limit: confirm ? 100 : 20, status: confirm ? "issued" : status.value || undefined, search: search.value?.trim() || undefined });
|
|
143
150
|
const got = confirm ? res.items.filter((r) => r.ackStatus === "awaiting" || r.ackStatus === "not_received") : res.items;
|
|
144
|
-
|
|
151
|
+
// ponytail: the linked request is found in the first page of "All" (the newest 20); an older one just shows the list.
|
|
152
|
+
const focused = p === 1 && props.focusRequest ? got.find((r) => r._id === props.focusRequest) : undefined;
|
|
153
|
+
const rows = focused ? [focused, ...got.filter((r) => r !== focused)] : got;
|
|
154
|
+
requests.value = p === 1 ? rows : [...requests.value, ...got];
|
|
145
155
|
page.value = p;
|
|
146
156
|
pages.value = res.pages;
|
|
147
157
|
} catch {
|
|
@@ -51,8 +51,9 @@ export default function useInventory() {
|
|
|
51
51
|
receiveReturn: (store: string, body: Record<string, any>) => api(`/stores/${store}/returns`, { method: "POST", body }),
|
|
52
52
|
recordUsage: (store: string, body: Record<string, any>) => api(`/stores/${store}/usage`, { method: "POST", body }),
|
|
53
53
|
/** Usage drafts: before photo now, after photo and submit after the job (stock is deducted on submit). */
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
/** Own drafts; "colleagues" = same-company colleagues' open drafts (anyone who records usage); "everyone" = supervisors. */
|
|
55
|
+
getUsageDrafts: (store: string, scope: "" | "colleagues" | "everyone" = "") =>
|
|
56
|
+
api<{ items: TInventoryUsageDraft[]; staleAfterDays: number }>(`/stores/${store}/usage-drafts`, { query: scope ? { [scope]: true } : {} }),
|
|
56
57
|
createUsageDraft: (store: string, body: Record<string, any>) => api<TInventoryUsageDraft>(`/stores/${store}/usage-drafts`, { method: "POST", body }),
|
|
57
58
|
updateUsageDraft: (id: string, body: Record<string, any>) => api<TInventoryUsageDraft>(`/usage-drafts/${id}`, { method: "PATCH", body }),
|
|
58
59
|
discardUsageDraft: (id: string) => api(`/usage-drafts/${id}/discard`, { method: "POST" }),
|
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.90.2-staging.
|
|
5
|
+
"version": "4.90.2-staging.494",
|
|
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.",
|
package/utils/inventory-usage.ts
CHANGED
|
@@ -36,6 +36,17 @@ export function draftCardText(d: TDraft, time: (iso: string) => string, staleDay
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** "Kevin started this job at Unit 05-12 at 9:40 - take it over to finish it with your own stock." (a colleague's open draft) */
|
|
40
|
+
export function colleagueDraftText(
|
|
41
|
+
d: { startedByName?: string; holderName?: string; location?: TLocation; reference?: string; draftedAt?: string; photosBeforeAt?: string | null },
|
|
42
|
+
time: (iso: string) => string,
|
|
43
|
+
) {
|
|
44
|
+
const who = d.holderName || d.startedByName || "A colleague";
|
|
45
|
+
const where = whereText(d.location) || d.reference || "";
|
|
46
|
+
const at = d.draftedAt || d.photosBeforeAt;
|
|
47
|
+
return `${who} started this job${where ? ` at ${where}` : ""}${at ? ` at ${time(at)}` : ""} - take it over to finish it with your own stock.`;
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
/** "Issued to Ali by Keeper (29/09) → Handed to Bala by Ali (29/09)". */
|
|
40
51
|
export function usagePathText(steps: TStep[] | undefined, day: (iso: string) => string) {
|
|
41
52
|
if (!steps?.length) return "";
|
|
@@ -45,7 +45,7 @@ export type TTargetRow = {
|
|
|
45
45
|
* (core `withNotificationKind`); `site` stands in for `siteId` on rows kept
|
|
46
46
|
* unscoped on purpose. Older rows have neither and are read from `module`.
|
|
47
47
|
*/
|
|
48
|
-
params?: { id?: string; org?: string; kind?: string; site?: string; storeId?: string } | null;
|
|
48
|
+
params?: { id?: string; org?: string; kind?: string; site?: string; storeId?: string; requestId?: string } | null;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
/** What the surrounding app knows that the stored row does not. */
|
|
@@ -144,7 +144,7 @@ export function notificationKind(item: TTargetRow | null | undefined): string {
|
|
|
144
144
|
*/
|
|
145
145
|
export function inventoryTabForAlert(title: string): "requests" | "stock" | "mine" | "" {
|
|
146
146
|
if (/stock handed to you|hand-over (confirmed|declined)/i.test(title)) return "mine";
|
|
147
|
-
if (/request waiting|request (was )?(approved|rejected)/i.test(title)) return "requests";
|
|
147
|
+
if (/request waiting|request (was )?(approved|rejected)|confirm you received/i.test(title)) return "requests";
|
|
148
148
|
if (/low (on )?stock/i.test(title)) return "stock";
|
|
149
149
|
return "";
|
|
150
150
|
}
|
|
@@ -237,7 +237,13 @@ function sitePageTarget(
|
|
|
237
237
|
// Open the alert's own store on the tab it is about (InventoryMain reads ?storeId=&tab=).
|
|
238
238
|
const storeId = String(item.params?.storeId ?? item.params?.id ?? "").trim();
|
|
239
239
|
const tab = inventoryTabForAlert(String(item.title ?? ""));
|
|
240
|
-
|
|
240
|
+
// Approved / please confirm / reminder alerts name their request (API 2026-10-03): open that request.
|
|
241
|
+
const requestId = String(item.params?.requestId ?? "").trim();
|
|
242
|
+
const query: Record<string, string> = {
|
|
243
|
+
...(storeId ? { storeId } : {}),
|
|
244
|
+
...(tab || requestId ? { tab: tab || "requests" } : {}),
|
|
245
|
+
...(requestId ? { requestId } : {}),
|
|
246
|
+
};
|
|
241
247
|
return Object.keys(query).length ? { candidates, query } : { candidates };
|
|
242
248
|
}
|
|
243
249
|
|