Compare commits
3 Commits
feature/ki
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6512e131b0 | ||
|
|
6136ad94a5 | ||
|
|
143734098e |
@ -5,6 +5,7 @@
|
||||
- The UI does not access DAL or internal services directly.
|
||||
- Route shell uses Ant Design layout/menu and keeps business views behind session checks.
|
||||
- Unauthenticated users are redirected to the central auth host OIDC start endpoint.
|
||||
- Session-expired responses are treated as an auth boundary concern and trigger revalidation before the UI prompts for login again.
|
||||
|
||||
## Runtime Base URLs
|
||||
|
||||
@ -16,10 +17,17 @@
|
||||
## Protected Workflow Endpoints
|
||||
|
||||
- `GET /api/kitchen/ops/board?contextId=...`
|
||||
- `POST /api/kitchen/ops/work-items/claim`
|
||||
- `POST /api/kitchen/ops/work-items/release`
|
||||
- `POST /api/kitchen/ops/work-items/transition`
|
||||
- `POST /api/kitchen/ops/board/priority`
|
||||
|
||||
## UI Workflow Coverage
|
||||
|
||||
- Kitchen board lookup
|
||||
- Kitchen priority updates
|
||||
- Protected route shell for board, priority update, and session inspection
|
||||
- Kitchen board lanes with work-item detail and station coverage
|
||||
- Board event feed derived from the loaded lane state and shared restaurant lifecycle progression
|
||||
- Claim, release, transition, and priority operator actions
|
||||
- Latest operator result and recent action history
|
||||
- Session-expired handling with reauthentication guidance
|
||||
- Protected route shell for board, operator actions, and session inspection
|
||||
- Kitchen transitions are presented as order progression toward floor handoff and payment eligibility.
|
||||
|
||||
@ -21,6 +21,14 @@ npm run dev
|
||||
- Login is executed via central Thalos OIDC start endpoint.
|
||||
- Business calls are gated behind session checks.
|
||||
- Session cookies are sent with `credentials: include`.
|
||||
- Workflow calls that return `401` trigger session revalidation and then guide the user back to central login.
|
||||
|
||||
## Available Screens
|
||||
|
||||
- `/board`: lane-based kitchen board and board event feed
|
||||
- `/board`: lane-based kitchen board tied to restaurant order progression and handoff readiness
|
||||
- `/actions`: claim, release, transition, and priority operator controls
|
||||
- `/session`: current Thalos session profile payload
|
||||
|
||||
## Build
|
||||
|
||||
|
||||
@ -15,10 +15,11 @@ npm run test:ci
|
||||
## Coverage Scope
|
||||
|
||||
- `src/api/client.test.ts`: runtime-config precedence and fallback behavior.
|
||||
- `src/api/dashboardApi.test.ts`: endpoint path/query composition and payload mapping.
|
||||
- `src/api/dashboardApi.test.ts`: endpoint path/query composition for board and operator action flows.
|
||||
- `src/auth/oidcLogin.test.ts`: OIDC start-url generation and safe return-url fallback.
|
||||
- `src/App.test.tsx`: central login screen, protected board flow, and priority update workflow.
|
||||
- `src/App.test.tsx`: central login screen, lifecycle-aware board loading, operator actions, and session-expired reauthentication guidance.
|
||||
|
||||
## Notes
|
||||
|
||||
- Use containerized Node execution when host `npm` is unavailable.
|
||||
- Prefer container-first validation before opening or updating runtime stack images.
|
||||
|
||||
115
src/App.test.tsx
115
src/App.test.tsx
@ -1,5 +1,6 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ApiError } from './api/client';
|
||||
|
||||
vi.mock('./api/sessionApi', () => ({
|
||||
getSessionMe: vi.fn(),
|
||||
@ -9,18 +10,30 @@ vi.mock('./api/sessionApi', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('./api/dashboardApi', () => ({
|
||||
claimKitchenWorkItem: vi.fn(),
|
||||
loadDashboard: vi.fn(),
|
||||
setKitchenOrderPriority: vi.fn()
|
||||
releaseKitchenWorkItem: vi.fn(),
|
||||
setKitchenOrderPriority: vi.fn(),
|
||||
transitionKitchenWorkItem: vi.fn()
|
||||
}));
|
||||
|
||||
import { loadDashboard, setKitchenOrderPriority } from './api/dashboardApi';
|
||||
import {
|
||||
claimKitchenWorkItem,
|
||||
loadDashboard,
|
||||
releaseKitchenWorkItem,
|
||||
setKitchenOrderPriority,
|
||||
transitionKitchenWorkItem
|
||||
} from './api/dashboardApi';
|
||||
import { getSessionMe } from './api/sessionApi';
|
||||
import App from './App';
|
||||
|
||||
describe('Kitchen Ops App', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(claimKitchenWorkItem).mockReset();
|
||||
vi.mocked(loadDashboard).mockReset();
|
||||
vi.mocked(releaseKitchenWorkItem).mockReset();
|
||||
vi.mocked(setKitchenOrderPriority).mockReset();
|
||||
vi.mocked(transitionKitchenWorkItem).mockReset();
|
||||
vi.mocked(getSessionMe).mockReset();
|
||||
window.__APP_CONFIG__ = {
|
||||
API_BASE_URL: 'http://localhost:8080',
|
||||
@ -42,14 +55,36 @@ describe('Kitchen Ops App', () => {
|
||||
expect(link.href).toContain('tenantId=demo-tenant');
|
||||
});
|
||||
|
||||
it('loads kitchen board for authenticated users', async () => {
|
||||
it('loads kitchen board with lanes and board events for authenticated users', async () => {
|
||||
vi.mocked(getSessionMe).mockResolvedValue({
|
||||
isAuthenticated: true,
|
||||
subjectId: 'demo-user',
|
||||
tenantId: 'demo-tenant',
|
||||
provider: 0
|
||||
provider: 2
|
||||
});
|
||||
vi.mocked(loadDashboard).mockResolvedValue({
|
||||
contextId: 'demo-context',
|
||||
summary: '2 active kitchen lanes',
|
||||
lanes: [
|
||||
{
|
||||
lane: 'Ready',
|
||||
items: [
|
||||
{
|
||||
workItemId: 'WORK-1',
|
||||
orderId: 'ORD-1',
|
||||
ticketId: 'TICK-1',
|
||||
tableId: 'T-01',
|
||||
station: 'Grill',
|
||||
state: 'Ready',
|
||||
priority: 2,
|
||||
claimedBy: 'chef-a',
|
||||
etaMinutes: 12
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
availableStations: ['Grill', 'Expo']
|
||||
});
|
||||
vi.mocked(loadDashboard).mockResolvedValue({ contextId: 'demo-context', summary: 'ready' });
|
||||
|
||||
render(<App />);
|
||||
|
||||
@ -57,29 +92,77 @@ describe('Kitchen Ops App', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Load Kitchen Board' }));
|
||||
|
||||
await waitFor(() => expect(loadDashboard).toHaveBeenCalledWith('demo-context'));
|
||||
expect(await screen.findByText('Lifecycle Stage: Ready')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Kitchen lanes are a live view of restaurant orders/)).toBeInTheDocument();
|
||||
expect(await screen.findByText('Ready: ORD-1 / TICK-1 at Grill is Ready and the order is ready for handoff to floor service')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates kitchen priority from action route', async () => {
|
||||
it('runs operator actions from the actions route', async () => {
|
||||
vi.mocked(getSessionMe).mockResolvedValue({
|
||||
isAuthenticated: true,
|
||||
subjectId: 'demo-user',
|
||||
tenantId: 'demo-tenant',
|
||||
provider: 0
|
||||
provider: 2
|
||||
});
|
||||
vi.mocked(setKitchenOrderPriority).mockResolvedValue({
|
||||
vi.mocked(claimKitchenWorkItem).mockResolvedValue({
|
||||
contextId: 'demo-context',
|
||||
orderId: 'ORD-2200',
|
||||
updated: true,
|
||||
summary: 'updated'
|
||||
workItemId: 'WORK-1',
|
||||
claimed: true,
|
||||
claimedBy: 'chef-a',
|
||||
message: 'claimed'
|
||||
});
|
||||
vi.mocked(transitionKitchenWorkItem).mockResolvedValue({
|
||||
orderId: 'ORD-1',
|
||||
ticketId: 'TICK-1',
|
||||
previousState: 'Cooking',
|
||||
currentState: 'Served',
|
||||
transitioned: true,
|
||||
error: null
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Set Priority')).toBeInTheDocument());
|
||||
fireEvent.click(screen.getByText('Set Priority'));
|
||||
fireEvent.change(screen.getByPlaceholderText('Order Id'), { target: { value: 'ORD-2200' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Set Kitchen Priority' }));
|
||||
await waitFor(() => expect(screen.getByText('Operator Actions')).toBeInTheDocument());
|
||||
fireEvent.click(screen.getByText('Operator Actions'));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Claim Work Item' }));
|
||||
|
||||
await waitFor(() => expect(setKitchenOrderPriority).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(claimKitchenWorkItem).toHaveBeenCalledTimes(1));
|
||||
expect(await screen.findAllByText('WORK-1')).toHaveLength(2);
|
||||
expect(screen.getAllByText('chef-a').length).toBeGreaterThan(0);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('Order Id'), { target: { value: 'ORD-1' } });
|
||||
fireEvent.change(screen.getAllByPlaceholderText('Context Id')[2], { target: { value: 'demo-context' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Transition Work Item' }));
|
||||
|
||||
await waitFor(() => expect(transitionKitchenWorkItem).toHaveBeenCalledTimes(1));
|
||||
expect(transitionKitchenWorkItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ contextId: 'demo-context', orderId: 'ORD-1' })
|
||||
);
|
||||
expect(await screen.findByText('Cooking -> Served (the order is complete and the check can move to payment)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows reauthentication guidance when board loading returns session expired', async () => {
|
||||
vi.mocked(getSessionMe)
|
||||
.mockResolvedValueOnce({
|
||||
isAuthenticated: true,
|
||||
subjectId: 'demo-user',
|
||||
tenantId: 'demo-tenant',
|
||||
provider: 2
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
isAuthenticated: false,
|
||||
subjectId: '',
|
||||
tenantId: '',
|
||||
provider: 2
|
||||
});
|
||||
vi.mocked(loadDashboard).mockRejectedValue(new ApiError(401, 'No active session.', 'session_missing', 'corr-1'));
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: 'Load Kitchen Board' })).toBeInTheDocument());
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Load Kitchen Board' }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Authentication Required')).toBeInTheDocument());
|
||||
expect(screen.getByText('Session expired')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
509
src/App.tsx
509
src/App.tsx
@ -1,15 +1,25 @@
|
||||
import { DeploymentUnitOutlined, OrderedListOutlined, ReloadOutlined, ToolOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
DeploymentUnitOutlined,
|
||||
OrderedListOutlined,
|
||||
ReloadOutlined,
|
||||
SyncOutlined,
|
||||
ToolOutlined
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Layout,
|
||||
List,
|
||||
Menu,
|
||||
Result,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
@ -18,25 +28,76 @@ import {
|
||||
} 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 {
|
||||
claimKitchenWorkItem,
|
||||
loadDashboard,
|
||||
releaseKitchenWorkItem,
|
||||
setKitchenOrderPriority,
|
||||
transitionKitchenWorkItem,
|
||||
type ClaimKitchenWorkItemRequest,
|
||||
type ClaimKitchenWorkItemResponse,
|
||||
type KitchenBoardItem,
|
||||
type KitchenOpsBoardResponse,
|
||||
type ReleaseKitchenWorkItemRequest,
|
||||
type ReleaseKitchenWorkItemResponse,
|
||||
type SetKitchenOrderPriorityRequest,
|
||||
type SetKitchenOrderPriorityResponse
|
||||
type SetKitchenOrderPriorityResponse,
|
||||
type TransitionKitchenWorkItemRequest,
|
||||
type TransitionKitchenWorkItemResponse
|
||||
} from './api/dashboardApi';
|
||||
import type { IdentityProvider } from './api/sessionApi';
|
||||
import { buildGoogleOidcStartUrl } from './auth/oidcLogin';
|
||||
import { SessionProvider, useSessionContext } from './auth/sessionContext';
|
||||
|
||||
type AppRoute = '/board' | '/priority' | '/session';
|
||||
type AppRoute = '/board' | '/actions' | '/session';
|
||||
|
||||
type WorkflowState = {
|
||||
error: string | null;
|
||||
sessionExpired: boolean;
|
||||
};
|
||||
|
||||
type ActionEvent =
|
||||
| { kind: 'claim'; response: ClaimKitchenWorkItemResponse }
|
||||
| { kind: 'release'; response: ReleaseKitchenWorkItemResponse }
|
||||
| { kind: 'transition'; response: TransitionKitchenWorkItemResponse }
|
||||
| { kind: 'priority'; response: SetKitchenOrderPriorityResponse };
|
||||
|
||||
const routeItems: Array<{ key: AppRoute; label: string; icon: ReactNode }> = [
|
||||
{ key: '/board', label: 'Kitchen Board', icon: <OrderedListOutlined /> },
|
||||
{ key: '/priority', label: 'Set Priority', icon: <ToolOutlined /> },
|
||||
{ key: '/actions', label: 'Operator Actions', icon: <ToolOutlined /> },
|
||||
{ key: '/session', label: 'Session', icon: <DeploymentUnitOutlined /> }
|
||||
];
|
||||
|
||||
const boardColumns = [
|
||||
{ title: 'Work Item', dataIndex: 'workItemId' },
|
||||
{ title: 'Order Id', dataIndex: 'orderId' },
|
||||
{ title: 'Ticket Id', dataIndex: 'ticketId' },
|
||||
{ title: 'Table', dataIndex: 'tableId' },
|
||||
{ title: 'Station', dataIndex: 'station' },
|
||||
{
|
||||
title: 'State',
|
||||
dataIndex: 'state',
|
||||
render: (value: string) => <Tag color={workflowTagColor(value)}>{value}</Tag>
|
||||
},
|
||||
{ title: 'Priority', dataIndex: 'priority' },
|
||||
{ title: 'Claimed By', render: (_: unknown, record: KitchenBoardItem) => record.claimedBy ?? 'Unclaimed' },
|
||||
{ title: 'ETA (min)', dataIndex: 'etaMinutes' }
|
||||
];
|
||||
|
||||
function getActionEventKey(event: ActionEvent) {
|
||||
switch (event.kind) {
|
||||
case 'claim':
|
||||
return `claim:${event.response.contextId}:${event.response.workItemId}:${event.response.claimedBy}`;
|
||||
case 'release':
|
||||
return `release:${event.response.contextId}:${event.response.workItemId}:${event.response.releasedBy}`;
|
||||
case 'transition':
|
||||
return `transition:${event.response.orderId}:${event.response.ticketId}:${event.response.currentState}`;
|
||||
case 'priority':
|
||||
return `priority:${event.response.contextId}:${event.response.workItemId}:${event.response.priority}`;
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<SessionProvider>
|
||||
@ -54,11 +115,14 @@ function KitchenOpsShell() {
|
||||
|
||||
const [contextId, setContextId] = useState('demo-context');
|
||||
const [boardPayload, setBoardPayload] = useState<KitchenOpsBoardResponse | null>(null);
|
||||
const [priorityResponse, setPriorityResponse] = useState<SetKitchenOrderPriorityResponse | null>(null);
|
||||
const [priorityHistory, setPriorityHistory] = useState<SetKitchenOrderPriorityResponse[]>([]);
|
||||
const [globalError, setGlobalError] = useState<string | null>(null);
|
||||
const [lastAction, setLastAction] = useState<ActionEvent | null>(null);
|
||||
const [actionHistory, setActionHistory] = useState<ActionEvent[]>([]);
|
||||
const [workflowState, setWorkflowState] = useState<WorkflowState>({ error: null, sessionExpired: false });
|
||||
const [loadingBoard, setLoadingBoard] = useState(false);
|
||||
const [submittingPriority, setSubmittingPriority] = useState(false);
|
||||
const [claimingItem, setClaimingItem] = useState(false);
|
||||
const [releasingItem, setReleasingItem] = useState(false);
|
||||
const [transitioningItem, setTransitioningItem] = useState(false);
|
||||
const [updatingPriority, setUpdatingPriority] = useState(false);
|
||||
|
||||
const loginUrl = useMemo(() => buildGoogleOidcStartUrl(window.location.href), []);
|
||||
const selectedKey = useMemo(() => {
|
||||
@ -66,32 +130,88 @@ function KitchenOpsShell() {
|
||||
return candidate?.key ?? '/board';
|
||||
}, [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 kitchen operations.', sessionExpired: true });
|
||||
await session.revalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
setWorkflowState({
|
||||
error: err instanceof Error ? err.message : fallbackMessage,
|
||||
sessionExpired: false
|
||||
});
|
||||
};
|
||||
|
||||
const pushActionHistory = (event: ActionEvent) => {
|
||||
setLastAction(event);
|
||||
setActionHistory((previous) => [event, ...previous].slice(0, 8));
|
||||
};
|
||||
|
||||
const loadBoard = async () => {
|
||||
setLoadingBoard(true);
|
||||
setGlobalError(null);
|
||||
clearWorkflowError();
|
||||
try {
|
||||
const payload = await loadDashboard(contextId);
|
||||
setBoardPayload(payload);
|
||||
} catch (err) {
|
||||
setGlobalError(err instanceof Error ? err.message : 'Failed to load kitchen board.');
|
||||
await handleWorkflowFailure(err, 'Failed to load kitchen board.');
|
||||
} finally {
|
||||
setLoadingBoard(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updatePriority = async (request: SetKitchenOrderPriorityRequest) => {
|
||||
setSubmittingPriority(true);
|
||||
setGlobalError(null);
|
||||
const executeClaim = async (request: ClaimKitchenWorkItemRequest) => {
|
||||
setClaimingItem(true);
|
||||
clearWorkflowError();
|
||||
try {
|
||||
const payload = await claimKitchenWorkItem(request);
|
||||
pushActionHistory({ kind: 'claim', response: payload });
|
||||
} catch (err) {
|
||||
await handleWorkflowFailure(err, 'Failed to claim kitchen work item.');
|
||||
} finally {
|
||||
setClaimingItem(false);
|
||||
}
|
||||
};
|
||||
|
||||
const executeRelease = async (request: ReleaseKitchenWorkItemRequest) => {
|
||||
setReleasingItem(true);
|
||||
clearWorkflowError();
|
||||
try {
|
||||
const payload = await releaseKitchenWorkItem(request);
|
||||
pushActionHistory({ kind: 'release', response: payload });
|
||||
} catch (err) {
|
||||
await handleWorkflowFailure(err, 'Failed to release kitchen work item.');
|
||||
} finally {
|
||||
setReleasingItem(false);
|
||||
}
|
||||
};
|
||||
|
||||
const executeTransition = async (request: TransitionKitchenWorkItemRequest) => {
|
||||
setTransitioningItem(true);
|
||||
clearWorkflowError();
|
||||
try {
|
||||
const payload = await transitionKitchenWorkItem(request);
|
||||
pushActionHistory({ kind: 'transition', response: payload });
|
||||
} catch (err) {
|
||||
await handleWorkflowFailure(err, 'Failed to transition kitchen work item.');
|
||||
} finally {
|
||||
setTransitioningItem(false);
|
||||
}
|
||||
};
|
||||
|
||||
const executePriorityUpdate = async (request: SetKitchenOrderPriorityRequest) => {
|
||||
setUpdatingPriority(true);
|
||||
clearWorkflowError();
|
||||
try {
|
||||
const payload = await setKitchenOrderPriority(request);
|
||||
setPriorityResponse(payload);
|
||||
// Keep recent responses bounded so the session view stays readable over long demos.
|
||||
setPriorityHistory((previous) => [payload, ...previous].slice(0, 8));
|
||||
pushActionHistory({ kind: 'priority', response: payload });
|
||||
} catch (err) {
|
||||
setGlobalError(err instanceof Error ? err.message : 'Failed to update kitchen order priority.');
|
||||
await handleWorkflowFailure(err, 'Failed to update kitchen order priority.');
|
||||
} finally {
|
||||
setSubmittingPriority(false);
|
||||
setUpdatingPriority(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -117,6 +237,15 @@ function KitchenOpsShell() {
|
||||
}
|
||||
/>
|
||||
{session.error && <Alert type="error" showIcon message={session.error} />}
|
||||
{workflowState.sessionExpired && (
|
||||
<Alert
|
||||
className="stack-gap"
|
||||
type="warning"
|
||||
showIcon
|
||||
message="Session expired"
|
||||
description="Your last kitchen workflow action returned 401, so the app is asking you to sign in again."
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -151,54 +280,153 @@ function KitchenOpsShell() {
|
||||
<Layout.Content className="content">
|
||||
<Typography.Title level={3}>Kitchen Operations</Typography.Title>
|
||||
<Typography.Paragraph type="secondary">
|
||||
Protected kitchen board workflows for board visibility and priority updates.
|
||||
Protected kitchen board workflows for board visibility, claim or release actions, transitions, and priority updates.
|
||||
</Typography.Paragraph>
|
||||
{session.error && <Alert className="stack-gap" type="warning" showIcon message={session.error} />}
|
||||
{globalError && <Alert className="stack-gap" type="error" showIcon message={globalError} />}
|
||||
{workflowState.error && (
|
||||
<Alert
|
||||
className="stack-gap"
|
||||
type={workflowState.sessionExpired ? 'warning' : 'error'}
|
||||
showIcon
|
||||
message={workflowState.error}
|
||||
action={
|
||||
workflowState.sessionExpired ? (
|
||||
<Button size="small" type="primary" href={loginUrl}>
|
||||
Reauthenticate
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Routes>
|
||||
<Route
|
||||
path="/board"
|
||||
element={
|
||||
<Card title="Kitchen Board">
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Space wrap>
|
||||
<Input
|
||||
value={contextId}
|
||||
onChange={(event) => setContextId(event.target.value)}
|
||||
placeholder="Context Id"
|
||||
style={{ width: 280 }}
|
||||
/>
|
||||
<Button type="primary" loading={loadingBoard} onClick={() => void loadBoard()}>
|
||||
Load Kitchen Board
|
||||
</Button>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Card title="Kitchen Board">
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Space wrap>
|
||||
<Input
|
||||
value={contextId}
|
||||
onChange={(event) => setContextId(event.target.value)}
|
||||
placeholder="Context Id"
|
||||
style={{ width: 280 }}
|
||||
/>
|
||||
<Button type="primary" loading={loadingBoard} onClick={() => void loadBoard()}>
|
||||
Load Kitchen Board
|
||||
</Button>
|
||||
<Button icon={<SyncOutlined />} onClick={() => void loadBoard()} disabled={loadingBoard}>
|
||||
Retry
|
||||
</Button>
|
||||
</Space>
|
||||
{boardPayload ? (
|
||||
<>
|
||||
<Descriptions bordered size="small" column={1}>
|
||||
<Descriptions.Item label="Context Id">{boardPayload.contextId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Summary">{boardPayload.summary}</Descriptions.Item>
|
||||
<Descriptions.Item label="Stations">{boardPayload.availableStations.join(', ')}</Descriptions.Item>
|
||||
<Descriptions.Item label="Lifecycle Note">
|
||||
Kitchen lanes are a live view of restaurant orders moving from accepted work into preparation,
|
||||
handoff readiness, and final service completion.
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{boardPayload.lanes.map((lane) => (
|
||||
<Card
|
||||
key={lane.lane}
|
||||
type="inner"
|
||||
title={`Lifecycle Stage: ${lane.lane}`}
|
||||
extra={<Tag color={workflowTagColor(lane.lane)}>{lane.lane}</Tag>}
|
||||
>
|
||||
<Table<KitchenBoardItem>
|
||||
pagination={false}
|
||||
rowKey={(record) => record.workItemId}
|
||||
dataSource={lane.items}
|
||||
columns={boardColumns}
|
||||
locale={{ emptyText: 'No work items in this lane.' }}
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<Empty description="Load a context to inspect kitchen board lanes and stations." />
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
<Card title="Board Events" extra={<ClockCircleOutlined />}>
|
||||
{boardPayload ? (
|
||||
<Descriptions bordered size="small" column={1}>
|
||||
<Descriptions.Item label="Context Id">{boardPayload.contextId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Summary">{boardPayload.summary}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<List
|
||||
bordered
|
||||
dataSource={boardPayload.lanes.flatMap((lane) =>
|
||||
lane.items.map(
|
||||
(item) =>
|
||||
`${lane.lane}: ${item.orderId} / ${item.ticketId} at ${item.station} is ${item.state} and ${workflowProgressHint(item.state)}`
|
||||
)
|
||||
)}
|
||||
renderItem={(item) => <List.Item>{item}</List.Item>}
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text type="secondary">No kitchen board payload loaded.</Typography.Text>
|
||||
<Empty description="Board events populate after loading kitchen board data." />
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</Card>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/priority"
|
||||
path="/actions"
|
||||
element={
|
||||
<Card title="Set Kitchen Priority">
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Form<SetKitchenOrderPriorityRequest>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Card title="Claim Work Item">
|
||||
<Form<ClaimKitchenWorkItemRequest>
|
||||
layout="vertical"
|
||||
initialValues={{ contextId, workItemId: 'WORK-1001', claimedBy: 'chef-a' }}
|
||||
onFinish={(values) => void executeClaim(values)}
|
||||
>
|
||||
<Form.Item name="contextId" label="Context Id" rules={[{ required: true }]}>
|
||||
<Input placeholder="Context Id" />
|
||||
</Form.Item>
|
||||
<Form.Item name="workItemId" label="Work Item Id" rules={[{ required: true }]}>
|
||||
<Input placeholder="Work Item Id" />
|
||||
</Form.Item>
|
||||
<Form.Item name="claimedBy" label="Claimed By" rules={[{ required: true }]}>
|
||||
<Input placeholder="Operator Id" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={claimingItem}>
|
||||
Claim Work Item
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
<Card title="Release Work Item">
|
||||
<Form<ReleaseKitchenWorkItemRequest>
|
||||
layout="vertical"
|
||||
initialValues={{ contextId, workItemId: 'WORK-1001', releasedBy: 'chef-a' }}
|
||||
onFinish={(values) => void executeRelease(values)}
|
||||
>
|
||||
<Form.Item name="contextId" label="Context Id" rules={[{ required: true }]}>
|
||||
<Input placeholder="Context Id" />
|
||||
</Form.Item>
|
||||
<Form.Item name="workItemId" label="Work Item Id" rules={[{ required: true }]}>
|
||||
<Input placeholder="Work Item Id" />
|
||||
</Form.Item>
|
||||
<Form.Item name="releasedBy" label="Released By" rules={[{ required: true }]}>
|
||||
<Input placeholder="Operator Id" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={releasingItem}>
|
||||
Release Work Item
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
<Card title="Transition Work Item State">
|
||||
<Form<TransitionKitchenWorkItemRequest>
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
contextId,
|
||||
orderId: 'ORD-1001',
|
||||
priority: 1,
|
||||
updatedBy: 'kitchen-supervisor'
|
||||
ticketId: 'TICK-1001',
|
||||
targetState: 'Ready',
|
||||
updatedBy: 'chef-a'
|
||||
}}
|
||||
onFinish={(values) => void updatePriority(values)}
|
||||
onFinish={(values) => void executeTransition(values)}
|
||||
>
|
||||
<Form.Item name="contextId" label="Context Id" rules={[{ required: true }]}>
|
||||
<Input placeholder="Context Id" />
|
||||
@ -206,40 +434,67 @@ function KitchenOpsShell() {
|
||||
<Form.Item name="orderId" label="Order Id" rules={[{ required: true }]}>
|
||||
<Input placeholder="Order Id" />
|
||||
</Form.Item>
|
||||
<Form.Item name="ticketId" label="Ticket Id" rules={[{ required: true }]}>
|
||||
<Input placeholder="Ticket Id" />
|
||||
</Form.Item>
|
||||
<Form.Item name="targetState" label="Target State" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Queued', value: 'Queued' },
|
||||
{ label: 'Cooking', value: 'Cooking' },
|
||||
{ label: 'Ready', value: 'Ready' },
|
||||
{ label: 'Served', value: 'Served' }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="updatedBy" label="Updated By" rules={[{ required: true }]}>
|
||||
<Input placeholder="Operator Id" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={transitioningItem}>
|
||||
Transition Work Item
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
<Card title="Set Kitchen Priority">
|
||||
<Form<SetKitchenOrderPriorityRequest>
|
||||
layout="vertical"
|
||||
initialValues={{ contextId, workItemId: 'WORK-1001', priority: 1, updatedBy: 'kitchen-supervisor' }}
|
||||
onFinish={(values) => void executePriorityUpdate(values)}
|
||||
>
|
||||
<Form.Item name="contextId" label="Context Id" rules={[{ required: true }]}>
|
||||
<Input placeholder="Context Id" />
|
||||
</Form.Item>
|
||||
<Form.Item name="workItemId" label="Work Item Id" rules={[{ required: true }]}>
|
||||
<Input placeholder="Work Item Id" />
|
||||
</Form.Item>
|
||||
<Form.Item name="priority" label="Priority" rules={[{ required: true, type: 'number', min: 0 }]}>
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="updatedBy" label="Updated By" rules={[{ required: true }]}>
|
||||
<Input placeholder="Operator Id" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={submittingPriority}>
|
||||
<Button type="primary" htmlType="submit" loading={updatingPriority}>
|
||||
Set Kitchen Priority
|
||||
</Button>
|
||||
</Form>
|
||||
{priorityResponse && (
|
||||
<Descriptions bordered size="small" column={1}>
|
||||
<Descriptions.Item label="Context Id">{priorityResponse.contextId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Order Id">{priorityResponse.orderId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Updated">{String(priorityResponse.updated)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Summary">{priorityResponse.summary}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
<Table<SetKitchenOrderPriorityResponse>
|
||||
</Card>
|
||||
<Card title="Latest Operator Result">
|
||||
{lastAction ? <ActionDescriptions event={lastAction} /> : <Empty description="Run a kitchen action to inspect the latest response." />}
|
||||
</Card>
|
||||
<Card title="Recent Operator Actions">
|
||||
<Table<ActionEvent>
|
||||
pagination={false}
|
||||
rowKey={(record) => `${record.contextId}-${record.orderId}`}
|
||||
dataSource={priorityHistory}
|
||||
rowKey={(record) => getActionEventKey(record)}
|
||||
dataSource={actionHistory}
|
||||
locale={{ emptyText: 'No recent kitchen actions yet.' }}
|
||||
columns={[
|
||||
{ title: 'Order Id', dataIndex: 'orderId' },
|
||||
{ title: 'Context Id', dataIndex: 'contextId' },
|
||||
{
|
||||
title: 'Updated',
|
||||
render: (_, record) => <Tag color={record.updated ? 'green' : 'red'}>{String(record.updated)}</Tag>
|
||||
},
|
||||
{ title: 'Summary', dataIndex: 'summary' }
|
||||
{ title: 'Action', render: (_, record) => <Tag>{record.kind}</Tag> },
|
||||
{ title: 'Reference', render: (_, record) => getActionReference(record) },
|
||||
{ title: 'Summary', render: (_, record) => getActionSummary(record) }
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
</Card>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
@ -259,6 +514,126 @@ function KitchenOpsShell() {
|
||||
);
|
||||
}
|
||||
|
||||
function ActionDescriptions({ event }: { event: ActionEvent }) {
|
||||
if (event.kind === 'claim') {
|
||||
return (
|
||||
<Descriptions bordered size="small" column={1}>
|
||||
<Descriptions.Item label="Context Id">{event.response.contextId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Work Item Id">{event.response.workItemId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Claimed">{String(event.response.claimed)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Claimed By">{event.response.claimedBy}</Descriptions.Item>
|
||||
<Descriptions.Item label="Message">{event.response.message}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
);
|
||||
}
|
||||
|
||||
if (event.kind === 'release') {
|
||||
return (
|
||||
<Descriptions bordered size="small" column={1}>
|
||||
<Descriptions.Item label="Context Id">{event.response.contextId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Work Item Id">{event.response.workItemId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Released">{String(event.response.released)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Released By">{event.response.releasedBy}</Descriptions.Item>
|
||||
<Descriptions.Item label="Message">{event.response.message}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
);
|
||||
}
|
||||
|
||||
if (event.kind === 'transition') {
|
||||
return (
|
||||
<Descriptions bordered size="small" column={1}>
|
||||
<Descriptions.Item label="Order Id">{event.response.orderId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Ticket Id">{event.response.ticketId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Previous State">
|
||||
<Tag color={workflowTagColor(event.response.previousState)}>{event.response.previousState}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Current State">
|
||||
<Tag color={workflowTagColor(event.response.currentState)}>{event.response.currentState}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Transitioned">{String(event.response.transitioned)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Next Step">{workflowProgressHint(event.response.currentState)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Error">{event.response.error ?? 'None'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Descriptions bordered size="small" column={1}>
|
||||
<Descriptions.Item label="Context Id">{event.response.contextId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Work Item Id">{event.response.workItemId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Updated">{String(event.response.updated)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Priority">{event.response.priority}</Descriptions.Item>
|
||||
<Descriptions.Item label="Summary">{event.response.summary}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
);
|
||||
}
|
||||
|
||||
function getActionReference(event: ActionEvent): string {
|
||||
if (event.kind === 'transition') {
|
||||
return `${event.response.orderId} / ${event.response.ticketId}`;
|
||||
}
|
||||
|
||||
return event.response.workItemId;
|
||||
}
|
||||
|
||||
function getActionSummary(event: ActionEvent): string {
|
||||
if (event.kind === 'claim' || event.kind === 'release') {
|
||||
return event.response.message;
|
||||
}
|
||||
|
||||
if (event.kind === 'transition') {
|
||||
return `${event.response.previousState} -> ${event.response.currentState} (${workflowProgressHint(event.response.currentState)})`;
|
||||
}
|
||||
|
||||
return event.response.summary;
|
||||
}
|
||||
|
||||
function workflowTagColor(state: string): string {
|
||||
switch (state.toLowerCase()) {
|
||||
case 'queued':
|
||||
case 'accepted':
|
||||
return 'blue';
|
||||
case 'cooking':
|
||||
case 'preparing':
|
||||
return 'orange';
|
||||
case 'ready':
|
||||
case 'readyforpickup':
|
||||
return 'cyan';
|
||||
case 'served':
|
||||
case 'delivered':
|
||||
return 'green';
|
||||
case 'paid':
|
||||
return 'purple';
|
||||
case 'blocked':
|
||||
case 'failed':
|
||||
case 'canceled':
|
||||
return 'red';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
function workflowProgressHint(state: string): string {
|
||||
switch (state.toLowerCase()) {
|
||||
case 'queued':
|
||||
case 'accepted':
|
||||
return 'waiting for a kitchen operator to start preparation';
|
||||
case 'cooking':
|
||||
case 'preparing':
|
||||
return 'the kitchen is actively preparing this order';
|
||||
case 'ready':
|
||||
case 'readyforpickup':
|
||||
return 'the order is ready for handoff to floor service';
|
||||
case 'served':
|
||||
case 'delivered':
|
||||
return 'the order is complete and the check can move to payment';
|
||||
case 'paid':
|
||||
return 'the restaurant workflow is fully closed';
|
||||
default:
|
||||
return 'the kitchen workflow is still progressing';
|
||||
}
|
||||
}
|
||||
|
||||
function providerLabel(provider: IdentityProvider): string {
|
||||
if (provider === 0 || provider === '0' || provider === 'InternalJwt') {
|
||||
return 'Internal JWT';
|
||||
|
||||
@ -6,7 +6,13 @@ vi.mock('./client', () => ({
|
||||
}));
|
||||
|
||||
import { getJson, postJson } from './client';
|
||||
import { loadDashboard, setKitchenOrderPriority } from './dashboardApi';
|
||||
import {
|
||||
claimKitchenWorkItem,
|
||||
loadDashboard,
|
||||
releaseKitchenWorkItem,
|
||||
setKitchenOrderPriority,
|
||||
transitionKitchenWorkItem
|
||||
} from './dashboardApi';
|
||||
|
||||
describe('kitchen ops dashboard api', () => {
|
||||
it('builds encoded board endpoint path', async () => {
|
||||
@ -17,19 +23,69 @@ describe('kitchen ops dashboard api', () => {
|
||||
expect(getJson).toHaveBeenCalledWith('/api/kitchen/ops/board?contextId=ctx%20kitchen%2F1');
|
||||
});
|
||||
|
||||
it('posts claim payload', async () => {
|
||||
vi.mocked(postJson).mockResolvedValue({ claimed: true });
|
||||
|
||||
await claimKitchenWorkItem({
|
||||
contextId: 'ctx',
|
||||
workItemId: 'WORK-1',
|
||||
claimedBy: 'chef'
|
||||
});
|
||||
|
||||
expect(postJson).toHaveBeenCalledWith('/api/kitchen/ops/work-items/claim', {
|
||||
contextId: 'ctx',
|
||||
workItemId: 'WORK-1',
|
||||
claimedBy: 'chef'
|
||||
});
|
||||
});
|
||||
|
||||
it('posts release payload', async () => {
|
||||
vi.mocked(postJson).mockResolvedValue({ released: true });
|
||||
|
||||
await releaseKitchenWorkItem({
|
||||
contextId: 'ctx',
|
||||
workItemId: 'WORK-1',
|
||||
releasedBy: 'chef'
|
||||
});
|
||||
|
||||
expect(postJson).toHaveBeenCalledWith('/api/kitchen/ops/work-items/release', {
|
||||
contextId: 'ctx',
|
||||
workItemId: 'WORK-1',
|
||||
releasedBy: 'chef'
|
||||
});
|
||||
});
|
||||
|
||||
it('posts transition payload', async () => {
|
||||
vi.mocked(postJson).mockResolvedValue({ transitioned: true });
|
||||
|
||||
await transitionKitchenWorkItem({
|
||||
orderId: 'ORD-1',
|
||||
ticketId: 'TICK-1',
|
||||
targetState: 'Ready',
|
||||
updatedBy: 'chef'
|
||||
});
|
||||
|
||||
expect(postJson).toHaveBeenCalledWith('/api/kitchen/ops/work-items/transition', {
|
||||
orderId: 'ORD-1',
|
||||
ticketId: 'TICK-1',
|
||||
targetState: 'Ready',
|
||||
updatedBy: 'chef'
|
||||
});
|
||||
});
|
||||
|
||||
it('posts priority update payload', async () => {
|
||||
vi.mocked(postJson).mockResolvedValue({ updated: true });
|
||||
|
||||
await setKitchenOrderPriority({
|
||||
contextId: 'ctx',
|
||||
orderId: 'ORD-1',
|
||||
workItemId: 'WORK-1',
|
||||
priority: 2,
|
||||
updatedBy: 'chef'
|
||||
});
|
||||
|
||||
expect(postJson).toHaveBeenCalledWith('/api/kitchen/ops/board/priority', {
|
||||
contextId: 'ctx',
|
||||
orderId: 'ORD-1',
|
||||
workItemId: 'WORK-1',
|
||||
priority: 2,
|
||||
updatedBy: 'chef'
|
||||
});
|
||||
|
||||
@ -1,21 +1,86 @@
|
||||
import { getJson, postJson } from './client';
|
||||
|
||||
export type KitchenBoardItem = {
|
||||
workItemId: string;
|
||||
orderId: string;
|
||||
ticketId: string;
|
||||
tableId: string;
|
||||
station: string;
|
||||
state: string;
|
||||
priority: number;
|
||||
claimedBy: string | null;
|
||||
etaMinutes: number;
|
||||
};
|
||||
|
||||
export type KitchenBoardLane = {
|
||||
lane: string;
|
||||
items: KitchenBoardItem[];
|
||||
};
|
||||
|
||||
export type KitchenOpsBoardResponse = {
|
||||
contextId: string;
|
||||
summary: string;
|
||||
lanes: KitchenBoardLane[];
|
||||
availableStations: string[];
|
||||
};
|
||||
|
||||
export type ClaimKitchenWorkItemRequest = {
|
||||
contextId: string;
|
||||
workItemId: string;
|
||||
claimedBy: string;
|
||||
};
|
||||
|
||||
export type ClaimKitchenWorkItemResponse = {
|
||||
contextId: string;
|
||||
workItemId: string;
|
||||
claimed: boolean;
|
||||
claimedBy: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type ReleaseKitchenWorkItemRequest = {
|
||||
contextId: string;
|
||||
workItemId: string;
|
||||
releasedBy: string;
|
||||
};
|
||||
|
||||
export type ReleaseKitchenWorkItemResponse = {
|
||||
contextId: string;
|
||||
workItemId: string;
|
||||
released: boolean;
|
||||
releasedBy: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type TransitionKitchenWorkItemRequest = {
|
||||
orderId: string;
|
||||
ticketId: string;
|
||||
targetState: string;
|
||||
updatedBy: string;
|
||||
contextId?: string;
|
||||
};
|
||||
|
||||
export type TransitionKitchenWorkItemResponse = {
|
||||
orderId: string;
|
||||
ticketId: string;
|
||||
previousState: string;
|
||||
currentState: string;
|
||||
transitioned: boolean;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
export type SetKitchenOrderPriorityRequest = {
|
||||
contextId: string;
|
||||
orderId: string;
|
||||
workItemId: string;
|
||||
priority: number;
|
||||
updatedBy: string;
|
||||
};
|
||||
|
||||
export type SetKitchenOrderPriorityResponse = {
|
||||
contextId: string;
|
||||
orderId: string;
|
||||
workItemId: string;
|
||||
updated: boolean;
|
||||
priority: number;
|
||||
summary: string;
|
||||
};
|
||||
|
||||
@ -23,6 +88,24 @@ export async function loadDashboard(contextId: string): Promise<KitchenOpsBoardR
|
||||
return getJson<KitchenOpsBoardResponse>(`/api/kitchen/ops/board?contextId=${encodeURIComponent(contextId)}`);
|
||||
}
|
||||
|
||||
export async function claimKitchenWorkItem(
|
||||
request: ClaimKitchenWorkItemRequest
|
||||
): Promise<ClaimKitchenWorkItemResponse> {
|
||||
return postJson<ClaimKitchenWorkItemResponse>('/api/kitchen/ops/work-items/claim', request);
|
||||
}
|
||||
|
||||
export async function releaseKitchenWorkItem(
|
||||
request: ReleaseKitchenWorkItemRequest
|
||||
): Promise<ReleaseKitchenWorkItemResponse> {
|
||||
return postJson<ReleaseKitchenWorkItemResponse>('/api/kitchen/ops/work-items/release', request);
|
||||
}
|
||||
|
||||
export async function transitionKitchenWorkItem(
|
||||
request: TransitionKitchenWorkItemRequest
|
||||
): Promise<TransitionKitchenWorkItemResponse> {
|
||||
return postJson<TransitionKitchenWorkItemResponse>('/api/kitchen/ops/work-items/transition', request);
|
||||
}
|
||||
|
||||
export async function setKitchenOrderPriority(
|
||||
request: SetKitchenOrderPriorityRequest
|
||||
): Promise<SetKitchenOrderPriorityResponse> {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user