Improve label quantity input and unit picker

This commit is contained in:
2026-04-06 17:34:11 +02:00
parent 76d8180f41
commit 218647b2cb
2 changed files with 113 additions and 17 deletions
+109 -17
View File
@@ -21,6 +21,8 @@ const STOCK_LEVEL_OPTIONS = [
{ value: 'gone', label: 'Gone (~= 0%)' },
];
const QUANTITY_UNIT_OPTIONS = ['g', 'ml', 'pc'];
export function renderLabelCreatePage() {
return `
<section class="container-xxl py-4 py-lg-5" x-data="labelCreatePage()" x-init="init()">
@@ -69,7 +71,7 @@ export function renderLabelCreatePage() {
<div class="row g-3">
<div class="col-12 col-md-8 position-relative text-field-with-clear">
<label class="form-label">Title / name</label>
<label class="form-label">Title / name <span class="text-danger">*</span></label>
<input class="form-control pe-5" type="text" x-model="form.name" required />
<button
type="button"
@@ -102,9 +104,12 @@ export function renderLabelCreatePage() {
</template>
</select>
</div>
<div class="col-12 col-md-6 grouped-field-with-clear" x-show="form.stockType === 'measured'">
<div class="col-12 col-md-6 grouped-field-with-clear">
<div class="grouped-field-footer mb-1">
<label class="form-label mb-0">Quantity</label>
<label class="form-label mb-0">
Quantity
<span class="text-danger" x-show="form.stockType === 'measured'">*</span>
</label>
<button
type="button"
class="btn btn-sm btn-link text-body-secondary p-0"
@@ -116,10 +121,55 @@ export function renderLabelCreatePage() {
</div>
<div class="row g-2">
<div class="col-7">
<input class="form-control" type="number" step="0.01" min="0" x-model="form.quantity" />
<input
class="form-control"
type="number"
step="0.01"
min="0"
x-model="form.quantity"
:required="form.stockType === 'measured'"
/>
</div>
<div class="col-5">
<input class="form-control" type="text" x-model="form.uom" placeholder="g" />
<div
class="position-relative"
x-ref="quantityUnitPicker"
@focusin="quantityUnitPickerOpen = true"
@focusout="handleQuantityUnitFocusOut($event)"
>
<input
class="form-control"
type="text"
x-model="form.uom"
@input="onQuantityUnitInput()"
@click="openQuantityUnitPicker()"
@keydown.escape="quantityUnitPickerOpen = false"
placeholder="g"
autocomplete="off"
/>
<template x-if="quantityUnitPickerOpen">
<div class="list-group shadow-sm position-absolute start-0 end-0 z-3 mt-1 quantity-unit-picker">
<template x-if="filteredQuantityUnits.length">
<div>
<template x-for="unit in filteredQuantityUnits" :key="unit">
<button
class="list-group-item list-group-item-action"
type="button"
@click="pickQuantityUnit(unit)"
>
<span class="fw-semibold" x-text="unit"></span>
</button>
</template>
</div>
</template>
<template x-if="!filteredQuantityUnits.length">
<div class="list-group-item text-body-secondary">
No matching units found.
</div>
</template>
</div>
</template>
</div>
</div>
</div>
<div class="row g-2 mt-1">
@@ -170,7 +220,7 @@ export function renderLabelCreatePage() {
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label">Storage location</label>
<label class="form-label">Storage location <span class="text-danger">*</span></label>
<div
class="position-relative location-field-with-clear"
x-ref="locationPicker"
@@ -305,6 +355,9 @@ export function renderLabelCreatePage() {
</div>
<button class="btn btn-outline-secondary" type="button" @click="reset()">Clear form</button>
</div>
<div class="small text-body-secondary">
<span class="text-danger">*</span> Required field
</div>
</form>
</div>
</div>
@@ -389,6 +442,10 @@ function loadLabelDraft() {
return {
...createDefaultForm(),
...draft,
quantity:
draft.quantity === 0 || draft.quantity === '0' || draft.quantity == null
? ''
: draft.quantity,
itemId: '',
search: '',
};
@@ -408,10 +465,12 @@ export function labelCreatePageData(store) {
createState: createAsyncState(),
stockTypeOptions: STOCK_TYPE_OPTIONS,
stockLevelOptions: STOCK_LEVEL_OPTIONS,
quantityUnitOptions: QUANTITY_UNIT_OPTIONS,
suggestions: [],
locations: [],
locationSearch: '',
locationPickerOpen: false,
quantityUnitPickerOpen: false,
previewUrl: '',
successMessage: '',
submitError: '',
@@ -463,7 +522,7 @@ export function labelCreatePageData(store) {
this.form.description = item.description || this.form.description;
this.form.uom = item.uom_symbol || this.form.uom;
this.form.quantity =
item.quantity === 0 || item.quantity
item.quantity
? String(item.quantity)
: this.form.quantity;
this.form.stockType = item.stock_type || this.form.stockType;
@@ -501,6 +560,26 @@ export function labelCreatePageData(store) {
`${location.name} ${location.pathLabel}`.toLowerCase().includes(query),
);
},
get filteredQuantityUnits() {
const query = this.form.uom.trim().toLowerCase();
if (!query) {
return this.quantityUnitOptions;
}
if (this.quantityUnitOptions.some((unit) => unit.toLowerCase() === query)) {
return this.quantityUnitOptions;
}
const matches = this.quantityUnitOptions.filter((unit) =>
unit.toLowerCase().includes(query),
);
if (matches.length) {
return matches;
}
return this.quantityUnitOptions;
},
get selectedLocation() {
return this.locations.find(
(location) => String(location.id) === String(this.form.locationId),
@@ -524,6 +603,9 @@ export function labelCreatePageData(store) {
openLocationPicker() {
this.locationPickerOpen = true;
},
openQuantityUnitPicker() {
this.quantityUnitPickerOpen = true;
},
onLocationInput() {
this.locationPickerOpen = true;
if (this.selectedLocation && this.locationSearch !== this.selectedLocation.name) {
@@ -538,6 +620,21 @@ export function labelCreatePageData(store) {
this.locationPickerOpen = false;
},
onQuantityUnitInput() {
this.quantityUnitPickerOpen = true;
},
handleQuantityUnitFocusOut(event) {
const nextTarget = event.relatedTarget;
if (nextTarget && this.$refs.quantityUnitPicker?.contains(nextTarget)) {
return;
}
this.quantityUnitPickerOpen = false;
},
pickQuantityUnit(unit) {
this.form.uom = unit;
this.quantityUnitPickerOpen = false;
},
syncLocationSelection() {
if (!this.form.locationId) {
this.locationSearch = '';
@@ -610,9 +707,6 @@ export function labelCreatePageData(store) {
return;
}
this.form.quantity = '';
this.form.uom = 'g';
if (stockType === 'binary' && this.form.level !== 'plenty') {
this.form.level = 'plenty';
return;
@@ -634,20 +728,18 @@ export function labelCreatePageData(store) {
},
buildPayload() {
const quantity =
this.form.stockType === 'measured'
? this.form.quantity === ''
? null
: Number(this.form.quantity)
: this.form.stockType === 'binary'
this.form.quantity === ''
? this.form.stockType === 'binary'
? 1
: null;
: null
: Number(this.form.quantity);
return {
item_id: this.form.itemId || null,
name: this.form.name.trim(),
description: this.form.description.trim(),
quantity_initial: quantity,
uom_symbol: this.form.stockType === 'measured' ? this.form.uom.trim() : null,
uom_symbol: this.form.uom.trim() || null,
calories: this.form.energy === '' ? null : Number(this.form.energy),
calories_unit: this.form.energyUnit.trim() || null,
stock_type: this.form.stockType,
+4
View File
@@ -180,6 +180,10 @@ body {
z-index: 4;
}
.quantity-unit-picker {
z-index: 4;
}
.location-level-badge {
display: inline-flex;
align-items: center;