Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions ARCHITECTURE_ESSENTIALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,23 @@ Undo behaviour: start/complete are applied optimistically and can be reverted wi
- `GET /facilities/:facilityId/fulfillment-queue` → `FulfillmentQueueItem[]` — queue items with action attribution (`attribution: { action, performedByUserId, performedByName, performedByRole, timestamp, notes }`).
- `POST /facilities/:facilityId/fulfillment-queue/:itemId/action` (body: `{action: 'fulfilled' | 'rejected', notes?}`) → `FulfillmentQueueItem` — records the acting staff user's identity and role for PRD §7 audit trail.

### Scan-to-verify orders with device camera (issue #52, docforum-core#71)
- `POST /facilities/me/verify-order` (body: `{ token: string }`) → `VerifyOrderResponse`
- Token verification endpoint returning 5 explicit states: `valid`, `superseded`, `expired`, `fulfilled`, `not_eligible`.
- Minimal order summary for fulfillment verification: `orderId`, `orderType`, `patientName`, `medicationOrTest`, `instructions`, `issuedAt`, `expiresAt`, `supersededBy`, `fulfilledAt`, `fulfilledByFacility`.
- Rate-limiting (HTTP 429) protection against brute-force token enumeration; error surfaced clearly in UI with retry guidance.
- Device camera QR scanning using `BarcodeDetector` API with fallback; strict camera media stream track teardown on unmount or screen exit.
- Manual token entry fallback.

### Facility payout ledger & escrow terms (issue #53, docforum-core#90)
- `GET /facilities/:facilityId/payout-ledger?cursor=&status=&asset=` → `FacilityLedgerPage`
- Cursor-paginated ledger of on-chain escrow deposits, releases, and refunds.
- Totals breakdown for held, released, and refunded funds separated by asset (`XLM` vs `USDC`).
- Strict BigInt-safe amount formatting for Stellar stroops (7 decimals: 1 XLM/USDC = 10,000,000 stroops).
- Escrow terms in plain language (releaser condition, refund deadline countdown).
- Approaching deadline warnings: prominent alerts when held escrow refund deadline is < 48 hours away and order is unfulfilled (Threat Model T8 compliance).
- Direct explorer links to Stellar testnet transactions (`https://stellar.expert/explorer/testnet/tx/...`).

## Roadmap / status
See `ROADMAP.md` in this repo, and Phase 7 in `docforum-core`'s
`ROADMAP.md` for the original (pre-split) frontend phase this was carved
Expand Down
9 changes: 8 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ real API" (no refresh-on-reload flow).
- [x] Doctor practice insights: utilisation, no-shows and turnaround. Tracked as [issue #50](https://github.com/DocForum/docforum-web/issues/50) (High, 200 pts) — `src/features/insights/`, `DoctorPracticeInsightsPage`, slot utilisation by week, no-show/late-cancel rates, referral turnaround, message response time; charts with accessible table alternatives; date range selector; small-sample caveats; doctor-only data (no peer comparisons).

## Phase W4 — Facility core flow
**Fully scoped into issues #8 and #51.**
**Fully scoped into issues #8, #51, #52, and #53.**
- [x] Facility staff accounts: invite, list and remove. Tracked as [issue #51](https://github.com/DocForum/docforum-web/issues/51) (High, 200 pts) — `src/features/facility-team/`, `FacilityTeamPage`, owner staff invite by email with fixed roles, pending invites with resend/revoke, member removal with backend session revocation and last-owner protection, read-only staff view, and fulfillment queue action attribution (PRD §7).
- [x] Scan-to-verify orders with the device camera. Tracked as [issue #52](https://github.com/DocForum/docforum-web/issues/52) (High, 200 pts) — `src/features/order-verify/`, `OrderVerifyPage` (`/facility/verify`), camera QR scanning via BarcodeDetector API with JS fallback, verified order states (`valid`, `superseded`, `expired`, `fulfilled`, `not_eligible`), instant jump into fulfillment queue, manual token entry fallback, camera permission and stream teardown, and 429 rate limit error handling.
- [x] Facility payout ledger with per-order escrow status and terms. Tracked as [issue #53](https://github.com/DocForum/docforum-web/issues/53) (High, 200 pts) — `src/features/payments/`, `FacilityPaymentsPage` (`/facility/payments`), cursor-paginated ledger, escrow totals cards (held, released, refunded), BigInt-safe amount formatting for Stellar stroops across assets (XLM vs USDC), plain-language escrow terms with releasers and refund countdowns, approaching deadline warnings (< 48h), and Stellar Expert testnet transaction explorer links.
- [ ] Fulfillment queue. Tracked as [issue #8](https://github.com/DocForum/docforum-web/issues/8) (Medium, 150 pts).
- [ ] Mark fulfilled/rejected, attach lab result. Folded into issue #8 above.

Expand Down Expand Up @@ -356,5 +358,10 @@ real API" (no refresh-on-reload flow).
- `#50 Doctor practice insights`: private insights page (`/doctor/insights`) with weekly slot utilisation, no-show and late-cancellation rates, referral acceptance turnaround, and messages response time. Includes SVG charts paired with accessible semantic `<table>` alternatives, date range selector (presets + custom), clearly labelled small-sample caveats, and strict peer-comparison suppression (doctor's own confidential data only). Wired into `DoctorDashboardPage`, command palette (`⌘K`), and navigation.
- `#51 Facility staff accounts & fulfillment attribution`: dedicated team management page (`/facility/team`) with member list, role assignment (`owner` vs `staff`), email invitations, resend/revoke pending invites, member removal with backend session revocation warning, and last-owner deletion protection. Enforced read-only view and action suppression for staff members. In the fulfillment queue (`/facility`), added action attribution display showing which staff member dispensed/fulfilled each order for PRD §7 audit trail.
- Updated `ARCHITECTURE_ESSENTIALS.md` "Known assumptions" and `ROADMAP.md`. 24 test files and 112/112 tests passing; typecheck and build green.
- 2026-10-18 — Built Phase W4 Scan-to-verify orders (#52) and Facility payout ledger with escrow terms (#53):
- `#52 Scan-to-verify orders with device camera`: dedicated verification page (`/facility/verify`) enabling facility staff to scan order QR codes via device camera or enter tokens manually. Uses native `BarcodeDetector` API with fallback; guarantees complete media stream track teardown on screen unmount/exit. Dispatches to `POST /facilities/me/verify-order` and cleanly displays all 5 result states (`valid`, `superseded`, `expired`, `fulfilled`, `not_eligible`) alongside minimal clinical order summaries. Valid results feature an immediate CTA to jump into fulfillment (`/facility?orderId=...`). Surfaces HTTP 429 rate-limit protection against enumeration.
- `#53 Facility payout ledger with escrow terms`: dedicated payments page (`/facility/payments`) displaying held, released, and refunded totals for both XLM and USDC. Uses BigInt-safe math (`formatStroops`) for Stellar 7-decimal stroop precision. Renders cursor-paginated escrow ledger with plain-language terms (releaser rules and refund deadline countdowns) adhering to threat model T8. Prominently flags approaching refund deadlines (< 48h) on unfulfilled orders. Integrates direct deep links to Stellar Expert testnet explorer for funding, release, and refund transactions.
- Updated `ARCHITECTURE_ESSENTIALS.md` "Known assumptions" and `ROADMAP.md`. 26 test files and 124/124 tests passing; typecheck and build green. Captured desktop and mobile screenshots into `screenshots/`.



Binary file added screenshots/facility-payments-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/facility-payments-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/facility-verify-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/facility-verify-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { ConsultationNote } from './features/consultation/components/Consultatio
import { DoctorInbox } from './features/messaging/components/DoctorInbox';
import { DoctorPracticeInsightsPage } from './pages/DoctorPracticeInsightsPage';
import { FacilityTeamPage } from './pages/FacilityTeamPage';
import { OrderVerifyPage } from './pages/OrderVerifyPage';
import { FacilityPaymentsPage } from './pages/FacilityPaymentsPage';

export default function App() {
return (
Expand Down Expand Up @@ -62,6 +64,22 @@ export default function App() {
<DoctorInbox />
</RequireAuth>
} />
<Route
path="facility/verify"
element={
<RequireAuth allowedRoles={['facility']}>
<OrderVerifyPage />
</RequireAuth>
}
/>
<Route
path="facility/payments"
element={
<RequireAuth allowedRoles={['facility']}>
<FacilityPaymentsPage />
</RequireAuth>
}
/>
<Route
path="facility/team"
element={
Expand Down
6 changes: 6 additions & 0 deletions src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,15 @@ export function Layout() {
<Link to="/facility" className={styles.navLink}>
Queue
</Link>
<Link to="/facility/verify" className={styles.navLink}>
Verify
</Link>
<Link to="/facility/team" className={styles.navLink}>
Team
</Link>
<Link to="/facility/payments" className={styles.navLink}>
Payouts
</Link>
</>
)}
<span className={styles.user}>
Expand Down
62 changes: 62 additions & 0 deletions src/features/order-verify/components/ManualTokenEntry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useState, type FormEvent } from 'react';
import { Button } from '../../../components/Button';
import styles from './OrderVerify.module.css';

interface ManualTokenEntryProps {
onSubmitToken: (token: string) => void;
isLoading?: boolean;
}

export function ManualTokenEntry({ onSubmitToken, isLoading = false }: ManualTokenEntryProps) {
const [tokenInput, setTokenInput] = useState('');
const [validationError, setValidationError] = useState<string | null>(null);

const handleSubmit = (e: FormEvent) => {
e.preventDefault();
const trimmed = tokenInput.trim();
if (!trimmed) {
setValidationError('Please enter an order token or alphanumeric verification code.');
return;
}
setValidationError(null);
onSubmitToken(trimmed);
};

return (
<form className={styles.manualForm} onSubmit={handleSubmit} noValidate>
<div className={styles.inputGroup}>
<label htmlFor="order-token-input">Order Verification Token</label>
<input
id="order-token-input"
type="text"
className={styles.tokenInput}
value={tokenInput}
onChange={(e) => {
setTokenInput(e.target.value);
if (validationError) setValidationError(null);
}}
placeholder="e.g. ord_tok_v1_9c28a7f4"
disabled={isLoading}
autoComplete="off"
spellCheck="false"
aria-describedby="order-token-help"
/>
<span id="order-token-help" className={styles.helperText}>
Enter the alphanumeric token from the patient's paper or digital order document.
</span>
</div>

{validationError && (
<div role="alert" className={`${styles.alert} ${styles.alertError}`}>
{validationError}
</div>
)}

<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button type="submit" variant="primary" loading={isLoading}>
{isLoading ? 'Verifying token…' : 'Verify order'}
</Button>
</div>
</form>
);
}
76 changes: 76 additions & 0 deletions src/features/order-verify/components/OrderScanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useCameraQrScanner } from '../hooks/useCameraQrScanner';
import { Button } from '../../../components/Button';
import styles from './OrderVerify.module.css';

interface OrderScannerProps {
onScan: (token: string) => void;
onSwitchToManual: () => void;
isVerifying?: boolean;
}

export function OrderScanner({ onScan, onSwitchToManual, isVerifying = false }: OrderScannerProps) {
const {
videoRef,
cameraStatus,
errorMessage,
isScanning,
startCamera,
stopCamera,
} = useCameraQrScanner({
onScan,
enabled: !isVerifying,
});

return (
<div className={styles.scannerCard}>
{errorMessage && (
<div
role="alert"
className={`${styles.alert} ${
cameraStatus === 'denied' || cameraStatus === 'error' ? styles.alertError : styles.alertWarning
}`}
>
<strong>Camera Access Notice</strong>
<span>{errorMessage}</span>
{cameraStatus === 'denied' && (
<div style={{ marginTop: 'var(--space-2)' }}>
<Button variant="secondary" onClick={onSwitchToManual}>
Switch to manual entry
</Button>
</div>
)}
</div>
)}

<div className={styles.viewfinderWrapper}>
<video
ref={videoRef}
className={styles.videoFeed}
playsInline
muted
aria-label="Order QR camera viewfinder"
/>
{isScanning && (
<div className={styles.reticle} aria-hidden="true">
<div className={styles.scanBeam} />
</div>
)}
</div>

<div className={styles.scannerControls}>
{isScanning ? (
<Button variant="secondary" onClick={stopCamera}>
Pause camera
</Button>
) : (
<Button variant="secondary" onClick={startCamera}>
Resume camera
</Button>
)}
<Button variant="secondary" onClick={onSwitchToManual}>
Manual code entry
</Button>
</div>
</div>
);
}
Loading
Loading