Portal Pages
Customer Portal (/portal/*)
Session-required. Customers can only see their own orders (API enforces ownerUserId filtering).
/portal/orders
Order list page. Shows non-archived, published orders belonging to the logged-in user.
Polling: The order list auto-refreshes every 30 seconds to pick up status changes pushed by staff. Implemented via useEffect + setInterval in the client component.
/portal/orders/[id]
Order detail page. Shows:
- Order status across all three axes (order status, payment status, fulfillment status)
- Line items (components, OS, services, warranties, shipping, peripherals, software)
- Timeline of status changes (
OrderStatusHistory) - Build timeline steps (public-facing only — no QA data, no internal notes)
- Attachments (invoice + any published attachments)
- Accept Quote button (when
orderStatusisquoteorclaimed) - Pay button (when
paymentStatusisawaiting_paymentandorderStatusisconfirmed)
Accept Quote flow: Calls POST /portal/orders/:id/accept-quote. Does not change order status — sends a staff notification. Staff must still convert the quote.
Pay flow:
- Calls
POST /payments/create-order - Gets back
{ orderCode } - Redirects browser to Viva checkout
/portal/claim
Intermediate page shown after login when a claim code is in the session. Calls POST /claim with the stored claim code.
/portal/settings
Profile settings: name, email change (requires re-verification), password change.
Staff Portal (/staff/portal/*)
Session + admin/superadmin role required. Staff subdomain (staff.pcmr.gr) also gated by Cloudflare Access.
/staff/portal/orders
Paginated order list with full filters:
type(quote/order)orderStatus,paymentStatus,fulfillmentStatus- Date range
- Customer email search
- Has invoice filter
/staff/portal/orders/[id]
Full order management panel. Actions available:
- Edit all fields (status transitions, line items, tracking info)
- Upload/replace invoice PDF
- Publish order to customer
- Convert quote to order
- Archive/unarchive
- Cancel
- View full status history
Line item panel: Add/edit/remove/reorder items from all 7 catalog types. Prices are snapshot-stored at the time of adding — catalog price changes don't retroactively affect order items.
Totals: Recalculated automatically after any line item change (OrderTotalsService.recalc()).
/staff/portal/builds
Build list with filters for status and builder (staff member).
/staff/portal/builds/[id]
Full build management panel:
- Build status transitions
- Photo upload (10 named slots via drag-drop)
- QA form (checklist, notes, pass/fail, sign-off)
- Test runs (benchmark, stress test records)
- Build timeline (chronological milestones)
- Event log (freeform notes, incidents, corrections)
- Temperature readings
- Booklet generation / download
Booklet download: Resolves presigned URL for the generated PDF attachment, opens in new tab. If quickStartBookletId / technicalDossierBookletId is null, shows a "Generate" button instead.
/staff/portal/components
Component catalog management. Full CRUD for components with per-category spec editing. Price history attached to each component.
/staff/portal/users
User list + detail. Staff can:
- Search users by email/name
- View order count, session count
- Ban/unban customers (role
useronly — cannot ban staff) - Change staff roles (cannot change own role)
/staff/portal/orders/create
New order form. Staff-initiated quote creation:
- Customer email autocomplete (
GET /admin/users/search?email=<q>) - All three order axes pre-set
claimCodegenerated if no customer selected
Key Client Components
| Component | Location | Notes |
|---|---|---|
OrderStatusBadge | components/ | Maps all three status axes to badge colors |
BuildPhotoUpload | components/build/ | Drag-drop with slot labeling |
QAChecklistForm | components/build/ | Structured checklist with pass/fail per item |
TimelineViewer | components/build/ | Chronological build timeline |
InvoiceUpload | components/order/ | PDF-only file upload with replace behavior |
OrderItemsPanel | components/order/ | Full CRUD for all 7 line item types |
Auto-Refresh Pattern
Customer portal polls for order status updates:
useEffect(() => {
const interval = setInterval(async () => {
const fresh = await fetch(`/api/portal/orders/${orderId}`);
const data = await fresh.json();
setOrder(data);
}, 30_000); // 30 seconds
return () => clearInterval(interval);
}, [orderId]);
Why 30 seconds? Balances freshness with API load. Staff status changes are not time-critical from the customer's perspective — a 30-second delay is acceptable.