Files
nobat724_front/CLAUDE.md
T
hamedandClaude Opus 5 15edc628e4 docs: describe the two booking modes and their front-end contract
Records what the service flow actually guarantees: mode is per location (a doctor
can be slot-based in their office and service-based in a clinic), duration is
server data and must never be summed in the front, and shift boundaries are not
derived client-side because a flat start_times list cannot tell a break between
shifts from a gap left by a booked appointment.

Also notes that user-panel reads of service fields are guarded, since slot-mode
appointments carry none of them.

Task: clinicpro/docs/new_feture/taskes/task-00b-nobat724-service-mode/

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:59:17 +03:30

7.4 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Commands

npm run dev      # Dev server at http://yazd-nobat.localhost:3000 (sets HOST via cross-env)
npm run build    # Production build
npm run start    # Start production server
npm run lint     # ESLint

The dev script forces HOST=yazd-nobat.localhost so multi-domain detection works locally. To test a different city subdomain, temporarily change HOST in the script.

Environment Variables

NEXT_PUBLIC_API_URL=https://api.clinic-pro.ir   # Backend API base URL
DEV_MODE=TRUE                                    # TRUE → blocks all crawlers + noindex

DEV_MODE=TRUE disables robots indexing and adds noindex metadata. Set to FALSE in production.

Architecture Overview

Next.js 15 App Router with MUI v5 + Tailwind CSS, RTL (Persian/Farsi), Jalali calendar.

Multi-Domain System (Core Concept)

Each city has its own domain (e.g. yazd-nobat.ir, tabriz-nobat.ir). The codebase serves all cities from one deployment, detecting which city to show via subdomain.

  • data/city.json — source of truth: every city's domain, title, description, keywords, site_name, socialMedia, etc.
  • lib/getStateInfo.js — server-side: reads host header, extracts subdomain, matches against city.json. Use in Server Components and generateMetadata.
  • context/ProvinceProvider.js — client-side equivalent using window.location.hostname. Exposes useProvince(){ isProvinceInclude }.
  • lib/getCanonicalUrl.js — reads x-pathname header set by middleware for canonical URL generation.
  • middleware.js — injects x-pathname header into every response so getCanonicalUrl can read it.

Page Metadata Pattern

Every page must export generateMetadata. Layout-level metadata is the fallback:

// app/layout.js — sets title/description/OG/Twitter from matchedCity
export async function generateMetadata() {
  const { matchedCity } = await getStateInfo();
  // ...
}

Page-level generateMetadata overrides layout for dynamic pages (doctor, blog, clinic):

export async function generateMetadata({ params }) {
  const { slug } = await params; // Always await params in Next.js 15
  // fetch data, build title/description, return metadata object
}

Data Fetching

Server Components use:

  • lib/req.jsfetchReq(url) — axios with SSL verification disabled (needed for dev backend)
  • axios directly for cases needing more control

Client Components use:

  • services/api.js — axios instance with baseURL = NEXT_PUBLIC_API_URL
  • services/response.jsrequest.* — all API call wrappers. Pass { requireAuth: true } to attach the access_token cookie as Authorization: Bearer.

Authentication & Authorization

Auth uses JWT stored in cookies: access_token, refresh_token, uuid, userInfo.

  • lib/auth.jsgetUser() — reads cookies server-side
  • lib/ability.jsdefineAbilitiesFor(user) — CASL rules. Roles: "representation" → Panel access
  • Protected pages call getUser() + defineAbilitiesFor() and redirect if unauthorized

Login flow: POST /api/v1/user/send-code (OTP) → POST oauth/token → set cookies.

Styling

  • Tailwind CSS with darkMode: "class" — public pages use data-theme attribute, panel uses class
  • MUI theme configured in mui/index.js with RTL direction and Vazir font
  • Font: Vazir only — defined in app/globals.css via @font-face with font-display: swap. No other fonts.
  • Custom CSS classes in globals.css: .bg-banner-home, .bg-banner-footer, .padding-responsive, etc.
  • Dark mode toggled by next-themes in app/Providers.js: public = attribute="data-", panel = attribute="class"

Routing Structure

app/
  layout.js                  # Root layout: metadata, ThemeRegistry, ProvinceProvider
  page.js                    # Home → components/home/
  robots.js                  # Blocks all when DEV_MODE=TRUE
  sitemap.js                 # Fetches doctors/clinics/blogs from API at runtime
  doctor/[slug]/page.js      # generateMetadata + JSON-LD (Physician schema)
  clinic/[slug]/page.js      # generateMetadata + JSON-LD (MedicalClinic schema)
  blog/[slug]/page.js        # generateMetadata + JSON-LD (Article schema)
  doctors/page.js            # generateMetadata using matchedCity
  clinics/page.js            # generateMetadata using matchedCity
  panel/(layout)/            # Route group — requires "representation" role

All public pages wrap content in <Layout name="/path"> from components/layout/StLayout.js (header + footer). Panel pages use components/layoutPanel/.

Key Data Files

  • data/city.json — city configs including domain, SEO fields, social media links
  • data/state.json — province/state data, joined to city via province_id
  • data/specialties.json — medical specialties; items with parent field are sub-specialties shown in FrequentSearches

Booking Modes (slot vs service)

The backend decides how a doctor is booked, per location: a doctor can be slot-based in their own office and service-based in a clinic. The mode arrives as booking_mode on each entry of getBookingLocations, so components/appointment/index.js reads it off the selected location, never off the doctor.

Mode Flow Slot source
slot date → time getAppointmentSlotsadaptSlots
service service → date → time getServiceSlotsadaptServiceSlots
  • lib/appointmentSlots.js is the split point. Both adapters return the same shape — an array of sessions { start_time, end_time, label, slots } — which app/component/date/dateTime/index.js turns into tabs (sessions.length > 1).
  • Duration is server data. total_duration_minutes comes from appointment-service-slots; never sum duration_minutes in the front. The backend formula is going to change to solo/additional minutes, and any parallel client calculation will silently start showing a wrong number. components/appointment/service/index.js keeps a client sum only as a labelled fallback with a console.warn.
  • The service step runs before the date step, yet total_duration_minutes does not depend on the date — the backend computes it before touching that day's shifts — so the picker may ask for it using today's date even if today is closed.
  • Shift boundaries are not derived in the front for service mode: a flat start_times list cannot distinguish a break between shifts from a gap left by a booked appointment. One session with the real range is returned instead. See the note in clinicpro/docs/api/appointment.md.
  • User panel: service_items and service_total_minutes come from GET /api/v1/appointments/user. Slot-mode appointments have neither, so every read is guarded — an unguarded .map crashes the card for all slot-mode appointments.

Backend reference: clinicpro/docs/architecture/booking-modes.md.

Doctor & Clinic Slugs

Both use uuid as the URL slug: /doctor/${doctor.uuid} and /clinic/${clinic.uuid}.

JSON-LD Structured Data

Added directly in page JSX (not via metadata API):

<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />

Present on /doctor/[slug] (Physician), /clinic/[slug] (MedicalClinic), /blog/[slug] (Article).