# Gate Pass — Web Frontend Reference

> React + Vite app. Entry point: `apps/web/src/App.tsx` (desktop) or `AppMobile.tsx` (Capacitor). Runs on port 3200 in dev.

---

## Tech Stack

| Category | Technology | Version |
|----------|-----------|---------|
| Framework | React | 19.2.4 |
| Build Tool | Vite | 8.0.4 |
| Routing | React Router | 6.30.3 |
| State (client) | Zustand | 5.0.12 |
| State (server) | TanStack React Query | 5.96.2 |
| HTTP | Axios | 1.14.0 |
| Forms | React Hook Form + Zod | 7.72.1 + 4.3.6 |
| Styling | Tailwind CSS | 4.2.2 |
| Icons | Lucide React | 1.7.0 |
| Real-time | socket.io-client | 4.8.3 |
| QR Generation | qrcode.react | 4.2.0 |
| QR Scanning (web fallback) | jsqr | 1.4.0 |
| CSV/Excel import | papaparse + xlsx | 5.5.3 + 0.18.5 |
| Image crop | react-easy-crop | 5.5.7 |
| Toasts | react-hot-toast | 2.6.0 |

---

## Source Structure

```
web/src/
├── App.tsx                     # Desktop entry — QueryClient + Router
├── AppMobile.tsx               # Mobile (Capacitor) entry
├── routes.tsx                  # Desktop router (12+ routes)
├── routes.mobile.tsx           # Mobile router (5 screens)
├── pages/
│   ├── auth/                   # LoginPage, OtpPage (desktop)
│   ├── dashboard/              # DashboardPage (owner overview)
│   ├── visitors/               # VisitorsPage, AddVisitorPage, VisitorScannerPage
│   ├── admins/                 # AdminsPage
│   ├── approvers/              # ApproversPage
│   ├── approvals-mobile/       # Mobile screens: Login, Otp, Approvals, VisitorDetail, More, Scan
│   ├── walk-in/                # Public self-registration form (/walk-in/:code)
│   ├── walk-in-qrs/            # Walk-in QR management
│   ├── notifications/          # Full notification inbox
│   ├── settings/               # Workspace settings
│   └── platform/               # Vendor console (PlatformAdmin role)
├── components/
│   ├── layout/
│   │   ├── AppShell.tsx        # Desktop sidebar nav + outlet
│   │   └── AppShellMobile.tsx  # Mobile bottom tab nav + safe-area + auth guard
│   ├── shared/
│   │   ├── NotificationBell.tsx       # Unread badge + dropdown + inline approve/reject
│   │   ├── ApprovalDecisionModal.tsx  # Approve/reject popup (desktop)
│   │   ├── MobileApprovalPopup.tsx    # Approve/reject popup (mobile)
│   │   ├── ImportVisitorsModal.tsx    # CSV/Excel bulk import
│   │   └── RouteErrorBoundary.tsx
│   └── ui/                     # Design system: Button, Input, Modal, Badge, etc.
├── hooks/
│   ├── usePushNotifications.ts # FCM token registration + foreground/tap listeners
│   ├── useSocket.ts            # Socket.IO connection + event routing
│   ├── useApproverSocket.ts    # Approver-room socket events
│   ├── useOwnerSocket.ts       # Owner-room socket events
│   └── useOwnerCounts.ts       # Dashboard count badges (polling)
├── store/
│   ├── auth.store.ts           # Zustand: token, role, user info, setSession, logout
│   └── notifications.store.ts  # Zustand: unread count, notification list
├── lib/
│   ├── axios.ts                # Main Axios instance (JWT inject + 401 redirect)
│   ├── scannerAxios.ts         # Scanner Axios (scannerToken from localStorage)
│   ├── approverAxios.ts        # Approver Axios (alias of main)
│   ├── socket.ts               # Socket.IO client singleton
│   ├── offlineDb.ts            # SQLite offline cache helpers (Capacitor only)
│   └── outboxDrain.ts          # Offline request queue + drain on reconnect
└── types/                      # Shared TypeScript types
```

---

## HTTP Clients

| Client | File | Auth header source | On 401 |
|--------|------|-------------------|--------|
| `api` (main) | `lib/axios.ts` | `localStorage.token` | Clear token, redirect `/login`, toast |
| `scannerApi` | `lib/scannerAxios.ts` | `localStorage.scannerToken` | Redirect `/visitor-scanner/login` |
| Approver (alias) | `lib/approverAxios.ts` | Same as main | Same as main |

Base URL: `import.meta.env.VITE_API_URL` (falls back to `/api` in dev via Vite proxy).

> **Debug flag:** `DEBUG_API_URL = true` in `axios.ts` shows a toast with the full request URL on every call.

---

## Desktop Routes (`routes.tsx`)

| Path | Component | Auth required |
|------|-----------|--------------|
| `/login` | `LoginPage` | No |
| `/login/otp` | `OtpPage` | No |
| `/` | Redirect → `/visitors` | — |
| `/dashboard` | `DashboardPage` | Yes (OWNER/ADMIN) |
| `/visitors` | `VisitorsPage` | Yes |
| `/visitors/add` | `AddVisitorPage` | Yes |
| `/visitors/:id/edit` | `AddVisitorPage` | Yes |
| `/visitors/scanner` | `VisitorScannerPage` | Yes (`canScanCheckpoint`) |
| `/admins` | `AdminsPage` | Yes (Owner) |
| `/approvers` | `ApproversPage` | Yes |
| `/walk-in-qrs` | `WalkInQRsPage` | Yes |
| `/walk-in/:code` | `WalkInPage` | No (public) |
| `/notifications` | `NotificationsPage` | Yes |
| `/settings` | `SettingsPage` | Yes |
| `/platform/login` | `PlatformLoginPage` | No |
| `/platform/login/otp` | `PlatformOtpPage` | No |
| `/platform/console` | `PlatformConsolePage` | Platform Admin |
| `/platform/organizations` | `OrganizationsPage` | Platform Admin |

---

## API Calls by Page

### Auth Pages (`pages/auth/`)

| Page | Method | Endpoint | Purpose |
|------|--------|----------|---------|
| `LoginPage` | POST | `/auth/send-otp` | Request OTP |
| `OtpPage` | POST | `/auth/verify-otp` | Verify OTP → receive JWT |
| `OtpPage` (dev) | GET | `/auth/dev-peek-otp` | Auto-fill OTP in dev |

### Dashboard (`pages/dashboard/`)

| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/visitors` | Full visitor list |
| GET | `/visitors/counts` | `{ awaitingApproval, arrived, total, … }` |
| GET | `/visitors/requests` | Pending walk-in requests |
| POST | `/visitors/:id/approve` | Approve visitor |
| POST | `/visitors/:id/reject` | Reject visitor |

### Visitors Page (`pages/visitors/`)

| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/visitors` | Paginated visitor list |
| GET | `/visitors/counts` | Count badges |
| GET | `/visitors/:id` | Single visitor + scan logs |
| GET | `/visitors/:id/history` | Edit audit log |
| POST | `/visitors/:id/approve` | Approve |
| POST | `/visitors/:id/reject` | Reject |
| POST | `/visitors/:id/checkout` | Manual check-out |
| DELETE | `/visitors/:id` | Delete |
| POST | `/visitors/bulk` | CSV/Excel bulk import |
| GET | `/visitors/approvers` | List approvers (for assignment picker) |
| GET | `/visitors/checkpoints` | List checkpoints |

### Add / Edit Visitor (`pages/visitors/AddVisitorPage`)

| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/visitors/admin-me` | Current admin profile + permission flags |
| GET | `/visitors/approvers` | Approver picker |
| GET | `/visitors/reasons` | Reason presets dropdown |
| GET | `/visitors/:id` | Pre-fill form for edit |
| POST | `/visitors` | Create visitor |
| PUT | `/visitors/:id` | Update visitor |

### Visitor Scanner (`pages/visitors/VisitorScannerPage`) — via `scannerApi`

| Method | Endpoint | Purpose |
|--------|----------|---------|
| POST | `/visitor-scanner/login` | Checkpoint login |
| GET | `/visitor-scanner/stats` | Check-in counts |
| GET | `/visitor-scanner/scan-history` | Recent scans |
| GET | `/visitor-scanner/approvers` | Approver list for routing |
| GET | `/visitor-scanner/lookup/:query` | Manual lookup |
| GET | `/visitor-scanner/status/:shortId` | Pre-check status before scan commit |
| POST | `/visitor-scanner/checkin` | Commit check-in |
| POST | `/visitor-scanner/walk-in-arrived` | Direct walk-in arrival |
| POST | `/visitor-scanner/requests` | Create approval request |
| POST | `/visitor-scanner/requests/:id/complete` | Close request |

### Admins (`pages/admins/`)

| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/visitors/admins` | List sub-admins |
| POST | `/visitors/admins` | Create sub-admin |
| PUT | `/visitors/admins/:id` | Update sub-admin |
| DELETE | `/visitors/admins/:id` | Delete sub-admin |

### Approvers (`pages/approvers/`)

| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/visitors/approvers` | List approvers |
| GET | `/visitors/approvers/activity` | All approver activity |
| GET | `/visitors/approvers/:id/history` | Single approver history |
| GET | `/departments` | Department list (for assignment) |
| POST | `/visitors/approvers` | Create approver |
| PUT | `/visitors/approvers/:id` | Update approver |
| DELETE | `/visitors/approvers/:id` | Delete approver |

### Walk-in QRs (`pages/walk-in-qrs/`)

| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/walk-in-qrs` | List all QR codes |
| GET | `/visitors/approvers` | Approver list (for assignment) |
| POST | `/walk-in-qrs` | Create QR |
| PATCH | `/walk-in-qrs/:id` | Update QR settings |
| DELETE | `/walk-in-qrs/:id` | Delete QR |

### Public Walk-in Form (`pages/walk-in/`)

| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/walk-in/:code` | Load QR metadata + form config |
| POST | `/walk-in/:code` | Submit self-registration |

### Settings (`pages/settings/`)

| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET/PUT | `/email-templates/:type` | Email template management |
| POST | `/email-templates/:type/preview` | Render template preview |
| GET/PATCH | `/email-automation` | Toggle email automations |
| GET/PATCH | `/automation-timings` | Auto-checkout / auto-cancel times |
| GET/POST | `/departments` | Department management |
| PUT/DELETE | `/departments/:id` | Update / delete department |
| GET/POST | `/visitors/reasons` | Visit reason presets |
| PUT/DELETE | `/visitors/reasons/:id` | Update / delete reason |
| GET/POST/PUT/DELETE | `/visitors/checkpoints` | Checkpoint management |
| GET/POST/PUT/DELETE | `/visitors/admins` | Admin management (also in Admins page) |

### Notifications (`pages/notifications/`)

| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/visitors/notifications` | Notification history (paginated) |
| POST | `/visitors/notifications/:id/read` | Mark single read |
| POST | `/visitors/notifications/read-all` | Mark all read |

### Platform Console (`pages/platform/`)

| Method | Endpoint | Purpose |
|--------|----------|---------|
| POST | `/platform/auth/send-otp` | Platform admin OTP |
| POST | `/platform/auth/verify-otp` | Verify → platform JWT |
| GET | `/platform/stats` | Platform-wide stats |
| GET | `/platform/orgs` | List customer orgs |
| POST | `/platform/orgs` | Create org |
| PATCH | `/platform/orgs/:id` | Update / suspend org |

---

## API Calls from Shared Components

| Component | Method | Endpoint | Purpose |
|-----------|--------|----------|---------|
| `AppShell` | GET | `/approver/me` or `/visitors/admin-me` | Identify current user in nav |
| `NotificationBell` | GET | `/visitors/notifications` | Unread count + list |
| `NotificationBell` | POST | `/visitors/notifications/:id/read` | Mark read |
| `NotificationBell` | POST | `/visitors/:id/approve` | Inline approve from bell |
| `NotificationBell` | POST | `/visitors/:id/reject` | Inline reject from bell |
| `ApprovalDecisionModal` | GET | `/visitors/by-short-id/:shortId` | Resolve visitor from QR |
| `ApprovalDecisionModal` | POST | `/visitors/:id/approve` | Approve |
| `ApprovalDecisionModal` | POST | `/visitors/:id/reject` | Reject |
| `MobileApprovalPopup` | GET | `/visitors/:id` | Load visitor for popup |
| `MobileApprovalPopup` | POST | `/visitors/:id/approve` | Approve |
| `MobileApprovalPopup` | POST | `/visitors/:id/reject` | Reject |
| `ImportVisitorsModal` | POST | `/visitors/bulk` | Bulk CSV/Excel import |

---

## API Calls from Hooks

| Hook | Method | Endpoint | Purpose |
|------|--------|----------|---------|
| `useOwnerCounts` | GET | `/visitors/counts` | Dashboard count badges |
| `useOwnerNotifications` | GET | `/visitors/notifications` | Notification list |

---

## State Management

### Zustand Stores

| Store | File | State |
|-------|------|-------|
| `useAuthStore` | `store/auth.store.ts` | `token`, `role`, `userName`, `userEmail`, `setSession()`, `logout()` |
| `useNotificationsStore` | `store/notifications.store.ts` | `unreadCount`, `notifications[]`, socket event handlers |

### localStorage Keys

| Key | Value |
|-----|-------|
| `token` | JWT for owner/admin/approver |
| `role` | `OWNER \| ADMIN \| APPROVER \| PLATFORM_ADMIN` |
| `userName` | Display name (shown immediately without API call) |
| `userEmail` | Email address |
| `scannerToken` | Checkpoint JWT (separate from main auth) |

All four main keys are written by `setSession()` and cleared together by `logout()` in `auth.store.ts`.

---

## Socket.IO (Real-time)

**Connection:** `lib/socket.ts` — singleton Socket.IO client, auto-connects on mount.

**Room join:** After login the app emits `join` with the appropriate room:
- `owner:{ownerId}` — OWNER / ADMIN role
- `approver:{approverId}` — APPROVER role

**Events received (desktop):**

| Event | Hook | Action |
|-------|------|--------|
| `visitor.awaiting` | `useOwnerSocket` | Refresh visitor list + increment unread badge |
| `visitor.arrived` | `useOwnerSocket` | Refresh counts |
| `visitor.decided` | `useOwnerSocket` | Update visitor status in list |
| `request.created` | `useOwnerSocket` | Show new walk-in badge |
| `notification.new` | `notifications.store` | Increment badge, prepend to list |
| `notification.read` | `notifications.store` | Mark item read |
| `notification.deleted` | `notifications.store` | Remove item from list |

---

## Web Push Notifications (Desktop Browser)

The desktop web app subscribes to Web Push via VAPID:

1. `GET /api/push/public-key` → VAPID public key
2. `navigator.serviceWorker.ready` → `registration.pushManager.subscribe()`
3. `POST /api/push/subscribe` → store subscription in DB
4. On logout: `POST /api/push/unsubscribe`

Service worker file: `web/dist/service-worker.js` (auto-copied by Vite build).

---

## Build Targets

| Target | Command | Output | Entry |
|--------|---------|--------|-------|
| Desktop browser | `npm run dev` / `npm run build` | `dist/` | `App.tsx` + `routes.tsx` |
| Mobile (Capacitor) | `npm run build:mobile` | `dist/` + Capacitor sync | `AppMobile.tsx` + `routes.mobile.tsx` |
| Electron | `npm run electron:dev` | — | Desktop entry wrapped in Electron |

**Mobile build** uses `--mode mobile` Vite flag which loads `.env.mobile` (sets `VITE_API_URL` to the production API domain so the WebView hits the real server, not localhost).

---

## Vite Configuration

| Feature | Config |
|---------|--------|
| Dev proxy | `/api` → `http://localhost:4200` (avoids CORS in dev) |
| Mobile mode | `--mode mobile` loads `.env.mobile` |
| Tailwind | `@tailwindcss/vite` plugin |
| Path alias | `@/` → `src/` |
