manual improvement 2 & add telegram bot
This commit is contained in:
+73
-12
@@ -24,6 +24,10 @@ i18n, cookies, and the rules every change must follow.
|
||||
- **next-intl v4** for i18n — locales `fa` (default, RTL) and `en`.
|
||||
- **TanStack Query v5** for server state; a small **AuthContext** (React context + reducer,
|
||||
`src/context/auth/`, seeded with server-read auth state) for auth/session state.
|
||||
- **react-hook-form v7** for every form with more than one field — uncontrolled fields + per-field
|
||||
subscriptions, so a keystroke re-renders one input rather than the whole screen. Never used through
|
||||
the raw `useController`/`register` API at a call site: bind through the `components/common/form`
|
||||
wrappers (`RhfTextField`, `RhfChipSelect`, `RhfJalaliDateField`, `RhfControlGroup`). See **Forms** below.
|
||||
- **notistack** for toasts; **js-cookie** (wrapped) for client cookies.
|
||||
- **Jest** + **Testing Library** for unit tests.
|
||||
- Quality gates: **tsc**, **ESLint 9** (flat config), **Prettier**.
|
||||
@@ -172,14 +176,14 @@ client/
|
||||
│ │ │ ├── practice/page.tsx # /nurse/practice — «حرفهٔ من» group root (NursePracticeScreen): a listing-status card (own TrustBadge + the real accepting-bookings state) over links to profile/services/coverage/verification, each with a count read off its already-cached query (omitted, never faked, while one is in flight)
|
||||
│ │ │ ├── finance/page.tsx # /nurse/finance — «مالی» group root (NurseFinanceScreen): the SIGNED net payable balance (an owed-back reads as an error tone, never clamped) over links to earnings/payout history/bank
|
||||
│ │ │ ├── more/page.tsx # /nurse/more — «بیشتر» group root (NurseMoreScreen): ProfileSummary + ActorSwitcher, support/notifications links with unread badges, SettingsPanel, SignOutRow. Everything the sidebar drawer header and footer used to carry
|
||||
│ │ │ ├── NurseDashboardScreen.tsx # ui-phase-7 — the «امروز» operational home replacing the old PlaceholderScreen: greeting+TrustBadge, NextVisitCard (useTodaySessions), RequestsStrip (useNurseRequestInbox, the most time-critical widget — sorts above earnings), EarningsSnapshotCard (useNurseEarningsBalance, signed net + eligible), DashboardActivationSlot, NotificationsEntryRow (useUnreadCount) — every widget is a read of an already-cached query, four-state pattern throughout
|
||||
│ │ │ ├── NurseDashboardScreen.tsx # ui-phase-7 — the «امروز» operational home replacing the old PlaceholderScreen: NextVisitCard (useTodaySessions), RequestsStrip (useNurseRequestInbox, the most time-critical widget — sorts above earnings), EarningsSnapshotCard (useNurseEarningsBalance, signed net + eligible), DashboardActivationSlot, NotificationsEntryRow (useUnreadCount) — every widget is a read of an already-cached query, four-state pattern throughout. iteration-2 removed the greeting+TrustBadge strip (it spent the screen's most valuable row restating the signed-in name and duplicating the activation tracker's badge) and replaced it with a plain `PageHeader` — load-bearing now that the nav is icon-only, since the page title is the only place the current section is named
|
||||
│ │ │ ├── DashboardActivationSlot.tsx # ui-phase-8 — the named composition point from ui-phase-7's hand-off, now filled with the shared `ActivationChecklist` (same component as `/nurse/services`) instead of the old single-row verification banner
|
||||
│ │ │ ├── requests/ # /nurse/requests — f7 incoming booking-requests inbox, ui-phase-7 redesign: page.tsx = decision-first cards (service+price headline when served — REQ-050, mock-tolerant) + an urgency-tinted CountdownTimer pill (teal/amber/terracotta tiers) + three tabs (در انتظار/پاسخداده merged-client-side page-1-only/منقضی, shared Pager) ↔ requests/[id]/page.tsx detail (only customerNotes + masked city/district; accept now behind a ConfirmDialog + a post-accept payment-window countdown; reject-with-reason invalidate inbox+detail)
|
||||
│ │ │ ├── profile/ # /nurse/profile — B7 profile bootstrap, ui-phase-8 pass: page.tsx adds editable education level/field (select + "سایر" free-text fallback) + specializations (chips, shared `SPECIALTY_PRESETS` vocab), a beforeunload guard on a staged-but-unsaved avatar, and a link into preview/ ↔ preview/page.tsx «نمایهٔ عمومی من» — the C3 trust-dossier pieces (TrustBadge/VerificationPanel/ServicePriceRow) composed entirely from the nurse's OWN cached data (own profile + useMyVariants + useServiceAreas + own badge), so it renders truthfully pre-publish
|
||||
│ │ │ ├── services/ # /nurse/services — B7 services half: offerings list ↔ variant builder (page.tsx switches mode; MyServicesList + VariantBuilder + PublishGate co-located). ui-phase-8: MyServicesList mounts the shared `ActivationChecklist` + a link to the profile preview above the list; PublishGate rewritten as the REAL `set_accepting_bookings` toggle (state-driven guidance/start/pause — no more no-op snackbar); VariantBuilder's step-3 renders the actual `VariantCard` as a live listing preview, option groups are chips (not a wrapped ToggleButtonGroup), and a 409 duplicate offers "edit the existing listing" (resolved via `optionSetSignature` against the cached `useMyVariants` list)
|
||||
│ │ │ ├── profile/ # /nurse/profile — B7 profile bootstrap, ui-phase-8 pass: page.tsx adds editable education level/field (select + "سایر" free-text fallback) + specializations (chips, shared `SPECIALTY_PRESETS` vocab), a beforeunload guard on a staged-but-unsaved avatar, and a link into preview/ ↔ preview/page.tsx «نمایهٔ عمومی من» — the C3 trust-dossier pieces (TrustBadge/VerificationPanel/ServicePriceRow) composed entirely from the nurse's OWN cached data (own profile + useMyVariants + useServiceAreas + own badge), so it renders truthfully pre-publish. iteration-2: rebuilt on react-hook-form + three `FormSection`s (معرفی / تجربه و تحصیلات / تخصصها), with the blocked-until-verified banner as an `AccentCard` and the trust badge in `PageHeader`'s `meta` slot
|
||||
│ │ │ ├── services/ # /nurse/services — B7 services half: offerings list ↔ variant builder (page.tsx switches mode; MyServicesList + VariantBuilder + PublishGate co-located). ui-phase-8: MyServicesList mounts the shared `ActivationChecklist` + a link to the profile preview above the list; PublishGate rewritten as the REAL `set_accepting_bookings` toggle (state-driven guidance/start/pause — no more no-op snackbar); VariantBuilder's step-3 renders the actual `VariantCard` as a live listing preview, option groups are chips (not a wrapped ToggleButtonGroup), and a 409 duplicate offers "edit the existing listing" (resolved via `optionSetSignature` against the cached `useMyVariants` list). iteration-2 made the create flow deterministic: the step list is DERIVED from the loaded option groups (a category with none skips straight to pricing instead of showing an empty middle step), Next is disabled with the unanswered required groups NAMED under it (was: always enabled, error only after the tap), and the price step recaps the chosen category+options as chips so it doubles as a review; RHF + `FormSection` throughout, sticky step footer
|
||||
│ │ │ ├── coverage/page.tsx # /nurse/coverage — F3 coverage-area editor. ui-phase-8: the separate whole-city/districts scope toggle is gone — `CascadingRegionSelect`'s own district level (its «کل شهر» empty option) is the ONLY control for the whole-city choice; `removeArea` now has an onError toast
|
||||
│ │ │ ├── bank/page.tsx # /nurse/bank — payout IBAN + ownership states (pending/verified/mismatch; `BankStatusPanel` unchanged). ui-phase-8: restructured as an accounts section — a persistent «افزودن حساب دیگر» CTA once ≥1 account exists (never dead-ends a nurse switching banks), an explicit pending-inquiry copy, and a real error state (never the empty-state form) on a failed query
|
||||
│ │ │ ├── verification/ # /nurse/verification — f5 trust flow: ONE cached VerificationStatus query, four views. ui-phase-8: rebuilt as a single vertical journey — one progress metaphor everywhere (the old flat "X از Y" meter + the B4/B5/B6 3-step `StepperHeader` are both gone)
|
||||
│ │ │ ├── verification/ # /nurse/verification — f5 trust flow: ONE cached VerificationStatus query, four views. ui-phase-8: rebuilt as a single vertical journey — one progress metaphor everywhere (the old flat "X از Y" meter + the B4/B5/B6 3-step `StepperHeader` are both gone). iteration-2: B4 and B5 are RHF forms grouped into `FormSection`s — B4's three asks (national id / card photo / selfie) each become a section with the card marked optional and the selfie marked required (the submit gate is a real form field, not a caption near the bottom); B5's four (شماره نظام / مدارک with an «n از m» count / تخصصها / جزئیات مدرک, the last two marked optional) mount only once the status query resolves, so the server read-back IS `defaultValues`
|
||||
│ │ │ │ ├── page.tsx # B3 hub — grouped step cards (هویت/مدارک حرفهای/بانک, `VerificationChecklist`) + `TrustBadgePreviewPanel` («این نشان را خانوادهها میبینند», fills per group) + single continue CTA + not_started/approved states; dev-only mock admin-decision sim
|
||||
│ │ │ │ ├── identity/page.tsx # B4 — national-ID (checksum) + card/selfie local capture → automated KYC + chained Shahkar; a cheap CSS `CaptureGuideFrame` (viewfinder corners / oval) + static hint per capture, `VerificationJourneyHeader` replaces the old StepperHeader
|
||||
│ │ │ │ ├── credentials/page.tsx # B5 — hydrates INO/specialties/registry fields from `status.credentialSubmission` (REQ-056, mock-tolerant) so a returning nurse sees a submitted summary, never blank fields; the INO number locks into a "شمارهٔ نظام ثبت شد" row (never re-prompted, never re-sent blank — the raw value is never read back by design); Jalali `JalaliDateField`s replace the native `type="date"` issue/expiry inputs; the submit gate considers server-side document state too, so a returning nurse is never dead-ended on a disabled button with no explanation
|
||||
@@ -250,13 +254,14 @@ client/
|
||||
│ │ ├── PageHeader/ # title+subtitle+actions+optional back button (backTo/backLabel); ui-phase-11 added `meta` (a chip-row slot below the title, distinct from the button-oriented `actions`) and `onBack` (a callback alternative to `backTo` — pairs with `useAdminBackToList` for `router.back()`-with-fallback semantics; takes precedence over `backTo` when both are given)
|
||||
│ │ ├── ConfirmDialog/ # promoted from admin/ — required-reason gating + busy-disable, now usable by any actor; ui-phase-11 added `requireTypedConfirmation`/`typedConfirmationLabel`/`typedConfirmationPlaceholder` — confirm stays disabled until the typed value matches one of the given strings, the guard for an irreversible money-moving action (the admin payout run confirm)
|
||||
│ │ ├── SurfaceCard/ # flat Paper wrapper, padding: 'sm'|'md'|'lg'
|
||||
│ │ ├── AccentCard/ # SurfaceCard + tone → 4px borderInlineStart accent (primary/secondary/success/error/warning/info/trust/neutral)
|
||||
│ │ ├── AccentCard/ # SurfaceCard + a semantic `tone` for a STATEFUL panel. iteration-2: the colored edge stripe is GONE — a column of cards read as a row of loose vertical rules down the RTL side of the screen. `tone` survives as the semantic label (reaches the DOM as `data-accent-tone`); state is carried by the StatusChip/icon/copy inside the card. Do not reintroduce the stripe
|
||||
│ │ ├── form/ # (iteration-2) the react-hook-form seam — `FormSection` (a titled/described group of fields, with an optional/status marker) + `RhfTextField` (`transform` normalizes keystrokes into form state; rule message replaces the helper text) / `RhfChipSelect` (multi or single; `allowCustomValues` renders a stored code that isn't in the option list) / `RhfJalaliDateField` / `RhfControlGroup` (label+hint+error shell around ANY non-input control — GenderToggle, RatingInput, a map picker). Every wrapper falls back to the enclosing `FormProvider`'s control, so a form wires it once (all tested)
|
||||
│ │ ├── Money/ # <Money amountIrr size tone deduction hideUnit strikethrough> — the one money-rendering primitive (wraps utils/money.ts); size gained `xl` (h4) in ui-phase-6 for the checkout/confirmation prominent-total hero; imports next-intl (see jest.config.ts transformIgnorePatterns note below)
|
||||
│ │ ├── StatusTimeline/ # ordered TimelineNode[] (completed/current/pending/failed) with animated pulse on current (respects prefers-reduced-motion)
|
||||
│ │ ├── JalaliDatePicker/ # calendarEngine.ts (jalaali-js-backed Jalali↔Gregorian) + grid/chips variants (ui-phase-4: chips variant takes optional `todayLabel`/`tomorrowLabel` overrides — the C1 «امروز»/«فردا» date-intent strip), RTL-aware keyboard nav
|
||||
│ │ ├── JalaliDateField/ # read-only TextField + Popover wrapping JalaliDatePicker
|
||||
│ │ ├── JalaliDateIntentPicker/ # ui-phase-5 — extracted from C1's local date-intent widget (near-day chip strip + a calendar-icon Popover entry into the full grid) so C4's real required date field reuses it too, not just C1's intent-only one; caller-owned today/tomorrow/pick-other labels (tested)
|
||||
│ │ ├── StickyActionBar/ # ui-phase-4 — a `position:sticky` bottom-pinned action-bar shell for a scrolling screen's primary CTA (C1's live-count CTA, C3's booking CTA); composes with the shell's existing BottomBar safe-area padding rather than reimplementing `env(safe-area-inset-bottom)`
|
||||
│ │ ├── StickyActionBar/ # ui-phase-4 — a `position:sticky` bottom-pinned action-bar shell for a scrolling screen's primary CTA (C1's live-count CTA, C3's booking CTA). iteration-2: offsets off `--bal-chrome-bottom` (published by AppFrame) so it clears the pinned nav; the property already carries `env(safe-area-inset-bottom)` and resolves to `0px` in a chrome-free shell
|
||||
│ │ ├── LocaleSwitcher/ # ui-2 fa/en toggle preserving the current route (`router.replace(pathname, {locale})` via `@/i18n/navigation`); sidebar footers, the customer profile hub, the public shell (tested)
|
||||
│ │ ├── Pager/ # ui-phase-7 — the shared prev/next "page X of Y" control (common namespace i18n) replacing the near-identical inline pagers hand-rolled per list screen (nurse inbox tabs, payout history/earnings) (tested)
|
||||
│ │ ├── InitialsAvatar/ # ui-phase-9 — warm auto-colored initials for a person with no photo: deterministic name-hash → one of 6 `--bal-avatar-*` token pairs (tokens.css, both scheme blocks); `aria-hidden` (decorative next to a visible name); used by `PatientHeader` and (via `ProfileSummary`'s new `initialsFallback` prop) the customer account hub (tested)
|
||||
@@ -317,22 +322,22 @@ client/
|
||||
│ ├── request.ts # getRequestConfig — loads messages/${locale}.json
|
||||
│ └── navigation.ts # ui-2 createNavigation(routing) — Link/usePathname/useRouter/redirect/getPathname. ALL chrome navigation goes through this: usePathname is locale-stripped (so unprefixed ROUTES.* compare directly) and Link/router add the locale automatically — no manual `/${locale}` prefixing, no middleware redirect hop
|
||||
├── layout/ # the one mobile app shell — per-actor tabs over a shared phone-width frame
|
||||
│ ├── AppFrame.tsx # 'use client' — THE device frame every shell renders inside: a centered `APP_FRAME_MAX_WIDTH` column on a `--bal-frame-canvas` backdrop, header/`<main>`/footer as flex siblings so the frame (not the document) owns the scroll, and `overflowX: hidden` + `minWidth: 0` so an over-wide child clips instead of dragging the app sideways. No shell computes a top offset any more (tested)
|
||||
│ ├── MobileShell.tsx # 'use client' — the ONE authenticated shell behind all four actor apps: AppFrame + a contextual TopBar (brand lockup on a tab's own path, back chevron + route title on anything deeper) + BottomBar + ErrorBoundary + RouteFadeIn + PageTitleProvider. Actors supply only `tabs` + `headerActions`
|
||||
│ ├── AppFrame.tsx # 'use client' — THE device frame every shell renders inside: a centered `APP_FRAME_MAX_WIDTH` column on a `--bal-frame-canvas` backdrop, `overflowX: hidden` + `minWidth: 0` so an over-wide child clips instead of dragging the app sideways. iteration-2: above `sm` the column FLOATS as a rounded, shadowed card with a gutter all round (edge-to-edge on a phone); header and footer are pinned OVER the single scrolling `<main>` with `position: absolute` — never `fixed`, which would break out of the centered column — and `<main>` reserves their exact height as padding. It also publishes `--bal-chrome-top`/`--bal-chrome-bottom` (0px in a chrome-free shell) so any `position: sticky` element in the tree can clear the bars without importing a constant. No shell computes a top offset any more (tested)
|
||||
│ ├── MobileShell.tsx # 'use client' — the ONE authenticated shell behind all four actor apps: AppFrame + a contextual TopBar (brand lockup on a tab's own path, back chevron + route title on anything deeper) + BottomBar + ErrorBoundary + RouteFadeIn + PageTitleProvider. Actors supply only `tabs` + `headerActions`. No identity affordance in the header — that lives in each actor's «بیشتر»/account hub, one tap away on the nav
|
||||
│ ├── PrivateLayout.tsx # authenticated wrapper (passthrough today); actor chrome lives in the shells below
|
||||
│ ├── CustomerLayout.tsx # 'use client' — customer tabs: خانه (+/search) · رزروها · حلقهٔ مراقبت · کیفپول · پروفایل (+/addresses, /support, /notifications — the account hub that owns appearance/language)
|
||||
│ ├── NurseLayout.tsx # 'use client' — nurse tabs, one per group the sidebar used to hide: امروز (/nurse) · حرفهٔ من (/nurse/practice) · مالی (/nurse/finance) · بیشتر (/nurse/more, `useSupportUnreadTotal` badge). Each group root is a real page; `matchPaths` keeps the historical destination URLs lighting up their group
|
||||
│ ├── NurseLayout.tsx # 'use client' — nurse tabs: امروز (/nurse) · درخواستها (/nurse/requests, pending-count badge off the already-cached `useNurseRequestInbox`) · حرفهٔ من (/nurse/practice) · مالی (/nurse/finance) · بیشتر (/nurse/more, `useSupportUnreadTotal` badge). درخواستها earned a tab in iteration-2: it is the one nurse screen with a deadline on it (a pending request expires unanswered) and was previously reachable only from a strip on the dashboard. Each group root is a real page; `matchPaths` keeps the historical destination URLs lighting up their group
|
||||
│ ├── AdminLayout.tsx # 'use client' — admin tabs: نمای کلی · اعتماد · مالی · پشتیبانی · سیستم, each a group root. A group tab hides when `useAdminCapabilities` permits nothing inside it (gating unchanged and still per-console; the server still enforces); سیستم is always present because it carries settings + sign-out
|
||||
│ ├── PartnerLayout.tsx # 'use client' — partner tabs: مرکز · پرستاران · رزروها · تسویه · بیشتر. The center identity + merchant-of-record indicator moved from the top bar into /partner/more
|
||||
│ ├── PublicLayout.tsx # unauthenticated shell — the frame and NOTHING else: no top bar, so the login card's own BrandMark is the only mark on screen and the locale/theme toggles that used to sit up here live in settings
|
||||
│ ├── FocusedLayout.tsx # ui-phase-3 — chrome-free framed shell for can't-tab-away flows (onboarding, and /select-role via its own layout.tsx): a slim logo strip + content, no bottom nav; the route group above it still applies RoleGuard
|
||||
│ ├── routeTitle.tsx # ui-2 static route→title map (longest-prefix over ROUTES.*, off the `nav` namespace) + `PageTitleProvider`/`usePageTitleOverride` per-page dynamic-title slot + `useRouteTitle`
|
||||
│ ├── matchActivePath.ts # ui-2 shared longest-prefix, winner-takes-all active-path matcher (tested) — BottomBar runs it over each tab's own path PLUS its `matchPaths` claims, so a nested route still lights up its parent tab, never a sibling
|
||||
│ ├── config.ts # APP_FRAME_MAX_WIDTH (480 — mirrored by components/config.ts's CONTENT_MAX_WIDTH) + TOP_BAR_HEIGHT
|
||||
│ ├── config.ts # APP_FRAME_MAX_WIDTH (480 — mirrored by components/config.ts's CONTENT_MAX_WIDTH) + TOP_BAR_HEIGHT + TOP_CHROME_HEIGHT/BOTTOM_NAV_HEIGHT (the space each floating bar occupies, which AppFrame reserves as `<main>` padding — keep in sync with the bars) + FLOATING_BAR_SX (the ONE definition of the two bars' shared shape, so header and footer cannot drift apart)
|
||||
│ ├── index.ts
|
||||
│ └── components/
|
||||
│ ├── TopBar.tsx # NOT an AppBar — no filled surface, no bottom rule, no elevation: it sits on `background.default` and reads as part of the page rather than a bar covering the top of it. A plain flex row inside AppFrame, never a fixed overlay: title | titleNode override, align ('start' breadcrumb-style | 'center')
|
||||
│ ├── BottomBar.tsx # the app's only navigation surface (tested) — a FLOATING pill bar (inset from the frame edges, `--bal-radius-pill`, `--bal-shadow-2`) rather than an edge-to-edge slab sealing off the bottom; still a flex sibling of the scrolling main, so nothing is ever hidden under it. ButtonBase tabs with an active pill that fills in behind the icon (`--bal-motion-fast`, so the app-wide reduced-motion gate already covers it), `LinkToPage.badgeCount` badges, and `matchPaths`-aware active matching
|
||||
│ ├── TopBar.tsx # NOT an AppBar — no elevation or surface of its own: `AppFrame` wraps it in the floating-pill container (`FLOATING_BAR_SX`), so it is the bottom nav mirrored. Content: title | titleNode override, align ('start' breadcrumb-style | 'center')
|
||||
│ ├── BottomBar.tsx # the app's only navigation surface (tested) — a FLOATING pill bar (inset from the frame edges, `FLOATING_BAR_SX`) rather than an edge-to-edge slab sealing off the bottom; pinned over the scrolling main by AppFrame, which reserves `BOTTOM_NAV_HEIGHT` of padding so nothing is ever hidden under it. ICON-ONLY (iteration-2: the caption was the widest thing in the bar at five tabs and cost a whole line; the label survives as `aria-label`/`title`), each tab a fixed 44px CIRCLE that is simultaneously the target, the hover/press tint and the active fill (`--bal-motion-fast`, so the app-wide reduced-motion gate already covers it), laid out `space-around` so the target keeps one size at any tab count; `LinkToPage.badgeCount` badges and `matchPaths`-aware active matching
|
||||
│ ├── BrandLockup.tsx # ui-2 compact horizontal logo+wordmark — the TopBar title on a shell's root tabs
|
||||
│ ├── ActorSwitcher.tsx # ui-2 dual customer+nurse session switcher (renders nothing for a single-role session); nurse «بیشتر» hub + customer profile hub (tested)
|
||||
│ └── index.tsx
|
||||
@@ -582,6 +587,62 @@ Rules:
|
||||
|
||||
---
|
||||
|
||||
## Forms
|
||||
|
||||
**Rule: any form with more than one field uses react-hook-form.** A single-field control (a search
|
||||
box, a filter select, a message composer) does not — it is state, not a form.
|
||||
|
||||
This is not a style preference. The pattern it replaced was one `useState` per input **plus** a
|
||||
parallel `useState` per error flag, which meant every keystroke re-rendered the whole screen —
|
||||
including query-backed cards, price previews and uploaders sitting beside the field — and left
|
||||
"is this form valid?" spread across ad-hoc `if` blocks at the top of each submit handler.
|
||||
|
||||
### How to build one
|
||||
|
||||
1. `useForm<Values>({ mode: 'onTouched', defaultValues })` — `onTouched` is the house default: an
|
||||
error appears once a field has been visited, never while it is first being typed into.
|
||||
2. Wrap the subtree in `<FormProvider {...form}>` and bind fields with the
|
||||
`@/components/common/form` wrappers. They read `control` off the provider, so it is threaded once.
|
||||
Never call `register`/`useController` at a call site.
|
||||
3. Put the rule on the field it governs (`rules={{ validate: … }}`), returning the **translated
|
||||
message**. Cross-field rules read the second `validate` argument (all values) — that is how the
|
||||
C4 request form's past-date guard reads the chosen start time.
|
||||
4. Render `<Stack component="form" noValidate onSubmit={handleSubmit(submit)}>` and make the primary
|
||||
button `type="submit"`. Enter-to-submit then works for free.
|
||||
5. **Async defaults come from a mounted-when-ready child, not an effect.** When the initial values
|
||||
depend on a query (the verification credentials read-back, the C4 variant/address defaults), keep
|
||||
the loading branch in the parent and mount the form component only once the data has resolved, so
|
||||
`defaultValues` *is* the server state instead of being copied into it later.
|
||||
|
||||
### Wrappers
|
||||
|
||||
| Wrapper | For |
|
||||
| --- | --- |
|
||||
| `RhfTextField` | any `TextField`, incl. `select`. `transform` normalizes keystrokes **into form state** (digit-stripping, max length) so the stored value is canonical, not just the displayed one. A rule message replaces `helperText`. |
|
||||
| `RhfChipSelect` | a chip group over stable codes — `string[]` (multi) or `string \| null` (single). `allowCustomValues` keeps a stored code that isn't in the option list visible. |
|
||||
| `RhfJalaliDateField` | a Jalali date field; stores the wire ISO (Gregorian) string or `null`. |
|
||||
| `RhfControlGroup` | **any** non-input control (`GenderToggle`, `RatingInput`, `CascadingRegionSelect`, the map-pin picker, a `Switch`, a `Checkbox`). Gives it the same label/hint/error shell the text fields get. |
|
||||
|
||||
### Two conventions worth knowing
|
||||
|
||||
- **A control that renders its own error text gets a message-less rule** (`validate: (v) => cond`,
|
||||
no string). `RhfControlGroup` then flags the field without printing a second identical line —
|
||||
`AddressForm`'s region and pin fields are the reference.
|
||||
- **When the displayed value isn't the stored value, drop to a bare `Controller`.** Two cases exist
|
||||
and both are commented at the call site: the variant builder's display-name (stored = the override
|
||||
only, blank ⇒ the server names it; shown = the live auto-generated name) and the admin refund
|
||||
channel (stored = "" until explicitly overridden; shown = the server's resolved channel).
|
||||
|
||||
### Structure: `FormSection`
|
||||
|
||||
A long form is grouped into `FormSection`s — a heading, a one-line statement of *why* the group is
|
||||
being asked for, and an optional/status marker. Applies to the nurse profile, the verification
|
||||
identity + credentials screens, and the variant builder. The point is that an optional group reads
|
||||
as skippable and a blocked submit has somewhere to attribute itself; a flat run of ten `TextField`s
|
||||
makes everything look equally mandatory.
|
||||
|
||||
---
|
||||
|
||||
## Theme System
|
||||
|
||||
### How it works (end-to-end, no-flash — CSS only, no boot script)
|
||||
|
||||
Reference in New Issue
Block a user