Add label printing functionality and error handling in stock and label flows
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful

This commit is contained in:
2026-04-10 22:08:01 +02:00
parent 1dc1bb4912
commit e1383c4d56
10 changed files with 627 additions and 21 deletions
+48 -14
View File
@@ -4,7 +4,11 @@ import {
searchItemDefinitions,
} from '../../api/stock.js';
import { fetchLocations } from '../../api/locations.js';
import { previewLabel } from '../../api/labels.js';
import {
formatPrintErrorMessage,
previewLabel,
printItemLabel,
} from '../../api/labels.js';
import { STORAGE_KEYS } from '../../app/config.js';
import { debounce, normalizeValidationError } from '../shared/form-utils.js';
import { loadStoredValue, saveStoredValue } from '../shared/storage.js';
@@ -404,7 +408,7 @@ export function renderLabelCreatePage() {
<div class="alert alert-success mb-0" x-text="successMessage"></div>
</template>
<template x-if="upsertPreview && !upsertPreview.error">
<template x-if="upsertPreview?.mode === 'preview' && !upsertPreview.error">
<div class="alert alert-info mb-0 py-2" x-text="upsertPreviewSummary()"></div>
</template>
@@ -412,18 +416,28 @@ export function renderLabelCreatePage() {
<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">
<template x-if="printIssue">
<div class="alert alert-warning mb-0 py-2" x-text="printIssue"></div>
</template>
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 label-actions-row">
<div class="d-flex flex-wrap gap-2 label-actions-primary">
<button class="btn btn-outline-primary label-action-btn" type="button" @click="preview()" :disabled="previewState.isLoading">
<span x-show="!previewState.isLoading">Preview label</span>
<span x-show="previewState.isLoading">Rendering preview...</span>
</button>
<button class="btn btn-primary" type="submit" :disabled="createState.isLoading">
<span x-show="!createState.isLoading">Save stock entry</span>
<span x-show="createState.isLoading">Saving...</span>
</button>
<div class="input-group input-group-label-submit">
<span class="input-group-text">
<input class="form-check-input mt-0 me-2" type="checkbox" x-model="printLabelOnSave" aria-label="Print label on save" />
Print
</span>
<button class="btn btn-primary label-action-btn" type="submit" :disabled="createState.isLoading">
<span x-show="!createState.isLoading">Save stock entry</span>
<span x-show="createState.isLoading">Saving...</span>
</button>
</div>
</div>
<button class="btn btn-outline-secondary" type="button" @click="reset()">Clear form</button>
<button class="btn btn-outline-secondary label-action-btn" type="button" @click="reset()">Clear form</button>
</div>
<div class="small text-body-secondary">
<span class="text-danger">*</span> Required field
@@ -561,6 +575,8 @@ export function labelCreatePageData(store) {
submitError: '',
fieldErrors: {},
upsertPreview: null,
printLabelOnSave: true,
printIssue: '',
form: {
...loadLabelDraft(),
},
@@ -997,6 +1013,10 @@ export function labelCreatePageData(store) {
return '';
}
if (this.upsertPreview.mode !== 'preview') {
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})` : '';
@@ -1009,6 +1029,7 @@ export function labelCreatePageData(store) {
this.submitError = '';
this.fieldErrors = {};
this.upsertPreview = null;
this.printIssue = '';
if (!this.validateBeforeSubmit()) {
this.previewState.error = 'Please fill out the required fields before previewing the label.';
@@ -1035,6 +1056,7 @@ export function labelCreatePageData(store) {
async create() {
this.submitError = '';
this.fieldErrors = {};
this.printIssue = '';
if (!this.validateBeforeSubmit()) {
this.submitError = 'Please fill out the required fields before saving the stock entry.';
@@ -1044,12 +1066,23 @@ export function labelCreatePageData(store) {
await runAsyncState(this.createState, async () => {
try {
const entry = await applyItemUpsert(store, this.buildUpsertPayload());
if (this.previewUrl && this.previewUrl.startsWith('blob:')) {
URL.revokeObjectURL(this.previewUrl);
}
this.previewUrl = '';
const entryName = entry.item?.name || this.form.name;
const operationVerb = entry.operation === 'update' ? 'updated' : 'created';
const createdUuidB64 = entry.item?.uuid_b64 || null;
if (this.printLabelOnSave && createdUuidB64) {
try {
await printItemLabel(store, createdUuidB64);
} catch (printError) {
const parsedPrintMessage = formatPrintErrorMessage(printError);
this.printIssue = parsedPrintMessage;
store.addAlert({
type: 'warning',
message: `${entryName} was ${operationVerb}, but printing has an issue: ${parsedPrintMessage}`,
});
}
}
this.successMessage = `${entryName} was ${operationVerb} successfully.`;
store.addAlert({
type: 'success',
@@ -1074,6 +1107,7 @@ export function labelCreatePageData(store) {
this.submitError = '';
this.fieldErrors = {};
this.upsertPreview = null;
this.printIssue = '';
saveStoredValue(STORAGE_KEYS.labelDraft, this.form);
if (revokePreview && this.previewUrl.startsWith('blob:')) {
URL.revokeObjectURL(this.previewUrl);