ui phase 11
This commit is contained in:
@@ -978,3 +978,80 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a
|
||||
URL), and `TicketMessageDto` gains `attachments: { id: string, url: string, contentType: string }[]`.
|
||||
- **Status:** deferred — the affordance is designed and gated off; nothing renders until this lands and the
|
||||
capability flag flips.
|
||||
|
||||
## REQ-062 — Verification queue enrichment: name/phone search + per-status counts — filed by ui-phase-11 — 2026-07-19
|
||||
- **Need:** Two additions to `GET admin_verifications` (the b6 admin review-queue list, folded client-side to
|
||||
one row per nurse — see REQ-034):
|
||||
1. A **name/phone search** query param (proposed `q`) that filters the queue to nurses whose name or phone
|
||||
matches (case-insensitive substring), combinable with the existing `status` filter.
|
||||
2. **Whole-desk per-status counts** in the response envelope — `pending`/`in_review` totals across the
|
||||
**entire** queue (not the current page or the applied filters), so the status-tab badges never drift
|
||||
from the true queue size. Optionally also a `submittedAt` sort param (`sort=submittedAt&dir=asc|desc`)
|
||||
for the new waiting-time column.
|
||||
- **Why:** The review queue (phase §3.3, the flagship trust-desk screen) previously offered only a 3-value
|
||||
status filter and no way to find a specific nurse or see how much work is queued per status — the audit's
|
||||
headline finding. The client's `AdminVerificationQueueFilters` now carries an optional `search` field and
|
||||
`AdminVerificationQueuePage` an optional `counts: { pending, in_review }` (both in
|
||||
`client/src/services/verification/types.ts`); the mock (`apis/mockApi.ts`) computes real counts over its
|
||||
whole unfiltered fixture set and matches `search` against the seeded `nurseName`. The real
|
||||
`verificationClientApi.listVerificationQueue` (`apis/clientApi.ts`) already sends `search` as `q` on the
|
||||
query string (a no-op today — the server currently ignores unknown query params rather than 400ing) and
|
||||
leaves `counts` `undefined`; the queue screen renders the status tabs **without** badge counts until this
|
||||
lands (mock-tolerant, never a fake count).
|
||||
- **Proposed shape:** `GET admin_verifications?status=&q=&page=&pageSize=&sort=&dir=` →
|
||||
`{ items: AdminPendingStepDto[], total, page, pageSize, counts: { pending: number, in_review: number } }`
|
||||
(`counts` computed over the whole queue, ignoring `status`/`q`/`page`).
|
||||
- **Status:** open
|
||||
|
||||
## REQ-063 — Ticket lifecycle mutations (close/reopen/assign) — filed by ui-phase-11 — 2026-07-19
|
||||
- **Need:** Close/reopen/assign endpoints for a ticket, plus an `assigneeUserId` field on the admin ticket
|
||||
DTOs (`AdminTicketSummary`/`AdminTicketDetail`) so the queue and thread can show and change who owns a
|
||||
case.
|
||||
- **Why:** Today a ticket has no lifecycle command at all — a resolved ticket can never leave the admin
|
||||
queue, and there is no way to hand a case to a specific staff member. The ops desk (f15 admin console)
|
||||
needs both for a working worklist. The client-side controls (`services/tickets` `useCloseTicket`/
|
||||
`useReopenTicket`/`useAssignTicket`, and the admin thread's close/reopen/"assign to me" affordances) are
|
||||
built against the mock and gated behind `TICKET_LIFECYCLE_ENABLED` (`services/tickets/constants.ts`,
|
||||
default `false`) so nothing points at a 404'ing route in production.
|
||||
- **Proposed shape:** `POST tickets/{id}/close` (no body) → ticket `status: 'closed'`, sets `closedAt`.
|
||||
`POST tickets/{id}/reopen` (no body) → `status: 'open'`, clears `closedAt`. `POST tickets/{id}/assign`
|
||||
with `{ ownerUserId: number }` → sets the ticket's assignee. `AdminTicketSummary`/`AdminTicketDetail` gain
|
||||
`assigneeUserId: number | null`.
|
||||
- **Status:** open
|
||||
|
||||
## REQ-064 — Partner scoped booking detail (read-only summary) — filed by ui-phase-11 — 2026-07-19
|
||||
- **Need:** A single-sponsored-booking read for the partner portal (`GET centers/me/bookings/{bookingId}`,
|
||||
sibling of the existing `GET centers/me/bookings` list route), returning everything the list row already
|
||||
has plus a status timeline for that booking.
|
||||
- **Why:** §3.7 — the partner portal's sponsored-bookings list (`/partner/bookings`) needs to link each row
|
||||
to a detail view so a center admin can confirm what happened on a booking without leaving the portal. The
|
||||
portal is explicitly scoped to read-only, non-clinical data (portal scope boundary, product/business) —
|
||||
patient display name + dates + status only, never clinical content, address, or money.
|
||||
- **Proposed shape:** `GET centers/me/bookings/{bookingId}` → `SponsoredBookingDetailDto` = the existing
|
||||
`SponsoredBookingDto` (`bookingId`, `patientName`, `scheduledDate`, `status`) plus
|
||||
`timeline: { status: string, occurredAt: string }[]` (server-truth status history, dates only — no
|
||||
clinical content). The client-side seam (`services/partnerCenter`) is built against this shape (mock
|
||||
primary, `USE_PARTNER_MOCK`); the real client call is written but unreachable until this route exists.
|
||||
- **Status:** open
|
||||
|
||||
## REQ-061 — Admin user lookup: name/phone search + batch id→label resolve — filed by ui-phase-11 — 2026-07-19
|
||||
- **Need:** Two endpoints backing a directory of platform users for admin use:
|
||||
1. `GET admin_users/search?q=&role=` — search by name/phone (min 2 chars), optionally narrowed to one
|
||||
coarse role (`customer`/`nurse`/`admin`/`partner`), returning up to ~10 matches: `{ id, displayName,
|
||||
maskedPhone, roles: string[], nurseProfileId?: number }` (`nurseProfileId` set only when `roles`
|
||||
includes `nurse` — the id `NursePicker` needs, a different id space than the user id).
|
||||
2. `POST admin_users/lookup` with `{ userIds: number[] }` — batch id→label resolve, returning the same
|
||||
shape for each found id, so a page with N actor ids (an audit log, a role grid) makes one request
|
||||
instead of N.
|
||||
- **Why:** The single scariest wrong-target failure mode in the backoffice — every audited action (role
|
||||
grants, partner-center admin assignment, sponsored-nurse assignment) previously targeted a hand-typed
|
||||
raw numeric id with no lookup or name echo-back (phase §3.2, the audit's flagship "high" finding), and
|
||||
every actor/owner id across the console (audit log, role grid) rendered as a bare `#42` with no way to
|
||||
find out who that is (§3.6, `AuditLogRow`). `UserPicker`/`NursePicker` (`client/src/components/admin/`)
|
||||
and the new read-first `/admin/users` directory console (§3.8) are both built against this shape,
|
||||
mock-primary behind `services/admin`'s existing `USE_ADMIN_MOCK` seam (`searchUsers`/`lookupUsers` on
|
||||
`AdminApi`) — no page points at a 404'ing route; the mock seeds a small but representative directory
|
||||
(admin/support/finance staff, nurses, customers, one partner-center contact) across both endpoints.
|
||||
- **Proposed shape:** see **Need** above; masked phone follows the existing `maskIranMobile` convention
|
||||
(`"0912•••1234"`, first-4/last-4) — never the full number.
|
||||
- **Status:** open
|
||||
|
||||
@@ -118,7 +118,7 @@ the frontend can build before the backend phase merges, and swap to the real HTT
|
||||
| `ServiceAreasApi` | `client/src/services/serviceAreas/apis/mockApi.ts` | Nurse coverage areas (list whole-city-first / add / remove). Enforces `UNIQUE(cityId, districtId)` exactly as the server — a duplicate (incl. a second whole-city row) throws the same **`409`** (`area_duplicate`) so the coverage editor's inline dup handling is demonstrable | `USE_SERVICE_AREAS_MOCK` (`services/serviceAreas/constants.ts`, default `true`) | b4 `nurse_service_areas/*` are live; set flag `false` — `serviceAreasClientApi` is wired (maps the server 409 to the same inline message). No hook/component change | 🟢 (real, refinement-phase-4) |
|
||||
| `AddressMapPicker` (map stand-in) | `client/src/components/geography/AddressMapPicker.tsx` | **Not a real map** — a bounded, tappable/draggable marker canvas (CSS grid, no Neshan/Google tiles, no network) that maps the pointer position to `{ latitude, longitude }` around the chosen city's centroid (`CITY_CENTROIDS`/`IRAN_CENTROID` in `services/geography/constants.ts`). Emits real coordinates for the create/update request | _none (component boundary)_ | Replace the canvas internals with a real map widget (Neshan/Google, inlined per the client CSP) that emits the same `{ latitude, longitude }` via `onChange` — `AddressForm` and every caller stay unchanged | 🟡 |
|
||||
| `CatalogApi` | `client/src/services/catalog/apis/mockApi.ts` (+ `apis/seed.ts`) | The catalog skeleton + nurse pricing layer. **Categories mirror the b5 seed exactly** (5 categories, ids 1–5, `sortOrder` 0–4). Seeds representative **option groups/values** the fresh backend does **not** (an admin authors them per category) — incl. required + optional groups and one **cross-category** (`serviceCategoryId=null`) group — so the builder's required-option gate + cross-category rendering demo. Enforces the server's create validation in-memory: `400` missing required dimension / bad price, and the `(nurse, category, option-set)` duplicate **`409`** (via `optionSetSignature`). Variant store seeded **empty** so the offerings empty-state demos; the nurse builds variants live (across price units). `create`/`update`/`set_active`/`list`(active-first, paginated)/`get`. Money stays an **IRR digit-string** end-to-end | `USE_CATALOG_MOCK` (`services/catalog/constants.ts`, default `true`) | b5 `catalog/*` + `nurse_variants/*` are live; set flag `false` — `catalogClientApi` is wired to the action-style routes (camelCase bodies, `pageSize` pagination per REQ-010, `category_id` snake_case filter). **When swapped, categories will have NO option groups until an admin authors them** (the mock's groups were illustrative). No hook/component change | 🟢 (real, refinement-phase-4) |
|
||||
| `VerificationApi` | `client/src/services/verification/apis/mockApi.ts` | The whole nurse trust journey (b6). Seeds the six required steps on `start` (idempotent); `runIdentityKyc` passes any well-formed 10-digit id **except** `0000000000` (→ `failed`/`kyc_no_match`, matches backend `MockIdentityKycProvider`); `runShahkarMatch` requires identity passed, fails **shared-SIM** when the bound national id is `1111111111` (→ `failed`/`shared_sim`); `runBankVerification` passes (assumes a primary bank account); `uploadStepDocument` simulates signed-URL PUT progress then moves the step to `in_review` (metadata only); `submitCredentialDetails` validates the INO number. Re-aggregates like the server (`approved` only when every step passes). **Dev-only** `__mockApproveAll()`/`__mockRejectStep(code,reason)` stand in for the deferred (f15) admin review queue so a human can watch `is_verified`/the trust badge/the publish gate flip — reachable from B3/B6 only while the flag is true. **ui-phase-8:** `VerificationStatus` gained two mock-tolerant fields the nurse-facing wire doesn't serve yet — `submittedAt` (stamped once when `start()` first seeds the steps, REQ-055) and `credentialSubmission` (stamped by `submitCredentialDetails` — `inoNumberSubmitted` boolean + specialties/registry fields, **never** the raw INO number, REQ-056) — so B6's timestamp and B5's hydrate-on-return both demo pre-REQ | `USE_VERIFICATION_MOCK` (`services/verification/constants.ts`, default `true`) | b6 `nurse_verification/*` + `nurses/{id}/trust_badge` are live; set flag `false` — `verificationClientApi` is wired (action-style routes, camelCase, XHR signed-URL PUT for upload progress + SHA-256 integrity hash). **Caveats:** the real `submitCredentialDetails` no-ops pending REQ-011 (no nurse-facing endpoint for the structured INO/specialties fields yet — the document uploads it accompanies are contract-backed); `submittedAt` (REQ-055) and `credentialSubmission` (REQ-056) are both undefined until served, degrading gracefully (B6 omits the timestamp line; B5 falls back to blank fields). No hook/component change | 🟡 |
|
||||
| `VerificationApi` | `client/src/services/verification/apis/mockApi.ts` | The whole nurse trust journey (b6). Seeds the six required steps on `start` (idempotent); `runIdentityKyc` passes any well-formed 10-digit id **except** `0000000000` (→ `failed`/`kyc_no_match`, matches backend `MockIdentityKycProvider`); `runShahkarMatch` requires identity passed, fails **shared-SIM** when the bound national id is `1111111111` (→ `failed`/`shared_sim`); `runBankVerification` passes (assumes a primary bank account); `uploadStepDocument` simulates signed-URL PUT progress then moves the step to `in_review` (metadata only); `submitCredentialDetails` validates the INO number. Re-aggregates like the server (`approved` only when every step passes). **Dev-only** `__mockApproveAll()`/`__mockRejectStep(code,reason)` stand in for the deferred (f15) admin review queue so a human can watch `is_verified`/the trust badge/the publish gate flip — reachable from B3/B6 only while the flag is true. **ui-phase-8:** `VerificationStatus` gained two mock-tolerant fields the nurse-facing wire doesn't serve yet — `submittedAt` (stamped once when `start()` first seeds the steps, REQ-055) and `credentialSubmission` (stamped by `submitCredentialDetails` — `inoNumberSubmitted` boolean + specialties/registry fields, **never** the raw INO number, REQ-056) — so B6's timestamp and B5's hydrate-on-return both demo pre-REQ. **ui-phase-11 addition:** `listVerificationQueue` gained `search` (name/phone substring match against the mock's own fixture set) + a `counts: {pending, in_review}` computed over the **whole unfiltered** queue (REQ-062) — backs the admin verification desk's search field + status-tab badge counts | `USE_VERIFICATION_MOCK` (`services/verification/constants.ts`, default `true`) | b6 `nurse_verification/*` + `nurses/{id}/trust_badge` are live; set flag `false` — `verificationClientApi` is wired (action-style routes, camelCase, XHR signed-URL PUT for upload progress + SHA-256 integrity hash). **Caveats:** the real `submitCredentialDetails` no-ops pending REQ-011 (no nurse-facing endpoint for the structured INO/specialties fields yet — the document uploads it accompanies are contract-backed); `submittedAt` (REQ-055) and `credentialSubmission` (REQ-056) are both undefined until served, degrading gracefully (B6 omits the timestamp line; B5 falls back to blank fields); `search`/`counts` (REQ-062) — the real client already sends `search` as `q` (currently a no-op, ignored by the server) and `counts` stays `undefined`, so the queue's status tabs render without badge counts until delivered. No hook/component change | 🟡 |
|
||||
| `BookingsApi` | `client/src/services/bookings/apis/mockApi.ts` | The post-payment engagement (b9). Seeds **2 confirmed bookings** (one 3-session multi-day, one single-visit) + `booking_care_instructions` + a per-session **EVV state machine** — `checkInVisit` flips the session→`in_progress`/`checked_in` (booking→`in_progress`) and computes the **advisory** `checkInAddressMatch` (haversine vs the seeded address ± `MOCK_EVV_TOLERANCE_METERS`, `null` when GPS was absent); `checkOutVisit` requires an open check-in (**`400 no_open_check_in`** otherwise), completes the session (stamps `payoutEligibleAt`), and completes the booking + opens the dispute window once **all** sessions settle. `getCareInstructions` **404s any viewer but the assigned nurse** (the two-stage-disclosure boundary; the UI `enabled` gate means the customer never even calls it). Money stays IRR digit-strings with `gross = commission + payout` and `Σ visitPayout = payout`. **ui-phase-7:** `forViewer` now unmasks `addressSnapshotJson` for the nurse once the booking is `confirmed`+ (simulating REQ-051, still delivered) instead of unconditionally nulling it — `addr5001` gained `latitude`/`longitude` matching the EVV reference point so the new address-card map link is demoable; `listTodaySessions` stamps a `variantLabel` off the booking's frozen variant snapshot (REQ-052) | `USE_BOOKINGS_MOCK` (`services/bookings/constants.ts`, default `false`) | b9 `bookings/*` + `booking_sessions/*` are live, but a booking only exists after `bookings/convert` runs on a **paid** request — both upstreams (`bookingRequests` mock, card capture b10) aren't real client-side yet. Once conversion is live, set flag `false` — `bookingsClientApi` maps the routes 1:1 (+ `bookingsServerApi` for the RSC prefetch). No hook/component change. Deliver **REQ-051** (nurse-view address post-confirmation) + **REQ-052** (today-feed `variantLabel`) | 🟢 (real, refinement-phase-4) |
|
||||
| `ILocationProvider` | `client/src/services/bookings/evv/locationProvider.ts` | **EVV GPS capture** — the only client seam f8 introduces. `getCurrentPosition()` never rejects (denied/unavailable → `null`, so a GPS problem is **advisory, never a block**). The **real** provider wraps `navigator.geolocation.getCurrentPosition`; the **mock** returns canned coordinates per mode so the in-range / advisory-out-of-range / denied paths are all demoable without a device (the mock `BookingsApi` computes the match against the same seeded reference point) | `NEXT_PUBLIC_EVV_MOCK_GPS` = `in_range` \| `out_of_range` \| `denied` \| `off` (default `in_range` while `USE_BOOKINGS_MOCK`, else `off`) | Set `NEXT_PUBLIC_EVV_MOCK_GPS=off` (or flip `USE_BOOKINGS_MOCK`) → the real `navigator.geolocation` provider is selected. Real **address-match math** stays server-side (backend geocoding seam), not here — this seam only *captures* the position | 🟢 (real, refinement-phase-4) |
|
||||
| `PaymentApi` | `client/src/services/payment/apis/mockApi.ts` | **The f9 checkout money path** — plays the PSP + webhook roles the client can't reach: `getCheckoutSummary` serves the unserved C6 breakdown (REQ-016; commission-net/VAT/service split via **integer parts-per-10000 BigInt math**, 12% fee / 10% VAT, reconciles to the rial); `initiatePayment` enforces b10 idempotency (same `Idempotency-Key` → same attempt; repeat after capture / lapsed window → **`409`**) and returns a `redirectUrl` into the local mock-gateway harness; `confirmGatewayReturn` on success is the **webhook-confirm stand-in and the missing f7↔f8 bridge** — flips the request `converted` (+ client-augmented `bookingId`, via `mockMarkBookingRequestConverted` in the f7 mock), inserts a **confirmed** booking into the f8 store (`mockInsertConvertedBooking`), and auto-issues the b11-shaped invoice (`moadianStatus: pending`, `pdfUrl: null` so the print path exercises); replayed returns converge idempotently; `getInvoice` 404s until issued. **ui-phase-6:** `initiatePayment`'s `redirectUrl` is now `null` (was a stale pointer to the deleted card-gateway harness page — a latent bug, since the harness itself was already removed in refinement-phase-4; the checkout page's `!redirectUrl` branch already reads the outcome directly, no behavior change); `getCheckoutSummary` adds mock `nurseAvatarUrl: null`/`nurseVerified: true` (REQ-046, the C6 identity moment); the capture path stamps `capturedAt`/`createdAt` on the transaction so `PaymentOutcomeDto` serves `trackingCode`/`paidAt` (REQ-046, the confirmation receipt) and the new `getPaymentHistory` reads the same transaction list (REQ-047, wallet «پرداختها»); invoice creation adds mock `paymentMethod: 'card'`/`transactionReference`/`sellerFiscalIdentity` (REQ-049, fiscal-grade invoice) | `USE_PAYMENT_MOCK` (`services/payment/constants.ts`, default `true`) | b10 initiate + b11 invoice are live and `paymentClientApi` maps them 1:1 (`Idempotency-Key` header, `GET invoices/{bookingId}`); deliver **REQ-016** (checkout summary — the real client already targets the proposed `booking_requests/checkout_summary/{id}` slug) + **REQ-017** (transaction status / `bookingId`; until then the real outcome poll maps `booking_requests/get` statuses and can't distinguish declined from slow) + **REQ-018** (invoice reachable post-capture) + **REQ-046** (nurse identity + tracking code/paid-at) + **REQ-047** (payment history) + **REQ-049** (invoice fiscal fields), make the upstream `bookingRequests` flow real, then set flag `false`. No hook/component change | 🟢 (real, refinement-phase-4) |
|
||||
@@ -130,8 +130,8 @@ the frontend can build before the backend phase merges, and swap to the real HTT
|
||||
| `ReviewsApi` | `client/src/services/reviews/apis/mockApi.ts` | **The f13 moderated-review trust loop.** b14 serves the review **submit** (`POST bookings/{id}/review`), the public **nurse reviews** page (`GET nurses/{id}/reviews`), and the tag rollup — those are mapped 1:1 in `reviewsClientApi`. But there is **no review-eligibility read** and **no my-review-for-booking read** (**REQ-026**), and the whole moderation transition (`pending_moderation → published`) is **admin-only (f15)**. The mock reads a booking from the shared **f8 bookings store** (`mockGetBookingForReview`) to gate eligibility on a **completed/closed** booking (aligns with the new completed seed 5005 / nurse 1 / patient 905), tracks the customer's submission as `pending_moderation` so eligibility flips `already_reviewed` + `getMyReviewForBooking` returns the persistent "under review" state, and seeds a **published list per nurse** (nurse 1 has 7 → the profile tab paginates; nurses 5/6 empty → empty state). The aggregate is **recomputed from the published list** (never a stored sum). A submitted review **never** enters any public list. Dev-only `__mockPublishSubmittedReview(bookingId)` stands in for the deferred (f15) admin queue so a human can watch a review appear on the profile. Money-free | `USE_REVIEWS_MOCK` (`services/reviews/constants.ts`, default `true`) | Deliver **REQ-026** (`review_eligibility` + `my_review` reads; confirm masked-author omission), then set flag `false` — `reviewsClientApi.getNurseReviews`/`createReview` already map the live b14 routes 1:1 and target the two proposed slugs for the gaps. Moderation UI itself is **f15** (admin). No hook/component change | 🟢 (real, refinement-phase-4) |
|
||||
| `PatientRecordsApi` | `client/src/services/patientRecords/apis/mockApi.ts` | **The f13 continuity-of-care surface.** Two very different things: (1) the **nurse-authored visit-note history** (`getPatientHistory`/`createVisitNote`) is **REAL b14** (`GET`/`POST patients/{id}/care_records`), mapped 1:1 in `patientRecordsClientApi` (the append composes the ticked task checklist into the note `body` since the wire has no structured task field); (2) the **family-owned editable record** (medications/routine/tasks — the داروها/روتین/وظایف tabs) and the **access check** have **NO backend at all** (neither the b14 contract nor `data-model/10-reviews-and-records.md` model them → **REQ-027**). The mock is **patient-scoped** and lazily seeds a coherent default per patient: a default family record (customer edits it), a **multi-nurse continuity history** (two prior notes from *different* nurses, proving the history persists across nurse changes; a nurse append prepends to the same patient's history), and a **foreign-patient access-denied** path (`MOCK_FOREIGN_PATIENT_ID = 8888` → `canView:false` + a `403` on every read) so the non-leaking access-denied card is demoable. Clinical text is fixture data (never logged) | `USE_PATIENT_RECORDS_MOCK` (`services/patientRecords/constants.ts`, default `true`) | Deliver **REQ-027** (family-owned `care_record` GET/PUT + `record_access` + structured `taskResults`), then set flag `false` — the history/append methods already map the real b14 routes; only the family-record/access methods flip. Confirm whether the family-owned record is a real MVP entity | 🟡 |
|
||||
| f8 bookings mock — completed-booking seed 5005 + f13 cross-mock reads | `client/src/services/bookings/apis/mockApi.ts` | **Non-seam additions (mirrors the f10 refunds precedent).** The f8 seeds had **no `completed` booking** (only `confirmed`/`in_progress`/`cancelled`), so f13's review flow needs one: added **booking 5005** (`status: 'completed'`, nurse 1, patient 905, one completed EVV session) so the customer can open a completed booking and leave a review. Also added a **cross-mock read helper** — `mockGetBookingForReview(id)` (single booking, clone) — imported by the reviews mock to gate eligibility and read the patient/nurse snapshot for a submission (the `listBookings` seam row omits `patientId`/`nurseId`). One-way edge INTO bookings (the bookings mock never imports f13), so no cycle | — (part of `USE_BOOKINGS_MOCK`) | When the bookings flow goes real (b9/b10 conversion live), 5005 stops being a static seed and the cross-mock helpers retire with the reviews/records mocks | 🟢 (real, refinement-phase-4) |
|
||||
| `TicketsApi` | `client/src/services/tickets/apis/mockApi.ts` | **The f14 ticket channel (b15).** b15 serves open/list/thread/message and `ticketsClientApi` maps them 1:1; **REQ-028 is now delivered** (`unreadCount`/`lastMessageAt` + `clientMessageId` idempotency are real on the wire), so `USE_TICKETS_MOCK = false` by default — the mock stays available for offline/demo use. It seeds 3 tickets (a booking-5001 **coordination** ticket with a **stored internal admin note the user view NEVER returns** — the no-leak demo — plus a support + a closed refund ticket), returns them newest-activity first with a per-ticket unread count that **clears on open**; `openTicket` is **idempotent for `coordination + bookingId`**; `postMessage` appends as the current viewer, throws `403` on a **closed** ticket, and throws `500` on the dev sentinel body `'/fail'` (the optimistic failure→retry path — **ui-phase-10:** a failed send now flips the bubble to `sendStatus:'failed'` in place instead of rolling it back, and a retry re-mutates the same `clientMessageId`). `MOCK_VIEWER_USER_ID` (per-role "me") drives `isMine`; **`isInternal` is never modelled in the user-app types**. **ui-phase-10 additions:** `toSummary()` now also computes `lastMessagePreview` (first ~80 chars of the last non-internal message) + `lastAuthorRole` (REQ-059 gap — real path maps both `null`, card degrades gracefully); a new `getUnreadTotal()` sums `unread` across every seeded ticket for the chrome support-badge (REQ-059 — real path returns `null`, badge doesn't render) | `USE_TICKETS_MOCK` (`services/tickets/constants.ts`, default **false** — REQ-028 delivered) | Deliver **REQ-059** (`lastMessagePreview`/`lastAuthorRole` on the summary + a cheap unread-total read) to fully retire the mock's enrichment role; **REQ-060** (message photo attachments) gates the composer's designed-but-off attachment affordance (`TICKETS_ATTACHMENTS_ENABLED`, default `false`). `ticketsClientApi` already maps the live b15 routes 1:1 (drops any leaked internal message defensively) | 🟢 real by default (REQ-028); 🟡 REQ-059/060 gaps remain mock-only |
|
||||
| `TicketsApi` | `client/src/services/tickets/apis/mockApi.ts` | **The f14 ticket channel (b15).** b15 serves open/list/thread/message and `ticketsClientApi` maps them 1:1; **REQ-028 is now delivered** (`unreadCount`/`lastMessageAt` + `clientMessageId` idempotency are real on the wire), so `USE_TICKETS_MOCK = false` by default — the mock stays available for offline/demo use. It seeds 3 tickets (a booking-5001 **coordination** ticket with a **stored internal admin note the user view NEVER returns** — the no-leak demo — plus a support + a closed refund ticket), returns them newest-activity first with a per-ticket unread count that **clears on open**; `openTicket` is **idempotent for `coordination + bookingId`**; `postMessage` appends as the current viewer, throws `403` on a **closed** ticket, and throws `500` on the dev sentinel body `'/fail'` (the optimistic failure→retry path — **ui-phase-10:** a failed send now flips the bubble to `sendStatus:'failed'` in place instead of rolling it back, and a retry re-mutates the same `clientMessageId`). `MOCK_VIEWER_USER_ID` (per-role "me") drives `isMine`; **`isInternal` is never modelled in the user-app types**. **ui-phase-10 additions:** `toSummary()` now also computes `lastMessagePreview` (first ~80 chars of the last non-internal message) + `lastAuthorRole` (REQ-059 gap — real path maps both `null`, card degrades gracefully); a new `getUnreadTotal()` sums `unread` across every seeded ticket for the chrome support-badge (REQ-059 — real path returns `null`, badge doesn't render). **ui-phase-11 additions:** `closeTicket`/`reopenTicket`/`assignTicket` (REQ-063) — the admin thread's close/reopen/"assign to me" controls, gated client-side behind `TICKET_LIFECYCLE_ENABLED` (default `false`, `services/tickets/constants.ts`) so nothing points at the still-404ing proposed routes in production; `StoredTicket` gained `assigneeUserId` | `USE_TICKETS_MOCK` (`services/tickets/constants.ts`, default **false** — REQ-028 delivered) | Deliver **REQ-059** (`lastMessagePreview`/`lastAuthorRole` on the summary + a cheap unread-total read) to fully retire the mock's enrichment role; **REQ-060** (message photo attachments) gates the composer's designed-but-off attachment affordance (`TICKETS_ATTACHMENTS_ENABLED`, default `false`); **REQ-063** (close/reopen/assign routes + `assigneeUserId` on the admin DTOs) — once delivered, flip `TICKET_LIFECYCLE_ENABLED` to `true` (the hooks/UI are already built against the mock). `ticketsClientApi` already maps the live b15 routes 1:1 (drops any leaked internal message defensively) | 🟢 real by default (REQ-028); 🟡 REQ-059/060/063 gaps remain mock-only |
|
||||
| `NotificationsApi` | `client/src/services/notifications/apis/mockApi.ts` | **The f14 notification center + polled bell (b1).** The b1 endpoints are live and `notificationsClientApi` maps them 1:1, but a notification only exists once some other backend domain **dispatches** one (`INotificationDispatcher`) — none run client-side while the upstream flows are mock-primary — so there'd be nothing to show. The mock seeds a realistic **unread-first** feed spanning **every deep-link class** (ticket_message/booking_confirmed/refund_processed/payment_captured/payout_paid/review_published + one unknown-type/no-payload row that degrades to no deep-link), each with a snake_case `dataJson` string the list maps through the **real** `parseNotificationData`; `getUnreadCount`/`markRead`/`markAllRead` mutate the in-memory feed. **Dev-only `__mockPushNotification(type,title,dataJson?,body?)`** prepends a fresh **unread** row so a human can watch the bell badge increment within the poll interval (phase §7 step 4). Ids align with the f8 bookings + tickets mocks so a deep-link lands on a real screen | `USE_NOTIFICATIONS_MOCK` (`services/notifications/constants.ts`, default `true`) | When the upstream domains dispatch real notifications, set flag `false` — `notificationsClientApi` already maps the live b1 `notifications/*` routes 1:1 (`page`/`pageSize`, `{count}`, `{notificationId}`). No hook/component change | 🟢 (real, refinement-phase-4) |
|
||||
| `AdminApi` | `client/src/services/admin/apis/mockApi.ts` | **The f15 backoffice-owned data (b1 + b15).** Fixtures engineered to exercise every console state: **one config per `data_type`** (decimal/int/bool/json/string — so the typed inputs + the 0–1 rate validation are all reachable) with a **change-history** trail; **holidays** with bank-closed days; a **paged audit log** with `changedFields` diffs (one row `<redacted>` for a PII field); a **support-alert** list spanning **every** `type` (`low_rating`/`evv_no_show`/`evv_location_mismatch`/`verification_expired`/`shared_sim`/`payment_anomaly`/`fraud_signal`/`nurse_clawback`/`emergency`) and all three statuses so the worklist filters are testable; and **RBAC** grants. Mutations mutate the in-memory arrays (a config save writes a history row; assign/resolve advance an alert; grant/revoke flip a role). Timestamps relative to `now` | `USE_ADMIN_MOCK` (`services/admin/constants.ts`, default `true`) | b1 config/holiday/audit/support-alert routes are live and `adminClientApi` maps them 1:1 — deliver **REQ-029** (config `updatedAt`/`updatedBy`) + **REQ-030** (audit actor/action/date filters) + **REQ-031** (the RBAC `admin_roles/*` endpoints, which don't exist yet), then set flag `false`. No hook/component change | 🟡 |
|
||||
| `PartnerCenterApi` | `client/src/services/partnerCenter/apis/mockApi.ts` | **The f15 partner centers (b15) — admin management + the center-scoped portal.** Returns **center #1 = merchant-of-record** (the settlement/invoice view renders) **and** #2 = non-MoR (the "settlement runs through Balinyaar" state) **and** a **draft** #3 (unverified banner); sponsored nurses (verified + unverified), sponsored bookings, and commission invoices whose **platform commission + BNPL commission + VAT = total** (VAT on the commission line only) with a fake 22-digit `moadianReferenceNumber` + a stub PDF url. `settlementIbanMasked` is **last-4 only** (write-then-masked: create/edit submit a full IBAN, only last-4 ever returns). Admin CRUD/verify/set-active/assign-nurse + the portal "my center" reads all mutate/read the in-memory world; "my center" resolves to `MOCK_MY_CENTER_ID` (=1, MoR) | `USE_PARTNER_MOCK` (`services/partnerCenter/constants.ts`, default `true`) + `MOCK_MY_CENTER_ID` | b15 admin partner-center CRUD/verify/sponsor are live; deliver **REQ-032** (portal split reads `centers/me[/nurses|/bookings|/settlement]` + the activate/suspend toggle + confirm the write-then-masked IBAN) + **REQ-033** (center-scoped invoice list + invoice `totalIrr`), then set flag `false` — `partnerCenterClientApi` maps the live admin routes and targets the proposed portal slugs. No hook/component change | 🟡 |
|
||||
| `AdminApi` | `client/src/services/admin/apis/mockApi.ts` | **The f15 backoffice-owned data (b1 + b15).** Fixtures engineered to exercise every console state: **one config per `data_type`** (decimal/int/bool/json/string — so the typed inputs + the 0–1 rate validation are all reachable) with a **change-history** trail; **holidays** with bank-closed days; a **paged audit log** with `changedFields` diffs (one row `<redacted>` for a PII field); a **support-alert** list spanning **every** `type` (`low_rating`/`evv_no_show`/`evv_location_mismatch`/`verification_expired`/`shared_sim`/`payment_anomaly`/`fraud_signal`/`nurse_clawback`/`emergency`) and all three statuses so the worklist filters are testable; and **RBAC** grants. Mutations mutate the in-memory arrays (a config save writes a history row; assign/resolve advance an alert; grant/revoke flip a role). Timestamps relative to `now`. **ui-phase-11 addition:** `searchUsers(query, roleFilter?)`/`lookupUsers(userIds)` (REQ-061) — a seeded ~13-entry user directory (admin/support/finance staff, nurses with `nurseProfileId`, customers, one partner contact) backing `UserPicker`/`NursePicker` (name+masked-phone+id search, replacing every raw numeric-id `TextField` on an audited action) and `AuditLogRow`'s batch actor-name resolve; `AdminUserSummary.maskedPhone` never exposes the mock's internal full-number field | `USE_ADMIN_MOCK` (`services/admin/constants.ts`, default `true`) | b1 config/holiday/audit/support-alert routes are live and `adminClientApi` maps them 1:1 — deliver **REQ-029** (config `updatedAt`/`updatedBy`) + **REQ-030** (audit actor/action/date filters) + **REQ-031** (the RBAC `admin_roles/*` endpoints, which don't exist yet) + **REQ-061** (the user-directory search/lookup endpoints — `client/src/services/admin/apis/clientApi.ts` already maps them to a proposed `admin_users/search`+`admin_users/lookup` route pair), then set flag `false`. No hook/component change | 🟡 |
|
||||
| `PartnerCenterApi` | `client/src/services/partnerCenter/apis/mockApi.ts` | **The f15 partner centers (b15) — admin management + the center-scoped portal.** Returns **center #1 = merchant-of-record** (the settlement/invoice view renders) **and** #2 = non-MoR (the "settlement runs through Balinyaar" state) **and** a **draft** #3 (unverified banner); sponsored nurses (verified + unverified), sponsored bookings, and commission invoices whose **platform commission + BNPL commission + VAT = total** (VAT on the commission line only) with a fake 22-digit `moadianReferenceNumber` + a stub PDF url. `settlementIbanMasked` is **last-4 only** (write-then-masked: create/edit submit a full IBAN, only last-4 ever returns). Admin CRUD/verify/set-active/assign-nurse + the portal "my center" reads all mutate/read the in-memory world; "my center" resolves to `MOCK_MY_CENTER_ID` (=1, MoR). **ui-phase-11 addition:** `getMySponsoredBookingDetail(bookingId)` (REQ-064) — a synthetic 2–4-step status timeline consistent with the booking's current status, backing the portal's new scoped read-only booking-detail page (dates + status timeline + patient display name only, no clinical content) | `USE_PARTNER_MOCK` (`services/partnerCenter/constants.ts`, default `true`) + `MOCK_MY_CENTER_ID` | b15 admin partner-center CRUD/verify/sponsor are live; deliver **REQ-032** (portal split reads `centers/me[/nurses|/bookings|/settlement]` + the activate/suspend toggle + confirm the write-then-masked IBAN) + **REQ-033** (center-scoped invoice list + invoice `totalIrr`) + **REQ-064** (the single-booking read + timeline), then set flag `false` — `partnerCenterClientApi` maps the live admin routes and targets the proposed portal slugs. No hook/component change | 🟡 |
|
||||
| Admin-endpoint additions to existing domain mocks (`verification`/`refunds`/`payouts`/`reviews`/`tickets`) | the same `apis/mockApi.ts` files (+ their `clientApi.ts`) | **The f15 staff lens over prior domains** — new admin methods added behind the existing seams (no new seam, no hook/component change on swap). **verification:** a nurse-level review queue (`pending`/`in_review`, one with an expiring credential) + a per-nurse case whose manual credential steps carry a document, and `getDocumentSignedUrl` that returns a **fresh short-lived URL each call** (sentinel `documentId 9999` throws → viewer error/re-request path); `decideStep`/`approve`/`reject` re-aggregate. **refunds:** a `getRefundPreview` with the fee/payout split reconciling to the rial per booking (a normal card, a BNPL w/ ETA, a post-payout w/ clawback notice, and a provider-decline **sentinel that fails then retries succeeds**). **payouts:** batches spanning `completed`/`partially_failed`/`processing` (one holiday-shifted), a preview w/ eligible + skipped(no-IBAN) + clawback line + holiday-shifted date, an **idempotency-keyed** run/retry (same key → same result, never double-pays), a `failed` payout to retry, and record-transfer-reference. **reviews:** a moderation queue incl. a low-rating flagged review; `moderateReview` returns a plausible recomputed aggregate. **tickets:** a global admin queue + a thread that **includes** the seeded internal note (the no-leak *inverse* demo) + `postAdminMessage` w/ `isInternal`; a refund-linked ticket (bookingId+refundId) so the RefundPanel opens from it | the owning domain's flag (`USE_VERIFICATION_MOCK` / `USE_REFUNDS_MOCK` / `USE_PAYOUTS_MOCK` / `USE_REVIEWS_MOCK` / `USE_TICKETS_MOCK`, all default `true`) | Deliver the per-domain admin gaps — **REQ-034** (verification nurse-queue + on-demand doc URL + whole-verification approve/reject), **REQ-035** (refund preview + explicit approve/reject), **REQ-036** (payout single-preview + `holidayShifted` + record-transfer-reference), **REQ-037** (moderation `tagCodes`) — then flip the owning domain's flag. The real `clientApi` methods already map the live admin routes 1:1 and target the proposed slugs for the gaps | 🟢 (real, refinement-phase-4) |
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
# UI Phase 11 — Admin & partner console — Report (2026-07-19)
|
||||
|
||||
## What was built
|
||||
|
||||
### 3.1 `useAdminListState` — URL-synced worklist state, adopted everywhere
|
||||
- New `client/src/hooks/useAdminListState.ts` — mirrors **applied** filters + page into `searchParams` via
|
||||
`router.replace({ scroll: false })`; draft stays local (typing never refetches or touches the URL) until
|
||||
`apply()`/`applyFilters(explicitValue)`/`clear()`/`goToPage(n)` commit it. Initial `applied`/`page` are
|
||||
read from the URL **once, on mount**. `applyFilters` exists because a discrete control (a status tab/
|
||||
select) that should commit the instant it changes cannot safely do `setDraft(next); apply()` in the same
|
||||
handler — `apply()` closes over the *previous* render's `draft`, so it would commit the stale value; two
|
||||
pages hit this bug during integration and were fixed to use `applyFilters` instead (a co-located test
|
||||
covers it). Also exports `useAdminBackToList(listHref)` — a real `router.back()` when there's browser
|
||||
history, falling back to pushing `listHref` otherwise (the detail-page "back" fix).
|
||||
- Because it calls `useSearchParams()`, every page using it wraps its body in `<Suspense>` (the existing
|
||||
`SearchScreen.tsx` pattern) — a default-exported thin wrapper + a `*Inner`/`*Screen` body component.
|
||||
- Adopted on **every** admin/partner queue page: tickets, audit, verification, reviews, payouts (list +
|
||||
batch detail), partners (list), alerts, holidays, config (list + history drawer), and the partner
|
||||
bookings/settlement lists. `roles` was intentionally **not** touched (it's an unpaginated, unfiltered
|
||||
grid — nothing to URL-sync).
|
||||
- **Fixed the hard-wired page-1 reads:** `admin/config/page.tsx` (`usePlatformConfigs`) and its
|
||||
change-history drawer, and `admin/holidays/page.tsx` (`useHolidays`) now carry real page state + an
|
||||
`AdminPager` — previously any row beyond page 1 was invisible/uneditable.
|
||||
- Detail pages (tickets thread, payout batch, partner center) now use `useAdminBackToList` (or `PageHeader`'s
|
||||
new `onBack`) instead of a hand-rolled `router.push` to the bare list.
|
||||
|
||||
### 3.2 `UserPicker`/`NursePicker` — killing raw-ID targeting
|
||||
- New `client/src/components/admin/UserPicker/` — an async MUI `Autocomplete` (name/phone search, 300ms
|
||||
debounce via the existing `useDebouncedValue`) rendering **name + masked phone + `#id`** per option, never
|
||||
a bare id. `NursePicker` is a thin `roleFilter="nurse"` wrapper — its selection carries `nurseProfileId`
|
||||
(a different id space than the user id, the one sponsorship/roster assignment actually needs). Both
|
||||
co-located-tested (mock the `useUserSearch` hook, no `QueryClientProvider` needed).
|
||||
- Backed by a new admin user-directory seam (REQ-061, gap): `AdminUserSummary` type, `searchUsers`/
|
||||
`lookupUsers` on `AdminApi`, a seeded ~13-entry mock directory (admin/support/finance staff, nurses with
|
||||
`nurseProfileId`, customers, one partner contact), `useUserSearch`/`useUserLookup` hooks. `useUserLookup`
|
||||
is the **batch** id→label resolve — one request for every actor/owner id a page renders, never one per row.
|
||||
- Wired into: `admin/roles`'s grant dialog (the confirm copy now names the resolved person —
|
||||
«نقش {role} به {name} اعطا شود؟» — never `#42`), `admin/partners`'s create/edit dialog (`adminUserId`) and
|
||||
detail page (sponsored-nurse assignment, via `NursePicker`). `admin/alerts`' "assign to me" doesn't need a
|
||||
picker (it targets the current admin, not an arbitrary user) — its fix is below.
|
||||
- **Fixed the alert assign-to-self fallback:** `admin/alerts/page.tsx`'s `meId = authState.currentUser?.id
|
||||
?? 1` is gone. The button is now `disabled` with a `title`/`Tooltip` («در حال بارگذاری حساب شما…») until
|
||||
the real id hydrates — it can never silently target user `#1`. `SupportAlertCard` gained
|
||||
`assignSelfDisabled`/`assignSelfDisabledTitle` props for this. The admin ticket thread's new "assign to
|
||||
me" control (§3.4) uses the identical pattern.
|
||||
|
||||
### 3.3 Verification desk — the flagship trust queue
|
||||
- **Status tabs with counts:** the lone 3-value select is now MUI `Tabs` (all/pending/in_review), each
|
||||
showing a `(count)` suffix when the server serves `counts` (REQ-062, gap — mock computes real counts over
|
||||
the whole unfiltered queue; the real client sends the param but the response field stays `undefined`, so
|
||||
the tabs render without badges until delivered — never a fake count).
|
||||
- **Name/phone search** behind the established draft-vs-applied Apply/Clear pattern (mirrors tickets/audit).
|
||||
- **Waiting-time column:** client-computed, display-only relative age off `submittedAt`, colored past
|
||||
`WAITING_TIME_WARNING_HOURS=48` (`--bal-warning`) and `WAITING_TIME_ALARM_HOURS=96` (`--bal-error`) —
|
||||
named constants, never magic numbers.
|
||||
- **Next/prev case navigation:** `admin/verification/[nurseId]/page.tsx` re-derives the queue's filters/page
|
||||
from the URL (a new `queueFilters.ts` shared by both pages) and calls `useVerificationQueue` with the same
|
||||
params — React Query serves it from the list's own cache, no extra fetch — to compute the previous/next
|
||||
`nurseVerificationId` in the current queue order. «پرونده بعدی»/«پرونده قبلی» buttons + `ArrowLeft`/
|
||||
`ArrowRight` window keydown bindings (ignored while focus is in a text input). A full split-pane case view
|
||||
stays explicitly **out of scope** (deferred post-chain) per the phase brief.
|
||||
- `CredentialDialog`'s two native `type="date"` inputs (issued/expires) are now `JalaliDateField`.
|
||||
- `DocumentViewer`'s signed-URL flow is untouched (do-not-regress, confirmed).
|
||||
|
||||
### 3.4 Ticket console — lifecycle + a safe composer
|
||||
- **Close/reopen/assign mutations** (`useCloseTicket`/`useReopenTicket`/`useAssignTicket`, REQ-063, gap) —
|
||||
full mock implementations (`StoredTicket` gained `assigneeUserId`) and `clientApi` methods mapped to
|
||||
proposed routes, all gated behind `TICKET_LIFECYCLE_ENABLED` (`services/tickets/constants.ts`, default
|
||||
`false`) so no control points at a 404ing route in production. The thread header gets close/reopen
|
||||
buttons (via `ConfirmDialog`, no reason required — closing is terminal, not destructive) + "assign to me"
|
||||
(disabled-with-tooltip until hydrated, never a fallback id), all also gated on `caps.canManageTickets`.
|
||||
- **Scroll-to-latest:** the thread reuses `useThreadScroll` (built in ui-phase-10, explicitly earmarked in
|
||||
its own docstring for "phase 11's admin thread scrollbox next") — attaching its `bottomRef` after the
|
||||
message list is the only change needed; the hook's own initial-scroll effect handles the rest.
|
||||
- **Internal-note mode made unmistakable:** the composer `Paper` turns amber (`--bal-warning`/
|
||||
`--bal-warning-soft`, both schemes) and the send button relabels to «ثبت یادداشت داخلی» whenever `mode ===
|
||||
'internal'` — the safety cue now lives on the action itself, not only on the toggle above it.
|
||||
- **Queue columns:** an activity column + a results footer (`AdminDataTable`'s new `footer` prop). REQ-028's
|
||||
`unreadCount`/`lastMessageAt` are real on the **user** ticket list but the admin queue (`AdminTicketSummary`)
|
||||
is explicitly served `unreadCount = 0` per that REQ's delivery note — this phase does **not** re-file that
|
||||
admin-side extension (it's ui-phase-10's to own); the queue renders a `createdAt`-based activity column as
|
||||
the honest fallback.
|
||||
|
||||
### 3.5 Money-desk safety
|
||||
- **Fixed the UTC off-by-one:** `admin/payouts/page.tsx`'s `isoDate` helper now formats via local
|
||||
`getFullYear()/getMonth()/getDate()` instead of `toISOString().slice()` — near Tehran midnight the
|
||||
prefilled payout window no longer lands on yesterday. Adopted `JalaliDateField` for the period inputs.
|
||||
- **Payout run confirm shows the movement summary + a typed confirmation:** the final run-confirm dialog now
|
||||
shows the batch total (via `<Money>`, sourced from the already-fetched preview — **never** recomputed),
|
||||
the eligible-nurse count, and the processing date, then requires typing «تایید» or the exact amount before
|
||||
the confirm button enables. This is a new, generic capability on the **shared** `ConfirmDialog`
|
||||
(`requireTypedConfirmation: string[]`/`typedConfirmationLabel`/`typedConfirmationPlaceholder`) — additive,
|
||||
zero behavior change for every other caller, co-located test coverage added.
|
||||
- **Reconcile/retry polish:** transfer-reference entry stays `dir="ltr"` (confirmed unchanged); failed-payout
|
||||
rows keep their visible failure reason + `useRetryPayout` retry. `SkippedNurse.reason` was checked against
|
||||
the real path (`previewPayoutBatch` currently always returns `skipped: []` — a REQ-036 gap; only the mock
|
||||
invents string reasons) and correctly left `dir="ltr"` free text, per the phase note, rather than inventing
|
||||
a translation map for something that isn't a documented stable code today.
|
||||
- The payout-batch detail page is unified onto `PageHeader` and adopts the page-only slice of
|
||||
`useAdminListState`.
|
||||
|
||||
### 3.6 Primitives v2
|
||||
- **`AdminDataTable`:** optional per-column `sortable` + a `sort`/`onSortChange` pair (renders MUI's native
|
||||
`TableSortLabel`, the caller owns the 3-state cycle), an opt-in `stickyHeader` (bounded scroll viewport,
|
||||
`stickyMaxHeight`, MUI's own `stickyHeader` mechanics — a self-contained viewport rather than depending on
|
||||
window-scroll math or a `layout/` import), per-column `minWidth`, and a `footer?: string` line (callers
|
||||
pass the existing `t('showing_range', {from,to,total})` i18n key — it already existed in the `admin`
|
||||
namespace, just unused until now). `align: 'inherit'` and the horizontal-scroll container are unchanged.
|
||||
- **`AdminPager`:** the `admin.page_indicator` i18n key regained its `{total}` («صفحه {page} از {total}» —
|
||||
it had regressed to `"صفحه {page}"` while the non-admin `common.page_indicator` kept the full form). The
|
||||
component's own API is unchanged (it still takes a caller-composed `indicator` string, matching its
|
||||
established pattern); every caller across the whole admin/partner surface now passes
|
||||
`t('page_indicator', { page, total: pageCount })`.
|
||||
- **Detail headers unified onto the shared `PageHeader`:** the four divergent patterns (verification case,
|
||||
ticket thread, payout batch, partner-center detail) all now render through `PageHeader`. It gained two
|
||||
small, additive props during integration: `meta?: ReactNode` (a chip-row slot below the title, distinct
|
||||
from the button-oriented `actions` — the ticket thread's category/status/linked-record chips) and
|
||||
`onBack?: () => void` (an alternative to `backTo` for `useAdminBackToList`-style back navigation, takes
|
||||
precedence when both are given). Both are additive/optional; no existing caller changed behavior.
|
||||
- **Jalali date inputs everywhere:** `JalaliDateField` replaces every native `type="date"` under `/admin` —
|
||||
audit from/to, the payout window, the holiday date, and the credential issued/expires fields. No Gregorian
|
||||
native input remains anywhere in the backoffice.
|
||||
- **`AuditLogRow`:** the expand chevron now rotates on open (CSS `transform`, `--bal-motion-fast`), the
|
||||
header carries `role="button"`/`tabIndex`/`aria-expanded` + `Enter`/`Space` keyboard support (kept as a
|
||||
`Stack` with ARIA semantics rather than a real `<button>`, avoiding a polymorphic-`component` TS friction
|
||||
point). A new `actorLabel` prop (+ exported `actorDisplay`/`actorLabelFrom` helpers) renders the resolved
|
||||
actor name, falling back to `#id` until the REQ-061 lookup lands; the audit page and the roles grid both
|
||||
batch-resolve every visible actor id in one `useUserLookup` call.
|
||||
|
||||
### 3.7 Partner portal — professional, light-touch
|
||||
- **Localized booking statuses:** the 7 wire codes (`pending_payment`…`cancelled`) now render through
|
||||
`StatusChip` + translated labels (`bstatus_*`) in **both** the table and the filter menu — the worst
|
||||
partner-facing defect the audit found (a raw English code shown to a Persian-speaking center admin) is
|
||||
gone. `partner/bookings/page.tsx` also adopts `useAdminListState`.
|
||||
- **Scoped read-only booking detail:** a new `partner/bookings/[id]/page.tsx` — dates, a status timeline
|
||||
(the shared `StatusTimeline`), and the patient's display name only, **no** clinical content, address, or
|
||||
money. Backed by a new `getMySponsoredBookingDetail` on `PartnerCenterApi` (REQ-064, gap — mock-backed
|
||||
with a synthetic timeline consistent with the booking's current status).
|
||||
- **CSV export on settlement:** a new dependency-free `client/src/utils/toCsv.ts` (CRLF line endings, proper
|
||||
comma/quote escaping, co-located tests) drives a «خروجی CSV» button on `partner/settlement/page.tsx` — a
|
||||
client-side, current-result-set export via a UTF-8-BOM-prefixed `Blob` download, so Persian text renders
|
||||
correctly when opened in Excel.
|
||||
- **Portal identity in the chrome:** `PartnerLayout`'s TopBar identity slot (already showing the center's
|
||||
name) gained a compact merchant-of-record `StatusChip` beside it, so the MoR state is now visible on
|
||||
every portal page, not only the home screen. Additive change to shared layout chrome; `ProfileSummary`
|
||||
itself is untouched.
|
||||
|
||||
### 3.8 Dead ends & honest nav
|
||||
- **`admin/users`** is now a real read-first directory (was a `PlaceholderScreen`) — search by name/phone
|
||||
over the same REQ-061 seam `UserPicker` uses, role chips, and a per-row link into the audit log
|
||||
(`/admin/audit?entityType=User&entityId={id}` — the one console that already supports an entity filter).
|
||||
A ticket-queue deep link was deliberately **not** offered: the admin ticket queue has no actor/user filter
|
||||
to land on, and a fake-looking link would be dishonest.
|
||||
- **`admin/notifications`** stays a placeholder, but it was never in `AdminLayout`'s nav to begin with
|
||||
(removed in ui-phase-10 — no real admin feed exists yet) — so "no placeholder screen reachable from the
|
||||
admin nav" is already satisfied without further action this phase.
|
||||
- Small verified cleanups: the dead `dataType==='int'||'decimal'?'text':'text'` ternary in
|
||||
`admin/config/page.tsx` is gone (the `TextField` just has no `type` prop now — `text` was always the only
|
||||
outcome); `admin/holidays/page.tsx`'s lying `TODAY_ISO = ''` constant is replaced by a real
|
||||
`todayLocalIso()` helper (local date, not UTC — the same class of fix as the payout window).
|
||||
|
||||
## How this was built (process note)
|
||||
Given the scope (8 independent subsystems touching ~50 files), four slices ran as **parallel background
|
||||
agents** with disjoint file ownership (ticket console, verification desk, money desk, partner portal), each
|
||||
briefed with the shared primitives' exact APIs up front; the remaining pages (roles, partners, alerts,
|
||||
config, holidays, audit, reviews, users) and every shared primitive (`useAdminListState`, `UserPicker`/
|
||||
`NursePicker`, `AdminDataTable`/`AdminPager` v2, `AuditLogRow` v2) were built directly. Two real integration
|
||||
bugs surfaced only once all slices landed together: `useAdminListState<F extends Record<string, unknown>>`'s
|
||||
generic constraint rejected every `interface`-declared filter type across five separate pages (TS quirk —
|
||||
`interface`s don't structurally satisfy a `Record<string, unknown>` constraint the way object-literal types
|
||||
do); fixed by dropping the constraint entirely (the hook never needed it). And the `setDraft(next); apply()`
|
||||
same-handler stale-closure bug (§3.1) appeared independently in two pages; fixed once in the hook
|
||||
(`applyFilters`) and both call sites updated. `PageHeader` was extended twice by two different agents in the
|
||||
same window (`meta` and `onBack`) without collision — both compose cleanly. Full `npm run check` (0 errors)
|
||||
and `npm run test:ci` (114 suites / 522 tests, all passing) were run only after every slice landed, to avoid
|
||||
transient noise from concurrent in-flight edits.
|
||||
|
||||
## What is now testable (and exactly how)
|
||||
1. `/fa/admin/tickets`: apply a status filter + go to page 2 → open a ticket → browser back → filter and
|
||||
page intact; paste the list URL into a new tab → same view.
|
||||
2. `/fa/admin/config`: page past page 1 (mock has >20 configs across data types once seeded further, or
|
||||
verify the pager renders correctly at 1 page today — the mechanism is real either way) → edit a row →
|
||||
saves; the history drawer pages too (`vat_rate`/`platform_fee_rate` have seeded history).
|
||||
3. `/fa/admin/roles` → «اعطای نقش»: type a partial name → options show name + masked phone + id → pick one
|
||||
→ the confirm text names the person, not a number.
|
||||
4. `/fa/admin/alerts` before `/me` hydrates (throttle the network in devtools): "assign to me" is disabled
|
||||
with a tooltip.
|
||||
5. `/fa/admin/verification`: search a seeded nurse by name → row found; waiting-time column shows amber/red
|
||||
for old cases; open a case → «پرونده بعدی» walks the queue without returning to the list; arrow keys work.
|
||||
6. `/fa/admin/tickets/[id]`: open a long thread → scrolled to the newest message; toggle internal mode →
|
||||
composer turns amber, send button reads «ثبت یادداشت داخلی»; close (behind `TICKET_LIFECYCLE_ENABLED` —
|
||||
flip it locally to demo) → leaves the open queue; reopen restores it.
|
||||
7. `/fa/admin/payouts`: window defaults match today's local date (test near midnight or spoof the clock);
|
||||
preview → run → the confirm shows total/count/date and stays disabled until «تایید» (or the exact amount)
|
||||
is typed.
|
||||
8. `/fa/admin/audit`: pick from/to with the Jalali picker (no native date input anywhere); expand a row →
|
||||
chevron rotates, `aria-expanded` toggles, the actor shows a resolved name (or `#id` until resolved).
|
||||
9. `/fa/partner/bookings`: statuses render as Persian `StatusChip`s in the table and the filter menu; click
|
||||
a row → the scoped detail (dates/timeline/patient name only); `/fa/partner/settlement` → «خروجی CSV» opens
|
||||
in Excel with correct Persian text.
|
||||
10. `/fa/admin/users`: search a seeded name/phone (min 2 chars) → results show role chips + a working audit
|
||||
link.
|
||||
11. Every pager/footer reads «صفحه ۲ از ۷» / «نمایش ۱–۲۰ از ۱۲۴» with locale digits across all four axes
|
||||
(`/fa`+`/en` × light+dark); no admin nav item leads to a placeholder.
|
||||
|
||||
## What is mocked / waiting on a real service
|
||||
See `mocks-registry.md`'s updated `AdminApi`/`TicketsApi`/`VerificationApi`/`PartnerCenterApi` rows for the
|
||||
full detail. Summary: **REQ-061** (admin user directory — `searchUsers`/`lookupUsers`) backs `UserPicker`/
|
||||
`NursePicker`/`AuditLogRow`'s name resolve; **REQ-062** (verification queue `search` + whole-desk `counts`);
|
||||
**REQ-063** (ticket close/reopen/assign + `assigneeUserId`, gated behind `TICKET_LIFECYCLE_ENABLED`);
|
||||
**REQ-064** (partner scoped single-booking read + timeline). All four are built UI-complete against their
|
||||
domain's mock, with `clientApi` already mapped to the proposed route — flipping the seam is a one-line
|
||||
change per domain once each lands, no hook/component change.
|
||||
|
||||
## Contracts
|
||||
No backend contract was consumed this phase (frontend-only phase). Requests filed in
|
||||
`dev/shared-working-context/frontend/requests/for-backend.md`:
|
||||
- **REQ-061** — Admin user lookup: name/phone search + batch id→label resolve.
|
||||
- **REQ-062** — Verification queue enrichment: name/phone search + per-status counts.
|
||||
- **REQ-063** — Ticket lifecycle mutations (close/reopen/assign).
|
||||
- **REQ-064** — Partner scoped booking detail (read-only summary).
|
||||
|
||||
Not re-filed (referenced instead, per the phase's explicit instruction): ui-phase-10's REQ-059 (admin ticket
|
||||
queue unread/last-activity enrichment).
|
||||
|
||||
## Docs updated
|
||||
- `client/CLAUDE.md` — the `admin/`/`partner/` Project Structure subtrees rewritten for every page's
|
||||
ui-phase-11 changes; a new `components/admin/` line describing `AdminDataTable` v2/`AdminPager`/
|
||||
`AuditLogRow` v2/`UserPicker`/`NursePicker`; `PageHeader`/`ConfirmDialog` entries updated for their new
|
||||
props; `hooks/` and `utils/` entries updated (`useAdminListState`/`useAdminBackToList`, `toCsv.ts`);
|
||||
`PartnerLayout.tsx`'s line updated for the MoR chip; the `services/admin` domain bullet updated for the
|
||||
user-directory hooks.
|
||||
- `dev/shared-working-context/reports/mocks-registry.md` — `AdminApi`/`TicketsApi`/`VerificationApi`/
|
||||
`PartnerCenterApi` rows updated with this phase's additions and the new REQ numbers.
|
||||
- `dev/shared-working-context/frontend/requests/for-backend.md` — REQ-061…064 appended (see Contracts).
|
||||
|
||||
## Foundation extensions (minimal, per the ownership rules)
|
||||
- **`components/common/PageHeader/PageHeader.tsx`** (phase-1-owned): additive `meta?: ReactNode` (chip-row
|
||||
slot) and `onBack?: () => void` (callback back-nav, takes precedence over `backTo`) — both optional, zero
|
||||
behavior change for existing callers, tests updated.
|
||||
- **`components/common/ConfirmDialog/ConfirmDialog.tsx`** (phase-1-owned): additive
|
||||
`requireTypedConfirmation?: string[]`/`typedConfirmationLabel?`/`typedConfirmationPlaceholder?` — the
|
||||
typed "type X to proceed" guard for an irreversible action, generalized from the payout-run use case so
|
||||
any future money-moving confirm can reuse it. Optional, zero behavior change when absent, tests updated.
|
||||
- **`components/admin/SupportAlertCard.tsx`**: additive `assignSelfDisabled`/`assignSelfDisabledTitle` props.
|
||||
- **`components/messaging/useThreadScroll.ts`** (ui-phase-10-owned, pre-earmarked for this handoff):
|
||||
consumed as-is by the admin ticket thread — no changes needed.
|
||||
|
||||
## Follow-ups for later phases
|
||||
- **REQ-061/062/063/064** as filed — each domain's real `clientApi` is already written and route-shaped;
|
||||
flipping `USE_*_MOCK` (or `TICKET_LIFECYCLE_ENABLED` for REQ-063 specifically) is the only change needed
|
||||
once the backend lands.
|
||||
- **ui-phase-12 (copy/motion)** sweeps these surfaces per the standard handoff — the URL-synced list state,
|
||||
the picker pattern, and the four new REQ numbers are the load-bearing decisions to carry forward (also
|
||||
saved to persistent memory, see below).
|
||||
- A full split-pane verification case view (queue rail + case detail) was explicitly deferred — the
|
||||
next/prev affordance delivers the throughput win this phase asked for at a fraction of the layout risk;
|
||||
worth revisiting if reviewer throughput is still a bottleneck after this ships.
|
||||
- Admin ticket-queue `unreadCount`/`lastMessageAt` enrichment (the REQ-028 admin-side gap) stays
|
||||
ui-phase-10's to file — referenced, not duplicated, here.
|
||||
Reference in New Issue
Block a user