import { CreditCardOutlined, DeploymentUnitOutlined, ReloadOutlined, SearchOutlined, WalletOutlined } from '@ant-design/icons'; import { Alert, Button, Card, Descriptions, Empty, Form, Input, InputNumber, Layout, 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 { capturePosPayment, loadDashboard, loadRecentPayments, loadTransactionDetail, type CapturePosPaymentRequest, type CapturePosPaymentResponse, type PosPaymentActivity, type PosTransactionDetailResponse, type PosTransactionSummaryResponse, type RecentPosPaymentsResponse } from './api/dashboardApi'; import type { IdentityProvider } from './api/sessionApi'; import { buildGoogleOidcStartUrl } from './auth/oidcLogin'; import { SessionProvider, useSessionContext } from './auth/sessionContext'; type AppRoute = '/summary' | '/payments' | '/session'; type WorkflowState = { error: string | null; sessionExpired: boolean; }; type PaymentAttempt = { request: CapturePosPaymentRequest; response: CapturePosPaymentResponse; }; const routeItems: Array<{ key: AppRoute; label: string; icon: ReactNode }> = [ { key: '/summary', label: 'Transaction Summary', icon: }, { key: '/payments', label: 'Capture Payment', icon: }, { key: '/session', label: 'Session', icon: } ]; const paymentColumns = [ { title: 'Transaction Id', dataIndex: 'transactionId' }, { title: 'Method', dataIndex: 'paymentMethod' }, { title: 'Amount', render: (_: unknown, record: PosPaymentActivity) => `${record.amount.toFixed(2)} ${record.currency}` }, { title: 'Status', render: (_: unknown, record: PosPaymentActivity) => ( {record.status} ) }, { title: 'Lifecycle Step', render: (_: unknown, record: PosPaymentActivity) => paymentProgressHint(record.status) }, { title: 'Captured At', render: (_: unknown, record: PosPaymentActivity) => formatUtc(record.capturedAtUtc) } ]; function App() { return ( ); } function PosTransactionsShell() { const session = useSessionContext(); const location = useLocation(); const navigate = useNavigate(); const [contextId, setContextId] = useState('demo-context'); const [transactionId, setTransactionId] = useState('POS-9001'); const [summaryPayload, setSummaryPayload] = useState(null); const [detailPayload, setDetailPayload] = useState(null); const [recentPayload, setRecentPayload] = useState(null); const [paymentResponse, setPaymentResponse] = useState(null); const [paymentHistory, setPaymentHistory] = useState([]); const [lastCaptureRequest, setLastCaptureRequest] = useState(null); const [workflowState, setWorkflowState] = useState({ error: null, sessionExpired: false }); const [loadingSummary, setLoadingSummary] = useState(false); const [loadingDetail, setLoadingDetail] = useState(false); const [loadingRecentPayments, setLoadingRecentPayments] = useState(false); const [submittingPayment, setSubmittingPayment] = 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 ?? '/summary'; }, [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 POS operations.', sessionExpired: true }); await session.revalidate(); return; } setWorkflowState({ error: err instanceof Error ? err.message : fallbackMessage, sessionExpired: false }); }; const loadSummary = async () => { setLoadingSummary(true); clearWorkflowError(); try { const payload = await loadDashboard(contextId); setSummaryPayload(payload); } catch (err) { await handleWorkflowFailure(err, 'Failed to load POS summary.'); } finally { setLoadingSummary(false); } }; const loadDetail = async () => { setLoadingDetail(true); clearWorkflowError(); try { const payload = await loadTransactionDetail(contextId, transactionId); setDetailPayload(payload); } catch (err) { await handleWorkflowFailure(err, 'Failed to load transaction detail.'); } finally { setLoadingDetail(false); } }; const refreshRecentPayments = async () => { setLoadingRecentPayments(true); clearWorkflowError(); try { const payload = await loadRecentPayments(contextId); setRecentPayload(payload); } catch (err) { await handleWorkflowFailure(err, 'Failed to load recent payments.'); } finally { setLoadingRecentPayments(false); } }; const submitCapturePayment = async (request: CapturePosPaymentRequest) => { setSubmittingPayment(true); clearWorkflowError(); setLastCaptureRequest(request); try { const payload = await capturePosPayment(request); setPaymentResponse(payload); setPaymentHistory((previous) => [{ request, response: payload }, ...previous].slice(0, 8)); await refreshRecentPayments(); await loadSummary(); await loadDetail(); } catch (err) { await handleWorkflowFailure(err, 'Failed to capture payment.'); } finally { setSubmittingPayment(false); } }; if (session.status === 'loading') { return (
); } if (session.status !== 'authenticated' || !session.profile) { return (
Continue with Google } /> {workflowState.sessionExpired && ( )} {session.error && }
); } return (
POS Transactions Web
navigate(event.key as AppRoute)} /> subject: {session.profile.subjectId} tenant: {session.profile.tenantId} provider: {providerLabel(session.profile.provider)} POS Transactions Protected POS workflows for payable restaurant checks, capture retries, and recent payment activity once service is complete. {session.error && } {workflowState.error && } {workflowState.sessionExpired && ( } /> )} setContextId(event.target.value)} placeholder="Context Id" style={{ width: 260 }} /> setTransactionId(event.target.value)} placeholder="Transaction Id" style={{ width: 260 }} /> {summaryPayload ? ( {summaryPayload.contextId} {summaryPayload.summary} {summaryPayload.openBalance.toFixed(2)} {summaryPayload.currency} Only restaurant orders that have completed service should appear here as payable checks for capture. ) : ( )} {detailPayload?.transaction ? ( {detailPayload.contextId} {detailPayload.summary} {detailPayload.transaction.transactionId} {detailPayload.transaction.paymentMethod} {detailPayload.transaction.amount.toFixed(2)} {detailPayload.transaction.currency} {detailPayload.transaction.status} {paymentProgressHint(detailPayload.transaction.status)} {formatUtc(detailPayload.transaction.capturedAtUtc)} ) : ( )} {recentPayload?.recentPayments.length ? ( pagination={false} rowKey={(record) => `${record.transactionId}-${record.capturedAtUtc}`} dataSource={recentPayload.recentPayments} columns={paymentColumns} /> ) : ( )} } /> layout="vertical" initialValues={{ contextId, transactionId: 'POS-9001', amount: 25.5, currency: 'USD', paymentMethod: 'card' }} onFinish={(values) => void submitCapturePayment(values)} > Capture should be used only after kitchen and floor service complete the order, making the check payable. {paymentResponse ? ( {paymentResponse.contextId} {paymentResponse.transactionId} {String(paymentResponse.succeeded)} {paymentResponse.status} {paymentProgressHint(paymentResponse.status)} {formatUtc(paymentResponse.capturedAtUtc)} {paymentResponse.summary} ) : ( )} pagination={false} rowKey={(record) => `${record.response.transactionId}-${record.response.capturedAtUtc}`} dataSource={paymentHistory} locale={{ emptyText: 'No capture attempts recorded in this session.' }} columns={[ { title: 'Transaction Id', render: (_, record) => record.response.transactionId }, { title: 'Method', render: (_, record) => record.request.paymentMethod }, { title: 'Amount', render: (_, record) => `${record.request.amount.toFixed(2)} ${record.request.currency}` }, { title: 'Status', render: (_, record) => ( {record.response.status} ) }, { title: 'Summary', render: (_, record) => record.response.summary }, { title: 'Lifecycle Step', render: (_, record) => paymentProgressHint(record.response.status) } ]} /> } />
{JSON.stringify(session.profile, null, 2)}
} /> } /> } />
); } function providerLabel(provider: IdentityProvider) { switch (provider) { case 1: return 'manual'; case 2: return 'google'; default: return 'unknown'; } } function paymentStatusColor(status: string) { switch (status.toLowerCase()) { case 'captured': case 'paid': return 'green'; case 'payable': case 'ready': return 'blue'; case 'pending': case 'authorized': return 'orange'; case 'failed': case 'blocked': return 'red'; default: return 'default'; } } function paymentProgressHint(status: string) { switch (status.toLowerCase()) { case 'payable': case 'ready': return 'ready for payment capture after service completion'; case 'pending': case 'authorized': return 'payment is in progress and should be monitored before retrying'; case 'captured': case 'paid': return 'payment is complete and the restaurant workflow can close'; case 'failed': case 'blocked': return 'operator review is required before attempting another capture'; default: return 'payment state is being resolved against the restaurant lifecycle'; } } function formatUtc(value: string) { return new Date(value).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short', timeZone: 'UTC' }); } export default App;