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%)' }, { value: 'gone', label: 'Gone (~= 0%)' },
]; ];
const QUANTITY_UNIT_OPTIONS = ['g', 'ml', 'pc'];
export function renderLabelCreatePage() { export function renderLabelCreatePage() {
return ` return `
<section class="container-xxl py-4 py-lg-5" x-data="labelCreatePage()" x-init="init()"> <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="row g-3">
<div class="col-12 col-md-8 position-relative text-field-with-clear"> <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 /> <input class="form-control pe-5" type="text" x-model="form.name" required />
<button <button
type="button" type="button"
@@ -102,9 +104,12 @@ export function renderLabelCreatePage() {
</template> </template>
</select> </select>
</div> </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"> <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 <button
type="button" type="button"
class="btn btn-sm btn-link text-body-secondary p-0" class="btn btn-sm btn-link text-body-secondary p-0"
@@ -116,10 +121,55 @@ export function renderLabelCreatePage() {
</div> </div>
<div class="row g-2"> <div class="row g-2">
<div class="col-7"> <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>
<div class="col-5"> <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> </div>
<div class="row g-2 mt-1"> <div class="row g-2 mt-1">
@@ -170,7 +220,7 @@ export function renderLabelCreatePage() {
</div> </div>
</div> </div>
<div class="col-12 col-md-6"> <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 <div
class="position-relative location-field-with-clear" class="position-relative location-field-with-clear"
x-ref="locationPicker" x-ref="locationPicker"
@@ -305,6 +355,9 @@ export function renderLabelCreatePage() {
</div> </div>
<button class="btn btn-outline-secondary" type="button" @click="reset()">Clear form</button> <button class="btn btn-outline-secondary" type="button" @click="reset()">Clear form</button>
</div> </div>
<div class="small text-body-secondary">
<span class="text-danger">*</span> Required field
</div>
</form> </form>
</div> </div>
</div> </div>
@@ -389,6 +442,10 @@ function loadLabelDraft() {
return { return {
...createDefaultForm(), ...createDefaultForm(),
...draft, ...draft,
quantity:
draft.quantity === 0 || draft.quantity === '0' || draft.quantity == null
? ''
: draft.quantity,
itemId: '', itemId: '',
search: '', search: '',
}; };
@@ -408,10 +465,12 @@ export function labelCreatePageData(store) {
createState: createAsyncState(), createState: createAsyncState(),
stockTypeOptions: STOCK_TYPE_OPTIONS, stockTypeOptions: STOCK_TYPE_OPTIONS,
stockLevelOptions: STOCK_LEVEL_OPTIONS, stockLevelOptions: STOCK_LEVEL_OPTIONS,
quantityUnitOptions: QUANTITY_UNIT_OPTIONS,
suggestions: [], suggestions: [],
locations: [], locations: [],
locationSearch: '', locationSearch: '',
locationPickerOpen: false, locationPickerOpen: false,
quantityUnitPickerOpen: false,
previewUrl: '', previewUrl: '',
successMessage: '', successMessage: '',
submitError: '', submitError: '',
@@ -463,7 +522,7 @@ export function labelCreatePageData(store) {
this.form.description = item.description || this.form.description; this.form.description = item.description || this.form.description;
this.form.uom = item.uom_symbol || this.form.uom; this.form.uom = item.uom_symbol || this.form.uom;
this.form.quantity = this.form.quantity =
item.quantity === 0 || item.quantity item.quantity
? String(item.quantity) ? String(item.quantity)
: this.form.quantity; : this.form.quantity;
this.form.stockType = item.stock_type || this.form.stockType; this.form.stockType = item.stock_type || this.form.stockType;
@@ -501,6 +560,26 @@ export function labelCreatePageData(store) {
`${location.name} ${location.pathLabel}`.toLowerCase().includes(query), `${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() { get selectedLocation() {
return this.locations.find( return this.locations.find(
(location) => String(location.id) === String(this.form.locationId), (location) => String(location.id) === String(this.form.locationId),
@@ -524,6 +603,9 @@ export function labelCreatePageData(store) {
openLocationPicker() { openLocationPicker() {
this.locationPickerOpen = true; this.locationPickerOpen = true;
}, },
openQuantityUnitPicker() {
this.quantityUnitPickerOpen = true;
},
onLocationInput() { onLocationInput() {
this.locationPickerOpen = true; this.locationPickerOpen = true;
if (this.selectedLocation && this.locationSearch !== this.selectedLocation.name) { if (this.selectedLocation && this.locationSearch !== this.selectedLocation.name) {
@@ -538,6 +620,21 @@ export function labelCreatePageData(store) {
this.locationPickerOpen = false; 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() { syncLocationSelection() {
if (!this.form.locationId) { if (!this.form.locationId) {
this.locationSearch = ''; this.locationSearch = '';
@@ -610,9 +707,6 @@ export function labelCreatePageData(store) {
return; return;
} }
this.form.quantity = '';
this.form.uom = 'g';
if (stockType === 'binary' && this.form.level !== 'plenty') { if (stockType === 'binary' && this.form.level !== 'plenty') {
this.form.level = 'plenty'; this.form.level = 'plenty';
return; return;
@@ -634,20 +728,18 @@ export function labelCreatePageData(store) {
}, },
buildPayload() { buildPayload() {
const quantity = const quantity =
this.form.stockType === 'measured' this.form.quantity === ''
? this.form.quantity === '' ? this.form.stockType === 'binary'
? null
: Number(this.form.quantity)
: this.form.stockType === 'binary'
? 1 ? 1
: null; : null
: Number(this.form.quantity);
return { return {
item_id: this.form.itemId || null, item_id: this.form.itemId || null,
name: this.form.name.trim(), name: this.form.name.trim(),
description: this.form.description.trim(), description: this.form.description.trim(),
quantity_initial: quantity, 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: this.form.energy === '' ? null : Number(this.form.energy),
calories_unit: this.form.energyUnit.trim() || null, calories_unit: this.form.energyUnit.trim() || null,
stock_type: this.form.stockType, stock_type: this.form.stockType,
+4
View File
@@ -180,6 +180,10 @@ body {
z-index: 4; z-index: 4;
} }
.quantity-unit-picker {
z-index: 4;
}
.location-level-badge { .location-level-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;