feat(customer-orders-web): show shared order progression

This commit is contained in:
José René White Enciso 2026-03-31 19:03:54 -06:00
parent 429b58a629
commit d8da4aae5e
5 changed files with 82 additions and 24 deletions

View File

@ -23,9 +23,9 @@
## UI Workflow Coverage
- Customer order status dashboard with current orders
- Customer order status dashboard with current orders from the shared restaurant lifecycle
- Selected order detail lookup
- Recent order history and event feed
- Customer order submission and recent submission results
- Customer order submission and recent submission results with shared-lifecycle progression hints
- Session-expired handling with reauthentication guidance
- Protected route shell for status, submission, and session inspection

View File

@ -25,8 +25,8 @@ npm run dev
## Available Screens
- `/status`: current order status, selected order detail, history, and recent events
- `/submit`: customer order submission and recent submission results
- `/status`: current order status, selected order detail, history, recent events, and shared-lifecycle guidance
- `/submit`: customer order submission and recent submission results with kitchen/payment readiness 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 for status, detail, history, and submit flows.
- `src/auth/oidcLogin.test.ts`: OIDC start-url generation and safe return-url fallback.
- `src/App.test.tsx`: central login screen, status/detail/history loading, order submission, and session-expired reauthentication guidance.
- `src/App.test.tsx`: central login screen, shared-lifecycle order messaging, order submission progression hints, and session-expired reauthentication guidance.
## Notes

View File

@ -57,20 +57,20 @@ describe('Customer Orders App', () => {
vi.mocked(loadDashboard).mockResolvedValue({
contextId: 'demo-context',
summary: '2 open orders',
orders: [{ orderId: 'CO-1001', tableId: 'T-08', status: 'Submitted', guestCount: 2, itemIds: ['ITEM-101'] }],
orders: [{ orderId: 'ORD-1001', tableId: 'T-08', status: 'Preparing', guestCount: 2, itemIds: ['ITEM-101'] }],
recentEvents: ['status payload event']
});
vi.mocked(loadOrderHistory).mockResolvedValue({
contextId: 'demo-context',
summary: 'recent history',
orders: [{ orderId: 'CO-0999', tableId: 'T-04', status: 'Completed', guestCount: 3, itemIds: ['ITEM-202'] }],
recentEvents: ['Order CO-0999 completed']
orders: [{ orderId: 'ORD-0999', tableId: 'T-04', status: 'Served', guestCount: 3, itemIds: ['ITEM-202'] }],
recentEvents: ['Order ORD-0999 completed service and is ready for payment capture']
});
vi.mocked(loadOrderDetail).mockResolvedValue({
contextId: 'demo-context',
summary: 'selected order',
order: { orderId: 'CO-1001', tableId: 'T-08', status: 'Submitted', guestCount: 2, itemIds: ['ITEM-101'] },
recentEvents: ['Order CO-1001 confirmed']
order: { orderId: 'ORD-1001', tableId: 'T-08', status: 'Preparing', guestCount: 2, itemIds: ['ITEM-101'] },
recentEvents: ['Order ORD-1001 confirmed']
});
render(<App />);
@ -80,9 +80,10 @@ describe('Customer Orders App', () => {
await waitFor(() => expect(loadDashboard).toHaveBeenCalledWith('demo-context'));
expect(loadOrderHistory).toHaveBeenCalledWith('demo-context');
expect(loadOrderDetail).toHaveBeenCalledWith('demo-context', 'CO-1001');
expect(loadOrderDetail).toHaveBeenCalledWith('demo-context', 'ORD-1001');
expect(await screen.findByText('2 open orders')).toBeInTheDocument();
expect(await screen.findByText('Order CO-0999 completed')).toBeInTheDocument();
expect(await screen.findByText('Order ORD-0999 completed service and is ready for payment capture')).toBeInTheDocument();
expect(await screen.findByText('Submitted customer orders progress through kitchen preparation and become payable only after service is complete.')).toBeInTheDocument();
});
it('submits customer order from action route', async () => {
@ -94,21 +95,22 @@ describe('Customer Orders App', () => {
});
vi.mocked(submitCustomerOrder).mockResolvedValue({
contextId: 'demo-context',
orderId: 'CO-2200',
orderId: 'ORD-2200',
accepted: true,
summary: 'accepted',
status: 'Submitted'
summary: 'Order ORD-2200 was accepted and is ready for kitchen dispatch.',
status: 'accepted'
});
render(<App />);
await waitFor(() => expect(screen.getByText('Submit Order')).toBeInTheDocument());
fireEvent.click(screen.getByText('Submit Order'));
fireEvent.change(screen.getByPlaceholderText('Order Id'), { target: { value: 'CO-2200' } });
fireEvent.change(screen.getByPlaceholderText('Order Id'), { target: { value: 'ORD-2200' } });
fireEvent.click(screen.getByRole('button', { name: 'Submit Customer Order' }));
await waitFor(() => expect(submitCustomerOrder).toHaveBeenCalledTimes(1));
expect((await screen.findAllByText('Submitted')).length).toBeGreaterThan(0);
expect((await screen.findAllByText('accepted')).length).toBeGreaterThan(0);
expect(await screen.findByText('The kitchen should receive this order next.')).toBeInTheDocument();
});
it('shows reauthentication guidance when status loading returns session expired', async () => {

View File

@ -71,7 +71,7 @@ const orderColumns = [
{
title: 'Status',
dataIndex: 'status',
render: (value: string) => <Tag color={value === 'Submitted' ? 'blue' : 'green'}>{value}</Tag>
render: (value: string) => <Tag color={workflowTagColor(value)}>{value}</Tag>
},
{ title: 'Guests', dataIndex: 'guestCount' },
{
@ -96,7 +96,7 @@ function CustomerOrdersShell() {
const navigate = useNavigate();
const [contextId, setContextId] = useState('demo-context');
const [statusOrderId, setStatusOrderId] = useState('CO-1001');
const [statusOrderId, setStatusOrderId] = useState('ORD-1001');
const [statusPayload, setStatusPayload] = useState<CustomerOrderStatusResponse | null>(null);
const [detailPayload, setDetailPayload] = useState<CustomerOrderDetailResponse | null>(null);
const [historyPayload, setHistoryPayload] = useState<CustomerOrderHistoryResponse | null>(null);
@ -241,7 +241,7 @@ function CustomerOrdersShell() {
<Layout.Content className="content">
<Typography.Title level={3}>Customer Orders</Typography.Title>
<Typography.Paragraph type="secondary">
Protected order workflows for status, detail, history, and submission through the customer-orders BFF.
Protected order workflows for status, detail, history, and submission through the shared restaurant lifecycle.
</Typography.Paragraph>
{session.error && <Alert className="stack-gap" type="warning" showIcon message={session.error} />}
{workflowState.error && (
@ -292,6 +292,9 @@ function CustomerOrdersShell() {
<Descriptions bordered size="small" column={1}>
<Descriptions.Item label="Context Id">{statusPayload.contextId}</Descriptions.Item>
<Descriptions.Item label="Summary">{statusPayload.summary}</Descriptions.Item>
<Descriptions.Item label="Lifecycle Note">
Submitted customer orders progress through kitchen preparation and become payable only after service is complete.
</Descriptions.Item>
</Descriptions>
<Table<CustomerOrderSummary>
pagination={false}
@ -313,9 +316,12 @@ function CustomerOrdersShell() {
<Descriptions.Item label="Summary">{detailPayload.summary}</Descriptions.Item>
<Descriptions.Item label="Order Id">{detailPayload.order.orderId}</Descriptions.Item>
<Descriptions.Item label="Table Id">{detailPayload.order.tableId}</Descriptions.Item>
<Descriptions.Item label="Status">{detailPayload.order.status}</Descriptions.Item>
<Descriptions.Item label="Status">
<Tag color={workflowTagColor(detailPayload.order.status)}>{detailPayload.order.status}</Tag>
</Descriptions.Item>
<Descriptions.Item label="Guest Count">{detailPayload.order.guestCount}</Descriptions.Item>
<Descriptions.Item label="Items">{detailPayload.order.itemIds.join(', ')}</Descriptions.Item>
<Descriptions.Item label="Next Step">{orderProgressHint(detailPayload.order.status)}</Descriptions.Item>
</Descriptions>
) : (
<Empty description="Provide an order id to inspect detail alongside the status dashboard." />
@ -354,7 +360,7 @@ function CustomerOrdersShell() {
layout="vertical"
initialValues={{
contextId,
orderId: 'CO-1001',
orderId: 'ORD-1001',
tableId: 'T-08',
guestCount: 2,
itemIdsText: 'ITEM-101,ITEM-202'
@ -385,8 +391,11 @@ function CustomerOrdersShell() {
<Descriptions.Item label="Context Id">{orderResponse.contextId}</Descriptions.Item>
<Descriptions.Item label="Order Id">{orderResponse.orderId}</Descriptions.Item>
<Descriptions.Item label="Accepted">{String(orderResponse.accepted)}</Descriptions.Item>
<Descriptions.Item label="Status">{orderResponse.status}</Descriptions.Item>
<Descriptions.Item label="Status">
<Tag color={workflowTagColor(orderResponse.status)}>{orderResponse.status}</Tag>
</Descriptions.Item>
<Descriptions.Item label="Summary">{orderResponse.summary}</Descriptions.Item>
<Descriptions.Item label="Next Step">{orderProgressHint(orderResponse.status)}</Descriptions.Item>
</Descriptions>
) : (
<Empty description="Submit an order to inspect the accepted response payload." />
@ -406,7 +415,10 @@ function CustomerOrdersShell() {
title: 'Accepted',
render: (_, record) => <Tag color={record.accepted ? 'green' : 'red'}>{String(record.accepted)}</Tag>
},
{ title: 'Status', dataIndex: 'status' },
{
title: 'Status',
render: (_, record) => <Tag color={workflowTagColor(record.status)}>{record.status}</Tag>
},
{ title: 'Summary', dataIndex: 'summary' }
]}
/>
@ -447,4 +459,48 @@ function providerLabel(provider: IdentityProvider): string {
return String(provider);
}
function workflowTagColor(status: string): string {
switch (status.toLowerCase()) {
case 'accepted':
case 'submitted':
return 'blue';
case 'preparing':
case 'cooking':
return 'gold';
case 'ready':
case 'readyforpickup':
return 'cyan';
case 'served':
return 'green';
case 'paid':
return 'purple';
case 'blocked':
case 'failed':
case 'canceled':
return 'red';
default:
return 'default';
}
}
function orderProgressHint(status: string): string {
switch (status.toLowerCase()) {
case 'accepted':
case 'submitted':
return 'The kitchen should receive this order next.';
case 'preparing':
case 'cooking':
return 'Kitchen is actively preparing this order.';
case 'ready':
case 'readyforpickup':
return 'This order is ready for handoff or table delivery.';
case 'served':
return 'The restaurant can now open payment capture for this check.';
case 'paid':
return 'This order and its check are fully completed.';
default:
return 'Track this order across the shared restaurant lifecycle.';
}
}
export default App;