@7365admin1/layer-common 4.60.0 → 4.61.1
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/components/InventoryItemDetail.vue +3 -1
- package/components/InventoryItemDialog.vue +4 -4
- package/components/InventoryLinesDialog.vue +5 -2
- package/components/InventoryMain.vue +16 -3
- package/components/InventoryMyStockTab.vue +1 -1
- package/components/InventoryReportsTab.vue +18 -2
- package/components/InventoryRequestsTab.vue +7 -10
- package/components/InventoryStockTab.vue +5 -3
- package/components/InventoryUsageDialog.vue +1 -1
- package/package.json +1 -1
- package/utils/inventory-request-text.ts +29 -0
- package/utils/inventory-store-label.ts +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.61.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 598f6c1: Inventory store switcher: when a person can see several stores of the same kind, each is named with its organisation ("Property store · Margaret Ville"); more than three stores are chosen from a list instead of a row of identical buttons.
|
|
8
|
+
|
|
9
|
+
## 4.61.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 872164d: Inventory is easier to use: a new store shows only **Add Items** until it has items; the item form explains Unit, Packing, the low-stock alert and unit price; the item picker is labelled; requests read "Waiting for approval" / "Approved" with "N handed over (asked M)" and who decided and when; cancelling a request asks first; item history shows who did each step; Record Usage says "Tenant unit"; the Usage report shows the before/after photos the supervisor checks; "Count diff." reads "Stock count +/-"; a technician's activity says "Taken from store".
|
|
14
|
+
|
|
3
15
|
## 4.60.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -1661,11 +1673,11 @@
|
|
|
1661
1673
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
1662
1674
|
own rule from two endpoints the console already calls, unprojected:
|
|
1663
1675
|
|
|
1664
|
-
|
|
1665
|
-
|
|
1676
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
1677
|
+
GET /api/roles/id/:role that membership's role document
|
|
1666
1678
|
|
|
1667
|
-
|
|
1668
|
-
|
|
1679
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
1680
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
1669
1681
|
|
|
1670
1682
|
`role.default` is the marker because it is the only property of a platform
|
|
1671
1683
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
<th class="text-right">Store</th>
|
|
47
47
|
<th class="text-right">Balance</th>
|
|
48
48
|
<th>Details</th>
|
|
49
|
+
<th>By</th>
|
|
49
50
|
</tr>
|
|
50
51
|
</thead>
|
|
51
52
|
<tbody>
|
|
@@ -57,9 +58,10 @@
|
|
|
57
58
|
</td>
|
|
58
59
|
<td class="text-right">{{ t.type === "use" ? "" : t.storeBalance }}</td>
|
|
59
60
|
<td class="text-caption">{{ describe(t) }}</td>
|
|
61
|
+
<td class="text-caption">{{ t.createdByName }}</td>
|
|
60
62
|
</tr>
|
|
61
63
|
<tr v-if="!history.length && !loading">
|
|
62
|
-
<td colspan="
|
|
64
|
+
<td colspan="6" class="text-center text-medium-emphasis py-4">Nothing recorded yet.</td>
|
|
63
65
|
</tr>
|
|
64
66
|
</tbody>
|
|
65
67
|
</v-table>
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
<v-row dense>
|
|
14
14
|
<v-col cols="12" sm="4">
|
|
15
15
|
<InputLabel title="Unit" required class="mb-1" />
|
|
16
|
-
<v-combobox v-model="f.unit" :items="units" :rules="[requiredRule]" density="comfortable" />
|
|
16
|
+
<v-combobox v-model="f.unit" :items="units" :rules="[requiredRule]" density="comfortable" hint="How you count it (pcs, box, can)" persistent-hint />
|
|
17
17
|
</v-col>
|
|
18
18
|
<v-col cols="12" sm="4">
|
|
19
19
|
<InputLabel title="Packing" class="mb-1" />
|
|
20
|
-
<v-text-field v-model="f.packing" density="comfortable" placeholder="e.g. 25 pcs / box" maxlength="60" />
|
|
20
|
+
<v-text-field v-model="f.packing" density="comfortable" placeholder="e.g. 25 pcs / box" maxlength="60" hint="How the supplier packs it (info only)" persistent-hint />
|
|
21
21
|
</v-col>
|
|
22
22
|
<v-col cols="12" sm="4">
|
|
23
23
|
<InputLabel title="Item code" class="mb-1" />
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
</v-col>
|
|
30
30
|
<v-col cols="12" sm="4">
|
|
31
31
|
<InputLabel title="Low stock alert at" class="mb-1" />
|
|
32
|
-
<v-text-field v-model.number="f.reorderLevel" type="number" min="0" density="comfortable" :rules="[wholeRule]" hint="0 = no alert" persistent-hint />
|
|
32
|
+
<v-text-field v-model.number="f.reorderLevel" type="number" min="0" density="comfortable" :rules="[wholeRule]" hint="Alert when stock falls to this. 0 = no alert" persistent-hint />
|
|
33
33
|
</v-col>
|
|
34
34
|
<v-col cols="12" sm="4">
|
|
35
35
|
<InputLabel title="Unit price" class="mb-1" />
|
|
36
|
-
<v-text-field v-model.number="f.unitCost" type="number" min="0" step="0.01" prefix="$" density="comfortable" :rules="[moneyRule]" />
|
|
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-col v-if="!item" cols="12" sm="4">
|
|
39
39
|
<InputLabel title="Current stock" class="mb-1" />
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
:items="choices"
|
|
40
40
|
item-title="name"
|
|
41
41
|
item-value="_id"
|
|
42
|
-
|
|
42
|
+
label="Item"
|
|
43
|
+
placeholder="Type to search"
|
|
43
44
|
density="comfortable"
|
|
44
45
|
:rules="[requiredRule]"
|
|
45
46
|
class="flex-grow-1"
|
|
@@ -75,7 +76,9 @@
|
|
|
75
76
|
/>
|
|
76
77
|
<v-btn v-if="rows.length > 1" icon="mdi-delete-outline" variant="text" aria-label="Remove line" @click="rows.splice(i, 1)" />
|
|
77
78
|
</div>
|
|
78
|
-
<
|
|
79
|
+
<div class="mb-4">
|
|
80
|
+
<v-btn variant="text" class="text-none" prepend-icon="mdi-plus" @click="rows.push(blank())">Add another item</v-btn>
|
|
81
|
+
</div>
|
|
79
82
|
|
|
80
83
|
<v-row v-if="mode === 'receive'" dense>
|
|
81
84
|
<v-col cols="12" md="6">
|
|
@@ -2,8 +2,20 @@
|
|
|
2
2
|
<div class="inventory">
|
|
3
3
|
<PageHeader :title="store ? store.name : 'Inventory'" :subtitle="subtitle">
|
|
4
4
|
<template #actions>
|
|
5
|
+
<!-- Many stores (staff of several organisations at one property): a list, not a row of buttons. -->
|
|
6
|
+
<v-select
|
|
7
|
+
v-if="stores.length > 3"
|
|
8
|
+
v-model="storeId"
|
|
9
|
+
:items="stores"
|
|
10
|
+
:item-title="(s: TInventoryStore) => inventoryStoreLabel(s, stores)"
|
|
11
|
+
item-value="_id"
|
|
12
|
+
label="Store"
|
|
13
|
+
density="compact"
|
|
14
|
+
hide-details
|
|
15
|
+
style="min-width: 300px"
|
|
16
|
+
/>
|
|
5
17
|
<v-btn-toggle
|
|
6
|
-
v-if="stores.length > 1"
|
|
18
|
+
v-else-if="stores.length > 1"
|
|
7
19
|
v-model="storeId"
|
|
8
20
|
mandatory
|
|
9
21
|
density="comfortable"
|
|
@@ -11,7 +23,7 @@
|
|
|
11
23
|
divided
|
|
12
24
|
>
|
|
13
25
|
<v-btn v-for="s in stores" :key="s._id" :value="s._id" class="text-none">
|
|
14
|
-
{{ s
|
|
26
|
+
{{ inventoryStoreLabel(s, stores) }}
|
|
15
27
|
</v-btn>
|
|
16
28
|
</v-btn-toggle>
|
|
17
29
|
</template>
|
|
@@ -102,6 +114,7 @@
|
|
|
102
114
|
import useInventory from "../composables/useInventory";
|
|
103
115
|
import useSite from "../composables/useSite";
|
|
104
116
|
import useUtils from "../composables/useUtils";
|
|
117
|
+
import { inventoryStoreLabel } from "../utils/inventory-store-label";
|
|
105
118
|
|
|
106
119
|
const props = defineProps({
|
|
107
120
|
orgId: { type: String, required: true },
|
|
@@ -144,7 +157,7 @@ const storeSites = computed(() =>
|
|
|
144
157
|
const subtitle = computed(() => {
|
|
145
158
|
if (!store.value) return "";
|
|
146
159
|
if (storeSites.value.length > 1) return `Supplies ${storeSites.value.map((s) => s.name).join(", ")}`;
|
|
147
|
-
return store.value.
|
|
160
|
+
return inventoryStoreLabel(store.value, stores.value);
|
|
148
161
|
});
|
|
149
162
|
|
|
150
163
|
const tabs = computed(() => {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
<tr v-for="t in activity" :key="t._id">
|
|
45
45
|
<td class="text-no-wrap">{{ standardFormatDate(t.date) }}</td>
|
|
46
46
|
<td>{{ t.itemName }}</td>
|
|
47
|
-
<td>{{ typeLabels[t.type] }}</td>
|
|
47
|
+
<td>{{ t.type === "issue" ? "Taken from store" : t.type === "return" ? "Given back to store" : typeLabels[t.type] }}</td>
|
|
48
48
|
<td class="text-right">{{ t.qty }}</td>
|
|
49
49
|
<td class="text-caption">
|
|
50
50
|
{{ [t.location?.type === "tenant_unit" ? `${t.location.unitName} ${t.location.tenantName || ""}` : t.location?.area, t.reference].filter(Boolean).join(" · ") }}
|
|
@@ -111,6 +111,15 @@
|
|
|
111
111
|
<template #item.totalCost="{ value }">{{ money(value) }}</template>
|
|
112
112
|
<template #item.receivedCost="{ value }">{{ money(value) }}</template>
|
|
113
113
|
<template #item.usedCost="{ value }">{{ money(value) }}</template>
|
|
114
|
+
<!-- The supervisor checks usage against the technician's before/after photos. -->
|
|
115
|
+
<template #item.photos="{ item }">
|
|
116
|
+
<div v-if="usagePhotos(item).length" class="d-flex ga-1 py-1">
|
|
117
|
+
<a v-for="ph in usagePhotos(item)" :key="ph.id" :href="getFileUrl(ph.id)" target="_blank" rel="noopener" :title="ph.label">
|
|
118
|
+
<v-img :src="getFileUrl(ph.id)" width="36" height="36" cover class="rounded" :alt="ph.label" />
|
|
119
|
+
</a>
|
|
120
|
+
</div>
|
|
121
|
+
<span v-else class="text-medium-emphasis">—</span>
|
|
122
|
+
</template>
|
|
114
123
|
<template #item.checkedByName="{ item }">
|
|
115
124
|
<v-chip v-if="item.checkedByName" size="x-small" color="success" variant="tonal">{{ item.checkedByName }}</v-chip>
|
|
116
125
|
<span v-else class="text-medium-emphasis">—</span>
|
|
@@ -125,6 +134,7 @@
|
|
|
125
134
|
<script setup lang="ts">
|
|
126
135
|
import useInventory, { saveBlob } from "../composables/useInventory";
|
|
127
136
|
import useUtils from "../composables/useUtils";
|
|
137
|
+
import useFile from "../composables/useFile";
|
|
128
138
|
|
|
129
139
|
const props = defineProps({
|
|
130
140
|
store: { type: Object as PropType<TInventoryStore>, required: true },
|
|
@@ -133,6 +143,11 @@ const props = defineProps({
|
|
|
133
143
|
|
|
134
144
|
const api = useInventory();
|
|
135
145
|
const { standardFormatDate } = useUtils();
|
|
146
|
+
const { getFileUrl } = useFile();
|
|
147
|
+
const usagePhotos = (row: any) => [
|
|
148
|
+
...(row.photos?.before ?? []).map((id: string) => ({ id, label: "Before" })),
|
|
149
|
+
...(row.photos?.after ?? []).map((id: string) => ({ id, label: "After" })),
|
|
150
|
+
];
|
|
136
151
|
const notify = inject<(text: string, color?: string) => void>("inventoryNotify", () => {});
|
|
137
152
|
|
|
138
153
|
const now = new Date();
|
|
@@ -174,7 +189,7 @@ const headers: Record<string, any[]> = {
|
|
|
174
189
|
{ title: "Purchase cost", key: "receivedCost" },
|
|
175
190
|
{ title: "Issued", key: "issued" },
|
|
176
191
|
{ title: "Returned", key: "returned" },
|
|
177
|
-
{ title: "
|
|
192
|
+
{ title: "Stock count +/-", key: "adjusted" },
|
|
178
193
|
{ title: "End", key: "closing" },
|
|
179
194
|
{ title: "Used", key: "usedByTechnicians" },
|
|
180
195
|
{ title: "Usage cost", key: "usedCost" },
|
|
@@ -185,11 +200,12 @@ const headers: Record<string, any[]> = {
|
|
|
185
200
|
{ title: "Where", key: "locationType" },
|
|
186
201
|
{ title: "Unit / Area", key: "location" },
|
|
187
202
|
{ title: "Tenant", key: "tenantName" },
|
|
188
|
-
{ title: "Item", key: "itemName" },
|
|
203
|
+
{ title: "Item", key: "itemName", minWidth: "180px" },
|
|
189
204
|
{ title: "Qty", key: "qty" },
|
|
190
205
|
{ title: "Unit price", key: "unitCost" },
|
|
191
206
|
{ title: "Total", key: "totalCost" },
|
|
192
207
|
{ title: "Technician", key: "technician" },
|
|
208
|
+
{ title: "Photos", key: "photos", sortable: false },
|
|
193
209
|
{ title: "Reference", key: "reference" },
|
|
194
210
|
{ title: "Reported by", key: "reportedBy" },
|
|
195
211
|
{ title: "Checked by", key: "checkedByName" },
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<div class="d-flex flex-wrap ga-2 align-center mb-3">
|
|
4
4
|
<v-btn-toggle v-model="status" mandatory density="comfortable" variant="outlined" divided>
|
|
5
5
|
<v-btn value="pending" class="text-none">Waiting</v-btn>
|
|
6
|
-
<v-btn value="issued" class="text-none">
|
|
6
|
+
<v-btn value="issued" class="text-none">Approved</v-btn>
|
|
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>
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
<v-progress-linear v-if="loading" indeterminate color="primary" />
|
|
17
17
|
<div v-else-if="!requests.length" class="text-center text-medium-emphasis py-10">
|
|
18
|
-
{{ status === "pending" ? "No requests waiting." : "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." }}
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
21
|
<AppCard v-for="r in requests" :key="r._id" class="pa-4 mb-3">
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
</template>
|
|
31
31
|
</div>
|
|
32
32
|
<div v-for="l in r.lines" :key="String(l.item)" class="text-body-2">
|
|
33
|
-
{{
|
|
33
|
+
{{ requestLineText(r.status, l) }}
|
|
34
34
|
</div>
|
|
35
35
|
<div v-if="r.reference || r.remarks" class="text-caption text-medium-emphasis mt-1">{{ [r.reference, r.remarks].filter(Boolean).join(" · ") }}</div>
|
|
36
36
|
<div v-if="r.decidedByName" class="text-caption mt-1">
|
|
37
|
-
{{ r
|
|
37
|
+
{{ requestOutcomeText(r, standardFormatDateTime) }}
|
|
38
38
|
</div>
|
|
39
39
|
</AppCard>
|
|
40
40
|
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
<script setup lang="ts">
|
|
87
87
|
import useInventory from "../composables/useInventory";
|
|
88
88
|
import useUtils from "../composables/useUtils";
|
|
89
|
+
import { REQUEST_STATUS, requestLineText, requestOutcomeText } from "../utils/inventory-request-text";
|
|
89
90
|
import useLocalAuth from "../composables/useLocalAuth";
|
|
90
91
|
|
|
91
92
|
const props = defineProps({
|
|
@@ -99,12 +100,7 @@ const { standardFormatDateTime } = useUtils();
|
|
|
99
100
|
const { currentUser } = useLocalAuth();
|
|
100
101
|
const notify = inject<(text: string, color?: string) => void>("inventoryNotify", () => {});
|
|
101
102
|
|
|
102
|
-
const chip
|
|
103
|
-
pending: { label: "Waiting", color: "warning" },
|
|
104
|
-
issued: { label: "Issued", color: "success" },
|
|
105
|
-
rejected: { label: "Rejected", color: "error" },
|
|
106
|
-
cancelled: { label: "Cancelled", color: "grey" },
|
|
107
|
-
};
|
|
103
|
+
const chip = REQUEST_STATUS;
|
|
108
104
|
|
|
109
105
|
const status = ref("pending");
|
|
110
106
|
const requests = ref<TInventoryRequest[]>([]);
|
|
@@ -136,6 +132,7 @@ function changed() {
|
|
|
136
132
|
}
|
|
137
133
|
|
|
138
134
|
async function cancel(r: TInventoryRequest) {
|
|
135
|
+
if (!confirm("Cancel this request? The store keeper will no longer see it.")) return;
|
|
139
136
|
busyId.value = r._id;
|
|
140
137
|
try {
|
|
141
138
|
await api.cancelRequest(r._id);
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
</v-row>
|
|
11
11
|
|
|
12
12
|
<div class="d-flex flex-wrap ga-2 align-center mb-3">
|
|
13
|
-
<v-btn v-if="store.access.canManage" class="text-none screen-btn-primary" variant="flat" prepend-icon="mdi-truck-delivery-outline" @click="open('receive')">
|
|
13
|
+
<v-btn v-if="store.access.canManage && !storeEmpty" class="text-none screen-btn-primary" variant="flat" prepend-icon="mdi-truck-delivery-outline" @click="open('receive')">
|
|
14
14
|
Receive Stock
|
|
15
15
|
</v-btn>
|
|
16
|
-
<v-btn v-if="store.access.canApprove" class="text-none screen-btn-primary" variant="flat" prepend-icon="mdi-hand-extended-outline" @click="open('issue')">
|
|
16
|
+
<v-btn v-if="store.access.canApprove && !storeEmpty" class="text-none screen-btn-primary" variant="flat" prepend-icon="mdi-hand-extended-outline" @click="open('issue')">
|
|
17
17
|
Issue Stock
|
|
18
18
|
</v-btn>
|
|
19
19
|
<v-btn v-if="store.access.canRequest && !store.access.canManage" class="text-none screen-btn-primary" variant="flat" prepend-icon="mdi-cart-arrow-down" @click="open('request')">
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
</v-btn>
|
|
22
22
|
<v-menu v-if="store.access.canManage">
|
|
23
23
|
<template #activator="{ props: menu }">
|
|
24
|
-
<v-btn v-bind="menu" class="text-none screen-btn-ghost" variant="outlined" append-icon="mdi-chevron-down">Add Items</v-btn>
|
|
24
|
+
<v-btn v-bind="menu" class="text-none" :class="storeEmpty ? 'screen-btn-primary' : 'screen-btn-ghost'" :variant="storeEmpty ? 'flat' : 'outlined'" append-icon="mdi-chevron-down">Add Items</v-btn>
|
|
25
25
|
</template>
|
|
26
26
|
<v-list density="compact">
|
|
27
27
|
<v-list-item prepend-icon="mdi-plus" title="Add one item" @click="editItem(null)" />
|
|
@@ -116,6 +116,8 @@ const lowCount = ref(0);
|
|
|
116
116
|
const search = ref("");
|
|
117
117
|
const lowOnly = ref(false);
|
|
118
118
|
const limit = 20;
|
|
119
|
+
// A new store has nothing to receive or hand out yet: its first step is Add Items.
|
|
120
|
+
const storeEmpty = computed(() => !loading.value && total.value === 0 && !search.value && !lowOnly.value);
|
|
119
121
|
const pageRange = computed(() => (total.value ? `${(page.value - 1) * limit + 1}-${Math.min(page.value * limit, total.value)} of ${total.value}` : "0 of 0"));
|
|
120
122
|
|
|
121
123
|
const headers = computed(() => [
|
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.
|
|
5
|
+
"version": "4.61.1",
|
|
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.",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a stock request reads to a technician and a store keeper: plain status
|
|
3
|
+
* words, how many were handed over against how many were asked, and who
|
|
4
|
+
* decided and when. Used by `InventoryRequestsTab`.
|
|
5
|
+
*/
|
|
6
|
+
type TLine = { itemName: string; unit: string; qty: number; issuedQty: number };
|
|
7
|
+
type TRequest = { status: string; decidedByName?: string; decidedAt?: string; decisionRemarks?: string };
|
|
8
|
+
|
|
9
|
+
export const REQUEST_STATUS: Record<string, { label: string; color: string }> = {
|
|
10
|
+
pending: { label: "Waiting for approval", color: "warning" },
|
|
11
|
+
issued: { label: "Approved", color: "success" },
|
|
12
|
+
rejected: { label: "Rejected", color: "error" },
|
|
13
|
+
cancelled: { label: "Cancelled", color: "grey" },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function requestLineText(status: string, l: TLine) {
|
|
17
|
+
return status === "issued"
|
|
18
|
+
? `${l.itemName} — ${l.issuedQty} ${l.unit} handed over (asked ${l.qty})`
|
|
19
|
+
: `${l.itemName} — ${l.qty} ${l.unit}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** "Approved and handed over by X on <when> — remarks", or "" before a decision. */
|
|
23
|
+
export function requestOutcomeText(r: TRequest, formatWhen: (d: string) => string) {
|
|
24
|
+
if (!r.decidedByName) return "";
|
|
25
|
+
const what = r.status === "issued" ? "Approved and handed over" : "Rejected";
|
|
26
|
+
const when = r.decidedAt ? ` on ${formatWhen(r.decidedAt)}` : "";
|
|
27
|
+
const why = r.decisionRemarks ? ` — ${r.decisionRemarks}` : "";
|
|
28
|
+
return `${what} by ${r.decidedByName}${when}${why}`;
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store switcher's button text. "Property store" / "Our store" is enough
|
|
3
|
+
* when each kind appears once; when a person can see several stores of the
|
|
4
|
+
* same kind (e.g. staff of several organisations at one property), each one
|
|
5
|
+
* also names its organisation so the buttons can be told apart.
|
|
6
|
+
*/
|
|
7
|
+
type TStoreLike = { ownerType: string; orgName?: string };
|
|
8
|
+
|
|
9
|
+
export function inventoryStoreLabel(store: TStoreLike, all: TStoreLike[]) {
|
|
10
|
+
const kind = store.ownerType === "property" ? "Property store" : "Our store";
|
|
11
|
+
const sameKind = all.filter((s) => s.ownerType === store.ownerType).length;
|
|
12
|
+
return sameKind > 1 && store.orgName ? `${kind} · ${store.orgName}` : kind;
|
|
13
|
+
}
|