Add scanner utility, modal, and stock scan page implementation
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed

This commit is contained in:
2026-05-01 23:32:13 +02:00
parent 50e147b079
commit 47434db5b5
15 changed files with 1952 additions and 353 deletions
+29 -20
View File
@@ -3,8 +3,8 @@ import {
listGroupedStockEntries,
listKitchenChanges,
listStockEntries,
markStockGone,
updateStockItem,
useStockItem,
} from '../../api/stock.js';
import { fetchLocations } from '../../api/locations.js';
import { STORAGE_KEYS } from '../../app/config.js';
@@ -578,7 +578,8 @@ export function renderStockListPage() {
<div class="quick-edit-stack">
<template x-if="entry.stock_type === 'binary'">
<div class="d-flex flex-wrap gap-2">
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="updateBinary(entry, 'gone')">Mark gone</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry, 'consumed')">Mark used</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry, 'spoiled')">Spoilt</button>
</div>
</template>
<template x-if="entry.stock_type === 'descriptive'">
@@ -589,14 +590,16 @@ export function renderStockListPage() {
</template>
</select>
<button class="btn btn-sm btn-primary" type="button" @click="saveLevel(entry)">Save</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry)">Mark gone</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry, 'consumed')">Mark used</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry, 'spoiled')">Spoilt</button>
</div>
</template>
<template x-if="entry.stock_type === 'measured'">
<div class="d-flex flex-wrap gap-2 align-items-center">
<input class="form-control form-control-sm quick-number" type="number" step="0.01" min="0" x-model="editForms[entry.id].quantity" />
<button class="btn btn-sm btn-primary" type="button" @click="saveQuantity(entry)">Save qty</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry)">Mark gone</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry, 'consumed')">Mark used</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry, 'spoiled')">Spoilt</button>
</div>
</template>
<template x-if="editErrors[entry.id]">
@@ -660,7 +663,8 @@ export function renderStockListPage() {
<div class="quick-edit-stack">
<template x-if="entry.stock_type === 'binary'">
<div class="d-flex flex-wrap gap-2">
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="updateBinary(entry, 'gone')">Mark gone</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry, 'consumed')">Mark used</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry, 'spoiled')">Spoilt</button>
</div>
</template>
<template x-if="entry.stock_type === 'descriptive'">
@@ -672,7 +676,8 @@ export function renderStockListPage() {
</select>
<div class="d-flex flex-wrap gap-2">
<button class="btn btn-sm btn-primary" type="button" @click="saveLevel(entry)">Save stock level</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry)">Mark gone</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry, 'consumed')">Mark used</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry, 'spoiled')">Spoilt</button>
</div>
</div>
</template>
@@ -681,7 +686,8 @@ export function renderStockListPage() {
<input class="form-control form-control-sm" type="number" step="0.01" min="0" x-model="editForms[entry.id].quantity" />
<div class="d-flex flex-wrap gap-2">
<button class="btn btn-sm btn-primary" type="button" @click="saveQuantity(entry)">Save quantity</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry)">Mark gone</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry, 'consumed')">Mark used</button>
<button class="btn btn-sm btn-outline-danger" type="button" :disabled="isItemRefreshing(entry)" @click="markGone(entry, 'spoiled')">Spoilt</button>
</div>
</div>
</template>
@@ -796,7 +802,8 @@ export function renderStockListPage() {
>
Details
</a>
<button class="btn btn-sm btn-outline-danger grouped-stock-mark-gone" type="button" :disabled="isItemRefreshing(item)" @click="markGoneFromGroup(item, group)">Mark gone</button>
<button class="btn btn-sm btn-outline-danger grouped-stock-mark-gone" type="button" :disabled="isItemRefreshing(item)" @click="markGoneFromGroup(item, group, 'consumed')">Mark used</button>
<button class="btn btn-sm btn-outline-danger grouped-stock-mark-gone" type="button" :disabled="isItemRefreshing(item)" @click="markGoneFromGroup(item, group, 'spoiled')">Spoilt</button>
<div class="small text-body-secondary stock-item-refresh-indicator" x-show="isItemRefreshing(item)">
Refreshing...
</div>
@@ -2202,12 +2209,12 @@ export function stockListPageData(store) {
return;
}
await this.useEntry(entry);
await this.useEntry(entry, 'consumed');
},
async saveLevel(entry) {
const level = this.editForms[entry.id]?.level || 'plenty';
if (level === 'gone') {
await this.useEntry(entry);
await this.useEntry(entry, 'consumed');
return;
}
@@ -2234,14 +2241,14 @@ export function stockListPageData(store) {
{ quantity },
);
},
async markGone(entry) {
async markGone(entry, reason = 'consumed') {
if (this.isItemRefreshing(entry)) {
return;
}
await this.useEntry(entry);
await this.useEntry(entry, reason);
},
async markGoneFromGroup(item, group) {
async markGoneFromGroup(item, group, reason = 'consumed') {
if (this.isItemRefreshing(item)) {
return;
}
@@ -2249,8 +2256,9 @@ export function stockListPageData(store) {
this.editErrors[item.id] = '';
try {
const result = await useStockItem(store, item.uuid_b64);
const result = await markStockGone(store, item.uuid_b64, reason);
const alreadyGone = result.status === 'already_gone';
const actionLabel = reason === 'spoiled' ? 'marked spoilt' : 'marked used';
this.removeGroupedItem(group.id, item.id);
this.removeEntryLocally(item.id);
delete this.editForms[item.id];
@@ -2259,11 +2267,11 @@ export function stockListPageData(store) {
type: alreadyGone ? 'info' : 'success',
message: alreadyGone
? `${item.name} was already out of stock and removed from the group.`
: `${item.name} was marked gone and removed from the group.`,
: `${item.name} was ${actionLabel} and removed from the group.`,
});
this.loadGroupedEntries({ expanded: 0, background: true }).catch(() => {});
} catch (error) {
this.editErrors[item.id] = error.message || 'Mark gone failed.';
this.editErrors[item.id] = error.message || 'Removal failed.';
}
},
async saveEntryUpdate(entry, payload, localPatch) {
@@ -2281,7 +2289,7 @@ export function stockListPageData(store) {
this.editErrors[entry.id] = error.message || 'Update failed.';
}
},
async useEntry(entry) {
async useEntry(entry, reason = 'consumed') {
if (this.isItemRefreshing(entry)) {
return;
}
@@ -2289,8 +2297,9 @@ export function stockListPageData(store) {
this.editErrors[entry.id] = '';
try {
const result = await useStockItem(store, entry.uuid_b64);
const result = await markStockGone(store, entry.uuid_b64, reason);
const alreadyGone = result.status === 'already_gone';
const actionLabel = reason === 'spoiled' ? 'marked spoilt' : 'marked used';
this.removeEntryLocally(entry.id);
delete this.editForms[entry.id];
delete this.editErrors[entry.id];
@@ -2298,11 +2307,11 @@ export function stockListPageData(store) {
type: alreadyGone ? 'info' : 'success',
message: alreadyGone
? `${entry.name} was already out of stock and removed from the list.`
: `${entry.name} was marked gone and removed from the list.`,
: `${entry.name} was ${actionLabel} and removed from the list.`,
});
this.refreshLoadedViewsInBackground().catch(() => {});
} catch (error) {
this.editErrors[entry.id] = error.message || 'Mark gone failed.';
this.editErrors[entry.id] = error.message || 'Removal failed.';
}
},
removeEntryLocally(entryId) {