import { ClockCircleOutlined, DeploymentUnitOutlined, OrderedListOutlined, ReloadOutlined, ShoppingCartOutlined, SyncOutlined } from '@ant-design/icons'; import { Alert, Button, Card, Descriptions, Empty, Form, Input, InputNumber, Layout, List, Menu, Result, Space, Spin, Table, Tag, Typography } from 'antd'; import { type ReactNode, useMemo, useState } from 'react'; import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { ApiError } from './api/client'; import type { IdentityProvider } from './api/sessionApi'; import { SessionProvider, useSessionContext } from './auth/sessionContext'; import { buildGoogleOidcStartUrl } from './auth/oidcLogin'; import { loadDashboard, loadRecentActivity, submitFloorOrder, updateFloorOrder, type SubmitFloorOrderRequest, type SubmitFloorOrderResponse, type UpdateFloorOrderRequest, type UpdateFloorOrderResponse, type WaiterAssignmentsResponse, type WaiterRecentActivityResponse } from './api/dashboardApi'; type AppRoute = '/assignments' | '/orders' | '/session'; type OrderEvent = | { kind: 'submitted'; response: SubmitFloorOrderResponse } | { kind: 'updated'; response: UpdateFloorOrderResponse }; type WorkflowState = { error: string | null; sessionExpired: boolean; }; const routeItems: Array<{ key: AppRoute; label: string; icon: ReactNode }> = [ { key: '/assignments', label: 'Assignments', icon: }, { key: '/orders', label: 'Order Actions', icon: }, { key: '/session', label: 'Session', icon: } ]; const assignmentColumns = [ { title: 'Waiter Id', dataIndex: 'waiterId' }, { title: 'Table Id', dataIndex: 'tableId' }, { title: 'Status', dataIndex: 'status', render: (value: string) => {value} }, { title: 'Active Orders', dataIndex: 'activeOrders' } ]; function App() { return ( ); } function WaiterFloorShell() { const session = useSessionContext(); const location = useLocation(); const navigate = useNavigate(); const [contextId, setContextId] = useState('demo-context'); const [assignments, setAssignments] = useState(null); const [recentActivity, setRecentActivity] = useState(null); const [lastOrderResponse, setLastOrderResponse] = useState(null); const [orderHistory, setOrderHistory] = useState([]); const [workflowState, setWorkflowState] = useState({ error: null, sessionExpired: false }); const [loadingAssignments, setLoadingAssignments] = useState(false); const [submittingOrder, setSubmittingOrder] = useState(false); const [updatingOrder, setUpdatingOrder] = useState(false); const loginUrl = useMemo(() => buildGoogleOidcStartUrl(window.location.href), []); const selectedKey = useMemo(() => { const candidate = routeItems.find((item) => location.pathname.startsWith(item.key)); return candidate?.key ?? '/assignments'; }, [location.pathname]); const clearWorkflowError = () => setWorkflowState({ error: null, sessionExpired: false }); const handleWorkflowFailure = async (err: unknown, fallbackMessage: string) => { if (err instanceof ApiError && err.status === 401) { setWorkflowState({ error: 'Your session expired. Sign in again to continue floor operations.', sessionExpired: true }); await session.revalidate(); return; } setWorkflowState({ error: err instanceof Error ? err.message : fallbackMessage, sessionExpired: false }); }; const loadAssignments = async () => { setLoadingAssignments(true); clearWorkflowError(); try { const [assignmentsPayload, activityPayload] = await Promise.all([ loadDashboard(contextId), loadRecentActivity(contextId) ]); setAssignments(assignmentsPayload); setRecentActivity(activityPayload); } catch (err) { await handleWorkflowFailure(err, 'Failed to load waiter assignments.'); } finally { setLoadingAssignments(false); } }; const submitOrder = async (request: SubmitFloorOrderRequest) => { setSubmittingOrder(true); clearWorkflowError(); try { const payload = await submitFloorOrder(request); setLastOrderResponse(payload); setOrderHistory((previous) => [{ kind: 'submitted' as const, response: payload }, ...previous].slice(0, 8)); } catch (err) { await handleWorkflowFailure(err, 'Failed to submit floor order.'); } finally { setSubmittingOrder(false); } }; const reviseOrder = async (request: UpdateFloorOrderRequest) => { setUpdatingOrder(true); clearWorkflowError(); try { const payload = await updateFloorOrder(request); setLastOrderResponse(payload); setOrderHistory((previous) => [{ kind: 'updated' as const, response: payload }, ...previous].slice(0, 8)); } catch (err) { await handleWorkflowFailure(err, 'Failed to update floor order.'); } finally { setUpdatingOrder(false); } }; if (session.status === 'loading') { return (
); } if (session.status !== 'authenticated' || !session.profile) { return (
Continue with Google } /> {session.error && } {workflowState.sessionExpired && ( )}
); } return (
Waiter Floor Web
navigate(event.key as AppRoute)} /> subject: {session.profile.subjectId} tenant: {session.profile.tenantId} provider: {providerLabel(session.profile.provider)} Waiter Floor Operations Protected floor workflows for assignment visibility, recent activity, and order actions that now feed the same restaurant lifecycle used by kitchen and POS. {session.error && } {workflowState.error && ( Reauthenticate ) : undefined } /> )} setContextId(event.target.value)} placeholder="Context Id" style={{ width: 280 }} /> {assignments ? ( <> {assignments.contextId} {assignments.locationId} {assignments.summary} Floor actions create or update shared restaurant orders that kitchen and POS observe next. `${record.waiterId}-${record.tableId}`} dataSource={assignments.assignments} columns={assignmentColumns} locale={{ emptyText: 'No active waiter assignments for this context.' }} /> ) : ( )} }> {recentActivity && recentActivity.recentActivity.length > 0 ? ( {item}} /> ) : ( )} } /> New floor orders are accepted into the shared restaurant lifecycle first, then they progress through kitchen preparation and payment readiness.
void submitOrder(values)} >
Updates keep the same shared order identity so the downstream kitchen and POS views stay consistent.
void reviseOrder(values)} >
{lastOrderResponse ? ( {lastOrderResponse.contextId} {lastOrderResponse.orderId} {String(lastOrderResponse.accepted)} {lastOrderResponse.status} {lastOrderResponse.summary} {orderProgressHint(lastOrderResponse.status)} {lastOrderResponse.processedAtUtc} ) : ( )} pagination={false} rowKey={(record) => `${record.kind}-${record.response.orderId}-${record.response.processedAtUtc}`} dataSource={orderHistory} locale={{ emptyText: 'No recent floor order actions yet.' }} columns={[ { title: 'Action', render: (_, record) => {record.kind} }, { title: 'Order Id', render: (_, record) => record.response.orderId }, { title: 'Status', render: (_, record) => {record.response.status} }, { title: 'Summary', render: (_, record) => record.response.summary }, { title: 'Processed At', render: (_, record) => record.response.processedAtUtc } ]} /> } />
{JSON.stringify(session.profile, null, 2)}
} /> } /> } /> ); } function providerLabel(provider: IdentityProvider): string { if (provider === 0 || provider === '0' || provider === 'InternalJwt') { return 'Internal JWT'; } if (provider === 1 || provider === '1' || provider === 'AzureAd') { return 'Azure AD'; } if (provider === 2 || provider === '2' || provider === 'Google') { return 'Google'; } 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;