From b11485a33633a1ffccac2e3dd3ef57d3a6316480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Bregar?= Date: Mon, 6 Apr 2026 21:34:02 +0200 Subject: [PATCH] Add `Mark Gone` action for stock items and enhance expiration and location filtering system --- src/features/labels/label-create-page.js | 40 +- src/features/stock/stock-detail-page.js | 36 +- src/features/stock/stock-list-page.js | 449 +++++++++++++++++++---- src/styles/app.css | 186 +++++++++- 4 files changed, 625 insertions(+), 86 deletions(-) diff --git a/src/features/labels/label-create-page.js b/src/features/labels/label-create-page.js index e9faadc..76462a3 100644 --- a/src/features/labels/label-create-page.js +++ b/src/features/labels/label-create-page.js @@ -59,7 +59,7 @@ export function renderLabelCreatePage() { × - +
+ + +
@@ -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) { diff --git a/src/styles/app.css b/src/styles/app.css index 0c65d2c..1ab1c25 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -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,