'use client'; import { FunctionComponent, useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useLocale, useTranslations } from 'next-intl'; import { Avatar, Box, ButtonBase, Paper, Skeleton, Stack, Typography } from '@mui/material'; import { AppButton, AppIcon, AppIconButton, AppLoading, CategoryTile, EmptyState, ErrorState, SurfaceCard, } from '@/components'; import { ROUTES } from '@/constants'; import { useMe } from '@/services/auth'; import { usePatients } from '@/services/patients'; import { useServiceCategories } from '@/services/catalog'; import { pickCatalogName } from '@/services/catalog/names'; import { useBookingDetail, useBookingList } from '@/services/bookings'; import type { BookingListItemDto } from '@/services/bookings/types'; interface NudgeCardProps { icon: string; title: string; body: string; ctaLabel: string; to: string; /** Optional dismiss affordance (session-scoped) — omit for the always-relevant profile nudge. */ onDismiss?: () => void; dismissLabel?: string; } const NudgeCard: FunctionComponent = ({ icon, title, body, ctaLabel, to, onDismiss, dismissLabel }) => ( {title} {body} {ctaLabel} {onDismiss ? ( ) : null} ); // Session-scoped dismiss: a plain module variable (not a cookie/localStorage — this is ephemeral UI // state, not app/auth state) survives client-side navigation within the same page load and resets on a // hard reload, matching "dismissible for this session, not permanently". let patientNudgeDismissedInSession = false; /** * A5 — the family Home: the front door of the app. Greeting + avatar, a compact ambient trust strip, a * tappable search entry point (routes to C1 — see `HomeSearchBar`), the **data-driven** service-category * grid (from the cached `services/catalog` reference data), a completeness-gated patient-record nudge, * and a "رزرو دوباره" (rebook) shortcut row sourced from recent bookings. * * First-login gate: a customer with no patients is sent into onboarding (A3). The redirect waits for * a settled list so a post-create refetch never bounces the user back to onboarding. */ export default function HomeScreen() { const t = useTranslations('home'); const tc = useTranslations('common'); const router = useRouter(); const locale = useLocale(); const { data: me } = useMe(); const { data, isError, refetch } = usePatients(); const [nudgeDismissed, setNudgeDismissed] = useState(patientNudgeDismissedInSession); const isEmpty = data?.total === 0; useEffect(() => { if (isEmpty) router.replace(`/${locale}${ROUTES.ONBOARDING}`); }, [isEmpty, router, locale]); if (isError) { return refetch()} />; } if (data == null || isEmpty) { return ; } const href = (path: string) => `/${locale}${path}`; const profileComplete = me?.hasCustomerProfile ?? false; const firstName = me?.firstName?.trim() || null; const greeting = firstName ? t('greeting_named', { name: firstName }) : t('greeting_plain'); const avatarInitial = firstName ? firstName.charAt(0).toUpperCase() : null; // Completeness signal derived from the cached patients data (no extra fetch): a patient with no // conditions recorded yet is an incomplete record — never a forever-nudge once every record is filled. const hasIncompletePatient = data.items.some((patient) => patient.conditions.length === 0); const showPatientNudge = hasIncompletePatient && !nudgeDismissed; const dismissPatientNudge = () => { patientNudgeDismissedInSession = true; setNudgeDismissed(true); }; return ( {avatarInitial ?? } {greeting} {t('subtitle')} router.push(`${href(ROUTES.SEARCH)}?category_id=${categoryId}`)} /> {showPatientNudge ? ( ) : null} {!profileComplete ? ( ) : null} ); } /** * Quiet, one-line ambient reassurance under the greeting — not a hero. Three icon+label items: escrow * payment, verified nurses, support. Purely presentational; tokens only. */ const TrustStrip: FunctionComponent = () => { const t = useTranslations('home'); const items: Array<{ icon: string; label: string }> = [ { icon: 'lock', label: t('trust_escrow') }, { icon: 'verification', label: t('trust_verified_nurses') }, { icon: 'support', label: t('trust_support') }, ]; return ( {items.map((item) => ( {item.label} ))} ); }; /** * The Home search entry point — a tappable faux-input (never a half-working free-text field: the search * index has no text column, variant names aren't client-queryable, and the only matchable dataset — 5–6 * cached category names — is already better served by the category grid directly below). Routes straight * to C1 (`/search`). **Upgrade path**: once the backend serves a `q` param on `search/nurses` (REQ-041, * matching nurse/variant/category names), this can become a real typeahead — the placeholder copy is * already written for that future, so only the tap target need change, not the copy/i18n keys. */ const HomeSearchBar: FunctionComponent = () => { const t = useTranslations('home'); const router = useRouter(); const locale = useLocale(); return ( router.push(`/${locale}${ROUTES.SEARCH}`)} aria-label={t('search_action')} sx={{ justifyContent: 'flex-start', gap: 1, width: '100%', px: 2, py: 1.5, borderRadius: 'var(--bal-radius-md)', border: '1px solid', borderColor: 'divider', bgcolor: 'background.paper', color: 'text.secondary', }} > {t('search_placeholder')} ); }; /** The data-driven service-category grid — one tile per `service_category`, with all four states. */ const CategoryGrid: FunctionComponent<{ onSelect: (categoryId: number) => void }> = ({ onSelect }) => { const t = useTranslations('home'); const tc = useTranslations('common'); const locale = useLocale(); const { data, isLoading, isError, refetch } = useServiceCategories(); const categories = data?.items ?? []; return ( {t('categories_title')} {isLoading ? ( {[0, 1, 2, 3].map((key) => ( ))} ) : isError ? ( refetch()} /> ) : categories.length === 0 ? ( ) : ( {categories.map((category) => ( onSelect(category.id)} /> ))} )} ); }; /** * The "رزرو دوباره" shortcut row — repeat care is the dominant pattern in home nursing. Sourced from the * existing `useBookingList('customer')` cache (no extra list fetch); renders up to 2 cards, deduplicated * by nurse, deep-linking to the nurse's C3 profile. Renders nothing (no empty state) when there is no * past-bookings history. */ const RebookRow: FunctionComponent = () => { const { data, isLoading, isError } = useBookingList('customer', { pageSize: 5 }); const items = data?.items ?? []; if (isLoading || isError || items.length === 0) return null; const seen = new Set(); const candidates: BookingListItemDto[] = []; for (const item of items) { if (seen.has(item.counterpartyName)) continue; seen.add(item.counterpartyName); candidates.push(item); if (candidates.length === 2) break; } if (candidates.length === 0) return null; return ( {candidates.map((booking) => ( ))} ); }; /** One rebook card — resolves the booking's `nurseId` (not on the list row) via the cached booking * detail, then deep-links to the nurse's C3 profile. Renders nothing while resolving. */ const RebookCard: FunctionComponent<{ booking: BookingListItemDto }> = ({ booking }) => { const t = useTranslations('home'); const router = useRouter(); const locale = useLocale(); const { data: detail } = useBookingDetail(booking.id, 'customer'); if (!detail) return null; const open = () => router.push(`/${locale}${ROUTES.SEARCH_NURSE}/${detail.nurseId}`); return ( { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); open(); } }} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 2, cursor: 'pointer' }} > {t('rebook_with', { name: booking.counterpartyName })} ); };