134 lines
5.7 KiB
JavaScript
134 lines
5.7 KiB
JavaScript
|
|
import { adjustStockEntry, 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';
|
||
|
|
|
||
|
|
export function renderStockDetailPage() {
|
||
|
|
return `
|
||
|
|
<section class="container-xxl py-4 py-lg-5" x-data="stockDetailPage()" x-init="init()">
|
||
|
|
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3 align-items-lg-center mb-4">
|
||
|
|
<div>
|
||
|
|
<a href="#/stock" class="link-secondary text-decoration-none small">← Back to stock</a>
|
||
|
|
<h1 class="h3 mb-1 mt-2">Stock detail</h1>
|
||
|
|
<p class="text-body-secondary mb-0">Inspect the entry and update its quantity without leaving the workflow.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<template x-if="state.error">
|
||
|
|
<div class="alert alert-danger" x-text="state.error"></div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<template x-if="entry">
|
||
|
|
<div class="row g-4">
|
||
|
|
<div class="col-12 col-lg-7">
|
||
|
|
<div class="card border-0 shadow-sm">
|
||
|
|
<div class="card-body p-4">
|
||
|
|
<div class="d-flex flex-wrap justify-content-between gap-3 mb-4">
|
||
|
|
<div>
|
||
|
|
<p class="eyebrow mb-2">Entry</p>
|
||
|
|
<h2 class="h4 mb-1" x-text="entry.name"></h2>
|
||
|
|
<p class="text-body-secondary mb-0" x-text="entry.description || 'No description'"></p>
|
||
|
|
</div>
|
||
|
|
<span class="badge rounded-pill text-bg-light border align-self-start" x-text="entry.status || 'ok'"></span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<dl class="row mb-0 detail-grid">
|
||
|
|
<dt class="col-5">Kitchen</dt>
|
||
|
|
<dd class="col-7" x-text="$store.app.activeKitchen?.name || 'Unknown'"></dd>
|
||
|
|
<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>
|
||
|
|
<dt class="col-5">Production date</dt>
|
||
|
|
<dd class="col-7" x-text="formatDate(entry.production_date)"></dd>
|
||
|
|
<dt class="col-5">Expiration date</dt>
|
||
|
|
<dd class="col-7" x-text="formatDate(entry.expiration_date)"></dd>
|
||
|
|
<dt class="col-5">Stock type</dt>
|
||
|
|
<dd class="col-7" x-text="entry.stock_type || 'Stored'"></dd>
|
||
|
|
</dl>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-12 col-lg-5">
|
||
|
|
<div class="card border-0 shadow-sm">
|
||
|
|
<div class="card-body p-4">
|
||
|
|
<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="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>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
</section>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function stockDetailPageData(store) {
|
||
|
|
return {
|
||
|
|
state: createAsyncState(),
|
||
|
|
adjustmentState: createAsyncState(),
|
||
|
|
entry: null,
|
||
|
|
adjustment: {
|
||
|
|
mode: 'increment',
|
||
|
|
quantity: '1',
|
||
|
|
},
|
||
|
|
async init() {
|
||
|
|
const { params } = getRouteContext();
|
||
|
|
await runAsyncState(this.state, async () => {
|
||
|
|
this.entry = await getStockEntry(store, params.id);
|
||
|
|
}).catch(() => {});
|
||
|
|
},
|
||
|
|
async submitAdjustment() {
|
||
|
|
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),
|
||
|
|
});
|
||
|
|
store.addAlert({ type: 'success', message: 'Stock quantity updated.' });
|
||
|
|
}).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();
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|