diff --git a/docs/architecture/frontend-boundary.md b/docs/architecture/frontend-boundary.md index f4d64e7..19bd217 100644 --- a/docs/architecture/frontend-boundary.md +++ b/docs/architecture/frontend-boundary.md @@ -25,8 +25,9 @@ ## UI Workflow Coverage - Kitchen board lanes with work-item detail and station coverage -- Board event feed derived from the loaded lane state +- Board event feed derived from the loaded lane state and shared restaurant lifecycle progression - Claim, release, transition, and priority operator actions - Latest operator result and recent action history - Session-expired handling with reauthentication guidance - Protected route shell for board, operator actions, and session inspection +- Kitchen transitions are presented as order progression toward floor handoff and payment eligibility. diff --git a/docs/runbooks/local-development.md b/docs/runbooks/local-development.md index 14838af..bbb9775 100644 --- a/docs/runbooks/local-development.md +++ b/docs/runbooks/local-development.md @@ -26,6 +26,7 @@ npm run dev ## Available Screens - `/board`: lane-based kitchen board and board event feed +- `/board`: lane-based kitchen board tied to restaurant order progression and handoff readiness - `/actions`: claim, release, transition, and priority operator controls - `/session`: current Thalos session profile payload diff --git a/docs/runbooks/testing.md b/docs/runbooks/testing.md index 5f4fe75..54417c5 100644 --- a/docs/runbooks/testing.md +++ b/docs/runbooks/testing.md @@ -17,7 +17,7 @@ npm run test:ci - `src/api/client.test.ts`: runtime-config precedence and fallback behavior. - `src/api/dashboardApi.test.ts`: endpoint path/query composition for board and operator action flows. - `src/auth/oidcLogin.test.ts`: OIDC start-url generation and safe return-url fallback. -- `src/App.test.tsx`: central login screen, board lane loading, operator actions, and session-expired reauthentication guidance. +- `src/App.test.tsx`: central login screen, lifecycle-aware board loading, operator actions, and session-expired reauthentication guidance. ## Notes diff --git a/src/App.test.tsx b/src/App.test.tsx index 1e13ced..f13ccfb 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -67,7 +67,7 @@ describe('Kitchen Ops App', () => { summary: '2 active kitchen lanes', lanes: [ { - lane: 'Cooking', + lane: 'Ready', items: [ { workItemId: 'WORK-1', @@ -75,7 +75,7 @@ describe('Kitchen Ops App', () => { ticketId: 'TICK-1', tableId: 'T-01', station: 'Grill', - state: 'Cooking', + state: 'Ready', priority: 2, claimedBy: 'chef-a', etaMinutes: 12 @@ -92,8 +92,9 @@ describe('Kitchen Ops App', () => { fireEvent.click(screen.getByRole('button', { name: 'Load Kitchen Board' })); await waitFor(() => expect(loadDashboard).toHaveBeenCalledWith('demo-context')); - expect(await screen.findByText('Lane: Cooking')).toBeInTheDocument(); - expect(await screen.findByText('Cooking: WORK-1 at Grill is Cooking')).toBeInTheDocument(); + expect(await screen.findByText('Lifecycle Stage: Ready')).toBeInTheDocument(); + expect(screen.getByText(/Kitchen lanes are a live view of restaurant orders/)).toBeInTheDocument(); + expect(await screen.findByText('Ready: ORD-1 / TICK-1 at Grill is Ready and the order is ready for handoff to floor service')).toBeInTheDocument(); }); it('runs operator actions from the actions route', async () => { @@ -114,7 +115,7 @@ describe('Kitchen Ops App', () => { orderId: 'ORD-1', ticketId: 'TICK-1', previousState: 'Cooking', - currentState: 'Ready', + currentState: 'Served', transitioned: true, error: null }); @@ -130,10 +131,14 @@ describe('Kitchen Ops App', () => { expect(screen.getAllByText('chef-a').length).toBeGreaterThan(0); fireEvent.change(screen.getByPlaceholderText('Order Id'), { target: { value: 'ORD-1' } }); + fireEvent.change(screen.getAllByPlaceholderText('Context Id')[2], { target: { value: 'demo-context' } }); fireEvent.click(screen.getByRole('button', { name: 'Transition Work Item' })); await waitFor(() => expect(transitionKitchenWorkItem).toHaveBeenCalledTimes(1)); - expect(await screen.findByText('Cooking -> Ready')).toBeInTheDocument(); + expect(transitionKitchenWorkItem).toHaveBeenCalledWith( + expect.objectContaining({ contextId: 'demo-context', orderId: 'ORD-1' }) + ); + expect(await screen.findByText('Cooking -> Served (the order is complete and the check can move to payment)')).toBeInTheDocument(); }); it('shows reauthentication guidance when board loading returns session expired', async () => { diff --git a/src/App.tsx b/src/App.tsx index 8b7643f..9a1eeb0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -78,7 +78,7 @@ const boardColumns = [ { title: 'State', dataIndex: 'state', - render: (value: string) => {value} + render: (value: string) => {value} }, { title: 'Priority', dataIndex: 'priority' }, { title: 'Claimed By', render: (_: unknown, record: KitchenBoardItem) => record.claimedBy ?? 'Unclaimed' }, @@ -326,9 +326,18 @@ function KitchenOpsShell() { {boardPayload.contextId} {boardPayload.summary} {boardPayload.availableStations.join(', ')} + + Kitchen lanes are a live view of restaurant orders moving from accepted work into preparation, + handoff readiness, and final service completion. + {boardPayload.lanes.map((lane) => ( - + {lane.lane}} + > pagination={false} rowKey={(record) => record.workItemId} @@ -349,7 +358,10 @@ function KitchenOpsShell() { - lane.items.map((item) => `${lane.lane}: ${item.workItemId} at ${item.station} is ${item.state}`) + lane.items.map( + (item) => + `${lane.lane}: ${item.orderId} / ${item.ticketId} at ${item.station} is ${item.state} and ${workflowProgressHint(item.state)}` + ) )} renderItem={(item) => {item}} /> @@ -407,9 +419,18 @@ function KitchenOpsShell() { layout="vertical" - initialValues={{ orderId: 'ORD-1001', ticketId: 'TICK-1001', targetState: 'Ready', updatedBy: 'chef-a' }} + initialValues={{ + contextId, + orderId: 'ORD-1001', + ticketId: 'TICK-1001', + targetState: 'Ready', + updatedBy: 'chef-a' + }} onFinish={(values) => void executeTransition(values)} > + + + @@ -523,9 +544,14 @@ function ActionDescriptions({ event }: { event: ActionEvent }) { {event.response.orderId} {event.response.ticketId} - {event.response.previousState} - {event.response.currentState} + + {event.response.previousState} + + + {event.response.currentState} + {String(event.response.transitioned)} + {workflowProgressHint(event.response.currentState)} {event.response.error ?? 'None'} ); @@ -556,12 +582,58 @@ function getActionSummary(event: ActionEvent): string { } if (event.kind === 'transition') { - return `${event.response.previousState} -> ${event.response.currentState}`; + return `${event.response.previousState} -> ${event.response.currentState} (${workflowProgressHint(event.response.currentState)})`; } return event.response.summary; } +function workflowTagColor(state: string): string { + switch (state.toLowerCase()) { + case 'queued': + case 'accepted': + return 'blue'; + case 'cooking': + case 'preparing': + return 'orange'; + case 'ready': + case 'readyforpickup': + return 'cyan'; + case 'served': + case 'delivered': + return 'green'; + case 'paid': + return 'purple'; + case 'blocked': + case 'failed': + case 'canceled': + return 'red'; + default: + return 'default'; + } +} + +function workflowProgressHint(state: string): string { + switch (state.toLowerCase()) { + case 'queued': + case 'accepted': + return 'waiting for a kitchen operator to start preparation'; + case 'cooking': + case 'preparing': + return 'the kitchen is actively preparing this order'; + case 'ready': + case 'readyforpickup': + return 'the order is ready for handoff to floor service'; + case 'served': + case 'delivered': + return 'the order is complete and the check can move to payment'; + case 'paid': + return 'the restaurant workflow is fully closed'; + default: + return 'the kitchen workflow is still progressing'; + } +} + function providerLabel(provider: IdentityProvider): string { if (provider === 0 || provider === '0' || provider === 'InternalJwt') { return 'Internal JWT'; diff --git a/src/api/dashboardApi.ts b/src/api/dashboardApi.ts index c9cc923..1e81b26 100644 --- a/src/api/dashboardApi.ts +++ b/src/api/dashboardApi.ts @@ -57,6 +57,7 @@ export type TransitionKitchenWorkItemRequest = { ticketId: string; targetState: string; updatedBy: string; + contextId?: string; }; export type TransitionKitchenWorkItemResponse = {