# Frontend Phase 6 — Search & discovery (find a verified, same-gender nurse)

> **Mission:** build the family-facing **discovery** experience — the heart of the marketplace. A
> customer picks a care category, narrows by city / gender / price, and gets a rating-sorted list of
> **only verified, accepting** nurses; opening one shows their trust badges, attribute chips, priced
> services, and latest review, ending in the **"درخواست رزرو"** call-to-action that hands off to the
> booking flow. This phase implements wireframe screens **C1 (search & filter)**, **C2 (results)**,
> and **C3 (nurse profile)** against the `search` domain (backend phase b7), and establishes the shared
> **nurse-result card** + **price-row** components that the booking flow will reuse.
>
> **Track:** frontend · **Depends on:** [frontend-phase-4-b5](./frontend-phase-4-b5.md) (catalog browse
> & service builder) · [frontend-phase-5-b6](./frontend-phase-5-b6.md) (verified-nurse / trust badge) ·
> backend **b7** contract ([`dev/contracts/domains/search.md`](../../contracts/domains/search.md)) ·
> **Unlocks:** [frontend-phase-7-b8](./frontend-phase-7-b8.md) (booking request flow)
> **Before you start, read [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md).** It is not optional.

---

## 1. Context — where this sits

We are at the pivot of the customer journey: everything before this phase let a family *enter* the app
(auth f1), describe *who needs care* (onboarding f2), say *where* (addresses f3), and *browse the catalog*
(f4). This phase is the first time a family sees **real nurses** and chooses one. It is the screen the
product calls the trust funnel: discovery surfaces **only platform-vetted, same-gender-filterable**
caregivers, which is Balinyaar's entire differentiation versus opaque incumbents. The output of this
phase — a selected nurse + the carried filter intent (especially `required_caregiver_gender`) — is the
input to the booking request (f7).

**What already exists (do not rebuild) — link, extend, never re-create:**
- **f0 foundations** ([frontend-phase-0](./frontend-phase-0.md)): the customer mobile shell with the
  5-tab bottom nav (خانه/رزروها/بیماران/کیف‌پول/پروفایل), the `services/{domain}` + TanStack Query
  caching pattern (`keys.ts` factory, `apis/clientApi.ts`, one-hook-per-file, mutation invalidation),
  the **money/format util** (`formatIrrToToman`, integer-safe IRR-string parse, Shamsi date display) in
  `src/utils/`, the i18n namespace conventions (incl. the `search` namespace), the RTL baseline, and the
  shared composite primitives (status chip, stepper, etc.). **Copy the `auth` service shape — do not
  invent a new data pattern.**
- **f4 catalog** ([frontend-phase-4-b5](./frontend-phase-4-b5.md)): the `catalog` service + the
  **category grid** (مراقبت سالمند / پرستار کودک / تزریقات و سرم / فیزیوتراپی) and category cards used on
  the customer Home (A5). C1 **reuses that category grid component** and the catalog query — do not
  rebuild category fetching here.
- **f5 verified-nurses** ([frontend-phase-5-b6](./frontend-phase-5-b6.md)): the **trust badge**
  component(s) — ✓ تاییدشده (verified) and نظام پرستاری (INO membership) — and the verification-status
  vocabulary. C2/C3 **reuse those badges**; do not re-implement the verified mark.
- **Money/format:** prices render through the f0 `formatIrrToToman` util (IRR Rials string → Toman
  display). Never format money inline.

> **Data note:** b7's `GET /search/nurses` reads the denormalized `nurse_search_index`, which by
> invariant contains a row **only** when the nurse is `is_verified` AND not suspended AND
> `is_accepting_bookings` AND the variant `is_active`. So *every* result you receive is already
> bookable — the UI must not need to re-filter for verification. (See §5.)

## 2. Required reading (do this first)

- [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md) and
  [`../_shared/frontend-conventions-checklist.md`](../_shared/frontend-conventions-checklist.md).
- [`../../../client/CLAUDE.md`](../../../client/CLAUDE.md) — the engineering contract (RSC/client
  boundary, the `[locale]` layout rule, i18n, theme/tokens, `clientFetch`/`serverFetch`, the
  `services/{domain}` layout, TanStack Query setup, the cookie manager). Don't break the boundary.
- **Invoke the `frontend-designer` skill** before any visual work — it is the design/brand contract
  (palette: teal `#1d4a40`, terracotta `#d98c6a`, cream; tokens; typography; the `App*` library; the
  mobile shell; the hard UI rules). C1/C2/C3 must come out branded, RTL, dark-mode-ready. **All visual
  work goes through it.**
- [`../../../product/wireframes/index.html`](../../../product/wireframes/index.html) — Section **C**
  (screens **C1**, **C2**, **C3**): the exact layout, copy, and controls you implement. C1 = selected
  category + filter pills (تهران/location, تاریخ/date, جنسیت/gender) + category grid + "مشاهده ۲۴ پرستار"
  results CTA; C2 = result count + "مرتب‌سازی: امتیاز" sort + nurse cards (photo, name, ✓ تاییدشده,
  rating/review count, distance km, "from X تومان/ساعت"); C3 = avatar, name, rating, verified + نظام
  پرستاری badges, attribute chips, services-and-prices rows, latest review snippet, "درخواست رزرو".
- [`../../../product/business/04-search-and-matching.md`](../../../product/business/04-search-and-matching.md)
  — the business rules: category + city/(optional district) geo search, rating sort, and the
  **same-gender caregiver** near-hard requirement (`nurse_gender` filter + the requested
  `required_caregiver_gender` carried *before* booking, not after).
- **The contract** [`../../contracts/domains/search.md`](../../contracts/domains/search.md) (b7) +
  [`../../contracts/conventions/money-and-types.md`](../../contracts/conventions/money-and-types.md) and
  [`api-conventions.md`](../../contracts/conventions/api-conventions.md) — the envelope, the
  IRR-as-string money rule, `gender` = `male`/`female`/`any`, `price_unit` =
  `per_hour`/`per_session`/`per_half_day`/`per_day`/`per_24h`, enums-as-codes (labels are i18n keys,
  never derived from the code), pagination params, and the exact request/response shapes for
  `GET /search/nurses` and the nurse-profile/variant payloads. **Types come from this doc — do not guess
  server shapes** (if a shape is missing, follow §4 + operating-rules §6).
- The backend handoff [`../../shared-working-context/backend/handoff/after-backend-phase-7.md`](../../shared-working-context/backend/handoff/after-backend-phase-7.md)
  — which endpoints are live, what's mocked, what to consume.
- The existing **`src/services/auth/*`** (the template) and the **f4 `catalog`** + **f5 trust-badge**
  code — the patterns you copy and the components you reuse.

## 3. Scope — build this

A vertical slice: `services/search` (types/keys/apis/hooks) → the three screens (C1, C2, C3) → the two
shared components (nurse-result card, price-row), all RTL/i18n/cache-correct.

### 3.1 The `search` domain service (`src/services/search/`)
Copy the f0/`auth` service shape exactly:
- **`types.ts`** — mirror the b7 contract as string-literal unions / interfaces (don't guess):
  - `NurseSearchFilters` — `serviceCategoryId: number`, `cityId: number`, `districtId?: number`,
    `nurseGender?: 'male' | 'female'` (omit = any), `priceMin?: string`, `priceMax?: string` (IRR
    strings), `sort: 'rating'` (the only MVP sort), `page`, `pageSize`.
  - `NurseSearchResult` — the C2 card row: `nurseId`, `variantId`, `displayName`/`nurseName`,
    `avatarUrl`, `isVerified` (always true by invariant), `averageRating`, `totalReviews`,
    `distanceKm?`, `priceFromIrr` (string), `priceUnit` (the `per_*` union), `nurseGender`.
  - `NurseProfile` — the C3 payload: identity (`nurseName`, `avatarUrl`, `bio`, `yearsExperience`),
    `averageRating`/`totalReviews`/`totalCompletedBookings`, `isVerified`, `inoMembership` (نظام پرستاری
    badge flag), `attributeChips` (specialties/تخصص‌ها labels), `services: NurseProfileServiceRow[]`
    (each = `variantId`, `displayName`, `priceIrr` string, `priceUnit`, optional `sessionCount`), and a
    `latestReview?` snippet (`rating`, `body`, `authorMasked`, `createdAt`).
  - `Paged<T>` — reuse the f0 paginated envelope type.
- **`keys.ts`** — the query-key factory: `searchKeys.results(filters)` keyed on the **full filter
  object** (this is the cache contract — see §5), `searchKeys.profile(nurseId)`.
- **`apis/clientApi.ts`** — wraps `clientFetch` (never raw `fetch`):
  - `searchNurses(filters): Promise<Paged<NurseSearchResult>>` → `GET /search/nurses` (filters as query
    params; omit absent optional filters so the key/URL stay stable).
  - `getNurseProfile(nurseId): Promise<NurseProfile>` → the b7 nurse-profile endpoint (consume the exact
    route from the contract).
  - Add a **`serverApi.ts`** only if you prefetch C2/C3 from an RSC to remove a client round-trip
    (optional, see §5).
- **`hooks/` (one hook per file):**
  - `useNurseSearch.ts` — `useQuery({ queryKey: searchKeys.results(filters), queryFn })` with a
    deliberate `staleTime` (results are read-heavy and change slowly) and `keepPreviousData` so the list
    doesn't flash empty while a new filter loads.
  - `useNurseProfile.ts` — `useQuery` on `searchKeys.profile(nurseId)`; `enabled` only when an id is
    present.
  - `useDebouncedSearchFilters.ts` (or fold the debounce into the C1 controller) — **debounce the
    free-text/quick-filter input** so keystrokes don't fan out one request per character.
- **`index.ts`** — barrel.

### 3.2 C1 — Search & filter screen (`جستجو و فیلتر`)
The entry screen (reachable from Home/A5 search bar and category tap). Build:
- **Selected-category** header + the **category grid** (reuse the f4 catalog category grid;
  selecting a category sets `serviceCategoryId`). Categories shown per wireframe: مراقبت سالمند، پرستار
  کودک، تزریقات و سرم، مراقبت زخم (driven by the live catalog, not hardcoded labels).
- **Filter pills** (the C1 row): **city** (تهران ▾ — required; the cascading province→city→district
  picker reused from f3 geo — district optional, "leaving district empty searches the whole city" helper
  copy), **date** (تاریخ ▾ — capture intent only; availability is **soft** at MVP and is *not* a hard
  search filter — pass it through to booking, don't filter results on it), **gender** (جنسیت ▾ — خانم /
  آقا / فرقی ندارد → `male`/`female`/omit). Make **gender a prominent, early, first-class control** with
  one line of copy on why same-gender matters for bodily care.
- An optional **price-range** control (min/max → IRR strings) and the **results CTA** that mirrors the
  wireframe's "مشاهده ۲۴ پرستار" — i.e. show the live result **count** and navigate to C2. (Wire the
  count off a lightweight `useNurseSearch` head/`totalCount`, or navigate and show the count on C2 — your
  call, but the number must be real, not hardcoded.)
- Hold filter state in a small **colocated controller** (a `useSearchFilters` hook or local reducer) —
  *not* in a high context provider (it changes fast). The filter object is what becomes the query key.

### 3.3 C2 — Results screen (`نتایج جستجو`)
- **Header:** result **count** ("۲۴ پرستار") + **sort control** "مرتب‌سازی: امتیاز ▾" (rating is the only
  MVP sort — render it as a control but it has one option; tag other sorts **(DEFERRED)**).
- **List** of **`NurseResultCard`** (§3.5) rendered from `useNurseSearch(filters)`, paginated (infinite
  scroll or a "load more" — reuse the f0 paginated pattern). The customer **bottom tab nav** stays
  visible (this is a shell screen).
- **States (all required):** **loading** = skeleton cards (not a spinner); **empty** = the "no nurses
  match → relax your filters" state with concrete suggestions (remove the gender filter / widen to whole
  city by clearing district / try a nearby city — lean on the white-space cities Mashhad/Isfahan/Shiraz);
  **error** = retry; **populated** = rating-sorted cards. Tapping a card → C3.
- Changing a filter on C1 and returning re-queries with the new key; **reverting to a prior filter set
  reuses the cached result** (no refetch) — this is an acceptance criterion (§5, §7).

### 3.4 C3 — Nurse profile screen (`پروفایل پرستار`)
From `useNurseProfile(nurseId)`:
- **Header:** avatar, name, **rating** (+ review count), and **badges** — ✓ تاییدشده (reuse f5 verified
  badge) and **نظام پرستاری** (INO membership; render only when `inoMembership` is true).
- **Attribute chips:** specialties / experience (سالمندان، تزریقات، ۸ سال سابقه) from `attributeChips` /
  `yearsExperience`.
- **Services & prices:** a list of **`ServicePriceRow`** (§3.5) — one row per offered variant
  (`displayName` + price rendered via `formatIrrToToman` + the localized `price_unit` label, e.g.
  "۲۸۰٬۰۰۰ تومان/ساعت", and a 12h night-shift variant). These are the bookable units.
- **Latest review snippet** (`latestReview`) — rating + masked author + truncated body; "no reviews yet"
  empty state. (The full reviews tab is **(DEFERRED)** → [frontend-phase-13-b14](./frontend-phase-13-b14.md).)
- **Primary CTA: "درخواست رزرو"** — navigates to the booking request form (f7), **carrying the selected
  nurse + variant + the filter intent** (especially `required_caregiver_gender` derived from the C1
  gender filter, and the city/category). f7 owns the form; this phase only hands off the intent — pass it
  via route params / a small handoff, do **not** build the request form here. Tag the form itself
  **(DEFERRED → f7)**.
- States: loading skeleton, not-found (404 → "this nurse is no longer available"), error/retry.

### 3.5 Shared components (built once, reused by f7+)
At the shared level (`src/components/…`), composed from MUI/`App*` primitives (never re-implement a root
primitive), each with a **co-located `*.test.tsx`** and i18n in both locales:
- **`NurseResultCard`** — the C2 card: `Avatar` (photo), name, the reused **verified badge**, rating +
  review count, distance chip (km, only when `distanceKm` present), and the **"from X تومان/ساعت"**
  price line (via `formatIrrToToman` + localized unit). Pure/presentational, memoized, stable props so a
  list of N cards doesn't re-render on unrelated state.
- **`ServicePriceRow`** — the C3 service line and a reusable price row: localized service name + the
  money formatting + the `price_unit` label. Reused on C3 now and by the booking summary later.
> These two are the load-bearing reusable pieces. The category grid (f4), trust badges (f5), geo picker
> (f3), and status chip (f0) are **reused**, not rebuilt.

### 3.6 i18n
Fill the **`search`** namespace (seeded in f0) in **both** `messages/en.json` and `messages/fa.json`,
in sync, RTL-first: filter labels (شهر/تاریخ/جنسیت/خانم/آقا/فرقی ندارد), sort label, result-count
pluralization, every empty/error/loading copy, badge labels (تاییدشده/نظام پرستاری), price-unit labels
(ساعتی/per_hour … per_24h), and the "درخواست رزرو" CTA. **No display label is derived from an enum code**
— each `price_unit`/`gender`/sort code maps to an i18n key.

## 4. Mocks & seams in this phase

This is a **frontend** phase — it introduces **no backend seam**; it **consumes** the b7 contract.

- **Reuse the `services/{domain}` seam pattern from f0.** All data goes through `clientFetch` inside
  `services/search/apis/`.
- **If b7 is not yet merged (or a needed shape is missing):** build a **mock `clientApi`** behind the
  *same* `services/search` seam (real-shaped fixtures: a handful of verified nurses with ratings,
  distances, prices, badges; one profile with services + a review) so C1/C2/C3 are fully demoable, **and**
  (a) append the exact missing/mismatched shape to
  [`../../shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md)
  (operating-rules §6 — you never edit backend files), and (b) record the mock in your frontend report so
  it's swapped out cleanly when the real endpoint lands. Selection between mock and real `clientApi` is by
  the seam (one import swap), never an `if (mock)` scattered through components.

No new entry is needed in the backend `mocks-registry.md` (that registry is for backend DI seams); the
client-side mock is recorded in your **frontend report** instead.

## 5. Critical rules you must not get wrong

- **Only verified + accepting nurses appear — and the UI must not have to enforce it.** The b7
  `nurse_search_index` invariant guarantees every returned row is verified, not suspended, accepting, and
  on an active variant. **Never** add client logic that *re-includes* hidden nurses, and never display an
  unverified/paused nurse. If a result somehow lacks the verified flag, treat it as a data defect and file
  it via `for-backend.md` — do not paper over it.
- **The filter object IS the query key — identical filters reuse cache, never refetch.** `queryKey =
  searchKeys.results(filters)` must be a *stable, canonical* serialization (sorted keys, omitted optional
  filters rather than `undefined`, IRR as strings). Changing a filter and **reverting** to a previous set
  must hit the React Query cache with **zero** network calls (verify in Devtools — §7). Use
  `keepPreviousData` so the list doesn't flash. This is the whole point of the phase's caching design.
- **Debounce input.** Free-text / quick-filter typing must **not** fire one request per keystroke —
  debounce before it becomes part of the query key.
- **Same-gender is first-class and carried *before* booking.** The gender filter
  (`male`/`female`/`any`) is a prominent, early control with explanatory copy; the chosen value is carried
  into the booking handoff as `required_caregiver_gender` (f7) — surfaced *before* booking, never
  discovered after. Never default or silently drop gender.
- **Geography semantics:** city is required; **district is optional and "empty district = whole city"** —
  the picker copy must say so; don't send a bogus district. (Backend matches city-only + district rows;
  the client just leaves `districtId` absent.)
- **Availability is soft at MVP — never a hard search filter.** The date pill captures intent for the
  booking flow; it must not remove nurses from results. Tag availability-window filtering **(DEFERRED)**.
- **Money renders through the f0 util only — IRR Rials are integer strings, no floats, Toman is
  display-only.** Format with `formatIrrToToman`; never parse IRR into a JS number for math, never compute
  a "from" price client-side beyond picking the min the server sent.
- **Enums are codes; labels are i18n keys.** `price_unit`, `gender`, and sort never render their raw
  code; each maps to a localized label in both `en.json`/`fa.json`.
- **RSC/client boundary + caching discipline:** prefetch C2/C3 from an RSC with `initialData` only if it
  removes a round-trip; otherwise client-fetch. No `next/headers`/`next-intl/server` in client components.
- **Minimal re-renders:** `NurseResultCard` is presentational/memoized; keep fast-changing filter state
  colocated (not in a high provider); use `select` to subscribe to slices where it helps.
- **MUI primitives stay MUI; the two new composites live shared** — not inline in a page.

## 6. Definition of Done

The shared [definition-of-done.md](../_shared/definition-of-done.md), plus:
- [ ] `services/search` exists (`types`/`keys`/`apis`/`hooks`/`index`), copying the f0 pattern; types
      derive from the b7 contract (or a mock behind the seam + a `for-backend.md` request if b7 isn't
      ready).
- [ ] **C1, C2, C3** are built per the wireframe, RTL, with category grid (reused), city/gender/(optional
      district)/price filters, rating sort, and the prominent same-gender control.
- [ ] C2 has **all four states** (loading skeletons / empty "relax filters" / error-retry / populated);
      C3 has loading / not-found / error states.
- [ ] **Caching proven:** the filter object is the query key; reverting a filter reuses cache with no
      network call; input is debounced; `keepPreviousData` set. (Demonstrable in Devtools — §7.)
- [ ] `NurseResultCard` + `ServicePriceRow` are **shared** components with co-located tests; the verified
      badge (f5), category grid (f4), geo picker (f3) are reused, not rebuilt.
- [ ] Prices render via the f0 money util; every string is an i18n key in **both** locales, in sync; no
      label derived from an enum code.
- [ ] "درخواست رزرو" hands off the selected nurse + variant + `required_caregiver_gender` + city/category
      to the f7 route (form itself deferred to f7).
- [ ] `npm run check` green; `npm run test:ci` green (the two new shared components are covered);
      `client/CLAUDE.md` *Project Structure* updated for the new `services/search`, the two shared
      components, and the C1/C2/C3 routes.

## 7. How to test (what a human can verify after this phase)

Run `npm run dev` (point `NEXT_PUBLIC_API_URL` at a b7-enabled server, or use the seam mock if b7 isn't
merged):
- **End-to-end discovery:** from Home, open **C1** → pick a category (e.g. مراقبت سالمند), set city
  (تهران), set gender (خانم) → the results CTA shows a real count → tap it → **C2** lists **only
  verified** nurses, rating-sorted, each card showing photo, name, ✓ تاییدشده badge, rating + review
  count, distance, and "from X تومان/ساعت". Confirm no unverified/paused nurse ever appears.
- **Profile:** tap a card → **C3** shows avatar, rating, ✓ تاییدشده + نظام پرستاری badges, attribute
  chips, the services-and-prices rows (correct Toman formatting + Persian unit labels), and the latest
  review snippet; **"درخواست رزرو"** navigates to the f7 route carrying nurse + variant + gender intent.
- **Empty state:** search a white-space city/category/gender combo with no matches → the "no nurses match
  → relax your filters" state with concrete suggestions (clear district / drop gender / try Mashhad).
- **Caching (the headline check):** open React Query Devtools → apply filter set A (cache entry A) →
  change to set B (entry B, one fetch) → **revert to A** → the list shows instantly with **zero new
  network requests** (cache hit on key A). Type quickly in the search input → confirm **one** debounced
  request, not one per keystroke.
- **i18n/RTL:** flip locale fa↔en → all C1/C2/C3 labels, badges, unit labels, and empty/error copy
  translate and mirror correctly; dark mode still renders.
- **Gate:** `npm run check` and `npm run test:ci` pass.

## 8. Hand off & document (close the phase)

- **Docs:** update the **Project Structure** tree in [`../../../client/CLAUDE.md`](../../../client/CLAUDE.md)
  for the new `services/search`, the C1/C2/C3 routes/segments, and the two new shared components
  (`NurseResultCard`, `ServicePriceRow`); note the "filter-object-as-query-key" caching pattern as a
  reusable convention. Fix any doc drift you touch.
- **Contracts:** **consume** [`../../contracts/domains/search.md`](../../contracts/domains/search.md)
  (b7) — derive `services/search/types.ts` from it; **do not** edit it. Any missing/ambiguous shape (e.g.
  the nurse-profile services array, the `latestReview` shape, distance units, or the count head) goes to
  [`../../shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md)
  as an append — the backend delivers it in a later change; you never edit backend files.
- **Handoff & report:** append your summary to
  [`../../shared-working-context/frontend/STATUS.md`](../../shared-working-context/frontend/STATUS.md);
  write [`../../shared-working-context/reports/frontend-phase-6-report.md`](../../shared-working-context/reports/frontend-phase-6-report.md)
  — what was built (C1/C2/C3 + `services/search` + the two shared components), **what is now testable and
  exactly how** (the steps in §7), what (if anything) is **mocked behind the `services/search` seam** and
  how f-next swaps it for the real b7 endpoint, the contract consumed + any `for-backend` requests filed,
  and the follow-ups (the f7 booking handoff contract for the carried intent; the C3 reviews tab deferred
  to f13).
- **Memory:** save a `project`-type memory note for the non-obvious decisions this phase locks in — the
  **filter-object-as-query-key** caching contract, the verified-only invariant the UI relies on (so a
  future agent doesn't add a client-side verification re-filter), and the **`required_caregiver_gender`
  carried-before-booking** handoff to f7 — with a one-line pointer added to `MEMORY.md`.
