Implement upsert label flow and use-based mark gone handling
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import { createStockEntry, searchItemDefinitions } from '../../api/stock.js';
|
||||
import {
|
||||
applyItemUpsert,
|
||||
previewItemUpsert,
|
||||
searchItemDefinitions,
|
||||
} from '../../api/stock.js';
|
||||
import { fetchLocations } from '../../api/locations.js';
|
||||
import { previewLabel } from '../../api/labels.js';
|
||||
import { STORAGE_KEYS } from '../../app/config.js';
|
||||
@@ -400,6 +404,14 @@ export function renderLabelCreatePage() {
|
||||
<div class="alert alert-success mb-0" x-text="successMessage"></div>
|
||||
</template>
|
||||
|
||||
<template x-if="upsertPreview && !upsertPreview.error">
|
||||
<div class="alert alert-info mb-0 py-2" x-text="upsertPreviewSummary()"></div>
|
||||
</template>
|
||||
|
||||
<template x-if="upsertPreview?.error">
|
||||
<div class="alert alert-warning mb-0 py-2" x-text="upsertPreview.error"></div>
|
||||
</template>
|
||||
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button class="btn btn-outline-primary" type="button" @click="preview()" :disabled="previewState.isLoading">
|
||||
@@ -407,7 +419,7 @@ export function renderLabelCreatePage() {
|
||||
<span x-show="previewState.isLoading">Rendering preview...</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" type="submit" :disabled="createState.isLoading">
|
||||
<span x-show="!createState.isLoading">Create stock entry</span>
|
||||
<span x-show="!createState.isLoading">Save stock entry</span>
|
||||
<span x-show="createState.isLoading">Saving...</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -478,6 +490,10 @@ function diffDays(fromIsoDate, toIsoDate) {
|
||||
function createDefaultForm() {
|
||||
return {
|
||||
itemId: '',
|
||||
itemUuidB64: '',
|
||||
identifierCode: '',
|
||||
externalSource: '',
|
||||
externalId: '',
|
||||
search: '',
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -505,6 +521,10 @@ function loadLabelDraft() {
|
||||
? ''
|
||||
: draft.quantity,
|
||||
itemId: '',
|
||||
itemUuidB64: '',
|
||||
identifierCode: '',
|
||||
externalSource: '',
|
||||
externalId: '',
|
||||
search: '',
|
||||
};
|
||||
}
|
||||
@@ -513,6 +533,10 @@ function buildDraftPayload(form) {
|
||||
return {
|
||||
...form,
|
||||
itemId: '',
|
||||
itemUuidB64: '',
|
||||
identifierCode: '',
|
||||
externalSource: '',
|
||||
externalId: '',
|
||||
search: '',
|
||||
};
|
||||
}
|
||||
@@ -536,6 +560,7 @@ export function labelCreatePageData(store) {
|
||||
successMessage: '',
|
||||
submitError: '',
|
||||
fieldErrors: {},
|
||||
upsertPreview: null,
|
||||
form: {
|
||||
...loadLabelDraft(),
|
||||
},
|
||||
@@ -590,6 +615,14 @@ export function labelCreatePageData(store) {
|
||||
}
|
||||
},
|
||||
onSearchInput() {
|
||||
this.upsertPreview = null;
|
||||
if (this.form.itemUuidB64 || this.form.itemId) {
|
||||
this.form.itemId = '';
|
||||
this.form.itemUuidB64 = '';
|
||||
this.form.identifierCode = '';
|
||||
this.form.externalSource = '';
|
||||
this.form.externalId = '';
|
||||
}
|
||||
this.persistDraft();
|
||||
this.searchDebounced();
|
||||
},
|
||||
@@ -603,6 +636,10 @@ export function labelCreatePageData(store) {
|
||||
: null;
|
||||
|
||||
this.form.itemId = item.id;
|
||||
this.form.itemUuidB64 = item.uuid_b64 || '';
|
||||
this.form.identifierCode = item.identifier_code || '';
|
||||
this.form.externalSource = item.external_source || '';
|
||||
this.form.externalId = item.external_id || '';
|
||||
this.form.search = item.name;
|
||||
this.form.name = item.name;
|
||||
this.form.description = item.description || this.form.description;
|
||||
@@ -623,7 +660,12 @@ export function labelCreatePageData(store) {
|
||||
},
|
||||
clearItemSearch() {
|
||||
this.form.itemId = '';
|
||||
this.form.itemUuidB64 = '';
|
||||
this.form.identifierCode = '';
|
||||
this.form.externalSource = '';
|
||||
this.form.externalId = '';
|
||||
this.form.search = '';
|
||||
this.upsertPreview = null;
|
||||
this.suggestions = [];
|
||||
this.persistDraft();
|
||||
},
|
||||
@@ -908,6 +950,8 @@ export function labelCreatePageData(store) {
|
||||
: null
|
||||
: Number(this.form.quantity);
|
||||
|
||||
const selectedLocationUuidB64 = this.selectedLocation?.uuid_b64 || null;
|
||||
|
||||
return {
|
||||
item_id: this.form.itemId || null,
|
||||
name: this.form.name.trim(),
|
||||
@@ -920,13 +964,51 @@ export function labelCreatePageData(store) {
|
||||
level: this.form.stockType === 'measured' ? null : this.form.level || null,
|
||||
date: this.form.productionDate || null,
|
||||
expire_date: this.form.expirationDate || null,
|
||||
location_initial: this.form.locationId || null,
|
||||
location_initial: selectedLocationUuidB64,
|
||||
kitchen_id: store.activeKitchen?.id || null,
|
||||
};
|
||||
},
|
||||
buildUpsertPayload() {
|
||||
const basePayload = this.buildPayload();
|
||||
const itemPayload = {
|
||||
name: basePayload.name,
|
||||
description: basePayload.description,
|
||||
quantity_initial: basePayload.quantity_initial,
|
||||
uom_symbol: basePayload.uom_symbol,
|
||||
calories: basePayload.calories,
|
||||
calories_unit: basePayload.calories_unit,
|
||||
stock_type: basePayload.stock_type,
|
||||
level: basePayload.level,
|
||||
date: basePayload.date,
|
||||
expire_date: basePayload.expire_date,
|
||||
location_initial: basePayload.location_initial,
|
||||
};
|
||||
|
||||
return {
|
||||
uuid_b64: this.form.itemUuidB64 || null,
|
||||
identifier_code: this.form.identifierCode || null,
|
||||
external_source: this.form.externalSource || null,
|
||||
external_id: this.form.externalId || null,
|
||||
item: itemPayload,
|
||||
};
|
||||
},
|
||||
upsertPreviewSummary() {
|
||||
if (!this.upsertPreview || this.upsertPreview.error) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (this.upsertPreview.operation === 'update') {
|
||||
const name = this.upsertPreview.matchedItem?.name || this.form.name;
|
||||
const matchType = this.upsertPreview.matchType ? ` (matched by ${this.upsertPreview.matchType})` : '';
|
||||
return `Submit will update: ${name}${matchType}.`;
|
||||
}
|
||||
|
||||
return 'Submit will create a new stock item.';
|
||||
},
|
||||
async preview() {
|
||||
this.submitError = '';
|
||||
this.fieldErrors = {};
|
||||
this.upsertPreview = null;
|
||||
|
||||
if (!this.validateBeforeSubmit()) {
|
||||
this.previewState.error = 'Please fill out the required fields before previewing the label.';
|
||||
@@ -940,6 +1022,13 @@ export function labelCreatePageData(store) {
|
||||
URL.revokeObjectURL(this.previewUrl);
|
||||
}
|
||||
this.previewUrl = result.objectUrl;
|
||||
try {
|
||||
this.upsertPreview = await previewItemUpsert(store, this.buildUpsertPayload());
|
||||
} catch (error) {
|
||||
this.upsertPreview = {
|
||||
error: error.message || 'Upsert preview failed.',
|
||||
};
|
||||
}
|
||||
this.persistDraft();
|
||||
});
|
||||
},
|
||||
@@ -948,22 +1037,25 @@ export function labelCreatePageData(store) {
|
||||
this.fieldErrors = {};
|
||||
|
||||
if (!this.validateBeforeSubmit()) {
|
||||
this.submitError = 'Please fill out the required fields before creating the stock entry.';
|
||||
this.submitError = 'Please fill out the required fields before saving the stock entry.';
|
||||
return;
|
||||
}
|
||||
|
||||
await runAsyncState(this.createState, async () => {
|
||||
try {
|
||||
const entry = await createStockEntry(store, this.buildPayload());
|
||||
const entry = await applyItemUpsert(store, this.buildUpsertPayload());
|
||||
if (this.previewUrl && this.previewUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(this.previewUrl);
|
||||
}
|
||||
this.previewUrl = '';
|
||||
this.successMessage = `${entry.name || this.form.name} was created successfully.`;
|
||||
const entryName = entry.item?.name || this.form.name;
|
||||
const operationVerb = entry.operation === 'update' ? 'updated' : 'created';
|
||||
this.successMessage = `${entryName} was ${operationVerb} successfully.`;
|
||||
store.addAlert({
|
||||
type: 'success',
|
||||
message: `${entry.name || this.form.name} was created successfully.`,
|
||||
message: `${entryName} was ${operationVerb} successfully.`,
|
||||
});
|
||||
this.upsertPreview = entry;
|
||||
saveStoredValue(STORAGE_KEYS.labelDraft, buildDraftPayload(this.form));
|
||||
} catch (error) {
|
||||
this.fieldErrors = normalizeValidationError(error);
|
||||
@@ -981,6 +1073,7 @@ export function labelCreatePageData(store) {
|
||||
this.successMessage = '';
|
||||
this.submitError = '';
|
||||
this.fieldErrors = {};
|
||||
this.upsertPreview = null;
|
||||
saveStoredValue(STORAGE_KEYS.labelDraft, this.form);
|
||||
if (revokePreview && this.previewUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(this.previewUrl);
|
||||
|
||||
Reference in New Issue
Block a user