@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +16 -4
  2. package/assets/css/primitives.css +0 -57
  3. package/assets/css/screens.css +4 -8
  4. package/components/AccessCardDetailsDialog.vue +6 -59
  5. package/components/AccessCardPreviewDialog.vue +53 -371
  6. package/components/AppSelect.vue +4 -143
  7. package/components/BuildingUnitFormEdit.vue +0 -6
  8. package/components/Dialog/UpdateMoreAction.vue +1 -11
  9. package/components/DocumentForm.vue +112 -407
  10. package/components/DocumentManagement.vue +12 -213
  11. package/components/Facility/BookingSetup.vue +0 -63
  12. package/components/HidAccessLogDashboard.vue +17 -74
  13. package/components/HidIntercomManagement.vue +14 -183
  14. package/components/HidQrCodeConfiguration.vue +195 -17
  15. package/components/HidReaderManagement.vue +0 -398
  16. package/components/HidReaderUserRoster.vue +3 -44
  17. package/components/HidUserEnrollment.vue +292 -2255
  18. package/components/InventoryItemDetail.vue +1 -2
  19. package/components/InventoryLinesDialog.vue +13 -91
  20. package/components/InventoryPhotoInput.vue +6 -44
  21. package/components/InventoryReportsTab.vue +3 -23
  22. package/components/InventoryRequestsTab.vue +4 -27
  23. package/components/InventoryStockTab.vue +0 -3
  24. package/components/InvitationClientForm.vue +1 -19
  25. package/components/InvitationForm.vue +4 -31
  26. package/components/InvitationMain.vue +22 -124
  27. package/components/SiteSettings.vue +0 -48
  28. package/components/TableMain.vue +7 -22
  29. package/components/VehicleManagement.vue +5 -80
  30. package/components/VisitorForm.vue +0 -12
  31. package/components/VisitorManagement.vue +5 -267
  32. package/composables/useAccessManagement.ts +1 -17
  33. package/composables/useDocument.ts +0 -7
  34. package/composables/useFacility.ts +0 -11
  35. package/composables/useHidAmico.ts +0 -165
  36. package/composables/useHidNavigation.ts +1 -25
  37. package/composables/useInventory.ts +1 -4
  38. package/composables/useMember.ts +0 -12
  39. package/composables/useNFCPatrolReportFilters.ts +3 -21
  40. package/composables/useNFCPatrolRoute.ts +1 -8
  41. package/composables/usePeople.ts +0 -19
  42. package/composables/useServiceProvider.ts +2 -45
  43. package/composables/useSettingsPermission.ts +0 -14
  44. package/composables/useUser.ts +2 -6
  45. package/composables/useVehicle.ts +1 -40
  46. package/composables/useVerification.ts +0 -3
  47. package/middleware/01.auth.ts +7 -3
  48. package/package.json +1 -1
  49. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
  50. package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
  51. package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
  52. package/types/document.d.ts +0 -14
  53. package/types/facility.d.ts +0 -4
  54. package/types/inventory.d.ts +0 -27
  55. package/types/member.d.ts +0 -4
  56. package/types/people.d.ts +1 -28
  57. package/types/service-provider.d.ts +0 -5
  58. package/types/site.d.ts +1 -7
  59. package/components/BuildingUnitManagement.vue +0 -298
  60. package/components/HidAccessPermissions.vue +0 -424
  61. package/components/HidProfileQrCode.vue +0 -332
  62. package/components/InventoryPoDialog.vue +0 -111
  63. package/components/VehicleQrStickerDialog.vue +0 -249
  64. package/composables/useHidReaderSelection.ts +0 -50
  65. package/composables/useSiteCategory.ts +0 -45
  66. package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
  67. package/utils/hid-enrolment-subject.ts +0 -344
  68. package/utils/hid-permission-assignments.ts +0 -198
  69. package/utils/hid-reader-selection.ts +0 -52
  70. package/utils/inventory-receiving.ts +0 -90
  71. package/utils/invite-group.ts +0 -61
  72. package/utils/invite-sites.ts +0 -25
  73. package/utils/occupancy-role.ts +0 -109
  74. package/utils/service-type.ts +0 -30
  75. 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 || "", receiptRefText(t));
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="6">
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 photos (optional)" class="mb-1" />
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; ordered?: 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 — up to what the store has (shown under each item). You will see them under My Stock once the store keeper approves.",
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, ordered: 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
- // Requests too (client 2026-10-02): nobody can ask for more than the store has; the server refuses it as well.
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 || (props.mode === "receive" && receivingCheck())) return;
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
- ...receivingFields({
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" :accept="PHOTO_ACCEPT" multiple hidden @change="upload" />
20
- <div class="text-caption text-medium-emphasis mt-1">{{ hint }}</div>
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
- const props = defineProps({
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
- const problems: string[] = [];
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 picked of files.slice(0, room)) {
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
- problems.push("A photo could not be uploaded. Please check your connection and try again.");
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 — by PO, DO or cash — and opening stock entered with new items, with totals by month and by supplier. Click a PO number (or Check a PO) to see ordered against 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: "receivingType" },
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: "Ordered", key: "poOrderedQty" },
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
- {{ search ? `No requests match "${search}". Try another word, or pick "All" to look through every status.` : 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." }}
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="Math.min(l.qty, l.onHand ?? l.qty)"
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}`, (v: any) => l.onHand === undefined || Number(v) <= l.onHand || `Only ${l.onHand} in store`]"
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, search: search.value?.trim() || 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: "organization", limit: 50 }),
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.sites"
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="[sitesRule]"
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(siteIds: string[]) {
576
- // The roles list follows `site`: the one chosen site, or "" for several
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