Add Mark Gone action for stock items and enhance expiration and location filtering system

This commit is contained in:
2026-04-06 21:34:02 +02:00
parent 34664be951
commit b11485a336
4 changed files with 625 additions and 86 deletions
+36 -2
View File
@@ -59,7 +59,7 @@ export function renderLabelCreatePage() {
×
</button>
<template x-if="suggestions.length">
<div class="list-group shadow-sm position-absolute start-0 end-0 z-3 mt-1">
<div class="list-group shadow-sm position-absolute start-0 end-0 z-3 mt-1 search-suggestions-picker">
<template x-for="item in suggestions" :key="item.id">
<button class="list-group-item list-group-item-action" type="button" @click="pickSuggestion(item)">
<div class="fw-semibold" x-text="item.name"></div>
@@ -319,7 +319,7 @@ export function renderLabelCreatePage() {
placeholder="30"
autocomplete="off"
/>
<template x-if="expireDaysPickerOpen">
<template x-if="expireDaysPickerOpen && !isCompactExpireDaysLayout()">
<div class="shadow-sm position-absolute start-0 end-0 z-3 mt-1 quantity-unit-picker expiration-days-picker">
<template x-if="filteredExpireDayOptions.length">
<div class="expiration-days-grid">
@@ -327,6 +327,8 @@ export function renderLabelCreatePage() {
<button
class="btn btn-outline-secondary btn-sm expiration-days-option"
type="button"
@mousedown.prevent
@touchstart.prevent="pickExpireDays(days)"
@click="pickExpireDays(days)"
:class="{ 'active': form.expireDays === days }"
>
@@ -353,6 +355,24 @@ export function renderLabelCreatePage() {
/>
</div>
</div>
<template x-if="isCompactExpireDaysLayout()">
<div class="expiration-days-inline mt-2">
<div class="small text-body-secondary mb-2">Quick picks</div>
<div class="expiration-days-grid expiration-days-grid-inline">
<template x-for="days in filteredExpireDayOptions" :key="days">
<button
class="btn btn-outline-secondary btn-sm expiration-days-option"
type="button"
@touchstart.prevent="pickExpireDays(days)"
@click="pickExpireDays(days)"
:class="{ 'active': form.expireDays === days }"
>
<span class="fw-semibold" x-text="days"></span>
</button>
</template>
</div>
</div>
</template>
<div class="row g-2 mt-1">
<div class="col-5">
<div class="small text-body-secondary">Days</div>
@@ -672,6 +692,11 @@ export function labelCreatePageData(store) {
this.quantityUnitPickerOpen = true;
},
openExpireDaysPicker() {
if (this.isCompactExpireDaysLayout()) {
this.expireDaysPickerOpen = false;
return;
}
this.expireDaysPickerOpen = true;
},
onLocationInput() {
@@ -701,10 +726,16 @@ export function labelCreatePageData(store) {
this.quantityUnitPickerOpen = false;
},
onExpireDaysInput() {
if (!this.isCompactExpireDaysLayout()) {
this.expireDaysPickerOpen = true;
}
this.syncExpireDateFromDays();
},
handleExpireDaysFocusOut(event) {
if (this.isCompactExpireDaysLayout()) {
return;
}
const nextTarget = event.relatedTarget;
if (nextTarget && this.$refs.expireDaysPicker?.contains(nextTarget)) {
return;
@@ -712,6 +743,9 @@ export function labelCreatePageData(store) {
this.expireDaysPickerOpen = false;
},
isCompactExpireDaysLayout() {
return window.matchMedia('(max-width: 575.98px)').matches;
},
pickQuantityUnit(unit) {
this.form.uom = unit;
this.quantityUnitPickerOpen = false;
+20
View File
@@ -84,10 +84,16 @@ export function renderStockDetailPage() {
<div class="alert alert-danger mb-0" x-text="adjustmentState.error"></div>
</template>
<div class="d-flex flex-wrap gap-2">
<button class="btn btn-primary" type="submit" :disabled="adjustmentState.isLoading">
<span x-show="!adjustmentState.isLoading">Save quantity</span>
<span x-show="adjustmentState.isLoading">Saving...</span>
</button>
<button class="btn btn-outline-danger" type="button" @click="markGone()" :disabled="adjustmentState.isLoading">
<span x-show="!adjustmentState.isLoading">Mark gone</span>
<span x-show="adjustmentState.isLoading">Removing...</span>
</button>
</div>
</form>
</template>
@@ -109,10 +115,16 @@ export function renderStockDetailPage() {
<div class="alert alert-danger mb-0" x-text="adjustmentState.error"></div>
</template>
<div class="d-flex flex-wrap gap-2">
<button class="btn btn-primary" type="submit" :disabled="adjustmentState.isLoading">
<span x-show="!adjustmentState.isLoading">Save stock level</span>
<span x-show="adjustmentState.isLoading">Saving...</span>
</button>
<button class="btn btn-outline-danger" type="button" @click="markGone()" :disabled="adjustmentState.isLoading">
<span x-show="!adjustmentState.isLoading">Mark gone</span>
<span x-show="adjustmentState.isLoading">Removing...</span>
</button>
</div>
</form>
</template>
@@ -193,6 +205,14 @@ export function stockDetailPageData(store) {
}
await runAsyncState(this.adjustmentState, async () => {
if (this.adjustment.level === 'gone') {
const entryName = this.entry.name;
await deleteStockItem(store, this.entry.uuid_b64);
store.addAlert({ type: 'success', message: `${entryName} was marked gone.` });
window.__loncApp.navigate('/stock');
return;
}
this.entry = await adjustStockEntry(store, this.entry.uuid_b64, {
level: this.adjustment.level,
});
+362 -57
View File
@@ -29,6 +29,8 @@ const EXPIRATION_LEGEND = [
{ key: 'none', label: 'No expiration', description: 'No expiration date is assigned.' },
];
const EXPIRATION_KEYS = EXPIRATION_LEGEND.map((state) => state.key);
function todayAtMidnight() {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
@@ -180,9 +182,12 @@ export function renderStockListPage() {
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center gap-3 mb-3">
<label class="form-label mb-0">Search stock</label>
<button class="btn btn-link p-0 text-decoration-none stock-filter-reset" type="button" @click="clearFilters()">Reset</button>
</div>
<div class="row g-3">
<div class="col-12">
<label class="form-label">Search stock</label>
<input
class="form-control"
type="text"
@@ -190,71 +195,128 @@ export function renderStockListPage() {
placeholder="Search by item, description, location, or id"
/>
</div>
<div class="col-12">
<div class="stock-filter-toolbar">
<details class="stock-filter-details w-100">
<summary class="btn btn-outline-secondary">More filters</summary>
<div class="stock-filter-panel mt-3">
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label">Expiration filter</label>
<select class="form-select" x-model="filters.expiration">
<option value="">All expiration states</option>
<option value="expired">Expired</option>
<option value="use-first">Use first</option>
<option value="upcoming">Upcoming expiration</option>
<option value="within-date">Within date</option>
<option value="none">No expiration</option>
</select>
</div>
<div class="col-12 col-md-6">
<label class="form-label">Location</label>
<select class="form-select" x-model="filters.location">
<option value="">All locations</option>
<template x-for="location in locations" :key="location.id">
<option :value="location.uuid_b64" x-text="location.pathLabel"></option>
</template>
</select>
</div>
</div>
</div>
</details>
<button class="btn btn-outline-secondary stock-filter-clear" type="button" @click="clearFilters()">Clear</button>
</div>
</div>
</div>
</div>
</div>
<details class="card border-0 shadow-sm mb-4 stock-guide">
<summary class="card-body p-4 d-flex justify-content-between align-items-center gap-3 stock-guide-summary">
<div class="row g-4 mb-4 align-items-start" :class="overviewRowClass()">
<div class="col-12" :class="overviewColClass('expiration')">
<details
class="card border-0 shadow-sm overview-panel"
x-ref="expirationOverview"
@toggle="setOverviewOpen('expiration', $event.target.open)"
>
<summary class="card-body p-4 overview-summary">
<div class="d-flex justify-content-between align-items-start gap-3">
<div>
<h2 class="h5 mb-1">Expiration overview</h2>
<p class="text-body-secondary small mb-0">
Show what each expiration color means.
</p>
<p class="text-body-secondary small mb-0">Tap to focus on one or more expiration states.</p>
</div>
<div class="small text-body-secondary">
<div class="d-flex flex-column align-items-end gap-1">
<button class="btn btn-link p-0 text-decoration-none stock-filter-reset" type="button" @click.prevent.stop="toggleAllExpirationFilters()">Show all</button>
<div class="small text-body-secondary text-end">
<span class="fw-semibold text-body" x-text="filteredEntries.length"></span>
item(s) visible
</div>
</div>
</div>
</summary>
<div class="card-body pt-0 px-4 pb-4">
<div class="row g-3">
<div class="overview-list" :class="{ 'overview-list-split': isOnlyOverviewOpen('expiration') }">
<template x-for="stateInfo in expirationLegend" :key="stateInfo.key">
<div class="col-12 col-md-6 col-xl-4">
<div class="legend-card h-100" :class="legendClass(stateInfo.key)">
<button
class="overview-option text-start"
type="button"
:class="legendClass(stateInfo.key)"
@click="toggleExpirationOverviewFilter(stateInfo.key)"
:aria-pressed="isExpirationFilterActive(stateInfo.key)"
>
<div class="d-flex justify-content-between align-items-start gap-3 mb-1">
<div class="fw-semibold" x-text="stateInfo.label"></div>
<div class="small fw-semibold" x-text="expirationCount(stateInfo.key)"></div>
</div>
<div class="small" x-text="stateInfo.description"></div>
</div>
</div>
</button>
</template>
</div>
</div>
</details>
</div>
<div class="col-12" :class="overviewColClass('location')">
<details
class="card border-0 shadow-sm overview-panel"
x-ref="locationOverview"
@toggle="setOverviewOpen('location', $event.target.open)"
>
<summary class="card-body p-4 overview-summary">
<div class="d-flex justify-content-between align-items-start gap-3">
<div>
<h2 class="h5 mb-1">Location overview</h2>
<p class="text-body-secondary small mb-0">Tap locations to focus the list. Parent locations include their children.</p>
</div>
<div class="d-flex flex-column align-items-end gap-1">
<button class="btn btn-link p-0 text-decoration-none stock-filter-reset" type="button" @click.prevent.stop="toggleAllLocations()">Show all</button>
<div class="small text-body-secondary text-end" x-text="selectedLocationSummary()"></div>
</div>
</div>
</summary>
<div class="card-body pt-0 px-4 pb-4">
<template x-if="isOnlyOverviewOpen('location')">
<div class="overview-list overview-list-locations location-overview-columns">
<template x-for="(columnGroups, columnIndex) in balancedLocationOverviewColumns()" :key="columnIndex">
<div class="location-overview-column">
<template x-for="group in columnGroups" :key="group.parent.id">
<div class="location-overview-group">
<template x-for="location in group.items" :key="location.id">
<button
class="overview-option overview-option-location text-start"
type="button"
:class="locationOverviewClass(location)"
:style="locationOverviewStyle(location)"
@click="toggleLocationOverviewFilter(location.uuid_b64)"
:aria-pressed="isLocationFilterActive(location.uuid_b64)"
>
<span class="stock-filter-location-rail" x-show="location.depth"></span>
<div class="d-flex justify-content-between align-items-start gap-3">
<div class="fw-semibold" :class="location.depth ? 'small mb-0' : ''" x-text="location.name"></div>
<div class="small fw-semibold" x-text="locationCount(location.uuid_b64)"></div>
</div>
</button>
</template>
</div>
</template>
</div>
</template>
</div>
</template>
<template x-if="!isOnlyOverviewOpen('location')">
<div class="overview-list overview-list-locations">
<template x-for="group in locationOverviewGroups()" :key="group.parent.id">
<div class="location-overview-group">
<template x-for="location in group.items" :key="location.id">
<button
class="overview-option overview-option-location text-start"
type="button"
:class="locationOverviewClass(location)"
:style="locationOverviewStyle(location)"
@click="toggleLocationOverviewFilter(location.uuid_b64)"
:aria-pressed="isLocationFilterActive(location.uuid_b64)"
>
<span class="stock-filter-location-rail" x-show="location.depth"></span>
<div class="d-flex justify-content-between align-items-start gap-3">
<div class="fw-semibold" :class="location.depth ? 'small mb-0' : ''" x-text="location.name"></div>
<div class="small fw-semibold" x-text="locationCount(location.uuid_b64)"></div>
</div>
</button>
</template>
</div>
</template>
</div>
</template>
</div>
</details>
</div>
</div>
<template x-if="state.isLoading">
<div class="alert alert-secondary">Loading stock review...</div>
@@ -332,13 +394,14 @@ export function renderStockListPage() {
</template>
</select>
<button class="btn btn-sm btn-primary" type="button" @click="saveLevel(entry)">Save</button>
<button class="btn btn-sm btn-outline-danger" type="button" @click="markGone(entry)">Mark gone</button>
</div>
</template>
<template x-if="entry.stock_type === 'measured'">
<div class="d-flex flex-wrap gap-2 align-items-center">
<input class="form-control form-control-sm quick-number" type="number" step="0.01" min="0" x-model="editForms[entry.id].quantity" />
<button class="btn btn-sm btn-primary" type="button" @click="saveQuantity(entry)">Save qty</button>
<button class="btn btn-sm btn-outline-danger" type="button" @click="markMeasuredGone(entry)">Gone</button>
<button class="btn btn-sm btn-outline-danger" type="button" @click="markGone(entry)">Mark gone</button>
</div>
</template>
<template x-if="editErrors[entry.id]">
@@ -399,7 +462,10 @@ export function renderStockListPage() {
<option :value="option.value" x-text="option.label"></option>
</template>
</select>
<div class="d-flex flex-wrap gap-2">
<button class="btn btn-sm btn-primary" type="button" @click="saveLevel(entry)">Save stock level</button>
<button class="btn btn-sm btn-outline-danger" type="button" @click="markGone(entry)">Mark gone</button>
</div>
</div>
</template>
<template x-if="entry.stock_type === 'measured'">
@@ -407,7 +473,7 @@ export function renderStockListPage() {
<input class="form-control form-control-sm" type="number" step="0.01" min="0" x-model="editForms[entry.id].quantity" />
<div class="d-flex flex-wrap gap-2">
<button class="btn btn-sm btn-primary" type="button" @click="saveQuantity(entry)">Save quantity</button>
<button class="btn btn-sm btn-outline-danger" type="button" @click="markMeasuredGone(entry)">Gone</button>
<button class="btn btn-sm btn-outline-danger" type="button" @click="markGone(entry)">Mark gone</button>
</div>
</div>
</template>
@@ -436,10 +502,14 @@ export function stockListPageData(store) {
editErrors: {},
levelOptions: LEVEL_OPTIONS,
expirationLegend: EXPIRATION_LEGEND,
overviewOpen: {
expiration: false,
location: false,
},
filters: {
search: '',
expiration: '',
location: '',
expiration: [],
location: [],
},
async init() {
if (!store.isConnected) {
@@ -495,10 +565,152 @@ export function stockListPageData(store) {
clearFilters() {
this.filters = {
search: '',
expiration: '',
location: '',
expiration: [],
location: [],
};
},
setOverviewOpen(key, open) {
this.overviewOpen[key] = open;
if (!open || !this.isCompactOverviewLayout()) {
return;
}
const otherKey = key === 'expiration' ? 'location' : 'expiration';
this.overviewOpen[otherKey] = false;
const otherPanel =
otherKey === 'expiration'
? this.$refs.expirationOverview
: this.$refs.locationOverview;
if (otherPanel?.open) {
otherPanel.open = false;
}
},
isCompactOverviewLayout() {
return window.matchMedia('(max-width: 1199.98px)').matches;
},
openOverviewCount() {
return Number(this.overviewOpen.expiration) + Number(this.overviewOpen.location);
},
isOnlyOverviewOpen(key) {
return this.openOverviewCount() === 1 && this.overviewOpen[key];
},
overviewRowClass() {
return this.openOverviewCount() === 1 ? 'overview-row-single-open' : '';
},
overviewColClass(key) {
if (this.openOverviewCount() === 1) {
return 'col-xl-12';
}
return key === 'expiration' ? 'col-xl-5' : 'col-xl-7';
},
expirationFilterSummary() {
if (this.isAllExpirationSelected()) {
return 'Show all';
}
if (!this.filters.expiration.length) {
return 'No expiration states selected';
}
if (this.filters.expiration.length === 1) {
return this.expirationLegend.find((state) => state.key === this.filters.expiration[0])?.label || '1 expiration state';
}
return `${this.filters.expiration.length} expiration states selected`;
},
isAllExpirationSelected() {
return this.filters.expiration.length === 0 || this.filters.expiration.length === EXPIRATION_KEYS.length;
},
toggleAllExpirationFilters() {
this.filters.expiration = [];
},
toggleExpirationOverviewFilter(key) {
if (this.isAllExpirationSelected()) {
this.filters.expiration = [key];
return;
}
this.toggleExpirationFilter(key);
},
toggleExpirationFilter(key) {
if (this.filters.expiration.includes(key)) {
this.filters.expiration = this.filters.expiration.filter((value) => value !== key);
return;
}
this.filters.expiration = [...this.filters.expiration, key];
},
locationFilterSummary() {
if (this.isAllLocationsSelected()) {
return 'All locations';
}
if (!this.filters.location.length) {
return 'No locations selected';
}
if (this.filters.location.length === 1) {
return this.locationMap[this.filters.location[0]] || '1 location selected';
}
return `${this.filters.location.length} locations selected`;
},
selectedLocationSummary() {
if (this.isAllLocationsSelected()) {
return '';
}
if (!this.filters.location.length) {
return 'No locations selected';
}
return `${this.filters.location.length} selected`;
},
locationOverviewGroups() {
const byParent = this.locations
.filter((location) => location.depth === 0)
.map((parent) => ({
parent,
items: this.locations.filter((location) =>
location.lineage_uuid_b64[0] === parent.uuid_b64,
),
}));
return byParent;
},
balancedLocationOverviewColumns() {
const columns = [[], []];
const sizes = [0, 0];
this.locationOverviewGroups().forEach((group) => {
const groupSize = group.items.length;
const targetIndex = sizes[0] <= sizes[1] ? 0 : 1;
columns[targetIndex].push(group);
sizes[targetIndex] += groupSize;
});
return columns;
},
isAllLocationsSelected() {
return this.filters.location.length === 0 || (this.locations.length > 0 && this.filters.location.length === this.locations.length);
},
toggleAllLocations() {
this.filters.location = [];
},
toggleLocationFilter(uuid) {
const subtree = this.locationSubtree(uuid);
if (this.filters.location.includes(uuid)) {
this.filters.location = this.filters.location.filter((value) => !subtree.includes(value));
return;
}
this.filters.location = [...new Set([...this.filters.location, ...subtree])];
},
get filteredEntries() {
return this.entries.filter((entry) => {
if (
@@ -509,15 +721,17 @@ export function stockListPageData(store) {
}
if (
this.filters.expiration &&
expirationInfo(entry).key !== this.filters.expiration
this.filters.expiration.length &&
this.filters.expiration.length !== EXPIRATION_KEYS.length &&
!this.filters.expiration.includes(expirationInfo(entry).key)
) {
return false;
}
if (
this.filters.location &&
!this.locationMatchesFilter(entry.location_initial_uuid_b64, this.filters.location)
this.filters.location.length &&
this.filters.location.length !== this.locations.length &&
!this.locationMatchesAnyFilter(entry.location_initial_uuid_b64, this.filters.location)
) {
return false;
}
@@ -528,6 +742,9 @@ export function stockListPageData(store) {
expirationFor(entry) {
return expirationInfo(entry);
},
isExpirationFilterActive(key) {
return this.isAllExpirationSelected() || this.filters.expiration.includes(key);
},
rowClass(entry) {
return `expiration-${expirationInfo(entry).key}`;
},
@@ -535,10 +752,34 @@ export function stockListPageData(store) {
return `expiration-badge-${expirationInfo(entry).key}`;
},
legendClass(key) {
return `legend-${key}`;
const hasActiveFilters = this.filters.expiration.length > 0 && !this.isAllExpirationSelected();
return [
`legend-${key}`,
hasActiveFilters && this.isExpirationFilterActive(key) ? 'legend-card-active' : '',
hasActiveFilters && !this.isExpirationFilterActive(key) ? 'legend-card-inactive' : '',
]
.filter(Boolean)
.join(' ');
},
expirationCount(key) {
return this.entries.filter((entry) => expirationInfo(entry).key === key).length;
return this.entries.filter((entry) => {
if (
this.filters.search &&
!searchBlob(entry, this.locationMap).includes(this.filters.search.toLowerCase())
) {
return false;
}
if (
this.filters.location.length &&
this.filters.location.length !== this.locations.length &&
!this.locationMatchesAnyFilter(entry.location_initial_uuid_b64, this.filters.location)
) {
return false;
}
return expirationInfo(entry).key === key;
}).length;
},
shortId(entry) {
return entry.uuid_b64 ? entry.uuid_b64.slice(0, 10) : 'No id';
@@ -546,6 +787,53 @@ export function stockListPageData(store) {
locationLabel(entry) {
return resolveLocationLabel(entry, this.locationMap);
},
isLocationFilterActive(uuid) {
return this.isAllLocationsSelected() || this.filters.location.includes(uuid);
},
toggleLocationOverviewFilter(uuid) {
if (this.isAllLocationsSelected()) {
this.filters.location = [...this.locationSubtree(uuid)];
return;
}
this.toggleLocationFilter(uuid);
},
locationOverviewClass(location) {
const hasActiveFilters = this.filters.location.length > 0 && !this.isAllLocationsSelected();
return [
'location-overview',
`location-type-${location.type || 'unknown'}`,
location.depth ? 'location-overview-child' : 'location-overview-parent',
hasActiveFilters && this.isLocationFilterActive(location.uuid_b64) ? 'legend-card-active' : '',
hasActiveFilters && !this.isLocationFilterActive(location.uuid_b64) ? 'legend-card-inactive' : '',
]
.filter(Boolean)
.join(' ');
},
locationOverviewStyle(location) {
const offset = location.depth;
return `margin-left: ${offset}rem; width: calc(100% - ${offset}rem);`;
},
locationCount(locationUuid) {
return this.entries.filter((entry) => {
if (
this.filters.search &&
!searchBlob(entry, this.locationMap).includes(this.filters.search.toLowerCase())
) {
return false;
}
if (
this.filters.expiration.length &&
this.filters.expiration.length !== EXPIRATION_KEYS.length &&
!this.filters.expiration.includes(expirationInfo(entry).key)
) {
return false;
}
return this.locationMatchesFilter(entry.location_initial_uuid_b64, locationUuid);
}).length;
},
locationMatchesFilter(entryLocationUuid, selectedLocationUuid) {
if (!selectedLocationUuid) {
return true;
@@ -554,6 +842,18 @@ export function stockListPageData(store) {
const allowed = this.locationDescendants[selectedLocationUuid] || [selectedLocationUuid];
return allowed.includes(entryLocationUuid);
},
locationSubtree(selectedLocationUuid) {
return this.locationDescendants[selectedLocationUuid] || [selectedLocationUuid];
},
locationMatchesAnyFilter(entryLocationUuid, selectedLocationUuids) {
if (!selectedLocationUuids.length) {
return true;
}
return selectedLocationUuids.some((selectedLocationUuid) =>
this.locationMatchesFilter(entryLocationUuid, selectedLocationUuid),
);
},
quantityLabel,
stockTypeDetail(entry) {
if (entry.stock_type === 'binary') {
@@ -570,6 +870,11 @@ export function stockListPageData(store) {
},
async saveLevel(entry) {
const level = this.editForms[entry.id]?.level || 'plenty';
if (level === 'gone') {
await this.deleteEntry(entry);
return;
}
await this.saveEntryUpdate(entry, {
level,
}, { level });
@@ -585,7 +890,7 @@ export function stockListPageData(store) {
quantity,
}, { quantity });
},
async markMeasuredGone(entry) {
async markGone(entry) {
await this.deleteEntry(entry);
},
async saveEntryUpdate(entry, payload, localPatch) {
+183 -3
View File
@@ -151,7 +151,7 @@ body {
top: 50%;
right: 0.75rem;
transform: translateY(-50%);
z-index: 5;
z-index: 2;
text-decoration: none;
font-size: 1.4rem;
line-height: 1;
@@ -159,6 +159,10 @@ body {
background: transparent;
}
.search-suggestions-picker {
z-index: 20;
}
.search-field-with-clear .search-clear-button {
top: calc(50% + 1rem);
}
@@ -177,11 +181,11 @@ body {
}
.location-picker {
z-index: 4;
z-index: 20;
}
.quantity-unit-picker {
z-index: 4;
z-index: 20;
border-radius: 0.9rem;
border: 1px solid var(--lonc-border);
background: rgba(255, 255, 255, 0.96);
@@ -200,6 +204,13 @@ body {
gap: 0.5rem;
}
.expiration-days-inline {
padding: 0.85rem 0.9rem;
border: 1px solid var(--lonc-border);
border-radius: 1rem;
background: rgba(255, 255, 255, 0.72);
}
.expiration-days-option {
padding: 0.55rem 0.4rem;
border-radius: 0.7rem;
@@ -223,6 +234,10 @@ body {
.expiration-days-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.expiration-days-grid-inline {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.location-level-badge {
@@ -251,11 +266,176 @@ body {
gap: 0.75rem;
}
.stock-filter-reset {
font-size: 0.95rem;
color: var(--lonc-muted);
}
.stock-filter-reset:hover {
color: var(--lonc-primary);
}
.overview-row-single-open > [class*='col-'] {
width: 100%;
}
.overview-panel {
align-self: flex-start;
height: auto;
}
.overview-summary {
display: block;
cursor: pointer;
list-style: none;
}
.overview-summary::-webkit-details-marker {
display: none;
}
.overview-list {
display: grid;
gap: 0.85rem;
}
.overview-list-split {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.overview-list-locations {
max-height: 28rem;
overflow-y: auto;
padding-right: 0.35rem;
}
.location-overview-columns {
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: start;
}
.location-overview-column {
display: grid;
gap: 0.85rem;
}
.location-overview-group {
display: grid;
gap: 0.85rem;
break-inside: avoid;
}
.overview-option {
appearance: none;
width: 100%;
padding: 0.95rem 1rem;
border-radius: 1rem;
border: 1px solid var(--lonc-border);
transition:
transform 160ms ease,
box-shadow 160ms ease,
opacity 160ms ease,
border-color 160ms ease,
filter 160ms ease;
}
.overview-option:hover {
transform: translateY(-1px);
box-shadow: 0 12px 24px rgba(24, 42, 79, 0.08);
}
.overview-option:focus-visible {
outline: 2px solid rgba(31, 75, 153, 0.4);
outline-offset: 2px;
}
.overview-option-location {
position: relative;
}
.location-overview-parent {
padding: 0.95rem 1rem;
}
.location-overview-child {
padding: 0.75rem 0.9rem;
border-radius: 0.85rem;
}
.location-type-frozen {
background: rgba(86, 156, 214, 0.14);
}
.location-type-refrigerated {
background: rgba(80, 180, 140, 0.12);
}
.location-type-ambient {
background: rgba(224, 176, 65, 0.14);
}
.location-type-unknown {
background: rgba(108, 117, 125, 0.08);
}
.stock-filter-location-rail {
position: absolute;
left: 0.65rem;
top: 0.65rem;
bottom: 0.65rem;
width: 2px;
border-radius: 999px;
background: rgba(31, 75, 153, 0.16);
}
@media (max-width: 575.98px) {
.overview-list-split,
.location-overview-columns {
grid-template-columns: 1fr;
}
.overview-list-locations {
max-height: 20rem;
}
}
.legend-card {
height: 100%;
padding: 1rem;
border-radius: 1rem;
border: 1px solid var(--lonc-border);
transition:
transform 160ms ease,
box-shadow 160ms ease,
opacity 160ms ease,
border-color 160ms ease,
filter 160ms ease;
}
button.legend-card {
appearance: none;
width: 100%;
}
button.legend-card:hover {
transform: translateY(-1px);
box-shadow: 0 12px 24px rgba(24, 42, 79, 0.08);
}
button.legend-card:focus-visible {
outline: 2px solid rgba(31, 75, 153, 0.4);
outline-offset: 2px;
}
.legend-card-active {
border-color: rgba(31, 75, 153, 0.35);
box-shadow: 0 16px 28px rgba(24, 42, 79, 0.1);
filter: saturate(1.05);
}
.legend-card-inactive {
opacity: 0.45;
filter: saturate(0.7);
}
.legend-expired,