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>
156 lines
7.4 KiB
Markdown
156 lines
7.4 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
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
|
|
|
|
```env
|
|
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:
|
|
|
|
```js
|
|
// 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):
|
|
|
|
```js
|
|
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)
|
|
- `axios` directly for cases needing more control
|
|
|
|
**Client Components** use:
|
|
- `services/api.js` — axios instance with `baseURL = NEXT_PUBLIC_API_URL`
|
|
- `services/response.js` → `request.*` — 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.js`** → `getUser()` — reads cookies server-side
|
|
- **`lib/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 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 | `getAppointmentSlots` → `adaptSlots` |
|
|
| `service` | **service → date → time** | `getServiceSlots` → `adaptServiceSlots` |
|
|
|
|
- `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):
|
|
```jsx
|
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
|
```
|
|
Present on `/doctor/[slug]` (Physician), `/clinic/[slug]` (MedicalClinic), `/blog/[slug]` (Article).
|