## Context

The project is a Laravel 12 React starter kit: Inertia v2 (page-split mode — each page loads via `resources/js/pages/<component>.tsx`), Fortify (login/register/password/passkeys/2FA), Tailwind v4 + shadcn/ui, Wayfinder-generated route helpers (`resources/js/routes` / `resources/js/actions`). The starter kit's auth flow is email-based and lives in Fortify controllers; our three controllers are Sanctum JSON-API style and unused by the browser app. `resources/views/ui.html` is a complete Alpine.js mockup ("Rokto Bondhu") defining the target UX: mobile-first 430px shell, blood-red palette, splash/login/register, directory with search + filter bottom-sheet, donor cards, public donor profile, my profile, edit profile, donation history timeline, add-donation sheet, call/logout confirmations, toasts, bottom nav.

Database constraint: `users.id` and `donations.user_id` are UUIDs. Any FK added to `users` must use `foreignUuid` — the current `$table->foreignId('district_id')` is both dangling (no districts migration) and the wrong type. Dev database may already contain `districts`/`unions`/`villages` tables imported from raw SQL; there is no production data.

## Goals / Non-Goals

**Goals:**
- One coherent session-auth Inertia app implementing the mockup, driven by the three rewritten controllers.
- Phone/username credential login via Fortify override (per user decision).
- `unions` + `villages` reference tables created by **new migrations** (per user decision), `union_id`/`village_id` on `users`, District removed entirely.
- `php artisan migrate:fresh --seed` running clean end-to-end with usable seed data.

**Non-Goals:**
- Multi-district support; password reset / email verification UI for donors; keeping the old JSON API endpoints; passkey/2FA flows in the donor UI (starter settings pages stay as-is); real-time anything; admin backoffice for unions/villages.

## Decisions

### D1: Keep Fortify for auth pipeline, override credential + registration logic
Fortify already owns session login/logout/register plumbing (rate limiting, redirects, `HandleInertiaRequests` shared props, passkeys). We override rather than bypass:
- `app/Fortify/FortifyServiceProvider` (or `Fortify::authenticateUsing`) → look up user by `phone` OR `username`, check `password_hash` (note: the app's password column is `password_hash`, not Fortify's default `password` — the User model's auth password name must be set accordingly, e.g. `getAuthPassword()` returning `password_hash`).
- `Fortify::createUsersUsing` / a custom `CreateNewUser` action → validates name, phone, username, password, blood_group, union_id, village_id, is_available; hashes password into `password_hash`.
- Login view field changes from `email` to `identifier` (single input matching mockup).

Alternative considered: rip out Fortify and use plain `Auth::attempt` in our controllers. Rejected — loses rate limiting, logout, and the starter kit's passkey/2FA settings pages for no gain.

Why rewrite of the 3 controllers is still needed: directory/profile/donation flows are custom; they become Inertia controllers returning `Inertia::render` + redirects with `withErrors()`, replacing `Validator::make` + `response()->json` with `$request->validate()` and Inertia idioms.

### D2: Password column stays `password_hash`
The migration and model already use `password_hash`. Rather than rename to Laravel's conventional `password` (touching migration + model + Fortify assumptions), we keep `password_hash` and implement `getAuthPassword()` on the User model so Fortify/Hash checks work. Lower churn than renaming.

### D3: Location schema — `unions`, `villages`, FKs on users
New migration `create_unions_and_villages_tables`:
- `unions`: `uuid('id')` primary? No — reference data has no external consumers; plain `id` bigints keep selects/filters simple. `string('name', 100)` unique, timestamps.
- `villages`: bigint `id`, `string('name', 100)`, `foreignId('union_id')->constrained()->cascadeOnDelete()`, unique(`union_id`,`name`), timestamps.
- `users`: replace `district_id` FK + `union_name`/`village_name` strings with `foreignId('union_id')->nullable()->constrained('unions')->nullOnDelete()` and `foreignId('village_id')->nullable()->constrained('villages')->nullOnDelete()`; adjust the composite index to `['blood_group', 'is_available', 'union_id']` and `['union_id','village_id']`.
- Village–union integrity (a village not belonging to the chosen union) is validated at request level (`village_id` validated with `exists:villages,id,union_id,{union_id}` when both present), not by a composite DB constraint — keeps the schema simple.

Seeder: a handful of unions and their villages matching the mockup's locale (e.g. Barura, Kachua, Cumilla Sadar…), plus sample donors with donations so directory/filters demonstrate out of the box.

Alternative considered: no migration, models over existing SQL-imported tables (user's original idea). User explicitly chose migrations instead — the SQL-imported tables must be dropped before migrating. Migration wraps nothing: we document `migrate:fresh --seed` as the dev reset path.

### D4: One DonorController query path for directory + stats
`DonorController::index` builds one filtered query (search over name/username/blood_group/union name/village name via joins; filters: blood_group, availability, union_id, village_id, donation_status via cached `last_donation_date`), paginates 15, and separately computes two cheap aggregates (`totalDonors`, `availableDonors` — `User::count()` / `where('is_available')->count()`). Search uses `ILIKE`-safe `whereLike`/lower-case comparison so it works on both MySQL (dev, Laragon) and Postgres. Returns `Inertia::render('Donors/Index', [...])` with donors, filters, stats, unions list for the filter sheet. Pagination + filters preserved via query string (`->withQueryString()`).

Phone privacy: the paginated collection is mapped to a public shape (no phone, no unavailable_reason); phone is exposed **only** on `Donors/Show` props for the call-confirm dialog, which is the mockup's reveal-at-confirm behavior.

### D5: Frontend structure — mockup look, starter kit plumbing
- Preserve the mockup's visual identity (blood palette, Hanken Grotesk, 430px shell, bottom nav, bottom sheets). Implement palette as CSS custom properties + Tailwind theme extension in `resources/css/app.css` (Tailwind v4 `@theme`), not the CDN config from the mockup.
- Pages under `resources/js/pages/`:
  - `welcome.tsx` (splash → Become a Donor / Login) — replaces starter welcome
  - `auth/login.tsx` (rewritten: identifier + password), `auth/register.tsx` (rewritten: blood-group grid, availability cards, union/village cascading selects, username availability via debounced check)
  - `donors/index.tsx` (search, chips, filter sheet, donor cards, pagination), `donors/show.tsx` (public profile + call confirm)
  - `profile/index.tsx` (my profile + availability toggle), `profile/edit.tsx`, `profile/donations.tsx` (history timeline + add-donation sheet), `settings.tsx` (light, per mockup)
- Shared: `layouts/donor-app-layout.tsx` (app shell + bottom nav, hides nav on auth pages), `components/donor/*` (donor-card, blood-group-picker, location-selects, filter-sheet, add-donation-sheet, call-confirm-dialog, toast via existing sonner).
- Forms use Inertia `useForm`/`<Form>` with Wayfinder route helpers; username availability uses `router.get` debounced 400ms.
- Village list for cascading selects: pass all villages (small dataset) with props once, filter client-side by `union_id` — avoids an endpoint.

### D6: Routes
`routes/web.php`:
- `/` → welcome (guest; redirect authed users to `/donors`)
- Fortify serves login/register/logout (views overridden)
- `auth` middleware group: `GET /donors` (index), `GET /donors/{user}` (show), `GET /profile`, `PUT /profile`, `PATCH /profile/availability`, `GET /profile/donations`, `POST /profile/donations`, `GET /settings`
- `GET /username-check?username=` (guest-allowed, used during registration)
Names follow Wayfinder conventions so `resources/js/routes` regenerate cleanly (`php artisan wayfinder:generate`).

### D7: Settings/profile pages of the starter kit
Dashboard/settings/security pages remain reachable but are not part of the donor nav. `dashboard` route redirects to `/donors`. Email-dependent settings (verification) may show stale UI for phone users — acceptable for this app (out of scope).

## Risks / Trade-offs

- [Dev DB has SQL-imported `districts`/`unions`/`villages` tables] → migrations will collide. Mitigation: run `php artisan migrate:fresh --seed` (drops all tables); documented as the only supported dev path since there is no production data.
- [Fortify assumes `email` + `password` columns in places (password reset, registration views)] → password-reset/email flows are out of scope; we override the views/actions we use and accept dead starter pages until removed later.
- [`ILIKE` is Postgres-only; dev is MySQL on Laragon] → use `where(fn($q) => $q->whereRaw('LOWER(col) LIKE ?', ...))` or `whereLike()` so search works on both drivers.
- [Passing all villages to the client could grow] → dataset is one district's villages (hundreds at most); acceptable. If it grows, add a `villages?union_id=` endpoint later.
- [Phone revealed on public show page enables scraping by authed users] → matches the app's purpose (call donors) and the mockup; directory listing never includes phones, and all routes are auth-gated.
- [UUID vs bigint mixing] → users/donations stay UUID; unions/villages are bigint reference data; FK types match their parents, so no mismatch arises.

## Migration Plan

1. Drop legacy SQL-imported location tables + re-migrate: `php artisan migrate:fresh --seed` (dev-only data loss accepted).
2. Regenerate Wayfinder routes after adding routes.
3. Rollback: git revert; database is dev/resettable, no data migration needed.

## Open Questions

- Exact union/village seed list (which unions and villages to seed) — safe to finalize during implementation from the mockup's sample data.
- Whether `settings.tsx` keeps Change Password / Privacy rows as stubs — mockup shows them disabled; implement as hidden-or-stub during build.
