pos-transactions-web/src/App.tsx
2026-03-08 16:23:00 -06:00

268 lines
8.6 KiB
TypeScript

import { useState } from 'react';
import {
capturePosPayment,
loadDashboard,
type CapturePosPaymentRequest,
type CapturePosPaymentResponse,
type PosTransactionSummaryResponse
} from './api/dashboardApi';
import { SessionProvider, useSessionContext } from './auth/sessionContext';
import type { IdentityProvider } from './api/sessionApi';
type RouteKey = 'overview' | 'actions';
function App() {
return (
<SessionProvider>
<PosTransactionsShell />
</SessionProvider>
);
}
function PosTransactionsShell() {
const session = useSessionContext();
const [route, setRoute] = useState<RouteKey>('overview');
const [contextId, setContextId] = useState('demo-context');
const [summaryPayload, setSummaryPayload] = useState<PosTransactionSummaryResponse | null>(null);
const [paymentRequest, setPaymentRequest] = useState<CapturePosPaymentRequest>({
contextId: 'demo-context',
transactionId: 'POS-9001',
amount: 25.5,
currency: 'USD',
paymentMethod: 'card'
});
const [paymentResponse, setPaymentResponse] = useState<CapturePosPaymentResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const loadSummary = async () => {
setLoading(true);
setError(null);
try {
const payload = await loadDashboard(contextId);
setSummaryPayload(payload);
setPaymentRequest((previous) => ({ ...previous, contextId }));
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load transaction summary.');
} finally {
setLoading(false);
}
};
const capturePayment = async () => {
setLoading(true);
setError(null);
try {
const payload = await capturePosPayment(paymentRequest);
setPaymentResponse(payload);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to capture payment.');
} finally {
setLoading(false);
}
};
if (session.status === 'loading') {
return (
<main className="app">
<h1>POS Transactions Web</h1>
<p className="muted">Restoring session...</p>
</main>
);
}
if (session.status !== 'authenticated' || !session.profile) {
return (
<main className="app">
<h1>POS Transactions Web</h1>
<p className="muted">Sign in with Thalos to access protected routes.</p>
{session.error && <div className="alert">{session.error}</div>}
<LoginCard />
</main>
);
}
return (
<main className="app">
<h1>POS Transactions Web</h1>
<p className="muted">Transaction summary and payment capture MVP workflows.</p>
<section className="card row">
<span className="badge">subject: {session.profile.subjectId}</span>
<span className="badge">tenant: {session.profile.tenantId}</span>
<span className="badge">provider: {providerLabel(session.profile.provider)}</span>
<span className="spacer" />
<button type="button" className="secondary" onClick={() => void session.refresh()}>
Refresh Session
</button>
<button type="button" className="warn" onClick={() => void session.logout()}>
Logout
</button>
</section>
<section className="card tabs" aria-label="route-shell">
<button
type="button"
className={route === 'overview' ? 'active' : undefined}
onClick={() => setRoute('overview')}
>
Summary
</button>
<button
type="button"
className={route === 'actions' ? 'active' : undefined}
onClick={() => setRoute('actions')}
>
Capture Payment
</button>
</section>
{session.error && <div className="alert">{session.error}</div>}
{error && <div className="alert">{error}</div>}
{route === 'overview' && (
<section className="card col">
<div className="row">
<label className="col">
Context Id
<input value={contextId} onChange={(event) => setContextId(event.target.value)} />
</label>
<button type="button" onClick={() => void loadSummary()} disabled={loading}>
{loading ? 'Loading...' : 'Load Summary'}
</button>
</div>
<pre>{JSON.stringify(summaryPayload, null, 2)}</pre>
</section>
)}
{route === 'actions' && (
<section className="card col">
<div className="grid">
<label className="col">
Context Id
<input
value={paymentRequest.contextId}
onChange={(event) => setPaymentRequest((previous) => ({ ...previous, contextId: event.target.value }))}
/>
</label>
<label className="col">
Transaction Id
<input
value={paymentRequest.transactionId}
onChange={(event) => setPaymentRequest((previous) => ({ ...previous, transactionId: event.target.value }))}
/>
</label>
<label className="col">
Amount
<input
type="number"
min={0}
step="0.01"
value={paymentRequest.amount}
onChange={(event) =>
setPaymentRequest((previous) => ({
...previous,
amount: Math.max(0, Number(event.target.value) || 0)
}))
}
/>
</label>
<label className="col">
Currency
<input
value={paymentRequest.currency}
onChange={(event) => setPaymentRequest((previous) => ({ ...previous, currency: event.target.value.toUpperCase() }))}
/>
</label>
<label className="col">
Payment Method
<input
value={paymentRequest.paymentMethod}
onChange={(event) => setPaymentRequest((previous) => ({ ...previous, paymentMethod: event.target.value }))}
/>
</label>
</div>
<button type="button" onClick={() => void capturePayment()} disabled={loading}>
{loading ? 'Capturing...' : 'Capture Payment Now'}
</button>
<pre>{JSON.stringify(paymentResponse, null, 2)}</pre>
</section>
)}
</main>
);
}
function LoginCard() {
const session = useSessionContext();
const [subjectId, setSubjectId] = useState('demo-user');
const [tenantId, setTenantId] = useState('demo-tenant');
const [provider, setProvider] = useState<IdentityProvider>(0);
const [externalToken, setExternalToken] = useState('');
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const onSubmit = async () => {
setSubmitting(true);
setError(null);
try {
await session.login({ subjectId, tenantId, provider, externalToken });
} catch (err) {
setError(err instanceof Error ? err.message : 'Login failed.');
} finally {
setSubmitting(false);
}
};
return (
<section className="card col">
<div className="grid">
<label className="col">
Subject Id
<input value={subjectId} onChange={(event) => setSubjectId(event.target.value)} />
</label>
<label className="col">
Tenant Id
<input value={tenantId} onChange={(event) => setTenantId(event.target.value)} />
</label>
<label className="col">
Provider
<select value={String(provider)} onChange={(event) => setProvider(Number(event.target.value) as IdentityProvider)}>
<option value="0">Internal JWT</option>
<option value="1">Azure AD (simulated)</option>
<option value="2">Google (simulated)</option>
</select>
</label>
</div>
<label className="col">
External Token (optional)
<input value={externalToken} onChange={(event) => setExternalToken(event.target.value)} />
</label>
<button type="button" onClick={() => void onSubmit()} disabled={submitting}>
{submitting ? 'Signing In...' : 'Sign In'}
</button>
{error && <div className="alert">{error}</div>}
</section>
);
}
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;