Refactor API client and stock management logic for improved clarity, error handling, and support for additional stock types.
This commit is contained in:
@@ -21,7 +21,10 @@ export function renderLoginPage() {
|
||||
<form class="vstack gap-3" @submit.prevent="submit()">
|
||||
<div>
|
||||
<label class="form-label" for="base-url">Tryton server URL</label>
|
||||
<input id="base-url" class="form-control" type="url" x-model="form.baseUrl" placeholder="https://tryton.example.com" required />
|
||||
<input id="base-url" class="form-control" type="url" x-model="form.baseUrl" placeholder="https://tryton.example.com" />
|
||||
<div class="form-text">
|
||||
Leave this empty to use the same origin as the current Lonc page.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="database">Database name</label>
|
||||
|
||||
@@ -18,7 +18,10 @@ export function renderSettingsPage() {
|
||||
<form class="vstack gap-3" @submit.prevent="save()">
|
||||
<div>
|
||||
<label class="form-label">Tryton server URL</label>
|
||||
<input class="form-control" type="url" x-model="form.baseUrl" required />
|
||||
<input class="form-control" type="url" x-model="form.baseUrl" />
|
||||
<div class="form-text">
|
||||
Leave empty to use the same origin as this deployed Lonc frontend.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Database name</label>
|
||||
@@ -36,6 +39,7 @@ export function renderSettingsPage() {
|
||||
<h2 class="h5">Integration notes</h2>
|
||||
<ul class="text-body-secondary small ps-3 mb-0">
|
||||
<li>Connection uses Tryton user application keys for the <code>kitchen</code> application.</li>
|
||||
<li>Leaving the server URL empty makes API calls use same-origin relative paths.</li>
|
||||
<li>Kitchen-scoped requests are built as <code>/{database}/kitchen/{kitchenId}/...</code>.</li>
|
||||
<li>Label preview accepts image blobs, image URLs, or SVG payloads.</li>
|
||||
</ul>
|
||||
|
||||
@@ -687,7 +687,7 @@ export function labelCreatePageData(store) {
|
||||
});
|
||||
saveStoredValue(STORAGE_KEYS.labelDraft, buildDraftPayload(this.form));
|
||||
} catch (error) {
|
||||
this.fieldErrors = normalizeValidationError(error.cause);
|
||||
this.fieldErrors = normalizeValidationError(error);
|
||||
this.submitError = error.message;
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,15 @@ export function fieldError(errors, field) {
|
||||
}
|
||||
|
||||
export function normalizeValidationError(error) {
|
||||
if (error?.details && typeof error.details === 'object') {
|
||||
return error.details;
|
||||
const details =
|
||||
error?.details ||
|
||||
error?.cause?.details ||
|
||||
error?.payload?.errors ||
|
||||
error?.payload?.details ||
|
||||
null;
|
||||
|
||||
if (details && typeof details === 'object') {
|
||||
return details;
|
||||
}
|
||||
|
||||
return {};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { adjustStockEntry, getStockEntry } from '../../api/stock.js';
|
||||
import {
|
||||
adjustStockEntry,
|
||||
deleteStockItem,
|
||||
getStockEntry,
|
||||
} from '../../api/stock.js';
|
||||
import { getRouteContext } from '../../app/router.js';
|
||||
import { createAsyncState, runAsyncState } from '../shared/ui-state.js';
|
||||
import { formatDate } from '../shared/date-utils.js';
|
||||
@@ -38,11 +42,11 @@ export function renderStockDetailPage() {
|
||||
<dt class="col-5">Quantity</dt>
|
||||
<dd class="col-7" x-text="formatQuantity(entry)"></dd>
|
||||
<dt class="col-5">Location</dt>
|
||||
<dd class="col-7" x-text="entry.location_name || 'Unassigned'"></dd>
|
||||
<dd class="col-7" x-text="entry.location_initial_uuid_b64 || 'Unassigned'"></dd>
|
||||
<dt class="col-5">Production date</dt>
|
||||
<dd class="col-7" x-text="formatDate(entry.production_date)"></dd>
|
||||
<dd class="col-7" x-text="formatDate(entry.date)"></dd>
|
||||
<dt class="col-5">Expiration date</dt>
|
||||
<dd class="col-7" x-text="formatDate(entry.expiration_date)"></dd>
|
||||
<dd class="col-7" x-text="formatDate(entry.expire_date)"></dd>
|
||||
<dt class="col-5">Stock type</dt>
|
||||
<dd class="col-7" x-text="entry.stock_type || 'Stored'"></dd>
|
||||
</dl>
|
||||
@@ -56,34 +60,78 @@ export function renderStockDetailPage() {
|
||||
<p class="eyebrow mb-2">Adjustment</p>
|
||||
<h2 class="h5 mb-3">Update current stock level</h2>
|
||||
|
||||
<form class="vstack gap-3" @submit.prevent="submitAdjustment()">
|
||||
<div>
|
||||
<label class="form-label">Adjustment mode</label>
|
||||
<select class="form-select" x-model="adjustment.mode">
|
||||
<option value="increment">Add quantity</option>
|
||||
<option value="decrement">Subtract quantity</option>
|
||||
<option value="set">Set exact quantity</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Quantity</label>
|
||||
<input class="form-control" type="number" min="0" step="0.01" x-model="adjustment.quantity" required />
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary" @click="quickAdjust(1)">+1</button>
|
||||
<button type="button" class="btn btn-outline-secondary" @click="quickAdjust(-1)">-1</button>
|
||||
<button type="button" class="btn btn-outline-secondary" @click="quickAdjust(0.5)">+0.5</button>
|
||||
</div>
|
||||
<template x-if="entry.stock_type === 'measured'">
|
||||
<form class="vstack gap-3" @submit.prevent="submitMeasuredAdjustment()">
|
||||
<div>
|
||||
<label class="form-label">Adjustment mode</label>
|
||||
<select class="form-select" x-model="adjustment.mode">
|
||||
<option value="increment">Add quantity</option>
|
||||
<option value="decrement">Subtract quantity</option>
|
||||
<option value="set">Set exact quantity</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Quantity</label>
|
||||
<input class="form-control" type="number" min="0" step="0.01" x-model="adjustment.quantity" required />
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary" @click="quickAdjust(1)">+1</button>
|
||||
<button type="button" class="btn btn-outline-secondary" @click="quickAdjust(-1)">-1</button>
|
||||
<button type="button" class="btn btn-outline-secondary" @click="quickAdjust(0.5)">+0.5</button>
|
||||
</div>
|
||||
|
||||
<template x-if="adjustmentState.error">
|
||||
<div class="alert alert-danger mb-0" x-text="adjustmentState.error"></div>
|
||||
</template>
|
||||
<template x-if="adjustmentState.error">
|
||||
<div class="alert alert-danger mb-0" x-text="adjustmentState.error"></div>
|
||||
</template>
|
||||
|
||||
<button class="btn btn-primary" type="submit" :disabled="adjustmentState.isLoading">
|
||||
<span x-show="!adjustmentState.isLoading">Save adjustment</span>
|
||||
<span x-show="adjustmentState.isLoading">Saving...</span>
|
||||
</button>
|
||||
</form>
|
||||
<button class="btn btn-primary" type="submit" :disabled="adjustmentState.isLoading">
|
||||
<span x-show="!adjustmentState.isLoading">Save quantity</span>
|
||||
<span x-show="adjustmentState.isLoading">Saving...</span>
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template x-if="entry.stock_type === 'descriptive'">
|
||||
<form class="vstack gap-3" @submit.prevent="submitLevelAdjustment()">
|
||||
<div>
|
||||
<label class="form-label">Stock level</label>
|
||||
<select class="form-select" x-model="adjustment.level">
|
||||
<option value="plenty">Plenty</option>
|
||||
<option value="good">Good</option>
|
||||
<option value="some">Some</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="trace">Trace</option>
|
||||
<option value="gone">Gone</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<template x-if="adjustmentState.error">
|
||||
<div class="alert alert-danger mb-0" x-text="adjustmentState.error"></div>
|
||||
</template>
|
||||
|
||||
<button class="btn btn-primary" type="submit" :disabled="adjustmentState.isLoading">
|
||||
<span x-show="!adjustmentState.isLoading">Save stock level</span>
|
||||
<span x-show="adjustmentState.isLoading">Saving...</span>
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template x-if="entry.stock_type === 'binary'">
|
||||
<div class="vstack gap-3">
|
||||
<p class="text-body-secondary mb-0">
|
||||
Binary stock items can be marked gone from this screen.
|
||||
</p>
|
||||
|
||||
<template x-if="adjustmentState.error">
|
||||
<div class="alert alert-danger mb-0" x-text="adjustmentState.error"></div>
|
||||
</template>
|
||||
|
||||
<button class="btn btn-outline-danger align-self-start" type="button" @click="markGone()" :disabled="adjustmentState.isLoading">
|
||||
<span x-show="!adjustmentState.isLoading">Mark gone</span>
|
||||
<span x-show="adjustmentState.isLoading">Removing...</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,33 +149,70 @@ export function stockDetailPageData(store) {
|
||||
adjustment: {
|
||||
mode: 'increment',
|
||||
quantity: '1',
|
||||
level: 'plenty',
|
||||
},
|
||||
async init() {
|
||||
const { params } = getRouteContext();
|
||||
await runAsyncState(this.state, async () => {
|
||||
this.entry = await getStockEntry(store, params.id);
|
||||
this.adjustment.level = this.entry?.level || 'plenty';
|
||||
}).catch(() => {});
|
||||
},
|
||||
async submitAdjustment() {
|
||||
async submitMeasuredAdjustment() {
|
||||
if (!this.entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
await runAsyncState(this.adjustmentState, async () => {
|
||||
this.entry = await adjustStockEntry(store, this.entry.id, {
|
||||
mode: this.adjustment.mode,
|
||||
quantity: Number(this.adjustment.quantity),
|
||||
const requestedQuantity = Number(this.adjustment.quantity);
|
||||
if (Number.isNaN(requestedQuantity) || requestedQuantity < 0) {
|
||||
throw new Error('Enter a valid quantity first.');
|
||||
}
|
||||
|
||||
const currentQuantity = Number(this.entry.quantity || 0);
|
||||
const exactQuantity =
|
||||
this.adjustment.mode === 'increment'
|
||||
? currentQuantity + requestedQuantity
|
||||
: this.adjustment.mode === 'decrement'
|
||||
? Math.max(currentQuantity - requestedQuantity, 0)
|
||||
: requestedQuantity;
|
||||
|
||||
this.entry = await adjustStockEntry(store, this.entry.uuid_b64, {
|
||||
quantity: exactQuantity,
|
||||
});
|
||||
store.addAlert({ type: 'success', message: 'Stock quantity updated.' });
|
||||
}).catch(() => {});
|
||||
},
|
||||
async submitLevelAdjustment() {
|
||||
if (!this.entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
await runAsyncState(this.adjustmentState, async () => {
|
||||
this.entry = await adjustStockEntry(store, this.entry.uuid_b64, {
|
||||
level: this.adjustment.level,
|
||||
});
|
||||
store.addAlert({ type: 'success', message: 'Stock level updated.' });
|
||||
}).catch(() => {});
|
||||
},
|
||||
async markGone() {
|
||||
if (!this.entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
await runAsyncState(this.adjustmentState, async () => {
|
||||
await deleteStockItem(store, this.entry.uuid_b64);
|
||||
store.addAlert({ type: 'success', message: `${this.entry.name} was marked gone.` });
|
||||
window.__loncApp.navigate('/stock');
|
||||
}).catch(() => {});
|
||||
},
|
||||
quickAdjust(step) {
|
||||
const current = Number(this.adjustment.quantity || 0);
|
||||
this.adjustment.quantity = String(Math.max(current + step, 0));
|
||||
},
|
||||
formatDate,
|
||||
formatQuantity(entry) {
|
||||
return `${entry.quantity ?? 0} ${entry.uom || ''}`.trim();
|
||||
return `${entry.quantity ?? 0} ${entry.uom_symbol || ''}`.trim();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user