cleanup phases 6

This commit is contained in:
hamid
2026-08-02 18:48:32 +03:30
parent e2db97392a
commit 51e86a1e5f
239 changed files with 118 additions and 70 deletions
@@ -0,0 +1,75 @@
# App shell — header, sidebar, bottom bar, layouts, navigation
## Current state
The shell is a two-tier system. The root layout (client/src/app/[locale]/layout.tsx) is genuinely well-built: it owns <html lang/dir>, conditionally loads the Mikhak Persian font only on fa routes, seeds the color scheme from a cookie (no flash), and pairs direction-keyed themes (APP_THEME_RTL/LTR) with a stylis-plugin-rtl Emotion cache. Below it, route groups map to per-actor shells: (public-routes) → PublicLayout, (private-routes) → useSessionRoleSync + a pass-through PrivateLayout, then (customer) → RoleGuard+CustomerLayout, nurse/ → NurseLayout, admin/ → AdminLayout, partner/ → PartnerLayout. RoleGuard (client/src/components/auth/RoleGuard.tsx) is solid: brand splash while /me resolves, explicit error recovery, toast+redirect on role mismatch.
The chrome itself is split. CustomerLayout (client/src/layout/CustomerLayout.tsx) is bespoke and closest to right: fixed TopBar (static "اپلیکیشن خانواده" title, support icon start, NotificationBell + dark toggle end), an 800px reading column with inner scroll, and a 5-tab BottomBar (Home/Bookings/Patients/Wallet/Profile). Everything else — NurseLayout (10 flat sidebar items), AdminLayout (12 capability-gated items via useAdminCapabilities), PartnerLayout (4 items), and PublicLayout (zero items) — reuses TopBarAndSideBarLayout.tsx, which is the untouched react-starter-kit engine: a stock MUI AppBar with a centered static app-label title, a 240px Drawer (persistent on desktop, temporary on mobile) whose toggle button is the starter's multicolor Twemoji PENCIL icon registered as `logo`, a SideBar containing a永-placeholder UserInfo card ("Current User" / "Loading..."), a default ListItemButton nav list, a dark-mode switch, and a logout icon. Nav items are defined inline in each *Layout.tsx via translated LinkToPage arrays; ROUTES constants live in client/src/constants/routes.ts.
The "old MUI starter" complaint has a precise root cause: client/src/theme/theme.ts contains palette, typography, and shape only — there is not a single `components` override in the theme, so the AppBar, Drawer, ListItemButton selected state, Toolbar, and BottomNavigation all render stock MUI. On top of that, sidebar navigation is functionally degraded: SideBarNavItem compares unprefixed paths against the locale-prefixed pathname so the active item never highlights, links push unprefixed hrefs through raw next/link (middleware redirect hop), and the SSR mobile-first useIsMobile causes a 240px desktop layout jump after hydration. There is no brand mark, no page-title wayfinding, no back affordance, no locale switcher, and no way for a dual customer+nurse account to switch apps.
## Problems (20)
- **[high]** `client/src/components/common/AppIcon/config.ts` — The brand 'logo' is the starter kit's multicolor emoji-style pencil (client/src/components/common/AppIcon/icons/PencilIcon.tsx with hard-coded fills #EA596E, #FFCC4D, #D99E82). It is the brand mark on the auth screens (BrandMark.tsx:21 passes color="var(--bal-primary)" which is ignored because every path has its own fill) and the sidebar-toggle button in every sidebar shell (TopBarAndSideBarLayout.tsx:68-75). A trust-first healthcare product presents a cartoon pencil as its identity.
- evidence: config.ts:115 `logo: PencilIcon`; PencilIcon.tsx `fill="#EA596E"` / `fill="#FFCC4D"`
- **[high]** `client/src/theme/theme.ts` — createAppTheme defines only cssVariables/colorSchemes/typography/shape/direction — zero `components` overrides. Every piece of chrome (AppBar shadow+solid primary, Drawer paper, grey ListItemButton selected state, BottomNavigation, Toolbar density) is stock MUI. This is the structural root cause of the 'default-MUI starter' look; no amount of per-layout tweaking fixes it without a theme components pass.
- evidence: theme.ts:20-35 — createTheme call has no `components` key
- **[high]** `client/src/layout/components/SideBarNavItem.tsx` — Sidebar active-item highlight never triggers. `pathname` from next/navigation is locale-prefixed ('/fa/nurse/requests') while nav paths are unprefixed ('/nurse/requests'); defineRouting (client/src/i18n/routing.ts) uses the default localePrefix 'always', so startsWith always fails. Nurse, admin, and partner users get no 'where am I' signal in the sidebar. AppLink's activeClassName (AppLinkNextNavigation.tsx:95 `pathname == currentPath`) has the same bug.
- evidence: SideBarNavItem.tsx:28 `(path && path.length > 1 && pathname.startsWith(path))`
- **[high]** `client/src/layout/components/SideBar.tsx` — The sidebar identity card renders a permanent placeholder: `<UserInfo showAvatar />` is never given a user, so every nurse/admin/partner sees an empty avatar, hard-coded English 'Current User', and an eternal 'Loading...' (UserInfo.tsx:34-36) — in the chrome of a product whose entire premise is verified identity. AuthState (context/auth/types.ts) has at least phone+roles, and profile services have name/avatar, but nothing is wired.
- evidence: SideBar.tsx:56 `<UserInfo showAvatar />`; UserInfo.tsx:34 `{fullName || 'Current User'}`
- **[high]** `client/src/layout/PublicLayout.tsx` — The login/first-impression screen carries starter junk: the TopBar title is the hard-coded English 'Unauthorized - Balinyaar' (line 9) shown even on the fa default locale; SIDE_BAR_ITEMS is [] yet the pencil button still opens a drawer containing only a dark-mode switch; and on mobile an EMPTY BottomBar (BOTTOM_BAR_ITEMS = [], line 19) renders as a bare elevated strip at the bottom of the login page (line 34).
- evidence: PublicLayout.tsx:9 `const TITLE_PUBLIC = 'Unauthorized - Balinyaar'`; line 34 `{bottomBarVisible && <BottomBar items={BOTTOM_BAR_ITEMS} />}`
- **[high]** `client/src/hooks/layout.ts` — SERVER_SIDE_MOBILE_FIRST = true makes every desktop SSR paint the mobile shell first (no sidebar, 56px top bar), then TopBarAndSideBarLayout's stackStyles flip after hydration and content jumps 240px sideways on every nurse/admin/partner desktop load — visible CLS on the daily-driver backoffice screens.
- evidence: layout.ts:8 `SERVER_SIDE_MOBILE_FIRST = true`; TopBarAndSideBarLayout.tsx:49-63 paddings keyed on `onMobile`/`sidebarProps`
- **[medium]** `client/src/layout/components/SideBarNavItem.tsx` — Sidebar links navigate via raw next/link with unprefixed paths (`to={path}`), forcing a middleware redirect hop on every click and risking a locale flip for /en users (cookie/accept-language re-detection). BottomBar and NotificationBell manually prefix with `/${locale}` instead. Three different locale-handling strategies exist in the chrome and there is no next-intl createNavigation wrapper.
- evidence: SideBarNavItem.tsx:34 `to={path}` vs BottomBar.tsx:14 `withLocale(locale, path)`
- **[medium]** `client/src/layout/components/TopBar.tsx` — The header is a wasted, static surface: a centered app-label title ('نمای پرستار', 'اپلیکیشن خانواده') with whiteSpace:'nowrap' (overflow risk between icon groups on small screens), no page title, no breadcrumbs, no user identity/avatar, plus leftover starter comment '// boxShadow: none // Uncomment to hide shadow'. Users get zero wayfinding from the chrome on every screen.
- evidence: TopBar.tsx:28-38 centered nowrap Typography; line 20 commented starter code
- **[medium]** `client/src/layout/AdminLayout.tsx` — Admin and Partner shells pass no headerActions: no notification bell (admin notifications is only a buried 12th sidebar item; partner has none at all), no admin identity or fine-grained-role chip in the header, no environment indicator. For a backoffice where a 'finance' vs 'support' admin see different consoles, the chrome never says who you are.
- evidence: AdminLayout.tsx:39-44 and PartnerLayout.tsx:29-36 — TopBarAndSideBarLayout called without headerActions
- **[medium]** `client/src/layout/NurseLayout.tsx` — The nurse sidebar is a flat, ungrouped list of 10 items (dashboard, requests, profile, services, coverage, bank, verification, visits, earnings, support) in default ListItemText styling — no sections separating daily work (requests/visits) from setup (services/coverage/bank/verification) from money, and no verification-status cue in nav even though an unverified nurse's single most important task is finishing verification.
- evidence: NurseLayout.tsx:19-33 — one flat useMemo array
- **[medium]** `client/src/layout/components/BottomBar.tsx` — No iOS safe-area handling — the Paper/BottomNavigation has no env(safe-area-inset-bottom) padding, so on iPhones the home indicator overlaps the tab labels of the customer app's primary navigation. The customer shell is explicitly the mobile-first primary experience.
- evidence: BottomBar.tsx:51-62 — sx only sets borderTop; no safe-area padding anywhere in globals.css either
- **[medium]** `client/src/layout/CustomerLayout.tsx` — The shell offers no back affordance or contextual title for detail screens (nurse profile, booking detail, checkout): the TopBar startNode is always the support icon and the title is always 'Family app'. Only one page in the whole app (bookings/[id]/review/page.tsx:98) renders a back button, and there is no 'back'/'arrow' icon in the AppIcon registry at all — mobile users must rely on browser chrome mid-funnel.
- evidence: CustomerLayout.tsx:45-61 static TopBar; AppIcon/config.ts has no back/arrow icon; only review/page.tsx uses router.back()
- **[medium]** `client/src/layout/components/SideBar.tsx` — The drawer-close handler is attached to the whole content Stack (onClick={handleAfterLinkClick} on line 52), so on mobile ANY tap inside the temporary drawer closes it — including toggling the dark-mode switch or a mis-tap on the divider, not just nav-link clicks.
- evidence: SideBar.tsx:49-53 `<Stack … onClick={handleAfterLinkClick}>` wrapping UserInfo, nav list, and DarkModeFormSwitch
- **[medium]** `client/src/layout/CustomerLayout.tsx` — The BottomBar renders unconditionally, so desktop customers get a full-width mobile tab bar pinned to the bottom of a wide viewport with a lone 800px column above it — a 'phone app stretched to desktop' effect with no desktop nav alternative (BOTTOM_BAR_DESKTOP_VISIBLE in config.ts is dead — only PublicLayout reads it).
- evidence: CustomerLayout.tsx:81 `<BottomBar items={bottomNavItems} />` with no breakpoint gate
- **[medium]** `client/src/layout/TopBarAndSideBarLayout.tsx` — Chrome strings hard-coded in English on a Persian-default product: 'Open Sidebar' tooltip (line 71), 'Logout Current User' (SideBar.tsx:76), 'Current User'/'Loading...' (UserInfo.tsx). Every other shell string goes through next-intl; these leak English into fa tooltips/labels.
- evidence: TopBarAndSideBarLayout.tsx:71 `title={sidebarProps.open ? undefined : 'Open Sidebar'}`
- **[low]** `client/src/layout/TopBarAndSideBarLayout.tsx` — RTL correctness rests on a fragile double-flip coincidence: content offset uses physical paddingLeft/paddingRight keyed to the physical anchor string (lines 53-60), which only aligns with the drawer because MUI flips Drawer anchor under theme.direction='rtl' AND stylis-plugin-rtl flips the generated padding CSS. Any future inline style, non-Emotion CSS, or plugin removal silently breaks the fa desktop layout. Logical properties (marginInlineStart / paddingInlineStart) would make it robust.
- evidence: TopBarAndSideBarLayout.tsx:53-60 `paddingLeft: … anchor?.includes('left') ? SIDE_BAR_WIDTH : undefined`
- **[low]** `client/src/layout/config.ts` — Starter residue: commented-out alternates ('right'; // 'right';) on the anchor constants and the dead BOTTOM_BAR_DESKTOP_VISIBLE=false // true; flag — config that documents the starter's indecision rather than Balinyaar's design.
- evidence: config.ts:8-9, 21
- **[low]** `client/src/components/UserInfo/UserInfo.tsx` — Starter-typed component: `user?: any`, name/email fallback logic for a phone-OTP product with Persian names, 64px avatar with fontSize '3rem' initials. Needs replacing, not patching, when the sidebar identity card is wired to real profile data.
- evidence: UserInfo.tsx:6 `user?: any`; line 18 `user?.phone || (user?.email as string)`
- **[low]** `client/src/app/globals.css` — Starter CSS reset sets max-height:100vh + overflow-x:hidden on html/body while the app runs two different scroll models (window scroll in TopBarAndSideBarLayout, inner overflowY:auto <main> in CustomerLayout). The inner-scroll model also defeats Next.js scroll restoration — back-navigating from a nurse profile to search results loses the list position.
- evidence: globals.css `max-height: 100vh` on html,body; CustomerLayout.tsx:68 `overflowY: 'auto'` on main
- **[low]** `client/src/layout/CustomerLayout.tsx` — No locale switcher exists anywhere in any shell (fa/en are both shipped), and a dual customer+nurse session — explicitly supported by RoleGuard ('passes either shell's guard and can move freely') — has no UI anywhere to switch between the family app and the nurse view, including the profile hub page.
- evidence: grep for LocaleSwitch/setLocale returns nothing; resolveRoleDestination used only in auth guards; (customer)/profile/page.tsx has no /nurse link
## Opportunities (10)
- **Design a real brand mark and put identity into the chrome** (impact: high, effort: medium) — Replace the Twemoji pencil with a proper Balinyaar logomark (teal/cream SVG that respects currentColor so dark mode works), register it as `logo` in AppIcon, and add a brand lockup to the chrome: wordmark in the customer Home header, a compact brand header at the top of the nurse/admin/partner sidebars, and a proper favicon/manifest icon. Auth screens (BrandMark) fix themselves for free since they already reference icon="logo".
- **One theme `components` pass to de-starter every shell at once** (impact: high, effort: medium) — Add component overrides in theme.ts using existing --bal-* tokens: AppBar → cream/paper surface with a hairline divider instead of solid-teal + default shadow (calm, clinical-warm); Drawer paper → bg-default with inset border; ListItemButton → rounded 'pill' selected state using --bal-primary-soft with teal text/icon; BottomNavigation → teal selected color, medium label weight; Toolbar → consistent gutters. Because all four shells render through these primitives, one file transforms the entire chrome without touching layout logic.
- **Contextual customer header: page title + back on detail routes** (impact: high, effort: medium) — Turn the customer TopBar from a static 'Family app' label into a contextual header: brand lockup on the 5 root tabs, and (title + back chevron) on pushed routes (nurse profile, booking detail, checkout steps, ticket thread). Implement via a tiny header context or a route-segment→title map; add a direction-aware back icon to the AppIcon registry. This is the single biggest mobile-UX upgrade available — the whole booking funnel currently has no in-app way back.
- **Nurse workspace shell with grouped nav and a real identity card** (impact: high, effort: medium) — Restructure the nurse sidebar into labeled sections — امروز (dashboard, requests, visits), حرفه من (services, coverage, verification), مالی (earnings, bank), پشتیبانی — with subheaders and dividers; replace the placeholder UserInfo with a real card: avatar, name, TrustBadge verification state (the existing f5 component), and a small 'complete your verification' progress affordance for unverified nurses. Verification status in the chrome directly serves the trust-first premise.
- **Dense admin backoffice chrome** (impact: medium, effort: large) — Give /admin real ops-console chrome: sectioned sidebar (Trust: verification/reviews · Money: payouts/refunds · Support: tickets/alerts · System: config/holidays/audit/roles/partners), a page-title + breadcrumb bar under the AppBar, the admin's fine-grained role chip (super_admin/finance/…) and a bell in the header, denser list typography, and full-width content (drop the 8px-gutter Stack for a proper content frame). Keep useAdminCapabilities gating exactly as is.
- **App switcher for dual-role users + locale switcher** (impact: high, effort: small) — Add a compact actor switcher ('نمای پرستار ⇄ اپلیکیشن خانواده') to the customer profile hub and the nurse sidebar for sessions holding both roles (roles already live in AuthContext), and a fa/en locale switcher in the sidebar/profile. Also gives nurses-who-are-also-family a discoverable path that currently does not exist at all.
- **Unify locale-aware navigation on next-intl createNavigation** (impact: high, effort: small) — Create src/i18n/navigation.ts (createNavigation from next-intl) and route ALL chrome navigation through its Link/usePathname/useRouter. This simultaneously fixes the never-highlighting sidebar selected state, the middleware redirect hop, the manual `/${locale}` prefixing scattered across BottomBar/NotificationBell/RoleGuard, and future-proofs against localePrefix changes.
- **Mobile-native polish for the customer shell** (impact: medium, effort: medium) — Safe-area padding (env(safe-area-inset-bottom)) on the BottomBar, a max-width phone-frame or top-nav variant for desktop customers instead of a full-width bottom tab bar, hide-on-scroll app bar for long lists, and a slim route-transition progress indicator. Consider making the search results list restore scroll on back (window-scroll model or manual restoration) since it is the discovery workhorse.
- **Trust cues woven into the chrome itself** (impact: medium, effort: small) — Beyond screens: a persistent 'پرداخت امن نزد بالین‌یار' escrow microcopy chip in the wallet tab header, verified-nurse badge treatment wherever a nurse identity appears in chrome, and an always-reachable emergency/support affordance in the nurse visit context (the customer support icon exists — mirror the guarantee on the nurse side). In a healthcare marketplace the shell, not just content, should keep signaling safety.
- **Fix desktop SSR flash with a CSS-first responsive shell** (impact: medium, effort: medium) — Replace the JS useIsMobile branching in the shells with CSS breakpoints (sx display/breakpoint props render both variants and let media queries pick), or read a UA hint server-side, so desktop first paint already includes the persistent sidebar and correct paddings — eliminating the 240px post-hydration jump on every nurse/admin/partner load.
## Keep (do not regress)
- The design-token architecture: client/src/theme/tokens.css (--bal-* custom properties, light + dark schemes keyed on data-mui-color-scheme) mirrored 1:1 by colors.ts BRAND/LIGHT_PALETTE/DARK_PALETTE — the palette itself (deep teal, sparing terracotta, cream) is on-brand and already dark-mode-complete.
- The root [locale] layout: correct lang/dir per locale, conditional Mikhak font loading only on fa routes, cookie-seeded color scheme with no flash, direction-keyed theme pair + stylis-plugin-rtl Emotion cache — this RTL/theming foundation is better than most production apps and must not be regressed.
- The per-actor shell architecture: separate CustomerLayout / NurseLayout / AdminLayout / PartnerLayout mapped 1:1 to route groups, with RoleGuard's resolved-vs-pending hydration (brand splash, explicit /me error recovery, mismatch redirect with toast — never the wrong shell as a stand-in). Restyle the chrome, keep this structure.
- AdminLayout's capability-gated sidebar via useAdminCapabilities — nav items filtered by fine-grained role codes with server-side enforcement acknowledged in comments; exactly the right display-convenience pattern.
- The customer 5-tab bottom-nav IA (Home/Bookings/Patients/Wallet/Profile) and BottomBar's implementation details: locale-prefixed navigation and longest-prefix active matching so /patients/123 still highlights the Patients tab — the one nav component that handles locale correctly.
- Performance-conscious chrome composition: DarkModeToggleButton/DarkModeFormSwitch are the only useColorScheme subscribers and NotificationBell isolates the polling unread count, so theme flips and bell updates never re-render the shells.
- CONTENT_MAX_WIDTH reading-column constraint (800px) for customer content, ErrorBoundary wrapping every shell's main content, and the RTL typography setup (Mikhak across headings+body for full Persian glyph coverage, button textTransform:'none').