# Frontend integration

How the [stayflow](https://github.com/dakamola-alt/stayflow) PMS talks to this
API, what has been wired up, and what has not.

---

## Starting position

The frontend was built mock-first. Before this work there was **no API client
at all** — `src/lib/api/` held a `getGreeting` example, and the OpenAPI spec in
`lib/api-spec/` was a health-check placeholder. All state lived in 13 React
contexts persisted to `localStorage`, and `AuthContext` accepted any password
for a known email address.

Replacing all of that at once would mean rewriting most of a 46,000-line
frontend. The agreed approach was therefore **backend-first**: build the API in
full, then wire up authentication, and leave the remaining contexts untouched
so nothing regresses.

---

## What is wired

### `src/lib/api/client.ts`

A small fetch wrapper. It unwraps the response envelope so callers get the
payload directly rather than reaching through `.data.data`, and turns failures
into a typed `ApiError` carrying `status`, `code` and the field-keyed `errors`
bag.

```ts
import { request, ApiError } from "@/lib/api/client";

try {
  const rooms = await request<Room[]>("/rooms", { query: { floor: 3 } });
} catch (e) {
  if (e instanceof ApiError && e.isConflict) toast.error(e.message);
}
```

`ApiError` exposes `isUnauthenticated`, `isForbidden`, `isConflict` and
`isValidation` so call sites do not compare status codes by hand. `firstError`
gives the first validation message, which is usually what a toast wants.

### `src/lib/api/auth.ts`

`login`, `logout` and `me`. Uses the **bearer token** flow rather than the
cookie session: it works unchanged when the API is on a different origin, which
is the normal development arrangement. The token is kept in `localStorage` so a
reload does not sign the user out.

### `src/contexts/AuthContext.tsx`

Now authenticates against the API. The public shape is deliberately close to
what it was, so consuming components did not need rewriting:

| Before | After |
|---|---|
| `login(email, password): boolean` | `login(email, password): Promise<boolean>` |
| `logout(): void` | `logout(): Promise<void>` |
| `hasAccess(module): boolean` | unchanged, but now answered by the server |
| — | `permissions` — the full server matrix |
| — | `accessLevel(module)` — `none` / `view` / `edit` |
| — | `loading` — true while the session is re-validated |

The bundled `roleAccess` table is kept as a **fallback** for when the matrix is
unavailable. It cannot grant anything real: the server re-checks every request,
so a client-side table that disagrees simply produces a 403.

### Consumers updated

- **`PMSLogin`** — awaits the async login, disables the button while in
  flight, and distinguishes "wrong credentials" from "cannot reach the server".
  The demo buttons now fill in the email rather than signing in directly,
  because the API validates the password for real.
- **`PMSSidebar`** — `logout()` is fired without awaiting, then navigates. The
  local session is cleared either way, so holding the user on the page while a
  revoke call completes adds nothing.
- **`PMSLayout`** — waits for `loading` before deciding whether to redirect.
  Without this, refreshing any `/pms/*` page would bounce a signed-in user to
  the login screen during the moment before `me()` resolves.

---

## Configuration

Set `VITE_API_URL` in the frontend's `.env` (see its `.env.example`):

```
VITE_API_URL=http://localhost:8000/api/v1
```

The API must allow the frontend's origin. In the API's `.env`:

```
CORS_ALLOWED_ORIGINS=http://localhost:3000
SANCTUM_STATEFUL_DOMAINS=localhost:3000
```

`CORS_ALLOWED_ORIGINS` cannot be `*` — the cookie flow sends credentials, and
browsers reject a wildcard on a credentialed response.

Then:

```bash
# API
php artisan serve                                   # :8000

# Frontend
pnpm --filter @workspace/hotel-pms run dev          # :3000
```

Sign in with a seeded account (`admin@e-lodge.com` and friends) using
`SEED_DEMO_PASSWORD`.

---

## What is **not** wired

Everything else still reads from `localStorage`. These contexts are untouched
and continue to work exactly as before:

`AdminContext`, `BookingContext`, `FinanceContext`, `FnbContext`,
`InventoryContext`, `NotificationContext`, `PolicyContext`, `PricingContext`,
`SchedulingContext`, `TasksContext`, `TravelAgentContext`, `WalletContext`.

That is deliberate. Each is an independent piece of work, and rewriting them
speculatively would risk regressions across screens that currently function.

### Suggested order for the rest

`BookingContext` is the natural next one, because the API side is complete and
tested and it is where the business value is. Roughly:

1. **Read paths first.** Replace the selectors (`getMaster`, `masterTotals`,
   `subFolio`) with `GET /bookings`, `GET /bookings/{id}` and
   `GET /bookings/{id}/folio`. The API already returns totals, so
   `masterTotals` becomes a field rather than a computation.
2. **Then the mutations**, one at a time: `createBooking` → `POST /bookings`,
   `recordPayment` → `POST /bookings/{id}/payments`, `checkInStay` →
   `POST /stays/{id}/check-in`, and so on. Each maps to exactly one endpoint.
3. **Delete the client-side rules as you go.** `paymentOptionsFor`, the
   availability logic in `assignRoomNumber`, and the balance arithmetic in
   `getMasterFinancials` are all enforced server-side now. Keeping duplicates
   invites the two from drifting.
4. `PMSDashboard` can move to `GET /dashboard` independently of any of this —
   it is read-only, and the endpoint returns every KPI the screen shows.

`@tanstack/react-query` is already a dependency and is the obvious way to
handle caching and refetching for these.

---

## Two behavioural differences to expect

**Rooms are now reused.** The mock `assignRoomNumber` treated every room number
that had ever been booked as permanently taken, so the property appeared to run
out of rooms. The API tracks occupancy per night, so a room genuinely frees up
after checkout and is offered again for non-overlapping dates.

**Passwords are checked.** `AuthContext.login` previously ignored the password
entirely. Anything that relied on that will now fail.

---

## A note on the frontend's type errors

`pnpm run typecheck` reports 259 errors on the stayflow repository. These are
**pre-existing** — implicit `any`s, a few stale route keys, a missing
`useMemo` import — and are unrelated to this integration. The figure is
identical before and after the changes described here, which is worth knowing
so the integration is not blamed for them. Fixing them is a separate,
worthwhile piece of housekeeping.
