diff --git a/README.md b/README.md index f9bd70b..4cd51e0 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ Project-specific operating conventions for future contributors and coding agents - Active kitchen selection and switching - Dashboard with quick actions - Label creation flow with item lookup, location loading, preview, and stock entry creation -- Stock list with search and filters +- Grouped-first stock review with search and overview filters - Stock detail page with stock adjustment workflow - PWA manifest, icons, service worker, and offline fallback @@ -165,6 +165,8 @@ Expected shapes today: Returns item definitions for autocomplete. - `GET /{database}/kitchen/items` Returns the current stock review list. +- `GET /{database}/kitchen/items/grouped?expanded=0|1` + Returns grouped stock data; grouped review uses summary-first loading and hydrates item children in background. - `GET /{database}/kitchen/items/{uuid_b64}` Returns one item detail payload. - `GET /{database}/kitchen/changes` diff --git a/package.json b/package.json index 3954513..1f571cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lonc-web", - "version": "0.2.0", + "version": "0.2.1", "private": true, "type": "module", "scripts": { diff --git a/src/api/stock.js b/src/api/stock.js index 4a4036c..fd8e2c7 100644 --- a/src/api/stock.js +++ b/src/api/stock.js @@ -21,7 +21,24 @@ export async function searchItemDefinitions(store, query) { } export async function listStockEntries(store, filters = {}) { - const payload = await apiRequest(store, getPath('items')); + const query = {}; + const searchName = filters.searchName || filters.search_name; + if (searchName) { + query.search_name = searchName; + } + if (filters.limit !== undefined && filters.limit !== null) { + query.limit = filters.limit; + } + if (filters.offset !== undefined && filters.offset !== null) { + query.offset = filters.offset; + } + if (filters.cursor) { + query.cursor = filters.cursor; + } + + const payload = await apiRequest(store, getPath('items'), { + query, + }); if (Array.isArray(payload)) { return payload; @@ -30,9 +47,26 @@ export async function listStockEntries(store, filters = {}) { return payload?.data || payload?.entries || payload?.items || []; } -export async function listGroupedStockEntries(store) { +export async function listGroupedStockEntries(store, options = {}) { + const query = {}; + const expanded = options.expanded ?? 1; + query.expanded = expanded; + const searchName = options.searchName || options.search_name; + if (searchName) { + query.search_name = searchName; + } + if (options.limit !== undefined && options.limit !== null) { + query.limit = options.limit; + } + if (options.offset !== undefined && options.offset !== null) { + query.offset = options.offset; + } + if (options.cursor) { + query.cursor = options.cursor; + } + const payload = await apiRequest(store, `${getPath('items')}/grouped`, { - query: { expanded: 1 }, + query, }); if (Array.isArray(payload)) { diff --git a/src/app/config.js b/src/app/config.js index b113193..7b3e317 100644 --- a/src/app/config.js +++ b/src/app/config.js @@ -1,5 +1,5 @@ export const APP_NAME = 'Lonc'; -export const APP_VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.2.0'; +export const APP_VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.2.1'; export const TRYTON_APPLICATION = 'kitchen'; export const CONNECTION_STATES = { @@ -14,6 +14,7 @@ export const STORAGE_KEYS = { session: 'lonc.auth.session', activeKitchen: 'lonc.kitchen.active', labelDraft: 'lonc.labels.draft', + stockListContext: 'lonc.stock.list.context', }; export const DEFAULT_CONFIG = { diff --git a/src/features/stock/stock-list-page.js b/src/features/stock/stock-list-page.js index 677091e..6b9693e 100644 --- a/src/features/stock/stock-list-page.js +++ b/src/features/stock/stock-list-page.js @@ -1,11 +1,15 @@ import { + getStockEntry, listGroupedStockEntries, + listKitchenChanges, listStockEntries, updateStockItem, useStockItem, } from '../../api/stock.js'; import { fetchLocations } from '../../api/locations.js'; -import { createAsyncState, runAsyncState } from '../shared/ui-state.js'; +import { STORAGE_KEYS } from '../../app/config.js'; +import { clearStoredValue, loadStoredValue, saveStoredValue } from '../shared/storage.js'; +import { createAsyncState } from '../shared/ui-state.js'; import { formatDate } from '../shared/date-utils.js'; const LEVEL_LABELS = { @@ -35,6 +39,26 @@ const EXPIRATION_LEGEND = [ ]; const EXPIRATION_KEYS = EXPIRATION_LEGEND.map((state) => state.key); +const GROUPED_PAGE_SIZE = 24; +const CHANGE_POLL_INTERVAL_MS = 60 * 1000; +const STOCK_LIST_CONTEXT_TTL_MS = 10 * 60 * 1000; +let stockListRuntimeCache = null; + +function cloneRuntimeSnapshot(value) { + if (typeof structuredClone === 'function') { + try { + return structuredClone(value); + } catch { + // Alpine/reactive proxies can throw DataCloneError in some browsers. + } + } + + try { + return JSON.parse(JSON.stringify(value)); + } catch { + return null; + } +} function todayAtMidnight() { const now = new Date(); @@ -54,6 +78,16 @@ function parseDateValue(value) { return new Date(year, month - 1, day); } +function daysUntilDate(value) { + const parsed = parseDateValue(value); + if (!parsed) { + return null; + } + + const today = todayAtMidnight(); + return Math.round((parsed - today) / (24 * 60 * 60 * 1000)); +} + function expirationInfo(entry) { const expireDateValue = Array.isArray(entry.items) && (entry.first_expire_date || entry.first_expire_in !== undefined) @@ -251,177 +285,205 @@ export function renderStockListPage() {
-
-
+
+
+
Stock view mode
+
+ + +
+
+
-
- +
+
+ Updating in background...
-
-
-
- - -
-
-
- -
-
-
-
- -
-
-
- -
+
+
-
-
- + +
+ +
-
-
-
-
-
- -
-
-

Location overview

-

Tap locations to focus the list. Parent locations include their children.

-
-
- -
+ +
+
Active scope
+
+
Expiration:
+
Location:
-
-
- -