## 1. Location data: upazila → union → village

- [x] 1.1 Create `upazilas` migration (id, unique name) + `upazila_id` nullable FK on `unions`; add `Upazila` model; add `upazila()` relation on `Union` and `upazilas()` on `Upazila`
- [x] 1.2 Write `UpazilaSeeder` (9 Noakhali upazilas) and rewrite `UnionSeeder` (91 unions mapped to upazilas, user-provided Bengali lists), both idempotent via `firstOrCreate`; register in `DatabaseSeeder`
- [x] 1.3 Update `VillageSeeder`/`DonorSeeder` to reference the new Noakhali union names; verify `migrate:fresh --seed` produces 9 upazilas + 91 unions with correct parentage
- [x] 1.4 Add `POST /villages` route + controller action (auth, throttled): validate `name` + `union_id`, `firstOrCreate` on (union_id, name), return the village; add Wayfinder route usage on the front end
- [x] 1.5 Build `VillageCombobox` component: searchable filter over the union's villages; "Create '<name>'" option when no exact match, posting to `/villages` and emitting the new selection
- [x] 1.6 Upgrade `LocationSelects` to upazila → union → village using the combobox; wire into register page, profile edit, and the donors filter sheet; pass `upazilas` props from `DonorController@index` and the Fortify register view
- [x] 1.7 Show village, union, upazila on donor cards/listings (eager-load `union.upazila`, update `formatPublicDonor` and donor card rendering)

## 2. Donation validation & modal UX

- [x] 2.1 Add `MIN_DONATION_GAP_DAYS = 75` constant on `Donation`; add gap validation rule in `ProfileController::storeDonation()` comparing the submitted date against the user's latest donation dated before it
- [x] 2.2 Return a clear gap error message (Bengali + English copy explaining the 2.5-month/75-day rule with the next eligible date); keep `before_or_equal:today`
- [x] 2.3 In `resources/js/pages/profile/donations.tsx`: close the `BottomSheet` on `onSuccess` and show a success confirmation (flash message); ensure validation failures keep the sheet open with values preserved and per-field errors (incl. gap error on the date field)
- [x] 2.4 Compute and display next eligible donation date ("eligible now" when past) on the profile/donations page from `last_donation_date + 75 days`
- [x] 2.5 Align the `recent` donation_status filter threshold in `DonorController@index` with the same 75-day constant
- [x] 2.6 Verify: future date, missing date, entry within 75 days (rejected with message), entry at/after 75 days (accepted), modal close + success, backdated entry gap check

## 3. Referral program

- [x] 3.1 Migration: add unique `referral_code` and nullable self-FK `referred_by_user_id` to `users`; backfill codes for existing (seeded) users
- [x] 3.2 `User` model: fillable/casts, `referrals()` HasMany + `referrer()` BelongsTo; generate unique referral code (8-char uppercase alnum) in `CreateNewUser` with uniqueness retry
- [x] 3.3 Registration: optional `referral_code` input in `auth/register.tsx` (validated `nullable|exists:users,referral_code`), resolved to `referred_by_user_id` in `CreateNewUser`
- [x] 3.4 Profile page: show the user's referral code (copy/share affordance) and referral count (`withCount('referrals')`)
- [x] 3.5 Verify: register without code, with valid code (count increments for referrer), with invalid code (validation error)

## 4. Navigation & back button

- [x] 4.1 Replace bottom-nav `<a>` tags in `donor-app-layout.tsx` with Inertia `<Link>`; fix active-state matching so Find (`/donors?tab=search`) highlights when search/filters are active (read `tab` server-side or match on active filters)
- [x] 4.2 Add in-app-navigation tracking (e.g. `resources/js/lib/back.ts` flag set on first Inertia navigate) and update `donor-page-header.tsx` back button: `router.back()` when in-app history exists, else `router.get(fallback)` with per-page fallback (default `/donors`)
- [x] 4.3 Verify: tab switches without full reload, correct highlights, back from donor detail returns to list, back on direct-load/deep-link falls back to `/donors`

## 5. Home page: filter area & infinite scroll

- [x] 5.1 Remove the blood-group chip row from `donors/index.tsx`; add a compact blood-group button beside the Filter button (popover/sheet with the 8 groups; shows current selection)
- [x] 5.2 Implement infinite scroll: accumulate donors in state (reset on filter/search change), `IntersectionObserver` sentinel triggering `router.get` with `only: ['donors']`, `preserveState` + `preserveScroll`, merging page N+1; loading spinner and end-of-list marker; remove Prev/Next links
- [x] 5.3 Verify: scrolling appends pages without scroll jump, filters reset to page 1, end of results stops requests, back/forward restores correct list

## 6. Wrap-up

- [x] 6.1 Run `php artisan migrate:fresh --seed` and build front end (`npm run build` / dev server); smoke-test all flows above
- [x] 6.2 Update/extend tests covering the 75-day gap rule, referral code validation, and village create-if-missing; run the test suite
