feat(waiter-floor-web): show shared order progression
This commit is contained in:
parent
a0dc0a29ed
commit
74c8567b14
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
64
src/App.tsx
64
src/App.tsx
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user