@7365admin1/layer-common 4.90.2-staging.491 → 4.90.3
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 +16 -4
- package/assets/css/primitives.css +0 -57
- package/assets/css/screens.css +4 -8
- package/components/AccessCardDetailsDialog.vue +6 -59
- package/components/AccessCardPreviewDialog.vue +53 -371
- package/components/AppSelect.vue +4 -143
- package/components/BuildingUnitFormEdit.vue +0 -6
- package/components/Dialog/UpdateMoreAction.vue +1 -11
- package/components/DocumentForm.vue +112 -407
- package/components/DocumentManagement.vue +12 -213
- package/components/Facility/BookingSetup.vue +0 -63
- package/components/HidAccessLogDashboard.vue +17 -74
- package/components/HidIntercomManagement.vue +14 -183
- package/components/HidQrCodeConfiguration.vue +195 -17
- package/components/HidReaderManagement.vue +0 -398
- package/components/HidReaderUserRoster.vue +3 -44
- package/components/HidUserEnrollment.vue +292 -2255
- package/components/InventoryItemDetail.vue +1 -2
- package/components/InventoryLinesDialog.vue +13 -91
- package/components/InventoryPhotoInput.vue +6 -44
- package/components/InventoryReportsTab.vue +3 -23
- package/components/InventoryRequestsTab.vue +4 -27
- package/components/InventoryStockTab.vue +0 -3
- package/components/InvitationClientForm.vue +1 -19
- package/components/InvitationForm.vue +4 -31
- package/components/InvitationMain.vue +22 -124
- package/components/SiteSettings.vue +0 -48
- package/components/TableMain.vue +7 -22
- package/components/VehicleManagement.vue +5 -80
- package/components/VisitorForm.vue +0 -12
- package/components/VisitorManagement.vue +5 -267
- package/composables/useAccessManagement.ts +1 -17
- package/composables/useDocument.ts +0 -7
- package/composables/useFacility.ts +0 -11
- package/composables/useHidAmico.ts +0 -165
- package/composables/useHidNavigation.ts +1 -25
- package/composables/useInventory.ts +1 -4
- package/composables/useMember.ts +0 -12
- package/composables/useNFCPatrolReportFilters.ts +3 -21
- package/composables/useNFCPatrolRoute.ts +1 -8
- package/composables/usePeople.ts +0 -19
- package/composables/useServiceProvider.ts +2 -45
- package/composables/useSettingsPermission.ts +0 -14
- package/composables/useUser.ts +2 -6
- package/composables/useVehicle.ts +1 -40
- package/composables/useVerification.ts +0 -3
- package/middleware/01.auth.ts +7 -3
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
- package/types/document.d.ts +0 -14
- package/types/facility.d.ts +0 -4
- package/types/inventory.d.ts +0 -27
- package/types/member.d.ts +0 -4
- package/types/people.d.ts +1 -28
- package/types/service-provider.d.ts +0 -5
- package/types/site.d.ts +1 -7
- package/components/BuildingUnitManagement.vue +0 -298
- package/components/HidAccessPermissions.vue +0 -424
- package/components/HidProfileQrCode.vue +0 -332
- package/components/InventoryPoDialog.vue +0 -111
- package/components/VehicleQrStickerDialog.vue +0 -249
- package/composables/useHidReaderSelection.ts +0 -50
- package/composables/useSiteCategory.ts +0 -45
- package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
- package/utils/hid-enrolment-subject.ts +0 -344
- package/utils/hid-permission-assignments.ts +0 -198
- package/utils/hid-reader-selection.ts +0 -52
- package/utils/inventory-receiving.ts +0 -90
- package/utils/invite-group.ts +0 -61
- package/utils/invite-sites.ts +0 -25
- package/utils/occupancy-role.ts +0 -109
- package/utils/service-type.ts +0 -30
- package/utils/vehicle-qr-sticker.ts +0 -184
|
@@ -79,7 +79,6 @@ import useFile from "../composables/useFile";
|
|
|
79
79
|
import useUtils from "../composables/useUtils";
|
|
80
80
|
import { inventoryItemMeta } from "../utils/inventory-store-label";
|
|
81
81
|
import { inventoryControls } from "../utils/inventory-presets";
|
|
82
|
-
import { receiptRefText } from "../utils/inventory-receiving";
|
|
83
82
|
|
|
84
83
|
const show = defineModel<boolean>({ default: false });
|
|
85
84
|
const props = defineProps({
|
|
@@ -108,7 +107,7 @@ const formatDate = (d: string) => standardFormatDate(d);
|
|
|
108
107
|
|
|
109
108
|
function describe(t: TInventoryTransaction) {
|
|
110
109
|
const parts: string[] = [];
|
|
111
|
-
if (t.type === "receive") parts.push(t.supplier || "",
|
|
110
|
+
if (t.type === "receive") parts.push(t.supplier || "", t.reference ? `PO ${t.reference}` : "");
|
|
112
111
|
const who = (t as any).holderLabel || t.holderName;
|
|
113
112
|
if (t.type === "issue") parts.push(`${t.qty} to ${who}`, t.reference || "");
|
|
114
113
|
if (t.type === "return") parts.push(`${t.qty} from ${who}`);
|
|
@@ -10,13 +10,6 @@
|
|
|
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
12
|
<v-form ref="form">
|
|
13
|
-
<template v-if="mode === 'receive'">
|
|
14
|
-
<InputLabel title="How did this stock come in?" required class="mb-1" />
|
|
15
|
-
<v-btn-toggle v-model="receivingType" mandatory divided variant="outlined" density="comfortable" class="mb-1 receiving-type">
|
|
16
|
-
<v-btn v-for="t in RECEIVING_TYPES" :key="t.value" :value="t.value" class="text-none">{{ t.title }}</v-btn>
|
|
17
|
-
</v-btn-toggle>
|
|
18
|
-
<p class="text-caption text-medium-emphasis mb-3">{{ receivingHelp }}</p>
|
|
19
|
-
</template>
|
|
20
13
|
<template v-if="mode === 'issue' || mode === 'return'">
|
|
21
14
|
<InputLabel :title="mode === 'issue' ? 'Give to' : 'Returned by'" required class="mb-1" />
|
|
22
15
|
<v-autocomplete
|
|
@@ -72,18 +65,6 @@
|
|
|
72
65
|
:rules="[qtyRule(line)]"
|
|
73
66
|
:suffix="unitOf(line.item)"
|
|
74
67
|
/>
|
|
75
|
-
<v-text-field
|
|
76
|
-
v-if="mode === 'receive' && receivingType !== 'CASH'"
|
|
77
|
-
v-model.number="line.ordered"
|
|
78
|
-
type="number"
|
|
79
|
-
min="1"
|
|
80
|
-
label="Ordered (optional)"
|
|
81
|
-
density="comfortable"
|
|
82
|
-
style="max-width: 150px"
|
|
83
|
-
:rules="[orderedRule]"
|
|
84
|
-
hint="Qty on the PO"
|
|
85
|
-
persistent-hint
|
|
86
|
-
/>
|
|
87
68
|
<v-text-field
|
|
88
69
|
v-if="mode === 'receive'"
|
|
89
70
|
v-model.number="line.unitCost"
|
|
@@ -108,35 +89,18 @@
|
|
|
108
89
|
<InputLabel title="Supplier" class="mb-1" />
|
|
109
90
|
<InventorySupplierField v-if="show" v-model="supplier" :store-id="store._id" />
|
|
110
91
|
</v-col>
|
|
111
|
-
<v-col cols="12" md="
|
|
92
|
+
<v-col cols="12" md="3">
|
|
93
|
+
<InputLabel title="PO number" class="mb-1" />
|
|
94
|
+
<v-text-field v-model="reference" density="comfortable" />
|
|
95
|
+
</v-col>
|
|
96
|
+
<v-col cols="12" md="3">
|
|
112
97
|
<InputLabel title="Date received" class="mb-1" />
|
|
113
98
|
<v-text-field v-model="date" type="date" :max="today" density="comfortable" />
|
|
114
99
|
</v-col>
|
|
115
|
-
<v-col v-if="receivingType !== 'CASH'" cols="12" md="4">
|
|
116
|
-
<InputLabel :title="receivingType === 'PO' ? 'PO number' : 'PO number (optional)'" :required="receivingType === 'PO'" class="mb-1" />
|
|
117
|
-
<v-text-field v-model="reference" density="comfortable" placeholder="e.g. PO-0412" />
|
|
118
|
-
</v-col>
|
|
119
|
-
<template v-if="receivingType === 'DO'">
|
|
120
|
-
<v-col cols="12" sm="6" md="4">
|
|
121
|
-
<InputLabel title="DO number" required class="mb-1" />
|
|
122
|
-
<v-text-field v-model="doNumber" density="comfortable" />
|
|
123
|
-
</v-col>
|
|
124
|
-
<v-col cols="12" sm="6" md="4">
|
|
125
|
-
<InputLabel title="DO date" required class="mb-1" />
|
|
126
|
-
<v-text-field v-model="doDate" type="date" :max="today" density="comfortable" />
|
|
127
|
-
</v-col>
|
|
128
|
-
</template>
|
|
129
|
-
<v-col v-if="receivingType === 'CASH'" cols="12" md="6">
|
|
130
|
-
<InputLabel title="Receipt number (optional)" class="mb-1" />
|
|
131
|
-
<v-text-field v-model="receiptNumber" density="comfortable" placeholder="From the shop receipt" />
|
|
132
|
-
</v-col>
|
|
133
100
|
<v-col cols="12">
|
|
134
|
-
<InputLabel title="Delivery
|
|
101
|
+
<InputLabel title="Delivery photo (optional)" class="mb-1" />
|
|
135
102
|
<InventoryPhotoInput v-model="photos" />
|
|
136
103
|
</v-col>
|
|
137
|
-
<v-col v-if="receivingError" cols="12">
|
|
138
|
-
<v-alert type="warning" variant="tonal" density="compact">{{ receivingError }}</v-alert>
|
|
139
|
-
</v-col>
|
|
140
104
|
</v-row>
|
|
141
105
|
|
|
142
106
|
<template v-if="mode === 'request' || mode === 'issue'">
|
|
@@ -170,10 +134,9 @@
|
|
|
170
134
|
import useInventory, { pickedDate } from "../composables/useInventory";
|
|
171
135
|
import useUtils from "../composables/useUtils";
|
|
172
136
|
import { normaliseUsedBy, usedByChips, usedByText } from "../utils/inventory-used-by";
|
|
173
|
-
import { RECEIVING_TYPES, receivingFields, receivingProblem, type TReceivingType } from "../utils/inventory-receiving";
|
|
174
137
|
|
|
175
138
|
type TMode = "receive" | "issue" | "request" | "return";
|
|
176
|
-
type TRow = { item: string; qty: number | null; unitCost?: number | null
|
|
139
|
+
type TRow = { item: string; qty: number | null; unitCost?: number | null };
|
|
177
140
|
|
|
178
141
|
const show = defineModel<boolean>({ default: false });
|
|
179
142
|
const props = defineProps({
|
|
@@ -206,7 +169,7 @@ const COPY: Record<TMode, { title: string; help: string; button: string; success
|
|
|
206
169
|
},
|
|
207
170
|
request: {
|
|
208
171
|
title: "Request Stock",
|
|
209
|
-
help: "Ask the store for the items you need
|
|
172
|
+
help: "Ask the store for the items you need. You will see them under My Stock once the store keeper approves.",
|
|
210
173
|
button: "Send request",
|
|
211
174
|
success: "Request sent to the store.",
|
|
212
175
|
},
|
|
@@ -221,7 +184,7 @@ const copy = computed(() => COPY[props.mode]);
|
|
|
221
184
|
|
|
222
185
|
const form = ref();
|
|
223
186
|
const saving = ref(false);
|
|
224
|
-
const blank = (): TRow => ({ item: "", qty: null, unitCost: null
|
|
187
|
+
const blank = (): TRow => ({ item: "", qty: null, unitCost: null });
|
|
225
188
|
const rows = ref<TRow[]>([blank()]);
|
|
226
189
|
const supplier = ref("");
|
|
227
190
|
const reference = ref("");
|
|
@@ -231,19 +194,6 @@ const today = new Date().toLocaleDateString("en-CA");
|
|
|
231
194
|
const date = ref(today);
|
|
232
195
|
const holder = ref("");
|
|
233
196
|
const site = ref(props.siteId);
|
|
234
|
-
// Receiving (client 2026-10-02): PO / DO / cash, each with its own paperwork.
|
|
235
|
-
const receivingType = ref<TReceivingType>("PO");
|
|
236
|
-
const doNumber = ref("");
|
|
237
|
-
const doDate = ref("");
|
|
238
|
-
const receiptNumber = ref("");
|
|
239
|
-
const tried = ref(false);
|
|
240
|
-
const receivingHelp = computed(() => RECEIVING_TYPES.find((t) => t.value === receivingType.value)?.help ?? "");
|
|
241
|
-
const hasOrdered = (r: TRow) => r.ordered !== null && r.ordered !== undefined && r.ordered !== ("" as any);
|
|
242
|
-
const receivingCheck = () =>
|
|
243
|
-
receivingProblem({ type: receivingType.value, poNumber: reference.value, doNumber: doNumber.value, doDate: doDate.value, anyOrdered: receivingType.value !== "CASH" && rows.value.some(hasOrdered) });
|
|
244
|
-
/** Shown once they have tried to save, so the form does not open with a warning. */
|
|
245
|
-
const receivingError = computed(() => (props.mode === "receive" && tried.value ? receivingCheck() : ""));
|
|
246
|
-
const orderedRule = (v: number | null | string) => v === null || v === "" || (Number.isInteger(Number(v)) && Number(v) >= 1) || "Whole number";
|
|
247
197
|
|
|
248
198
|
const itemChoices = ref<TInventoryItem[]>([]);
|
|
249
199
|
const choices = computed<any[]>(() =>
|
|
@@ -268,9 +218,7 @@ function qtyRule(line: TRow) {
|
|
|
268
218
|
if (props.mode === "receive") return true;
|
|
269
219
|
const available = availableOf(line.item);
|
|
270
220
|
const total = rows.value.filter((r) => r.item === line.item).reduce((s, r) => s + Number(r.qty || 0), 0);
|
|
271
|
-
|
|
272
|
-
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";
|
|
221
|
+
if (props.mode !== "request" && available !== undefined && total > available) return `Only ${available} available`;
|
|
274
222
|
return true;
|
|
275
223
|
};
|
|
276
224
|
}
|
|
@@ -318,29 +266,16 @@ const searchPeople = debounce(async (q: string) => {
|
|
|
318
266
|
}, 300);
|
|
319
267
|
|
|
320
268
|
async function submit() {
|
|
321
|
-
tried.value = true;
|
|
322
269
|
const { valid } = await form.value.validate();
|
|
323
|
-
if (!valid
|
|
270
|
+
if (!valid) return;
|
|
324
271
|
const lines = rows.value.filter((r) => r.item && r.qty).map((r) => ({ item: r.item, qty: Number(r.qty) }));
|
|
325
272
|
saving.value = true;
|
|
326
273
|
try {
|
|
327
274
|
if (props.mode === "receive") {
|
|
328
275
|
await api.receive(props.store._id, {
|
|
329
|
-
lines: rows.value.map((r) => ({
|
|
330
|
-
item: r.item,
|
|
331
|
-
qty: Number(r.qty),
|
|
332
|
-
...(r.unitCost !== null && r.unitCost !== undefined && r.unitCost !== ("" as any) ? { unitCost: Number(r.unitCost) } : {}),
|
|
333
|
-
...(receivingType.value !== "CASH" && hasOrdered(r) ? { poOrderedQty: Number(r.ordered) } : {}),
|
|
334
|
-
})),
|
|
276
|
+
lines: rows.value.map((r) => ({ item: r.item, qty: Number(r.qty), ...(r.unitCost !== null && r.unitCost !== undefined && r.unitCost !== ("" as any) ? { unitCost: Number(r.unitCost) } : {}) })),
|
|
335
277
|
supplier: supplier.value,
|
|
336
|
-
|
|
337
|
-
type: receivingType.value,
|
|
338
|
-
poNumber: reference.value,
|
|
339
|
-
doNumber: doNumber.value,
|
|
340
|
-
// The server refuses a future time: today's DO date is "now", an earlier one noon that day.
|
|
341
|
-
doDateIso: doDate.value ? (pickedDate(doDate.value) ?? new Date().toISOString()) : undefined,
|
|
342
|
-
receiptNumber: receiptNumber.value,
|
|
343
|
-
}),
|
|
278
|
+
reference: reference.value,
|
|
344
279
|
date: pickedDate(date.value),
|
|
345
280
|
photos: photos.value,
|
|
346
281
|
remarks: remarks.value,
|
|
@@ -372,20 +307,7 @@ watch(show, (open) => {
|
|
|
372
307
|
date.value = today;
|
|
373
308
|
holder.value = "";
|
|
374
309
|
site.value = props.siteId;
|
|
375
|
-
receivingType.value = "PO";
|
|
376
|
-
doNumber.value = "";
|
|
377
|
-
doDate.value = "";
|
|
378
|
-
receiptNumber.value = "";
|
|
379
|
-
tried.value = false;
|
|
380
310
|
if (props.mode !== "return") loadItems();
|
|
381
311
|
if (props.mode === "issue") searchPeople("");
|
|
382
312
|
});
|
|
383
313
|
</script>
|
|
384
|
-
|
|
385
|
-
<style scoped>
|
|
386
|
-
/* Three choices side by side on a computer, wrapping on a phone. */
|
|
387
|
-
.receiving-type {
|
|
388
|
-
height: auto !important;
|
|
389
|
-
flex-wrap: wrap;
|
|
390
|
-
}
|
|
391
|
-
</style>
|
|
@@ -16,18 +16,16 @@
|
|
|
16
16
|
{{ label }}
|
|
17
17
|
</v-btn>
|
|
18
18
|
</div>
|
|
19
|
-
<input ref="picker" type="file"
|
|
20
|
-
<div class="text-caption text-
|
|
21
|
-
<div v-if="error" class="text-caption text-error mt-1" role="alert">{{ error }}</div>
|
|
19
|
+
<input ref="picker" type="file" accept="image/*" multiple hidden @change="upload" />
|
|
20
|
+
<div v-if="error" class="text-caption text-error mt-1">{{ error }}</div>
|
|
22
21
|
</div>
|
|
23
22
|
</template>
|
|
24
23
|
|
|
25
24
|
<script setup lang="ts">
|
|
26
25
|
import useFile from "../composables/useFile";
|
|
27
|
-
import { photoCheck, scaledSize, PHOTO_ACCEPT, SHRINK_STEPS, PHOTO_MAX_BYTES } from "../utils/inventory-receiving";
|
|
28
26
|
|
|
29
27
|
const model = defineModel<string[]>({ default: () => [] });
|
|
30
|
-
|
|
28
|
+
defineProps({
|
|
31
29
|
label: { type: String, default: "Add photo" },
|
|
32
30
|
max: { type: Number, default: 5 },
|
|
33
31
|
});
|
|
@@ -37,56 +35,20 @@ const picker = ref<HTMLInputElement>();
|
|
|
37
35
|
const uploading = ref(false);
|
|
38
36
|
const error = ref("");
|
|
39
37
|
|
|
40
|
-
const hint = computed(() => `JPG, PNG or WebP, up to ${props.max} photo${props.max === 1 ? "" : "s"}, 5 MB each. Bigger photos are made smaller for you.`);
|
|
41
|
-
|
|
42
|
-
/** Redraw a too-big photo smaller as a JPEG (no library: the browser's canvas). Null when it cannot get under 5 MB. */
|
|
43
|
-
async function shrink(file: File): Promise<File | null> {
|
|
44
|
-
try {
|
|
45
|
-
const bitmap = await createImageBitmap(file);
|
|
46
|
-
for (const step of SHRINK_STEPS) {
|
|
47
|
-
const { width, height } = scaledSize(bitmap.width, bitmap.height, step.side);
|
|
48
|
-
const canvas = document.createElement("canvas");
|
|
49
|
-
Object.assign(canvas, { width, height });
|
|
50
|
-
const ctx = canvas.getContext("2d")!;
|
|
51
|
-
ctx.fillStyle = "#fff"; // a see-through PNG would turn black as a JPEG
|
|
52
|
-
ctx.fillRect(0, 0, width, height);
|
|
53
|
-
ctx.drawImage(bitmap, 0, 0, width, height);
|
|
54
|
-
const blob = await new Promise<Blob | null>((done) => canvas.toBlob(done, "image/jpeg", step.quality));
|
|
55
|
-
if (blob && blob.size <= PHOTO_MAX_BYTES) return new File([blob], file.name.replace(/\.[^.]+$/, "") + ".jpg", { type: "image/jpeg" });
|
|
56
|
-
}
|
|
57
|
-
} catch {
|
|
58
|
-
/* the browser could not read it */
|
|
59
|
-
}
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
38
|
async function upload(event: Event) {
|
|
64
39
|
const files = Array.from((event.target as HTMLInputElement).files ?? []);
|
|
65
40
|
(event.target as HTMLInputElement).value = "";
|
|
66
41
|
if (!files.length) return;
|
|
67
42
|
uploading.value = true;
|
|
68
|
-
|
|
69
|
-
const room = props.max - model.value.length;
|
|
70
|
-
if (files.length > room) problems.push(`Only ${props.max} photo${props.max === 1 ? "" : "s"} can be added here, so the first ${room} were kept.`);
|
|
43
|
+
error.value = "";
|
|
71
44
|
try {
|
|
72
|
-
for (const
|
|
73
|
-
const check = photoCheck(picked);
|
|
74
|
-
let file: File | null = picked;
|
|
75
|
-
if (check === "shrink") {
|
|
76
|
-
file = await shrink(picked);
|
|
77
|
-
if (!file) problems.push(`"${picked.name}" is too big (over 5 MB) and could not be made smaller. Please pick another photo.`);
|
|
78
|
-
} else if (check !== "ok") {
|
|
79
|
-
problems.push(check);
|
|
80
|
-
file = null;
|
|
81
|
-
}
|
|
82
|
-
if (!file) continue;
|
|
45
|
+
for (const file of files.slice(0, 5 - model.value.length)) {
|
|
83
46
|
const res = await addFile(file);
|
|
84
47
|
if (res?.id) model.value = [...model.value, res.id as string];
|
|
85
48
|
}
|
|
86
49
|
} catch {
|
|
87
|
-
|
|
50
|
+
error.value = "A photo could not be uploaded. Please try again.";
|
|
88
51
|
} finally {
|
|
89
|
-
error.value = problems.join(" ");
|
|
90
52
|
uploading.value = false;
|
|
91
53
|
}
|
|
92
54
|
}
|
|
@@ -62,9 +62,6 @@
|
|
|
62
62
|
/>
|
|
63
63
|
<v-checkbox v-if="report === 'usage' && canCheck" v-model="uncheckedOnly" label="Not checked yet" density="compact" hide-details class="flex-grow-0" />
|
|
64
64
|
<v-spacer />
|
|
65
|
-
<v-btn v-if="report === 'purchases'" class="text-none screen-btn-ghost" variant="outlined" prepend-icon="mdi-clipboard-search-outline" @click="openPo('')">
|
|
66
|
-
Check a PO
|
|
67
|
-
</v-btn>
|
|
68
65
|
<v-btn v-if="ctl.fullReports || report === 'usage'" class="text-none screen-btn-ghost" variant="outlined" prepend-icon="mdi-file-excel-outline" :loading="exporting" @click="exportExcel">
|
|
69
66
|
Export Excel
|
|
70
67
|
</v-btn>
|
|
@@ -172,16 +169,6 @@
|
|
|
172
169
|
{{ item.received }}<span v-if="item.receivedOpening" class="d-block text-caption text-medium-emphasis">incl. {{ item.receivedOpening }} opening stock</span>
|
|
173
170
|
</template>
|
|
174
171
|
<template #item.receivedCost="{ value }">{{ money(value) }}</template>
|
|
175
|
-
<template #item.receivingType="{ item }">{{ item.kind === "Opening stock" ? "Opening stock" : receivingLabel(item.receivingType) }}</template>
|
|
176
|
-
<template #item.reference="{ item }">
|
|
177
|
-
<a v-if="report === 'purchases' && item.reference" href="#" class="text-primary" title="See ordered vs received for this PO" @click.prevent="openPo(item.reference)">{{ item.reference }}</a>
|
|
178
|
-
<span v-else>{{ item.reference || "—" }}</span>
|
|
179
|
-
</template>
|
|
180
|
-
<template #item.doNumber="{ item }">
|
|
181
|
-
<span v-if="item.doNumber">{{ item.doNumber }}<span v-if="item.doDate" class="d-block text-caption text-medium-emphasis">{{ standardFormatDate(item.doDate) }}</span></span>
|
|
182
|
-
<span v-else>—</span>
|
|
183
|
-
</template>
|
|
184
|
-
<template #item.poOrderedQty="{ value }">{{ value ?? "—" }}</template>
|
|
185
172
|
<template #item.usedCost="{ value }">{{ money(value) }}</template>
|
|
186
173
|
<template #item.siteUsedCost="{ value }">{{ money(value) }}</template>
|
|
187
174
|
<!-- The supervisor checks usage against the technician's before/after photos (click to enlarge, with the time taken). -->
|
|
@@ -219,7 +206,6 @@
|
|
|
219
206
|
</template>
|
|
220
207
|
</v-data-table>
|
|
221
208
|
<InventoryHistoryDialog v-model="drill.show" :store="store" :holder="drill.holder" :item="drill.item" :title="drill.title" />
|
|
222
|
-
<InventoryPoDialog v-model="po.show" :store="store" :initial-po="po.number" />
|
|
223
209
|
</div>
|
|
224
210
|
</template>
|
|
225
211
|
|
|
@@ -230,7 +216,6 @@ import useUtils from "../composables/useUtils";
|
|
|
230
216
|
import { inventoryControls } from "../utils/inventory-presets";
|
|
231
217
|
import { USED_BY_FILTERS } from "../utils/inventory-used-by";
|
|
232
218
|
import { USAGE_FLAG_WORDS, USAGE_STATUS_CHOICES, usagePathText, usageQuery, usageTotals } from "../utils/inventory-usage";
|
|
233
|
-
import { receivingLabel } from "../utils/inventory-receiving";
|
|
234
219
|
|
|
235
220
|
const props = defineProps({
|
|
236
221
|
store: { type: Object as PropType<TInventoryStore>, required: true },
|
|
@@ -268,8 +253,6 @@ const staleDays = ref(3);
|
|
|
268
253
|
const flagWords = computed(() => ({ ...USAGE_FLAG_WORDS, old_draft: `Draft older than ${staleDays.value} days` }) as Record<string, string>);
|
|
269
254
|
const flagChoices = computed(() => [{ value: "", title: "Everything" }, ...Object.entries(flagWords.value).map(([value, title]) => ({ value, title }))]);
|
|
270
255
|
const drill = reactive({ show: false, holder: "", item: "", title: "" });
|
|
271
|
-
const po = reactive({ show: false, number: "" });
|
|
272
|
-
const openPo = (number: string) => Object.assign(po, { show: true, number });
|
|
273
256
|
function openPerson(r: any) {
|
|
274
257
|
Object.assign(drill, { show: true, holder: String(r.holder ?? ""), item: "", title: r.holderLabel || r.technician });
|
|
275
258
|
}
|
|
@@ -295,7 +278,7 @@ const help: Record<string, string> = {
|
|
|
295
278
|
movement: "For each item: stock at the start of the month, what came in (deliveries and opening stock), what was issued and returned, count differences, and stock at the end.",
|
|
296
279
|
usage: "Every usage of this store's items, by anyone: who (company and service), where, which work order, before and after photos with their times, and how the stock reached them. Click a person or an item to see its full history. Use it for tenant billing; tick records and mark them checked once verified.",
|
|
297
280
|
technicians: "For each person: stock they held at the start, took, used, returned, and hold at the end of the month.",
|
|
298
|
-
purchases: "Every delivery received
|
|
281
|
+
purchases: "Every delivery received, and opening stock entered with new items (Type: Opening stock), with totals by month and by supplier.",
|
|
299
282
|
handovers: "Stock passed from one person to a colleague: who gave it, who received it, and whether they confirmed.",
|
|
300
283
|
};
|
|
301
284
|
|
|
@@ -408,15 +391,12 @@ const headers: Record<string, any[]> = {
|
|
|
408
391
|
],
|
|
409
392
|
purchases: [
|
|
410
393
|
{ title: "Date", key: "date" },
|
|
411
|
-
{ title: "Type", key: "
|
|
394
|
+
{ title: "Type", key: "kind" },
|
|
412
395
|
{ title: "Supplier", key: "supplier" },
|
|
413
396
|
{ title: "PO No.", key: "reference" },
|
|
414
|
-
{ title: "DO No. / date", key: "doNumber" },
|
|
415
|
-
{ title: "Receipt No.", key: "receiptNumber" },
|
|
416
397
|
{ title: "Item", key: "itemName" },
|
|
417
398
|
{ title: "Unit", key: "unit" },
|
|
418
|
-
{ title: "
|
|
419
|
-
{ title: "Received", key: "qty" },
|
|
399
|
+
{ title: "Qty", key: "qty" },
|
|
420
400
|
{ title: "Unit price", key: "unitCost" },
|
|
421
401
|
{ title: "Total", key: "totalCost" },
|
|
422
402
|
{ title: "Received by", key: "receivedBy" },
|
|
@@ -7,15 +7,6 @@
|
|
|
7
7
|
<v-btn value="rejected" class="text-none">Rejected</v-btn>
|
|
8
8
|
<v-btn value="" class="text-none">All</v-btn>
|
|
9
9
|
</v-btn-toggle>
|
|
10
|
-
<v-text-field
|
|
11
|
-
v-model="search"
|
|
12
|
-
placeholder="Search item, person, reference or remarks"
|
|
13
|
-
prepend-inner-icon="mdi-magnify"
|
|
14
|
-
density="compact"
|
|
15
|
-
hide-details
|
|
16
|
-
clearable
|
|
17
|
-
class="request-search"
|
|
18
|
-
/>
|
|
19
10
|
<v-spacer />
|
|
20
11
|
<v-btn v-if="ctl.request" class="text-none screen-btn-primary" variant="flat" prepend-icon="mdi-cart-arrow-down" @click="showRequest = true">
|
|
21
12
|
Request Stock
|
|
@@ -24,7 +15,7 @@
|
|
|
24
15
|
|
|
25
16
|
<v-progress-linear v-if="loading" indeterminate color="primary" />
|
|
26
17
|
<div v-else-if="!requests.length" class="text-center text-medium-emphasis py-10">
|
|
27
|
-
{{
|
|
18
|
+
{{ 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." }}
|
|
28
19
|
</div>
|
|
29
20
|
|
|
30
21
|
<AppCard v-for="r in requests" :key="r._id" class="pa-4 mb-3">
|
|
@@ -72,11 +63,11 @@
|
|
|
72
63
|
v-model.number="l.give"
|
|
73
64
|
type="number"
|
|
74
65
|
min="0"
|
|
75
|
-
:max="
|
|
66
|
+
:max="l.qty"
|
|
76
67
|
label="Give"
|
|
77
68
|
density="comfortable"
|
|
78
69
|
style="max-width: 120px"
|
|
79
|
-
:rules="[(v: any) => (Number.isInteger(Number(v)) && Number(v) >= 0 && Number(v) <= l.qty) || `0 to ${l.qty}
|
|
70
|
+
:rules="[(v: any) => (Number.isInteger(Number(v)) && Number(v) >= 0 && Number(v) <= l.qty) || `0 to ${l.qty}`]"
|
|
80
71
|
/>
|
|
81
72
|
</div>
|
|
82
73
|
<InputLabel title="Remarks" class="mb-1 mt-2" />
|
|
@@ -116,7 +107,6 @@ const notify = inject<(text: string, color?: string) => void>("inventoryNotify",
|
|
|
116
107
|
const chip = REQUEST_STATUS;
|
|
117
108
|
|
|
118
109
|
const status = ref("pending");
|
|
119
|
-
const search = ref("");
|
|
120
110
|
const requests = ref<TInventoryRequest[]>([]);
|
|
121
111
|
const page = ref(1);
|
|
122
112
|
const pages = ref(0);
|
|
@@ -129,7 +119,7 @@ const isMine = (r: TInventoryRequest) => String(r.requestedBy) === String(curren
|
|
|
129
119
|
async function load(p = 1) {
|
|
130
120
|
loading.value = true;
|
|
131
121
|
try {
|
|
132
|
-
const res = await api.getRequests(props.store._id, { page: p, limit: 20, status: status.value || undefined
|
|
122
|
+
const res = await api.getRequests(props.store._id, { page: p, limit: 20, status: status.value || undefined });
|
|
133
123
|
requests.value = p === 1 ? res.items : [...requests.value, ...res.items];
|
|
134
124
|
page.value = p;
|
|
135
125
|
pages.value = res.pages;
|
|
@@ -224,19 +214,6 @@ async function reject() {
|
|
|
224
214
|
}
|
|
225
215
|
|
|
226
216
|
watch(status, () => load(1));
|
|
227
|
-
// Typed search waits until the typing stops.
|
|
228
|
-
let typing: ReturnType<typeof setTimeout> | undefined;
|
|
229
|
-
watch(search, () => {
|
|
230
|
-
clearTimeout(typing);
|
|
231
|
-
typing = setTimeout(() => load(1), 400);
|
|
232
|
-
});
|
|
233
217
|
watch(() => props.store._id, () => load(1));
|
|
234
218
|
onMounted(() => load(1));
|
|
235
219
|
</script>
|
|
236
|
-
|
|
237
|
-
<style scoped>
|
|
238
|
-
.request-search {
|
|
239
|
-
min-width: 220px;
|
|
240
|
-
max-width: 340px;
|
|
241
|
-
}
|
|
242
|
-
</style>
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
<v-list density="compact">
|
|
18
18
|
<v-list-item prepend-icon="mdi-truck-delivery-outline" title="Record a delivery" @click="open('receive')" />
|
|
19
19
|
<v-list-item prepend-icon="mdi-file-excel-outline" title="Import deliveries from Excel" @click="showReceiveImport = true" />
|
|
20
|
-
<v-list-item prepend-icon="mdi-clipboard-search-outline" title="Check a purchase order (PO)" @click="showPo = true" />
|
|
21
20
|
</v-list>
|
|
22
21
|
</v-menu>
|
|
23
22
|
<v-btn v-if="ctl.issue && !storeEmpty" class="text-none screen-btn-primary" variant="flat" prepend-icon="mdi-hand-extended-outline" @click="open('issue')">
|
|
@@ -118,7 +117,6 @@
|
|
|
118
117
|
<InventoryItemDialog v-model="itemDialog.show" :store="store" :item="itemDialog.item" @done="done" />
|
|
119
118
|
<InventoryImportDialog v-model="showImport" :store="store" @done="done" />
|
|
120
119
|
<InventoryImportDialog v-model="showReceiveImport" :store="store" kind="receipts" @done="done" />
|
|
121
|
-
<InventoryPoDialog v-model="showPo" :store="store" />
|
|
122
120
|
<InventoryLegacyImportDialog v-model="showLegacy" :store="store" @done="done" />
|
|
123
121
|
<v-dialog v-model="bulk.show" max-width="480">
|
|
124
122
|
<v-card class="screen-modal">
|
|
@@ -258,7 +256,6 @@ function openItem(item: TInventoryItem) {
|
|
|
258
256
|
|
|
259
257
|
const showImport = ref(false);
|
|
260
258
|
const showReceiveImport = ref(false);
|
|
261
|
-
const showPo = ref(false);
|
|
262
259
|
const showLegacy = ref(false);
|
|
263
260
|
// Offer the old Equipment copy only where this store's sites have old records not yet copied.
|
|
264
261
|
const legacyToCopy = ref(false);
|
|
@@ -388,27 +388,9 @@ watch(
|
|
|
388
388
|
const roles = ref<any[]>([])
|
|
389
389
|
const { getRoles } = useRole()
|
|
390
390
|
|
|
391
|
-
/*
|
|
392
|
-
* `type` is the ROLE type, not the service the client subscribes to, and the
|
|
393
|
-
* only caller (`ClientMain.vue` "Invite Client") passes no `app` prop -- so
|
|
394
|
-
* this sent `type: ""` and the endpoint applied no type filter at all. The
|
|
395
|
-
* dropdown therefore listed every org-less role of every type, which includes
|
|
396
|
-
* the platform's OWN staff roles (`type: "admin"` -- the two rows the console's
|
|
397
|
-
* `/super-admin/role-permissions` screen edits). Offering a Seven365 console
|
|
398
|
-
* staff role on a CLIENT invitation is the defect.
|
|
399
|
-
*
|
|
400
|
-
* A client admin's role is `type: "organization"`, which is what the client
|
|
401
|
-
* roles screen (`org/[organization]/roles-permissions.vue`) already lists. This
|
|
402
|
-
* narrows to exactly that. It is a narrowing of this one picker only: no role
|
|
403
|
-
* changes, no permission changes, and the org-less `organization` roles that
|
|
404
|
-
* were already being picked are all still offered.
|
|
405
|
-
*
|
|
406
|
-
* Keying off the selected service instead would empty the dropdown -- no
|
|
407
|
-
* org-less role carries a service type -- so `props.app` is not the filter.
|
|
408
|
-
*/
|
|
409
391
|
const { data: roleData, refresh: refreshRoles } = await useLazyAsyncData(
|
|
410
392
|
"roles",
|
|
411
|
-
() => getRoles({ org: org.value?._id, type:
|
|
393
|
+
() => getRoles({ org: org.value?._id, type: props.app, limit: 50 }),
|
|
412
394
|
{ watch: [() => props.app] }
|
|
413
395
|
)
|
|
414
396
|
|
|
@@ -88,24 +88,13 @@
|
|
|
88
88
|
persistent-hint
|
|
89
89
|
data-test="invite-fixed-site"
|
|
90
90
|
></v-text-field>
|
|
91
|
-
<!--
|
|
92
|
-
Several sites (owner request 2026-10-02): one invitation per
|
|
93
|
-
site, one e-mail. One role for all of them, so 2+ sites offer
|
|
94
|
-
organisation roles only (see `roleSiteFor`).
|
|
95
|
-
-->
|
|
96
91
|
<v-autocomplete
|
|
97
92
|
v-else
|
|
98
|
-
v-model="invite.
|
|
93
|
+
v-model="invite.site"
|
|
99
94
|
:items="sites"
|
|
100
|
-
multiple
|
|
101
|
-
chips
|
|
102
|
-
closable-chips
|
|
103
95
|
class="filter-field"
|
|
104
96
|
density="comfortable"
|
|
105
|
-
:rules="[
|
|
106
|
-
:hint="invite.sites.length > 1 ? severalSitesHint : ''"
|
|
107
|
-
:persistent-hint="invite.sites.length > 1"
|
|
108
|
-
data-test="invite-sites"
|
|
97
|
+
:rules="[requiredRule]"
|
|
109
98
|
@update:model-value="handleUpdateSite"
|
|
110
99
|
></v-autocomplete>
|
|
111
100
|
</v-col>
|
|
@@ -174,7 +163,6 @@ import { useLocalSetup } from '../composables/useLocalSetup';
|
|
|
174
163
|
import useRole from '../composables/useRole';
|
|
175
164
|
import useSite from '../composables/useSite';
|
|
176
165
|
import { inviteRoleOptions } from '../utils/invite-role-options';
|
|
177
|
-
import { roleSiteFor } from '../utils/invite-sites';
|
|
178
166
|
import useUser from '../composables/useUser';
|
|
179
167
|
import useUtils from '../composables/useUtils';
|
|
180
168
|
|
|
@@ -288,8 +276,6 @@ const invite = ref<Record<string, any>>({
|
|
|
288
276
|
role: "",
|
|
289
277
|
org: "",
|
|
290
278
|
site: "",
|
|
291
|
-
// the sites picked in the Site field (several allowed); `site` follows it
|
|
292
|
-
sites: [] as string[],
|
|
293
279
|
siteName: "",
|
|
294
280
|
isMemberInvite: false,
|
|
295
281
|
});
|
|
@@ -515,17 +501,9 @@ watchEffect(() => {
|
|
|
515
501
|
|
|
516
502
|
const roleOptions = computed(() => inviteRoleOptions(roles.value));
|
|
517
503
|
|
|
518
|
-
const severalSitesHint =
|
|
519
|
-
"Several sites: pick an organisation role - it applies at each site. For a different role per site, invite one site at a time.";
|
|
520
|
-
|
|
521
|
-
const sitesRule = (v: string[]) =>
|
|
522
|
-
(Array.isArray(v) && v.length > 0) || "Pick at least one site";
|
|
523
|
-
|
|
524
504
|
const noRolesText = computed(() =>
|
|
525
505
|
rolesError?.value
|
|
526
506
|
? "The roles could not be loaded. You may not have access to this organisation's roles; refresh, or sign in again."
|
|
527
|
-
: !fixedSite.value && invite.value.sites.length > 1
|
|
528
|
-
? "No organisation roles for this service yet. Create one in Roles & Permissions (Roles for <service>, use at any site), or invite one site at a time."
|
|
529
507
|
: fixedSite.value
|
|
530
508
|
? "No roles have been created for this site yet. Create one in Roles & Permissions first."
|
|
531
509
|
: "No roles have been created yet. Create one in Roles & Permissions first."
|
|
@@ -547,7 +525,6 @@ const rolesPageLink = computed(() => {
|
|
|
547
525
|
async function handleUpdateApp(value: string) {
|
|
548
526
|
invite.value.role = "";
|
|
549
527
|
if (!fixedSite.value) {
|
|
550
|
-
invite.value.sites = [];
|
|
551
528
|
invite.value.site = "";
|
|
552
529
|
invite.value.siteName = "";
|
|
553
530
|
}
|
|
@@ -567,16 +544,12 @@ function resetInvite() {
|
|
|
567
544
|
invite.value.role = "";
|
|
568
545
|
// Same site as before: a fixed site stays fixed, a picked one is cleared.
|
|
569
546
|
invite.value.site = fixedSite.value;
|
|
570
|
-
invite.value.sites = [];
|
|
571
547
|
if (!invite.value.site) invite.value.siteName = "";
|
|
572
548
|
message.value = "";
|
|
573
549
|
}
|
|
574
550
|
|
|
575
|
-
function handleUpdateSite(
|
|
576
|
-
|
|
577
|
-
// (organisation roles only); the role watch clears a role no longer offered.
|
|
578
|
-
invite.value.site = roleSiteFor(siteIds);
|
|
579
|
-
const obj = sites.value.find((x) => x?.value === invite.value.site);
|
|
551
|
+
function handleUpdateSite(siteId: string) {
|
|
552
|
+
const obj = sites.value.find((x) => x?.value === siteId);
|
|
580
553
|
invite.value.siteName = obj?.title || "";
|
|
581
554
|
}
|
|
582
555
|
|