feat(customer-orders-web): show shared order progression
This commit is contained in:
parent
429b58a629
commit
d8da4aae5e
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
70
src/App.tsx
70
src/App.tsx
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user