99ebf5d881
Light up the two faces of the configurable service catalog over a new cached services/catalog domain (consumes the b5 contract; unlocks f6 search). - services/catalog: types/keys/constants/apis(client+mock+seam)/hooks/index + names.ts. Categories & option groups are session-cached reference data (Infinity staleTime, like geography); variant mutations invalidate myVariantsLists() and setQueryData the edited row. Mock-primary (USE_CATALOG_MOCK), one-line swap; mock reproduces the 400 missing-required and (nurse,category,option-set) 409 duplicate rules. - Customer Home (A5): greeting+avatar, search bar (navigates toward f6), data-driven category grid (loading/empty/error), patient nudge from the cached f2 query. Deferred /search placeholder stub. - Nurse Services & prices (B7) at /nurse/services: offerings list (active vs deactivated, edit, soft-deactivate w/ confirm, reactivate, no delete) and a 3-step variant builder (category -> required/optional options -> price+unit+ duration). Required-group gate; Toman->IRR digit-string at the field boundary (no float); live unit-aware estimated total (never from price alone); editable auto display_name; inline 409 duplicate warning; locked category edit form. - Shared, tested components: CategoryTile, PriceDisplay, VariantCard. Money util: tomanToRial + multiplyIrr (integer-safe) + tests. - i18n: catalog/services/search namespaces + home additions + nav.services (both locales, in sync). Icons, routes (SEARCH, NURSE_SERVICES), nurse nav. Gate: npm run check green; npm run test:ci green (147 tests, +18 across 4 suites); npm run build green with NEXT_PUBLIC_API_URL set. Docs: client/CLAUDE.md (Project Structure, caching note, namespaces), STATUS, for-backend REQ-010 (pagination param casing), phase report, mocks registry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
11 KiB
Frontend → Backend requests (append-only)
The frontend lane appends here when it needs a contract that doesn't exist yet, finds a shape mismatch, or needs a new field/filter/endpoint. The backend agent reads this at the start of each phase and delivers fixes in its own change. Frontend never edits backend code to "fix" a gap — it requests it.
REQ-001 — Confirm response envelope, wire casing & pagination shape — filed by frontend-phase-0 — 2026-07-02
- Need: Authoritative confirmation of three things the frontend types depend on:
- Envelope unwrapping. The b0 swagger shows every response wrapped in
ApiResult({ isSuccess, statusCode, message, requestId, data }). The frontend'sclientFetchcurrently returns the raw body, so domainclientApis read the payload viaunwrap()(data). Confirm this is the intended shape for all endpoints (i.e. payload always underdata), so the pattern is correct before f1+ copy it. - Wire casing. Observed swagger properties are camelCase (
isSuccess,serverTimeUtc) — not the snake_caseapi-conventions.mdimplies for URL segments. Please confirm JSON body casing is camelCase (and, if so, we can note it in the convention doc), or state where it differs. - Pagination payload.
api-conventions.mdsays lists returnitems+total(+page/page_size). Confirm the exact field names/casing on the wire (we've typedPaginated<T>as{ items, total, page, pageSize }inclient/src/lib/api/types.ts).
- Envelope unwrapping. The b0 swagger shows every response wrapped in
- Why: These fix the shared
ApiEnvelope<T>/Paginated<T>types and theservices/{domain}reference pattern every later frontend phase inherits. - Proposed shape:
{ isSuccess: boolean, statusCode: number, message?: string, requestId?: string, data?: T }anddata: { items: T[], total: number, page: number, pageSize: number }for lists. - Status: open
REQ-002 — OTP length + expiry in RequestOtpResult — filed by frontend-phase-1-b2 — 2026-07-02
- Need: Add
codeLength(int) andexpiresInSeconds(int) toRequestOtpResult. - Why: The A2/B2 OTP screen renders one box per digit and (later) a code-expiry hint.
RequestOtpResultcurrently exposes onlyotpSent+resendAvailableInSeconds, so the frontend hardcodes the box count (OTP_CODE_LENGTH = 6, inferred from the live 6-digit verify example, not the 4-box wireframe). Surfacing the length makes the box count contract-driven; the expiry lets us show "code expires in …". - Proposed shape:
{ otpSent: boolean, resendAvailableInSeconds: number, codeLength: number, expiresInSeconds: number } - Status: open
REQ-003 — Machine-readable error codes for verify_otp failures — filed by frontend-phase-1-b2 — 2026-07-02
- Need: A stable
codeon the 400 envelope for verify_otp that distinguishes wrong code vs expired code vs max-attempts lockout (e.g.otp_invalid|otp_expired|otp_locked), and — for lockout — aretryAfterSeconds(orlockedUntil) field. - Why: The OTP screen has explicit wrong-code, expired-code, and max-attempts-lockout states
(per the phase spec), but the contract returns the same safe 400 message for all of them, so the frontend
can't reliably tell them apart. Today it maps a lockout only when it sees the mock's
otp_lockedcode and otherwise degrades to a generic "incorrect or expired" message. A stable machine code (kept generic enough to avoid account enumeration) would let the UI render the precise state + the unlock countdown. - Proposed shape:
{ isSuccess: false, statusCode: 400, message: "…", code: "otp_locked", data: { retryAfterSeconds: 60 } } - Status: open
REQ-005 — Patient relation + conditions fields — filed by frontend-phase-2-b3 — 2026-07-02
- Need: Add two fields to
PatientDtoand thepatients/create+patients/updatebodies:relation(parent|spouse|child|self, nullable) — the care-recipient's relation to the payer.conditions(string[] of stable codes, e.g.elderly/post_surgery/diabetes/mobility/dementia).
- Why: The A3 onboarding step captures the relation, and the A4 form + E1 patient cards show condition
chips. Neither field exists on the wire
PatientDto(onlyinitialMedicalNotesfree-text). The client currently augments them behind theservices/patientsseam (the mock persists them;USE_PATIENTS_MOCK=true) and drops them on the real path. Adding the columns lets the client flip the flag to the live endpoints. - Proposed shape:
PatientDto { …, relation: string|null, conditions: string[] }; same fields accepted on create/update. Enum forrelation;conditionsa stable code list (could also be a normalized child table). - Status: open
REQ-006 — Avatar / object-storage upload route (nurse & customer) — filed by frontend-phase-2-b3 — 2026-07-02
- Need: A multipart image-upload endpoint backed by
IObjectStoragethat returns a stored URL, plus anavatarUrlfield onNurseProfileDto(and laterCustomerProfileDto). e.g.POST api/v1/nurse_profiles/avatar(multipart/form-data) →{ url }, and persistavatar_urlon the profile. - Why: The B7 nurse profile bootstrap and the customer profile both take a profile photo. The b3 contract
has no avatar field or upload route, and the client fetch layer is JSON-only (can't send multipart). The
client mocks this behind the
services/profilesseam (uploadAvatarreturns an object URL). The realprofilesClientApi.uploadAvatarthrows501until this lands. - Status: open
REQ-007 — Customer name + preferred-language update — filed by frontend-phase-2-b3 — 2026-07-02
- Need: Either add
firstName/lastName/preferredLanguageto thecustomer_profiles/upsertbody +CustomerProfileDto, or confirm the customer name is only ever set elsewhere (and how).MeResultexposesfirstName/lastNameread-only with no update endpoint;CustomerProfileDtocarries only the emergency contact. - Why: The customer profile screen edits first/last name + preferred language alongside the emergency
contact. Absent a wire field/endpoint, the client augments name/language behind the
services/profilesseam (mock-persisted; the real upsert sends only the emergency contact). Confirm the intended home for these so the client stops augmenting. - Status: open
REQ-004 — Confirm multi-role disambiguation (activeRole?) — filed by frontend-phase-1-b2 — 2026-07-02
- Need: Confirm whether
MeResultwill gain anactiveRole(the user's currently-selected actor) for a user who holds bothcustomerandnurse, or whether the client should keep owning that choice. - Why: The role router must pick one app for a dual-role user. Absent an
activeRolein the contract, it currently uses the intended role carried from the login switch (A1 vs B1), defaulting to the family app. If the backend intends to persist a "current role", the router should prefer it. Also note: verify_otp returnsrolesbut no userid(only/mehas it) — fine for now (context id is hydrated from/me), flagging in case that changes. - Status: open
REQ-008 — Accept the client-picked map pin on address create/update — filed by frontend-phase-3-b4 — 2026-07-02
- Need: Let
customer_addresses/createandcustomer_addresses/update/{id}accept optionallatitude/longitude(decimals) from the request body — the coordinates the user dropped with the map-pin picker — and persist those when provided, only falling back to theIGeocoderwhen the client sends none. - Why: The b4 contract's create body geocodes server-side from
addressLine+city and does not accept client coordinates, but f3 requires the user to drop a pin on the map (a hard client-side validation) so the stored coordinate is the user's exact door location for the later EVV distance check (b9) — a geocoded street centroid is coarser. The client already sendslatitude/longitudein the create/update body and echoes them locally; until the server accepts them, the real path silently ignores them and geocodes instead. - Proposed shape: create/update body gains
latitude?: number, longitude?: number; when both present, store them (and mark the geocode source as "user-pin"); when absent, geocode as today.CustomerAddressDtoalready returnslatitude/longitude. - Status: open
REQ-009 — Add provinceId to CustomerAddressDto — filed by frontend-phase-3-b4 — 2026-07-02
- Need: Add
provinceId(long) toCustomerAddressDto(the province that owns the address'scityId). - Why: The address book's edit form prefills the cascading province → city → district dropdowns from a
saved address, and the city list is fetched per province (
geo/cities?province_id=). The DTO carriescityIdbut not its province, so the client can't drive the city query to preselect the city without the province id. The client currently augmentsprovinceIdbehind theservices/addressesseam (the mock persists it; the real client echoes the just-saved choice), so editing an address that was loaded fresh from the server can't prefill the province until this lands.cityIdstill implies the province server-side — this is purely to prefill the client cascade. - Proposed shape:
CustomerAddressDto { …, provinceId: long }(join fromcities.province_id). - Status: open
REQ-010 — Confirm/align the list pagination query-param name (catalog + all lists) — filed by frontend-phase-4-b5 — 2026-07-05
- Need: Confirm the exact query-param name the paginated list endpoints bind for page size. The
catalog.mdroute examples write?page=&page_size=(snake_case), but the working b4serviceAreasclient bindspageSize(camelCase, case-insensitive to the server'sPageSizeproperty) — the f3 report flagged this as thepage_size→pageSizegotcha. The f4 catalog client follows the provenpageSizeforcatalog/categoriesandnurse_variants/list; the domain filtercategory_idstays snake_case per the doc. - Why: So the real-endpoint swap (f6 flips
USE_CATALOG_MOCK=false) doesn't silently paginate wrong. If the server truly bindspageSize, please update thepage_sizeoccurrences in the contract docs to match; if it bindspage_size, tell us and we'll switch the client (one line per list call). - Proposed shape: list query =
?page={1-based}&pageSize={≤100}; responsedata={ items, total, page, pageSize }. - Status: open