From 6ca09cdf1f988bdaf7f91be5cf0d598de2cd152c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Bregar?= Date: Sun, 12 Apr 2026 22:46:28 +0200 Subject: [PATCH] Add inactive item fallback for dashboard change feed lookups --- src/api/stock.js | 7 ++- src/features/dashboard/dashboard-page.js | 13 ++++- tests/api/stock.test.js | 30 +++++++++++ .../features/dashboard/dashboard-page.test.js | 52 ++++++++++++++++++- 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/api/stock.js b/src/api/stock.js index 7473ae1..a92fc9e 100644 --- a/src/api/stock.js +++ b/src/api/stock.js @@ -116,8 +116,11 @@ export async function listGroupedStockEntries(store, options = {}) { return fetchAllListPages(store, `${getPath('items')}/grouped`, baseQuery); } -export async function getStockEntry(store, stockId) { - const payload = await apiRequest(store, `${getPath('items')}/${stockId}`); +export async function getStockEntry(store, stockId, { allowInactive = false } = {}) { + const path = `${getPath('items')}/${stockId}`; + const payload = allowInactive + ? await apiRequest(store, path, { query: { allow_inactive: 1 } }) + : await apiRequest(store, path); return unwrapEntryPayload(payload); } diff --git a/src/features/dashboard/dashboard-page.js b/src/features/dashboard/dashboard-page.js index 822ad70..715facf 100644 --- a/src/features/dashboard/dashboard-page.js +++ b/src/features/dashboard/dashboard-page.js @@ -165,7 +165,18 @@ export function dashboardPageData(store) { if (missingItemUuids.length) { const results = await Promise.allSettled( - missingItemUuids.map((uuid) => getStockEntry(store, uuid)), + 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) => { diff --git a/tests/api/stock.test.js b/tests/api/stock.test.js index b1dbc72..6ce7d28 100644 --- a/tests/api/stock.test.js +++ b/tests/api/stock.test.js @@ -16,6 +16,7 @@ vi.mock('../../src/api/client.js', () => ({ const { adjustStockEntry, applyItemUpsert, + getStockEntry, listGroupedStockEntries, listKitchenChanges, listStockEntries, @@ -125,6 +126,35 @@ describe('api/stock', () => { ); }); + it('getStockEntry fetches item without allow_inactive by default', async () => { + apiRequestMock.mockResolvedValueOnce({ uuid_b64: 'item-1', name: 'Milk' }); + + const result = await getStockEntry({ config: { database: 'db' } }, 'item-1'); + + expect(result).toEqual({ uuid_b64: 'item-1', name: 'Milk' }); + expect(apiRequestMock).toHaveBeenCalledWith( + { config: { database: 'db' } }, + 'kitchen/items/item-1', + ); + }); + + it('getStockEntry forwards allow_inactive when requested', async () => { + apiRequestMock.mockResolvedValueOnce({ uuid_b64: 'item-2', active: false }); + + const result = await getStockEntry( + { config: { database: 'db' } }, + 'item-2', + { allowInactive: true }, + ); + + expect(result).toEqual({ uuid_b64: 'item-2', active: false }); + expect(apiRequestMock).toHaveBeenCalledWith( + { config: { database: 'db' } }, + 'kitchen/items/item-2', + { query: { allow_inactive: 1 } }, + ); + }); + it('listKitchenChanges returns normalized changes payload', async () => { apiRequestMock.mockResolvedValueOnce({ since: 'cursor-1', diff --git a/tests/features/dashboard/dashboard-page.test.js b/tests/features/dashboard/dashboard-page.test.js index 18c6c9a..9b6ff2a 100644 --- a/tests/features/dashboard/dashboard-page.test.js +++ b/tests/features/dashboard/dashboard-page.test.js @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; const listKitchenChangesMock = vi.fn(); const getStockEntryMock = vi.fn(); @@ -16,6 +16,12 @@ vi.mock('../../../src/api/locations.js', () => ({ const { dashboardPageData, renderDashboardPage } = await import('../../../src/features/dashboard/dashboard-page.js'); describe('features/dashboard/dashboard-page', () => { + beforeEach(() => { + listKitchenChangesMock.mockReset(); + getStockEntryMock.mockReset(); + fetchLocationsMock.mockReset(); + }); + it('renders dashboard with recent changes section', () => { const html = renderDashboardPage(); expect(html).toContain('Recent changes'); @@ -107,6 +113,50 @@ describe('features/dashboard/dashboard-page', () => { expect(getStockEntryMock).toHaveBeenCalledWith(expect.anything(), 'item-uuid-1'); }); + it('retries stock event item lookup with allowInactive after 404', async () => { + listKitchenChangesMock.mockResolvedValueOnce({ + since: null, + nextCursor: null, + changes: [{ + type: 'stock', + action: 'upsert', + timestamp: '2026-04-10T10:00:00Z', + stock: { + item_uuid_b64: 'item-uuid-2', + quantity: 1, + uom_symbol: 'pcs', + }, + }], + }); + getStockEntryMock + .mockRejectedValueOnce(Object.assign(new Error('Not found'), { status: 404 })) + .mockResolvedValueOnce({ + uuid_b64: 'item-uuid-2', + name: 'Archived pasta', + stock_type: 'measured', + }); + fetchLocationsMock.mockResolvedValueOnce({ flat: [] }); + + const store = { + isConnected: true, + setActiveKitchen: vi.fn(), + addAlert: vi.fn(), + }; + const data = dashboardPageData(store); + + await data.refreshChanges(); + + expect(getStockEntryMock).toHaveBeenNthCalledWith(1, store, 'item-uuid-2'); + expect(getStockEntryMock).toHaveBeenNthCalledWith( + 2, + store, + 'item-uuid-2', + { allowInactive: true }, + ); + expect(data.changeHeadline(data.recentChanges[0])).toBe('Stock saved: Archived pasta'); + expect(data.changeStateLine(data.recentChanges[0])).toContain('Quantity: 1 pcs'); + }); + it('keeps empty state when API returns no changes', async () => { listKitchenChangesMock.mockResolvedValueOnce({ since: null,