frontend phase 4: catalog browse (Home A5) & nurse service builder (B7)

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>
This commit is contained in:
hamid
2026-07-05 17:33:44 +03:30
parent 5839b3508f
commit 99ebf5d881
41 changed files with 2382 additions and 26 deletions
+82 -2
View File
@@ -7,6 +7,7 @@
"profile": "Profile",
"bank": "Bank account",
"coverage": "Coverage",
"services": "Services",
"dashboard": "Dashboard",
"verification": "Verification",
"visits": "Visits",
@@ -43,8 +44,14 @@
"placeholder_body": "This area will be built in a later phase."
},
"home": {
"greeting": "Welcome to Balinyaar",
"greeting_named": "Hi, {name}",
"greeting_plain": "Hi there",
"subtitle": "Manage care for the people you love.",
"search_placeholder": "Search a service or nurse…",
"search_action": "Search",
"categories_title": "Services",
"categories_error": "Couldn't load services.",
"categories_empty": "No services have been set up yet.",
"nudge_patient_title": "Complete the patient record",
"nudge_patient_body": "Add conditions, medications and routine so nurses arrive prepared.",
"nudge_patient_cta": "Open patients",
@@ -134,7 +141,7 @@
"unverified_title": "Your profile isn't active yet",
"unverified_body": "Complete identity verification to appear to families and receive bookings.",
"unverified_cta": "Complete verification",
"deferred_services": "You'll add your services & prices and available days in a later step."
"deferred_services": "Manage your services & prices in the Services section; available days come in a later step."
},
"bank": {
"title": "Bank account",
@@ -235,6 +242,79 @@
"remove_confirm": "Remove",
"removed": "Coverage area removed"
},
"catalog": {
"unit_per_hour": "hourly",
"unit_per_session": "per session",
"unit_per_half_day": "per half-day",
"unit_per_day": "daily",
"unit_per_24h": "24-hour (live-in)",
"count_per_hour": "hours",
"count_per_session": "sessions",
"count_per_half_day": "half-days",
"count_per_day": "days",
"count_per_24h": "days (24h)",
"estimate_label": "Estimated total",
"estimate_for": "for {count} {unit}"
},
"services": {
"title": "Services & prices",
"subtitle": "The services you offer and what each costs.",
"add": "Add service",
"empty_title": "No services yet",
"empty_body": "Add at least one active, priced service so families can find and book you.",
"active_chip": "Active",
"inactive_chip": "Deactivated",
"inactive_hint": "This service is deactivated and can't be booked.",
"edit": "Edit",
"deactivate": "Deactivate",
"activate": "Reactivate",
"deactivate_title": "Deactivate this service?",
"deactivate_body": "It becomes unbookable and drops out of search. You can reactivate it anytime — it is never deleted.",
"deactivate_confirm": "Deactivate",
"deactivated_toast": "Service deactivated",
"activated_toast": "Service reactivated",
"toggle_error": "That couldn't be done. Please try again.",
"builder_add_title": "Add a new service",
"builder_edit_title": "Edit service",
"step_category": "Category",
"step_options": "Options",
"step_price": "Price",
"category_title": "Choose the service category",
"category_subtitle": "Every service belongs to one category.",
"categories_error": "Couldn't load categories. Please try again.",
"category_locked": "The category can't be changed after a service is created.",
"options_title": "Set the service options",
"options_subtitle": "Pick one option for each required item.",
"options_none": "This category has no options to configure.",
"required_badge": "Required",
"optional_badge": "Optional",
"options_incomplete": "Answer every required item to continue.",
"price_title": "Set the price and unit",
"price_label": "Price (Toman)",
"price_hint": "Enter the price in Toman.",
"price_required": "Enter a valid price greater than zero.",
"unit_label": "Price unit",
"duration_label": "Duration / count",
"duration_hint": "Optional — add a duration to preview the estimated total.",
"rate_note": "This is the base rate per unit, not the full total.",
"display_name_label": "Display name",
"display_name_hint": "Auto-generated from your options — you can edit it.",
"duplicate_warning": "You already have a service with these exact details. Change an option or the category.",
"summary_options": "Options",
"summary_none": "No options",
"next": "Next",
"submit_create": "Add service",
"submit_save": "Save changes",
"create_error": "The service couldn't be saved. Check your inputs and try again.",
"created_toast": "Service added",
"saved_toast": "Changes saved"
},
"search": {
"title": "Search",
"deferred": "Search and results arrive in the next phase.",
"query_echo": "You searched for “{query}”.",
"category_echo": "Filtered to the selected service category."
},
"auth": {
"customer_title": "Sign in to Balinyaar",
"customer_subtitle": "Sign in with your mobile number",
+82 -2
View File
@@ -7,6 +7,7 @@
"profile": "پروفایل",
"bank": "حساب بانکی",
"coverage": "پوشش",
"services": "خدمات",
"dashboard": "داشبورد",
"verification": "احراز هویت",
"visits": "ویزیت‌ها",
@@ -43,8 +44,14 @@
"placeholder_body": "این بخش در فازهای بعدی تکمیل می‌شود."
},
"home": {
"greeting": "به بلینیار خوش آمدید",
"greeting_named": "سلام، {name}",
"greeting_plain": "سلام",
"subtitle": "مراقبت از عزیزانتان را مدیریت کنید.",
"search_placeholder": "جستجوی خدمت یا پرستار…",
"search_action": "جستجو",
"categories_title": "خدمات",
"categories_error": "خدمات بارگذاری نشد.",
"categories_empty": "هنوز خدمتی تعریف نشده است.",
"nudge_patient_title": "تکمیل پروندهٔ بیمار",
"nudge_patient_body": "وضعیت‌ها، داروها و روتین را اضافه کنید تا پرستار آماده حاضر شود.",
"nudge_patient_cta": "مشاهدهٔ بیماران",
@@ -134,7 +141,7 @@
"unverified_title": "پروفایل شما هنوز فعال نیست",
"unverified_body": "برای نمایش به خانواده‌ها و دریافت رزرو، احراز هویت را تکمیل کنید.",
"unverified_cta": "تکمیل احراز هویت",
"deferred_services": "خدمات و قیمت‌ها و روزهای کاری را در مرحله بعد اضافه می‌کنید."
"deferred_services": "خدمات و قیمت‌های خود را در بخش «خدمات و قیمت‌ها» مدیریت کنید؛ روزهای کاری در مرحله بعد اضافه می‌شود."
},
"bank": {
"title": "حساب بانکی",
@@ -235,6 +242,79 @@
"remove_confirm": "حذف",
"removed": "منطقهٔ تحت پوشش حذف شد"
},
"catalog": {
"unit_per_hour": "ساعتی",
"unit_per_session": "هر جلسه",
"unit_per_half_day": "نیم‌روزی",
"unit_per_day": "روزانه",
"unit_per_24h": "شبانه‌روزی",
"count_per_hour": "ساعت",
"count_per_session": "جلسه",
"count_per_half_day": "نیم‌روز",
"count_per_day": "روز",
"count_per_24h": "شبانه‌روز",
"estimate_label": "برآورد کل",
"estimate_for": "برای {count} {unit}"
},
"services": {
"title": "خدمات و قیمت‌ها",
"subtitle": "خدماتی که ارائه می‌دهید و قیمت هرکدام.",
"add": "افزودن خدمت",
"empty_title": "هنوز خدمتی اضافه نکرده‌اید",
"empty_body": "برای دیده‌شدن و دریافت رزرو، حداقل یک خدمت فعال و دارای قیمت اضافه کنید.",
"active_chip": "فعال",
"inactive_chip": "غیرفعال",
"inactive_hint": "این خدمت غیرفعال است و قابل رزرو نیست.",
"edit": "ویرایش",
"deactivate": "غیرفعال‌سازی",
"activate": "فعال‌سازی دوباره",
"deactivate_title": "این خدمت غیرفعال شود؟",
"deactivate_body": "این خدمت قابل رزرو نخواهد بود و از نتایج جستجو حذف می‌شود. هر زمان می‌توانید دوباره فعالش کنید — هرگز حذف نمی‌شود.",
"deactivate_confirm": "غیرفعال کن",
"deactivated_toast": "خدمت غیرفعال شد",
"activated_toast": "خدمت دوباره فعال شد",
"toggle_error": "این عملیات انجام نشد. دوباره تلاش کنید.",
"builder_add_title": "افزودن خدمت جدید",
"builder_edit_title": "ویرایش خدمت",
"step_category": "دسته",
"step_options": "گزینه‌ها",
"step_price": "قیمت",
"category_title": "دستهٔ خدمت را انتخاب کنید",
"category_subtitle": "هر خدمت به یک دسته تعلق دارد.",
"categories_error": "دسته‌ها بارگذاری نشد. دوباره تلاش کنید.",
"category_locked": "دسته پس از ایجاد خدمت قابل تغییر نیست.",
"options_title": "گزینه‌های خدمت را مشخص کنید",
"options_subtitle": "برای هر مورد الزامی یک گزینه انتخاب کنید.",
"options_none": "این دسته گزینه‌ای برای تنظیم ندارد.",
"required_badge": "الزامی",
"optional_badge": "اختیاری",
"options_incomplete": "برای ادامه، همهٔ موارد الزامی را انتخاب کنید.",
"price_title": "قیمت و واحد را تعیین کنید",
"price_label": "قیمت (تومان)",
"price_hint": "قیمت را به تومان وارد کنید.",
"price_required": "قیمتی معتبر و بزرگ‌تر از صفر وارد کنید.",
"unit_label": "واحد قیمت",
"duration_label": "مدت / تعداد",
"duration_hint": "اختیاری — برای نمایش برآورد هزینهٔ کل، مدت را وارد کنید.",
"rate_note": "این نرخ پایه به ازای هر واحد است، نه هزینهٔ کل.",
"display_name_label": "نام نمایشی",
"display_name_hint": "به‌صورت خودکار از گزینه‌ها ساخته می‌شود؛ می‌توانید تغییرش دهید.",
"duplicate_warning": "شما قبلاً خدمتی با همین مشخصات دارید. یک گزینه یا دسته را تغییر دهید.",
"summary_options": "گزینه‌ها",
"summary_none": "بدون گزینه",
"next": "بعدی",
"submit_create": "ثبت خدمت",
"submit_save": "ذخیره تغییرات",
"create_error": "ثبت خدمت انجام نشد. ورودی‌ها را بررسی کرده و دوباره تلاش کنید.",
"created_toast": "خدمت اضافه شد",
"saved_toast": "تغییرات ذخیره شد"
},
"search": {
"title": "جستجو",
"deferred": "جستجو و نتایج در فاز بعدی اضافه می‌شود.",
"query_echo": "جستجوی شما: «{query}».",
"category_echo": "محدود به دستهٔ خدمت انتخاب‌شده."
},
"auth": {
"customer_title": "ورود به بلینیار",
"customer_subtitle": "با شماره موبایل خود وارد شوید",