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>
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'sdomain,title,description,keywords,site_name,socialMedia, etc.lib/getStateInfo.js— server-side: readshostheader, extracts subdomain, matches againstcity.json. Use in Server Components andgenerateMetadata.context/ProvinceProvider.js— client-side equivalent usingwindow.location.hostname. ExposesuseProvince()→{ isProvinceInclude }.lib/getCanonicalUrl.js— readsx-pathnameheader set by middleware for canonical URL generation.middleware.js— injectsx-pathnameheader into every response sogetCanonicalUrlcan 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.js→fetchReq(url)— axios with SSL verification disabled (needed for dev backend)axiosdirectly for cases needing more control
Client Components use:
services/api.js— axios instance withbaseURL = NEXT_PUBLIC_API_URLservices/response.js→request.*— all API call wrappers. Pass{ requireAuth: true }to attach theaccess_tokencookie asAuthorization: Bearer.
Authentication & Authorization
Auth uses JWT stored in cookies: access_token, refresh_token, uuid, userInfo.
lib/auth.js→getUser()— reads cookies server-sidelib/ability.js→defineAbilitiesFor(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 usedata-themeattribute, panel usesclass - MUI theme configured in
mui/index.jswith RTL direction and Vazir font - Font: Vazir only — defined in
app/globals.cssvia@font-facewithfont-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-themesinapp/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 linksdata/state.json— province/state data, joined to city viaprovince_iddata/specialties.json— medical specialties; items withparentfield 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 | getAppointmentSlots → adaptSlots |
service |
service → date → time | getServiceSlots → adaptServiceSlots |
lib/appointmentSlots.jsis the split point. Both adapters return the same shape — an array of sessions{ start_time, end_time, label, slots }— whichapp/component/date/dateTime/index.jsturns into tabs (sessions.length > 1).- Duration is server data.
total_duration_minutescomes fromappointment-service-slots; never sumduration_minutesin 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.jskeeps a client sum only as a labelled fallback with aconsole.warn. - The service step runs before the date step, yet
total_duration_minutesdoes 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_timeslist 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 inclinicpro/docs/api/appointment.md. - User panel:
service_itemsandservice_total_minutescome fromGET /api/v1/appointments/user. Slot-mode appointments have neither, so every read is guarded — an unguarded.mapcrashes 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).