import { fetchLocations } from '../../api/locations.js'; import { getStockEntry, listKitchenChanges } from '../../api/stock.js'; import { createAsyncState, runAsyncState } from '../shared/ui-state.js'; export function renderDashboardPage() { return `

Kitchen stock management

Keep labels, stock, and adjustments in one focused workflow.

This MVP is shaped for fast household operations on a phone or desktop, with the Tryton backend staying in charge of business logic.

Active kitchen
Switch without signing out when you need to work across kitchens in the same Tryton database.

Recent changes

Latest item and stock updates from the kitchen change feed.

Saved means the backend created or updated a record.

`; } export function dashboardPageData(store) { return { showKitchenPicker: false, changesState: createAsyncState(), recentChanges: [], locationLabelByUuid: {}, itemByUuid: {}, async init() { if (!store.isConnected) { return; } await this.refreshChanges(); }, async refreshChanges() { await runAsyncState(this.changesState, async () => { const payload = await listKitchenChanges(store, { limit: 10 }); this.recentChanges = payload.changes; await this.loadContextForChanges(payload.changes); }).catch(() => {}); }, async loadContextForChanges(changes) { const stockItemUuids = Array.from(new Set( changes .map((change) => change?.stock?.item_uuid_b64) .filter(Boolean), )); const missingItemUuids = stockItemUuids.filter((uuid) => !this.itemByUuid[uuid]); if (missingItemUuids.length) { const results = await Promise.allSettled( missingItemUuids.map(async (uuid) => { try { return await getStockEntry(store, uuid); } catch (error) { const status = error?.status || error?.cause?.status; if (status !== 404) { throw error; } return getStockEntry(store, uuid, { allowInactive: true }); } }), ); results.forEach((result) => { if (result.status !== 'fulfilled' || !result.value?.uuid_b64) { return; } this.itemByUuid[result.value.uuid_b64] = result.value; }); } if (Object.keys(this.locationLabelByUuid).length) { return; } try { const { flat } = await fetchLocations(store); this.locationLabelByUuid = Object.fromEntries( flat .filter((location) => location.uuid_b64) .map((location) => [location.uuid_b64, location.pathLabel || location.name]), ); } catch { this.locationLabelByUuid = {}; } }, setKitchen(kitchen) { store.setActiveKitchen(kitchen); this.showKitchenPicker = false; store.addAlert({ type: 'success', message: `Working in ${kitchen.name}.` }); this.locationLabelByUuid = {}; this.itemByUuid = {}; this.refreshChanges(); }, resolveItemForChange(change) { if (change?.item?.uuid_b64) { return change.item; } const stockItemUuid = change?.stock?.item_uuid_b64; if (!stockItemUuid) { return null; } return this.itemByUuid[stockItemUuid] || null; }, humanStockType(value) { if (!value) { return null; } return value.charAt(0).toUpperCase() + value.slice(1); }, formatQuantity(quantity, uomSymbol) { if (quantity === null || quantity === undefined || quantity === '') { return null; } return `${quantity}${uomSymbol ? ` ${uomSymbol}` : ''}`; }, formatLevel(level) { if (!level) { return null; } return level.charAt(0).toUpperCase() + level.slice(1); }, formatShortDate(value) { if (!value) { return null; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return String(value); } return date.toLocaleDateString(); }, resolveLocationLabel(change, item) { const locationUuid = change?.stock?.location_uuid_b64 || item?.location_initial_uuid_b64 || null; if (!locationUuid) { return null; } return this.locationLabelByUuid[locationUuid] || locationUuid; }, changeHeadline(change) { const item = this.resolveItemForChange(change); const itemName = item?.name || 'Unknown item'; const type = String(change?.type || 'change'); const action = String(change?.action || 'updated'); if (action === 'upsert' && type === 'item') { return `Item saved: ${itemName}`; } if (action === 'upsert' && type === 'stock') { return `Stock saved: ${itemName}`; } return `${type} ${action}: ${itemName}`; }, changeStateLine(change) { const item = this.resolveItemForChange(change); const stock = change?.stock || {}; const state = []; const stockType = this.humanStockType(item?.stock_type); if (stockType) { state.push(`Type: ${stockType}`); } const quantity = this.formatQuantity( stock.quantity ?? item?.quantity, stock.uom_symbol || item?.uom_symbol, ); if (quantity) { state.push(`Quantity: ${quantity}`); } const level = this.formatLevel(stock.level || item?.level); if (level) { state.push(`Level: ${level}`); } const expiry = this.formatShortDate(item?.expire_date); if (expiry) { state.push(`Expires: ${expiry}`); } const location = this.resolveLocationLabel(change, item); if (location) { state.push(`Location: ${location}`); } if (!state.length) { return 'Saved (created or updated).'; } return state.join(' • '); }, formatChangeTimestamp(value) { if (!value) { return 'Unknown time'; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return String(value); } return date.toLocaleString(); }, }; }