import { ControlOutlined, DeploymentUnitOutlined, HistoryOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons'; import { Alert, Button, Card, Descriptions, Empty, Form, Input, Layout, Menu, Result, Select, Space, Spin, Table, Tag, TimePicker, Typography } from 'antd'; import dayjs, { type Dayjs } from 'dayjs'; import { type ReactNode, useMemo, useState } from 'react'; import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { ApiError } from './api/client'; import { loadDashboard, loadRecentChanges, setServiceWindow, type ConfigChange, type FeatureFlagState, type RecentAdminChangesResponse, type RestaurantAdminConfigResponse, type ServiceWindow, type SetServiceWindowRequest, type SetServiceWindowResponse } from './api/dashboardApi'; import type { IdentityProvider } from './api/sessionApi'; import { buildGoogleOidcStartUrl } from './auth/oidcLogin'; import { SessionProvider, useSessionContext } from './auth/sessionContext'; type AppRoute = '/config' | '/window' | '/session'; type WorkflowState = { error: string | null; sessionExpired: boolean; }; type ServiceWindowFormValues = { contextId: string; day: number; openAt: Dayjs; closeAt: Dayjs; updatedBy: string; }; const routeItems: Array<{ key: AppRoute; label: string; icon: ReactNode }> = [ { key: '/config', label: 'Config', icon: }, { key: '/window', label: 'Service Window', icon: }, { key: '/session', label: 'Session', icon: } ]; const dayOptions = [ { label: 'Sunday', value: 0 }, { label: 'Monday', value: 1 }, { label: 'Tuesday', value: 2 }, { label: 'Wednesday', value: 3 }, { label: 'Thursday', value: 4 }, { label: 'Friday', value: 5 }, { label: 'Saturday', value: 6 } ]; function App() { return ( ); } function RestaurantAdminShell() { const session = useSessionContext(); const location = useLocation(); const navigate = useNavigate(); const [contextId, setContextId] = useState('demo-context'); const [configPayload, setConfigPayload] = useState(null); const [changesPayload, setChangesPayload] = useState(null); const [windowResponse, setWindowResponse] = useState(null); const [windowHistory, setWindowHistory] = useState([]); const [workflowState, setWorkflowState] = useState({ error: null, sessionExpired: false }); const [loadingConfig, setLoadingConfig] = useState(false); const [loadingChanges, setLoadingChanges] = useState(false); const [submittingWindow, setSubmittingWindow] = 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 ?? '/config'; }, [location.pathname]); const recentChanges = changesPayload?.recentChanges ?? configPayload?.recentChanges ?? []; 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 restaurant administration work.', sessionExpired: true }); await session.revalidate(); return; } setWorkflowState({ error: err instanceof Error ? err.message : fallbackMessage, sessionExpired: false }); }; const loadConfig = async () => { setLoadingConfig(true); clearWorkflowError(); try { const payload = await loadDashboard(contextId); setConfigPayload(payload); } catch (err) { await handleWorkflowFailure(err, 'Failed to load restaurant admin configuration.'); } finally { setLoadingConfig(false); } }; const refreshChanges = async () => { setLoadingChanges(true); clearWorkflowError(); try { const payload = await loadRecentChanges(contextId); setChangesPayload(payload); } catch (err) { await handleWorkflowFailure(err, 'Failed to load recent configuration changes.'); } finally { setLoadingChanges(false); } }; const applyServiceWindow = async (values: ServiceWindowFormValues) => { setSubmittingWindow(true); clearWorkflowError(); const request: SetServiceWindowRequest = { contextId: values.contextId, day: values.day, openAt: `${values.openAt.format('HH:mm')}:00`, closeAt: `${values.closeAt.format('HH:mm')}:00`, updatedBy: values.updatedBy }; try { const payload = await setServiceWindow(request); setWindowResponse(payload); setWindowHistory((previous) => [payload, ...previous].slice(0, 8)); await loadConfig(); await refreshChanges(); } catch (err) { await handleWorkflowFailure(err, 'Failed to set service window.'); } finally { setSubmittingWindow(false); } }; if (session.status === 'loading') { return (
); } if (session.status !== 'authenticated' || !session.profile) { return (
Continue with Google } /> {workflowState.sessionExpired && ( )} {session.error && }
); } return (
Restaurant Admin Web
navigate(event.key as AppRoute)} /> subject: {session.profile.subjectId} tenant: {session.profile.tenantId} provider: {providerLabel(session.profile.provider)} Restaurant Administration Protected control-plane workflows for configuration snapshots, change history, and service-window management. {session.error && } {workflowState.error && } {workflowState.sessionExpired && ( } /> )} setContextId(event.target.value)} placeholder="Context Id" style={{ width: 280 }} /> {configPayload ? ( {configPayload.contextId} {configPayload.version} {configPayload.summary} ) : ( )} pagination={false} rowKey={(record) => record.key} dataSource={configPayload?.featureFlags ?? []} locale={{ emptyText: 'No feature flags loaded.' }} columns={[ { title: 'Key', dataIndex: 'key' }, { title: 'Enabled', render: (_, record) => ( {record.enabled ? 'Enabled' : 'Disabled'} ) } ]} /> pagination={false} rowKey={(record) => `${record.day}-${record.openAt}-${record.closeAt}`} dataSource={configPayload?.serviceWindows ?? []} locale={{ emptyText: 'No service windows loaded.' }} columns={[ { title: 'Day', render: (_, record) => dayLabel(record.day) }, { title: 'Open At', dataIndex: 'openAt' }, { title: 'Close At', dataIndex: 'closeAt' }, { title: 'Status', render: (_, record) => ( {record.isClosed ? 'Closed' : 'Open'} ) } ]} /> pagination={false} rowKey={(record) => record.changeId} dataSource={recentChanges} locale={{ emptyText: 'No recent changes loaded.' }} columns={[ { title: 'Change Id', dataIndex: 'changeId' }, { title: 'Category', dataIndex: 'category' }, { title: 'Description', dataIndex: 'description' }, { title: 'Updated By', dataIndex: 'updatedBy' }, { title: 'Updated At', render: (_, record) => formatUtc(record.updatedAtUtc) } ]} /> } /> layout="vertical" initialValues={{ contextId, day: 1, openAt: dayjs('08:00', 'HH:mm'), closeAt: dayjs('22:00', 'HH:mm'), updatedBy: 'admin-operator' }} onFinish={(values) => void applyServiceWindow(values)} > {windowResponse ? ( {windowResponse.contextId} {String(windowResponse.applied)} {windowResponse.message} {dayLabel(windowResponse.serviceWindow.day)} {windowResponse.serviceWindow.openAt} {windowResponse.serviceWindow.closeAt} ) : ( )} pagination={false} rowKey={(record) => `${record.contextId}-${record.message}-${record.serviceWindow.day}-${record.serviceWindow.openAt}` } dataSource={windowHistory} locale={{ emptyText: 'No service window actions submitted yet.' }} columns={[ { title: 'Context Id', dataIndex: 'contextId' }, { title: 'Day', render: (_, record) => dayLabel(record.serviceWindow.day) }, { title: 'Window', render: (_, record) => `${record.serviceWindow.openAt} - ${record.serviceWindow.closeAt}` }, { title: 'Applied', render: (_, record) => {String(record.applied)} }, { title: 'Message', dataIndex: 'message' } ]} /> } />
{JSON.stringify(session.profile, null, 2)}
} /> } /> } />
); } function dayLabel(day: number): string { return dayOptions.find((option) => option.value === day)?.label ?? String(day); } function formatUtc(value: string): string { return new Date(value).toLocaleString(); } 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); } export default App;