frontend phase 14
This commit is contained in:
@@ -12,6 +12,45 @@ for awareness.
|
||||
- **Requests filed:** frontend/requests/for-backend.md (yes/no)
|
||||
-->
|
||||
|
||||
## frontend-phase-14-b15 — Messaging (tickets) & notifications — 2026-07-10
|
||||
- **Shipped:** the social/communication layer for the customer **and** nurse apps (role decides chrome, not
|
||||
the components; admin lens DEFERRED to f15). Two new domains — **`services/tickets`** (`useMyTickets` /
|
||||
`useTicket` = the whole thread in one `detail(id)`, no message pagination / `useTicketThread` = select over
|
||||
detail / `useOpenTicket` / `usePostMessage` **optimistic, draft-preserving**, reconcile by `clientMessageId`)
|
||||
and **`services/notifications`** (`useNotifications` unread-first / `useUnreadCount` the **polled** bell —
|
||||
count-only, stale-while-revalidate / `useMarkNotificationRead` + `useMarkAllRead` optimistic `setQueryData`).
|
||||
Screens shared by both shells: **My Tickets inbox** (`/support/tickets`, `/nurse/support/tickets`) with
|
||||
prominent `referenceCode` + unread indicator + null-safe linked-entity hint + Contact-support dialog (shows the
|
||||
new `referenceCode`); **thread** (`/…/tickets/[id]`) role-aware bubbles + sticky composer; **notification
|
||||
center** (`/notifications`, `/nurse/notifications`) unread-first, mark-read-on-open + mark-all, deep-links via
|
||||
`notificationDeepLink`. **Notification bell** in the customer TopBar + the nurse shell (subscribes to the poll so
|
||||
only it re-renders). **Emergency banner** + **"Get support / Open ticket"** on the f8 booking detail
|
||||
(`BookingSupportEntry`, reuses the cached booking + nurse-gated care query — no refetch); a support icon in the
|
||||
customer TopBar + a Support item in the nurse sidebar. New shared/tested composites: `MessageBubble`,
|
||||
`TicketListCard`, `EmergencyBanner`, `NotificationRow`, `NotificationBellView`, `ContactSupportDialog` (+
|
||||
screens); `notificationDeepLink`/`parseNotificationData` unit-tested; `support`/`send` icons; `tickets` +
|
||||
`notifications` i18n namespaces + `nav.support` (both locales).
|
||||
- **Consumes:** dev/contracts/domains/messaging-notifications-admin.md (b15 tickets) + config-reference.md (b1
|
||||
notifications) + openapi/swagger.v1.json. Real & mapped 1:1 by the client APIs: `POST/GET /tickets`,
|
||||
`GET /tickets/{id}`, `POST /tickets/{id}/messages`; `GET notifications/get_notifications`,
|
||||
`get_unread_count`, `POST mark_notification_read` / `mark_all_read`.
|
||||
- **Critical rules honoured:** `is_internal` is **never** modelled in the user-app types — both API mappers DROP
|
||||
any internal message (server-strip mimic), no internal affordance anywhere; the emergency surface is a
|
||||
**post-confirmation `tel:` playbook only** (nurse-gated care contact, no VoIP seam, never a general phone);
|
||||
the unread **count** polls politely (60s interval + 45s staleTime + refetch-on-focus, auth-gated) and the list
|
||||
is never polled; `data_json` is parsed into a typed union and degrades to no-deep-link.
|
||||
- **Mocked client-side:** `services/tickets` (`USE_TICKETS_MOCK`) — b15 is live and `ticketsClientApi` maps it
|
||||
1:1, but the linked bookings are mock-primary and the summary lacks `unreadCount`/`lastMessageAt` (REQ-028);
|
||||
`services/notifications` (`USE_NOTIFICATIONS_MOCK`) — b1 is live and mapped 1:1, but nothing dispatches
|
||||
notifications client-side yet. Both default `true`; swap is one flag. See mocks-registry.
|
||||
- **Gate:** npm run check green · npm run test:ci green (66 suites / 289 tests, +32) · production build compiles
|
||||
+ type-checks clean (a **pre-existing** "Missing .env variable" prerender guard fails on `/en/addresses` +
|
||||
`/en/nurse/verification/identity` only — unrelated to this phase; sibling pages under the same modified shells
|
||||
prerender fine). 6-dimension adversarial review with per-finding verification.
|
||||
- **Requests filed:** frontend/requests/for-backend.md — yes (REQ-028: `unreadCount`+`lastMessageAt` on the
|
||||
ticket summary, a message author label / masked-name confirmation, a by-booking user ticket lookup, and an
|
||||
optimistic `clientMessageId` idempotency field).
|
||||
|
||||
## frontend-phase-13-b14 — Reviews & patient care records — 2026-07-10
|
||||
- **Shipped:** the last feature-domain phase. Two new domains — **`services/reviews`** (`useNurseReviews`
|
||||
infinite published-only aggregate+list / `useReviewEligibility` / `useMyReviewForBooking` / `useCreateReview`
|
||||
|
||||
@@ -456,3 +456,35 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a
|
||||
until they land. **Note:** the wireframe's four-tab E2 record is not in the data model — please confirm whether
|
||||
the family-owned record is a real MVP entity or a future addition (the client treats it as forward-looking).
|
||||
- **Status:** open
|
||||
|
||||
## REQ-028 — Ticket inbox enrichment (unread + last-activity), message author name, by-booking lookup, optimistic idempotency — filed by frontend-phase-14-b15 — 2026-07-10
|
||||
- **Need:** Four additions the f14 messaging UI renders that the b15 `TicketSummaryDto`/`TicketThreadDto`/message
|
||||
routes don't yet expose (the two ticket screens work today because `services/tickets` is **mock-primary** —
|
||||
`USE_TICKETS_MOCK = true`; the real `ticketsClientApi` maps every live b15 route 1:1 and leaves these blank):
|
||||
1. **`unreadCount` + `lastMessageAt` on `TicketSummaryDto`.** The inbox card shows an **unread indicator** and
|
||||
sorts/labels by **last activity**, but the wire summary carries only `createdAt` (no unread, no last-message
|
||||
time). Today the client falls back to `createdAt` and hides the unread badge on the real path; the mock
|
||||
supplies both. Proposed: `TicketSummaryDto { …, lastMessageAt: string (UTC), unreadCount: int }` (unread =
|
||||
messages after the caller's `last_read_at`, internal excluded).
|
||||
2. **A message author display label on `TicketMessageDto`** (or on `TicketParticipantDto`). The thread bubble
|
||||
labels each message by author, but the DTO carries only `senderId` — no name. The client derives the label
|
||||
from the participant **role** (`author_customer`/`author_nurse`/`author_support`/`author_system`) and never
|
||||
shows a raw identity (privacy — the thread is deliberately not a contact directory). Please **confirm this
|
||||
role-label approach is intended**, or add a **server-masked** display name if a name is wanted. (No phone /
|
||||
no real name is the safe default.)
|
||||
3. **A user-facing by-booking ticket lookup/filter.** `GET /tickets` (the user list) filters only by
|
||||
`Status`/`ReferenceCode` — there's no `bookingId` filter (only the **admin** list has one). The
|
||||
"Get support" action on a booking is specced to **jump to the existing coordination ticket** if one exists;
|
||||
without a by-booking read the client can't find it. The mock approximates this via idempotent
|
||||
`openTicket(category:'coordination', bookingId)` (returns the existing thread). Proposed: add a `bookingId`
|
||||
query param to `GET /tickets`, or `GET /tickets/by_booking/{bookingId}?category=coordination`.
|
||||
4. **An idempotency / `clientMessageId` field on `POST /tickets/{id}/messages`.** The optimistic send generates a
|
||||
`clientMessageId` to reconcile the pending bubble; the server has no field for it, so a retried send (network
|
||||
flap) could create a duplicate server-side. The client does **not** send it today (reconciliation is
|
||||
client-only). Proposed: accept an optional `clientMessageId`/`idempotencyKey` on the post-message body and
|
||||
dedupe on it, echoing it back on `PostMessageResult`.
|
||||
- **Why:** the f14 inbox (unread + last-activity), the thread (author labels + jump-to-coordination), and the
|
||||
optimistic composer render these; the domain is mock-primary precisely because (1)/(3) aren't served and the
|
||||
linked bookings are themselves mock-primary. When they land the swap is a single `USE_TICKETS_MOCK = false` flip
|
||||
(no hook/component change) — `ticketsClientApi` already maps the live routes.
|
||||
- **Status:** open
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# Frontend Phase 14 — Messaging (tickets) & notifications — report
|
||||
|
||||
**Track:** frontend · **Depends on:** frontend-phase-8-b9 (booking detail) · **Consumes:** b15 tickets
|
||||
([messaging-notifications-admin.md](../../contracts/domains/messaging-notifications-admin.md)) + b1 notifications
|
||||
([config-reference.md](../../contracts/domains/config-reference.md)) · **Unlocks:** frontend-phase-15-b15 (admin
|
||||
& partner consoles — reuses these services with the admin lens). Date: 2026-07-10.
|
||||
|
||||
The mission: give families and nurses the **only** sanctioned way to talk after a booking — the admin-readable
|
||||
**ticket** system — plus the in-app **notification center** + polled **bell**, and the **emergency playbook
|
||||
banner** on booking/support entry. No live chat by design; structured, auditable, anti-disintermediation.
|
||||
|
||||
---
|
||||
|
||||
## 1. What was built
|
||||
|
||||
### Two new domain services (copied the `auth`/`reviews` skeleton exactly)
|
||||
- **`services/tickets`** — `types.ts` (client model, deliberately **no `isInternal`**), `keys.ts` (one
|
||||
`detail(id)` = the whole thread; there is no message pagination in the contract — a `select`-based
|
||||
`useTicketThread` derives the messages, mirroring f8 `useBookingSessions = select over detail`), `constants.ts`
|
||||
(`USE_TICKETS_MOCK`, stale times, `MOCK_SEND_FAIL_SENTINEL`, per-role `MOCK_VIEWER_USER_ID`), `apis/`
|
||||
(`clientApi` maps b15 1:1 + **drops any internal message defensively**, `mockApi` PRIMARY, selecting `index`),
|
||||
hooks `useMyTickets` / `useTicket` / `useTicketThread` / `useOpenTicket` / `usePostMessage` (+ internal
|
||||
`useTicketViewer`), barrel.
|
||||
- **`services/notifications`** — `types.ts` (`AppNotification` + discriminated `NotificationData` union),
|
||||
`parse.ts` (`parseNotificationData` — snake/camel-tolerant, degrades to `{kind:'none'}`), `deepLink.ts`
|
||||
(`notificationDeepLink(n, role)` — role-aware, `null` when nothing to open), `keys.ts`, `constants.ts`
|
||||
(`USE_NOTIFICATIONS_MOCK`, poll interval/staleTime), `apis/` (`clientApi` maps b1 1:1, `mockApi` PRIMARY),
|
||||
hooks `useNotifications` / `useUnreadCount` (**the poller**) / `useMarkNotificationRead` / `useMarkAllRead`,
|
||||
barrel (+ re-exports `notificationDeepLink`).
|
||||
|
||||
### Screens (shared by the customer **and** nurse apps — role decides chrome, not components)
|
||||
- **My Tickets inbox** — `/support/tickets` (customer) + `/nurse/support/tickets` (nurse), both thin wrappers
|
||||
over `<TicketInboxScreen role=…>`: prominent `referenceCode`, status chip (reused `StatusChip`), unread
|
||||
indicator, null-safe linked-entity hint, relative Shamsi time; empty/loading-skeleton/error→retry; a
|
||||
**Contact support** dialog (category → subject → message → submit → shows the new `referenceCode`).
|
||||
- **Ticket thread** — `/…/tickets/[id]`: role-aware bubbles (mine vs theirs, **RTL-mirrored**), `referenceCode`
|
||||
in the header, linked-booking chip, sticky **optimistic** composer; thread skeleton / empty / error states;
|
||||
**never any internal-note content or affordance**.
|
||||
- **Notification center** — `/notifications` + `/nurse/notifications` (`<NotificationCenter role=…>`):
|
||||
unread-first list, each row **marks read on open** (optimistic) and **deep-links via `notificationDeepLink`**;
|
||||
**Mark all read**; empty ("بهروز هستید") / loading / error→retry; a growing-limit "load more".
|
||||
- **Notification bell** — `<NotificationBell role=…>` in the customer TopBar (+ a support icon) and the nurse
|
||||
shell (via a new `headerActions` slot on `TopBarAndSideBarLayout`; a Support item added to the nurse sidebar).
|
||||
It subscribes to the polling count so **only the bell re-renders** on a change, never the shell.
|
||||
- **Emergency banner + support entry on the f8 booking detail** — `<BookingSupportEntry bookingId role>`
|
||||
(page-local glue, mounted below `BookingDetailView` on both the customer `/bookings/[id]` and nurse
|
||||
`/nurse/visits/[id]` pages). It **reuses the cached booking query** (same key/viewer — no refetch) and, for
|
||||
the **nurse on a post-confirmation booking**, the cached care-instructions read to surface the emergency
|
||||
banner's `tel:` contact. Pre-confirmation → nothing. Customer → the support CTA only (no clinical phone).
|
||||
|
||||
### Shared composites (co-located `*.test.tsx`)
|
||||
`MessageBubble` (mine/theirs, RTL, no internal styling), `TicketListCard` (prominent ref code + unread + null-safe
|
||||
link), `EmergencyBanner` (post-confirmation `tel:` playbook, no VoIP seam), `NotificationRow` (unread emphasis +
|
||||
server title/body), `NotificationBellView` (pure badge), `ContactSupportDialog`. Plus unit tests for
|
||||
`notificationDeepLink` and `parseNotificationData`. **+32 test cases; the full suite is 66 suites / 289 tests.**
|
||||
|
||||
### i18n + icons + routes
|
||||
`tickets` + `notifications` namespaces added to **both** `fa.json` and `en.json` (in sync, RTL-first) + `nav.support`;
|
||||
`support` + `send` icons; route constants + role-aware path helpers (`ticketThreadPath`, `notificationsPath`, …).
|
||||
|
||||
---
|
||||
|
||||
## 2. Critical rules honoured (phase §5)
|
||||
|
||||
- **`is_internal` never reaches the user app.** No `isInternal` field in the user-app types, no internal styling,
|
||||
no internal-note affordance anywhere. **Both** API mappers (`ticketsClientApi.mapThread` and the mock's
|
||||
`toDetail`) **drop** any message flagged internal — the mock even stores an internal admin note it never
|
||||
returns, so the no-leak behaviour is demonstrable.
|
||||
- **No out-of-band channel except the post-confirmation emergency `tel:`** — drawn from the nurse-gated f8 care
|
||||
read; the customer never sees a phone; no VoIP/calling seam; no SLA timers.
|
||||
- **`referenceCode` shown prominently** in the inbox card + thread header.
|
||||
- **Polite polling** — only `useUnreadCount` polls (60s `refetchInterval` + 45s `staleTime` + refetch-on-focus,
|
||||
auth-gated); the notification **list is never polled**.
|
||||
- **Optimistic send is draft-preserving** — `onMutate` appends a pending bubble; `onError` rolls back to the
|
||||
snapshot; the composer keeps the draft (cleared **only** on server confirm) and retries; reconcile by
|
||||
`clientMessageId` (no double-render); submit disabled while sending. The composer is **keyed on `ticketId`**
|
||||
so a draft / in-flight send never crosses a thread→thread navigation.
|
||||
- **`data_json` is a typed contract** — parsed into the discriminated union, deep-linked off that, degrades to
|
||||
no-deep-link for an unknown type / missing id, never trusts a blob.
|
||||
- **Tenancy / null links** — reads are server-scoped; ticket↔booking/refund chips render only when present.
|
||||
|
||||
---
|
||||
|
||||
## 3. How to test (what a human can verify) — the phase §7 steps
|
||||
|
||||
Run `npm run dev` (mocks are primary — `USE_TICKETS_MOCK`/`USE_NOTIFICATIONS_MOCK` default `true`):
|
||||
|
||||
1. **Open a ticket from a booking.** Confirmed booking detail → **"Get support / Open ticket"** → a coordination
|
||||
ticket opens (booking 5001 already has one → it **jumps to the existing thread**, else creates) and lands in
|
||||
**My Tickets** with its `referenceCode`. *Expected:* it appears at the top of the inbox with no manual refresh.
|
||||
2. **Post a message (optimistic).** Open a thread, type, send → the bubble appears **immediately** ("sending") then
|
||||
resolves to "sent". Send the dev sentinel body **`/fail`** → the bubble rolls back, **the text stays in the
|
||||
composer**, and the "send failed" hint shows; edit + resend succeeds. *Expected:* no duplicate bubble, no lost draft.
|
||||
3. **No internal notes leak.** Ticket 1201 (coordination, booking 5001) has a **seeded internal admin note** — the
|
||||
user thread shows **none of it**, no styling, no affordance.
|
||||
4. **Notification bell.** From the console call `window`-reachable dev helper (or import) `__mockPushNotification('ticket_message','پیام جدید','{"ticket_id":1201}')` → the **bell badge increments** within ~60s. Open the center,
|
||||
open a notification → it **marks read**, the **badge decrements**, and it **deep-links** (booking → `/bookings/{id}`,
|
||||
ticket → `/support/tickets/{id}`, etc.). **Mark all read** clears the badge. *Expected:* the count is served from
|
||||
cache instantly and revalidates in the background; the endpoint isn't hit more often than the interval.
|
||||
5. **Emergency banner.** On a **confirmed** booking in the **nurse** app, the banner shows with a `tel:` link
|
||||
(booking 5001/5002 have a seeded contact) + the playbook copy; on an **unconfirmed** booking it is **absent**.
|
||||
The support entry (inbox) shows the playbook without a specific phone.
|
||||
6. **RTL + locales.** Switch `fa`/`en`: bubbles mirror, the badge sits correctly, every string is translated.
|
||||
`npm run check` + `npm run test:ci` pass.
|
||||
|
||||
---
|
||||
|
||||
## 4. Mocks behind the two seams (swap = one flag)
|
||||
|
||||
Both domains are **mock-primary** — recorded in [mocks-registry.md](./mocks-registry.md):
|
||||
|
||||
- **`services/tickets` (`USE_TICKETS_MOCK = true`)** — `ticketsClientApi` maps every live b15 route 1:1
|
||||
(`POST/GET /tickets`, `GET /tickets/{id}`, `POST /tickets/{id}/messages`) and defensively drops any leaked
|
||||
internal message. The mock is primary because the linked bookings are themselves mock-primary and the wire
|
||||
summary lacks `unreadCount`/`lastMessageAt` (**REQ-028**). It seeds 3 tickets (incl. the no-leak internal note
|
||||
+ a booking-linked coordination ticket with idempotent open), a clears-on-open unread count, an optimistic
|
||||
append attributed to the current viewer (`openTicket` now takes the viewer id; `postMessage` tracks the
|
||||
last-viewed thread's viewer), a closed-ticket `403`, and the `/fail` failure trigger.
|
||||
- **`services/notifications` (`USE_NOTIFICATIONS_MOCK = true`)** — `notificationsClientApi` maps every live b1
|
||||
route 1:1. The mock is primary because nothing dispatches notifications client-side yet. It seeds an
|
||||
unread-first feed spanning every deep-link class (each mapped through the **real** `parseNotificationData`) and
|
||||
exposes `__mockPushNotification` for the bell-increment demo.
|
||||
|
||||
Flip either flag to `false` (one line in `apis/index.ts` selection via `constants.ts`) — no hook/component change.
|
||||
|
||||
---
|
||||
|
||||
## 5. Contract consumed + gaps filed
|
||||
|
||||
Consumed: `messaging-notifications-admin.md` (b15) + `config-reference.md` (b1) + `openapi/swagger.v1.json`
|
||||
(`TicketSummaryDto`/`TicketThreadDto`/`TicketMessageDto`/`OpenTicketCommand`/`OpenTicketResult`/`PostMessageCommand`/
|
||||
`PostMessageResult`; `NotificationDto`/`UnreadCountResult`/`MarkNotificationReadCommand`).
|
||||
|
||||
Gap filed — **REQ-028** in [for-backend.md](../frontend/requests/for-backend.md): (1) `unreadCount` + `lastMessageAt`
|
||||
on `TicketSummaryDto`; (2) a message author label / confirm masked-by-design (the client uses the participant
|
||||
**role** label, never a raw name); (3) a **user-facing by-booking ticket lookup** (only the admin list filters by
|
||||
`bookingId`) so "Get support" can jump to the existing coordination thread on the real path; (4) an optional
|
||||
`clientMessageId`/idempotency field on `POST …/messages`. Business-rule drift check: the contract does **not**
|
||||
expose `is_internal` to users (the user `GET /tickets/{id}` is server-stripped) — no drift; the client models it
|
||||
accordingly.
|
||||
|
||||
---
|
||||
|
||||
## 6. Review + gate
|
||||
|
||||
- **6-dimension adversarial review** (leak-and-tenancy, optimistic-send, notifications-cache-poll,
|
||||
conventions-rtl-i18n, contract-fidelity, reuse-bugs) with per-finding adversarial verification. **1 finding
|
||||
survived verification** — the mock `openTicket` mis-attributing a nurse-opened ticket's first message to the
|
||||
customer (module-global `lastViewerUserId` defaulted to the customer). **Fixed** by threading the viewer id
|
||||
through the `openTicket` seam (real path ignores it; server infers the sender) and adding the opener as a
|
||||
participant. A second flagged item (composer state crossing a thread→thread navigation) did **not** survive
|
||||
verification, but the composer was **keyed on `ticketId`** anyway as a correct-by-construction safeguard.
|
||||
- **Gate:** `npm run check` green; `npm run test:ci` green (66 suites / 289 tests, +32); production build
|
||||
**compiles + type-checks clean**. A pre-existing "Missing .env variable" prerender guard fails SSG on
|
||||
`/en/addresses` + `/en/nurse/verification/identity` only — unrelated to this phase (sibling pages under the
|
||||
same modified shells prerender fine); not introduced here.
|
||||
|
||||
---
|
||||
|
||||
## 7. Follow-ups for frontend-phase-15-b15 (the admin lens)
|
||||
|
||||
- The admin **global ticket queue** (`GET /admin/tickets` + `GET /admin/tickets/{id}` with internal messages) and
|
||||
the **internal-note composer** layer on top of `services/tickets` — reuse the domain, add an admin-only
|
||||
`isInternal` toggle **only in the admin view** (never in the user-app types).
|
||||
- The **support-alert worklist** (`support_alerts/*`) + **partner-center** console + **audit viewer** are b15/b1
|
||||
admin surfaces (DEFERRED here).
|
||||
- When REQ-028 lands, flip `USE_TICKETS_MOCK = false`; when upstream domains dispatch real notifications, flip
|
||||
`USE_NOTIFICATIONS_MOCK = false`.
|
||||
@@ -80,3 +80,5 @@ 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 | 🟡 |
|
||||
| `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 | 🟡 |
|
||||
| `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 — but the linked bookings are themselves mock-primary and the wire summary lacks `unreadCount`/`lastMessageAt` (**REQ-028**), so the mock is primary. 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`** (so "Get support" from a booking jumps to the existing thread) and prepends a new ticket to the inbox; `postMessage` appends as the current viewer (tracked from the last `getTicket` so an optimistic message reconciles as **mine** in whichever app is open), throws `403` on a **closed** ticket, and throws `500` on the dev sentinel body `'/fail'` (the optimistic failure→retry path). `MOCK_VIEWER_USER_ID` (per-role "me") drives `isMine`; **`isInternal` is never modelled in the user-app types** | `USE_TICKETS_MOCK` (`services/tickets/constants.ts`, default `true`) | Deliver **REQ-028** (`unreadCount`/`lastMessageAt` on the summary + a by-booking user lookup + optional author name + optional `clientMessageId` idempotency) and make the upstream bookings flow real, then set flag `false` — `ticketsClientApi` already maps the live b15 routes 1:1 (drops any leaked internal message defensively). No hook/component change | 🟡 |
|
||||
| `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 | 🟡 |
|
||||
|
||||
Reference in New Issue
Block a user