feat(waiter-floor-web): show shared order progression

This commit is contained in:
José René White Enciso 2026-03-31 19:00:52 -06:00
parent a0dc0a29ed
commit 74c8567b14
5 changed files with 73 additions and 15 deletions

View File

@ -23,8 +23,8 @@
## UI Workflow Coverage
- Waiter assignment snapshot with location metadata and active-order counts
- Waiter assignment snapshot with location metadata and active-order counts derived from the shared restaurant lifecycle
- Recent waiter activity history feed
- Floor order submission and order update workflows
- Floor order submission and order update workflows that feed the shared restaurant order/check model
- Session-expired handling with reauthentication guidance
- Protected route shell for assignments, order actions, and session inspection

View File

@ -26,7 +26,7 @@ npm run dev
## Available Screens
- `/assignments`: waiter assignment snapshot and recent activity feed
- `/orders`: floor order submit and update actions
- `/orders`: floor order submit and update actions with shared-lifecycle progression hints
- `/session`: current Thalos session profile payload
## Build

View File

@ -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, activity loading, and order update payload mapping.
- `src/auth/oidcLogin.test.ts`: OIDC start-url generation and safe return-url fallback.
- `src/App.test.tsx`: central login screen, assignment and activity loading, order submit/update workflows, and session-expired reauthentication guidance.
- `src/App.test.tsx`: central login screen, shared-lifecycle assignment messaging, order submit/update progression hints, and session-expired reauthentication guidance.
## Notes

View File

@ -58,7 +58,7 @@ describe('Waiter Floor App', () => {
contextId: 'demo-context',
locationId: 'floor-a',
summary: '2 waiters assigned',
assignments: [{ waiterId: 'w-1', tableId: 'T-12', status: 'Assigned', activeOrders: 3 }],
assignments: [{ waiterId: 'service-pool', tableId: 'T-12', status: 'Preparing', activeOrders: 3 }],
recentActivity: ['legacy assignment feed']
});
vi.mocked(loadRecentActivity).mockResolvedValue({
@ -77,6 +77,7 @@ describe('Waiter Floor App', () => {
expect(loadRecentActivity).toHaveBeenCalledWith('demo-context');
expect(await screen.findByText('floor-a')).toBeInTheDocument();
expect(await screen.findByText('Waiter w-1 picked up table T-12')).toBeInTheDocument();
expect(await screen.findByText('Floor actions create or update shared restaurant orders that kitchen and POS observe next.')).toBeInTheDocument();
});
it('submits and updates floor orders from the order route', async () => {
@ -90,16 +91,16 @@ describe('Waiter Floor App', () => {
contextId: 'demo-context',
orderId: 'ORD-2200',
accepted: true,
summary: 'submitted',
status: 'Queued',
summary: 'Order ORD-2200 was accepted and is ready for kitchen dispatch.',
status: 'accepted',
processedAtUtc: '2026-03-31T12:00:00Z'
});
vi.mocked(updateFloorOrder).mockResolvedValue({
contextId: 'demo-context',
orderId: 'ORD-2200',
accepted: true,
summary: 'updated',
status: 'Updated',
summary: 'Updated order ORD-2200. Order ORD-2200 was accepted and is ready for kitchen dispatch.',
status: 'accepted',
processedAtUtc: '2026-03-31T12:05:00Z'
});
@ -111,13 +112,14 @@ describe('Waiter Floor App', () => {
fireEvent.click(screen.getByRole('button', { name: 'Submit Floor Order' }));
await waitFor(() => expect(submitFloorOrder).toHaveBeenCalledTimes(1));
expect((await screen.findAllByText('Queued')).length).toBeGreaterThan(0);
expect((await screen.findAllByText('accepted')).length).toBeGreaterThan(0);
expect(await screen.findByText('Kitchen should pick this order up next.')).toBeInTheDocument();
fireEvent.change(screen.getAllByPlaceholderText('Order Id')[1], { target: { value: 'ORD-2200' } });
fireEvent.click(screen.getByRole('button', { name: 'Update Floor Order' }));
await waitFor(() => expect(updateFloorOrder).toHaveBeenCalledTimes(1));
expect((await screen.findAllByText('Updated')).length).toBeGreaterThan(0);
expect((await screen.findAllByText('accepted')).length).toBeGreaterThan(0);
});
it('shows reauthentication guidance when the workflow returns session expired', async () => {

View File

@ -66,7 +66,7 @@ const assignmentColumns = [
{
title: 'Status',
dataIndex: 'status',
render: (value: string) => <Tag color={value === 'Assigned' ? 'blue' : 'gold'}>{value}</Tag>
render: (value: string) => <Tag color={workflowTagColor(value)}>{value}</Tag>
},
{ title: 'Active Orders', dataIndex: 'activeOrders' }
];
@ -227,7 +227,7 @@ function WaiterFloorShell() {
<Layout.Content className="content">
<Typography.Title level={3}>Waiter Floor Operations</Typography.Title>
<Typography.Paragraph type="secondary">
Protected floor workflows for assignment visibility, recent activity, and order submit or update actions.
Protected floor workflows for assignment visibility, recent activity, and order actions that now feed the same restaurant lifecycle used by kitchen and POS.
</Typography.Paragraph>
{session.error && <Alert className="stack-gap" type="warning" showIcon message={session.error} />}
{workflowState.error && (
@ -273,6 +273,9 @@ function WaiterFloorShell() {
<Descriptions.Item label="Context Id">{assignments.contextId}</Descriptions.Item>
<Descriptions.Item label="Location Id">{assignments.locationId}</Descriptions.Item>
<Descriptions.Item label="Summary">{assignments.summary}</Descriptions.Item>
<Descriptions.Item label="Lifecycle Note">
Floor actions create or update shared restaurant orders that kitchen and POS observe next.
</Descriptions.Item>
</Descriptions>
<Table
pagination={false}
@ -306,6 +309,9 @@ function WaiterFloorShell() {
element={
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card title="Submit Floor Order">
<Typography.Paragraph type="secondary">
New floor orders are accepted into the shared restaurant lifecycle first, then they progress through kitchen preparation and payment readiness.
</Typography.Paragraph>
<Form
layout="vertical"
initialValues={{
@ -334,6 +340,9 @@ function WaiterFloorShell() {
</Form>
</Card>
<Card title="Update Existing Order">
<Typography.Paragraph type="secondary">
Updates keep the same shared order identity so the downstream kitchen and POS views stay consistent.
</Typography.Paragraph>
<Form
layout="vertical"
initialValues={{
@ -367,8 +376,11 @@ function WaiterFloorShell() {
<Descriptions.Item label="Context Id">{lastOrderResponse.contextId}</Descriptions.Item>
<Descriptions.Item label="Order Id">{lastOrderResponse.orderId}</Descriptions.Item>
<Descriptions.Item label="Accepted">{String(lastOrderResponse.accepted)}</Descriptions.Item>
<Descriptions.Item label="Status">{lastOrderResponse.status}</Descriptions.Item>
<Descriptions.Item label="Status">
<Tag color={workflowTagColor(lastOrderResponse.status)}>{lastOrderResponse.status}</Tag>
</Descriptions.Item>
<Descriptions.Item label="Summary">{lastOrderResponse.summary}</Descriptions.Item>
<Descriptions.Item label="Next Step">{orderProgressHint(lastOrderResponse.status)}</Descriptions.Item>
<Descriptions.Item label="Processed At">{lastOrderResponse.processedAtUtc}</Descriptions.Item>
</Descriptions>
) : (
@ -387,7 +399,10 @@ function WaiterFloorShell() {
render: (_, record) => <Tag color={record.kind === 'submitted' ? 'green' : 'orange'}>{record.kind}</Tag>
},
{ title: 'Order Id', render: (_, record) => record.response.orderId },
{ title: 'Status', render: (_, record) => record.response.status },
{
title: 'Status',
render: (_, record) => <Tag color={workflowTagColor(record.response.status)}>{record.response.status}</Tag>
},
{ title: 'Summary', render: (_, record) => record.response.summary },
{ title: 'Processed At', render: (_, record) => record.response.processedAtUtc }
]}
@ -429,4 +444,45 @@ function providerLabel(provider: IdentityProvider): string {
return String(provider);
}
function workflowTagColor(status: string): string {
switch (status.toLowerCase()) {
case 'accepted':
return 'blue';
case 'preparing':
case 'cooking':
return 'gold';
case 'ready':
case 'readyforpickup':
return 'cyan';
case 'served':
case 'paid':
return 'green';
case 'blocked':
case 'failed':
case 'canceled':
return 'red';
default:
return 'default';
}
}
function orderProgressHint(status: string): string {
switch (status.toLowerCase()) {
case 'accepted':
return 'Kitchen should pick this order up next.';
case 'preparing':
case 'cooking':
return 'Kitchen is actively preparing this order.';
case 'ready':
case 'readyforpickup':
return 'The order is ready for handoff or service.';
case 'served':
return 'POS can now treat this check as payable.';
case 'paid':
return 'This restaurant check is fully closed.';
default:
return 'Track this order across the shared restaurant lifecycle.';
}
}
export default App;