# CLAUDE.md

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

## Project Overview

**ClinicPro** — a clinic management and appointment booking platform migrated from Drupal to Symfony 7. It consists of a Symfony REST API backend and a React 19 admin SPA bundled inside Symfony via Webpack Encore.

- **Local URL:** `https://clinic-pro.ddev.site`
- **Admin panel:** `https://clinic-pro.ddev.site/admin`
- **Swagger UI:** `https://clinic-pro.ddev.site/api/doc`

---

## Commands

All commands run inside ddev: prefix with `ddev exec` unless noted.

### First-time setup
```bash
ddev exec composer install
ddev exec php bin/console lexik:jwt:generate-keypair   # generate JWT keys
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
ddev exec yarn install && ddev exec yarn dev
ddev exec php bin/console app:create-admin             # create first admin user
```

### Backend (PHP/Symfony)
```bash
ddev exec php bin/console cache:clear
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
ddev exec php bin/console doctrine:migrations:diff --no-interaction   # generate migration after entity change
ddev exec php bin/console debug:router | grep api
ddev exec php bin/console messenger:consume async                      # start queue worker

# Tests
ddev exec php bin/phpunit
ddev exec php bin/phpunit tests/SomeTest.php                          # single test file

# Static analysis (level 5, with Symfony + Doctrine extensions)
ddev exec php vendor/bin/phpstan analyse
```

### Frontend (React/TypeScript)
```bash
ddev exec yarn dev          # one-off dev build (use this to check for errors)
ddev exec yarn watch        # watch mode
ddev exec yarn build        # production build

# Type check only (faster)
ddev exec npx tsc --noEmit --project tsconfig.json
```

> **Note:** The CSS build has a known `lightningcss.linux-arm64-gnu.node` native module error inside ddev — this is pre-existing and does not block JS/TS compilation. TypeScript errors only appear in TSC output.

---

## Architecture

### Backend — `src/`

Domain-driven structure; each domain is its own namespace under `App\<Domain>\`:

```
src/
  Admin/Controller/AdminApiController.php   # all admin-only list/stats endpoints
  Appointment/     Doctor/     Clinic/
  Auth/            Payment/    Rating/
  Blog/            Representation/  Secretary/
  Category/        Settlement/      Sms/
  Shared/Controller/BaseController.php      # all controllers extend this
  Shared/Constant/ErrorCodes.php
```

**Every controller extends `BaseController`** which provides four response helpers:

| Method | Shape | When to use |
|--------|-------|-------------|
| `$this->success($data)` | `{ success, data: $data }` | Single resource / action |
| `$this->paginated($items, $total, $page, $limit)` | `{ success, data: $items[], meta: { totalRecords, totalPages, currentPage } }` | Admin list endpoints |
| `$this->error($code, $message, $status)` | `{ success:false, errors:[{code,message}] }` | All error responses |
| `$this->validationError($violations)` | `{ success:false, errors:[{code,field,message}] }` HTTP 422 | Input validation failures |

**Domain exceptions:** throw `AppException(ErrorCodes::ERR_XXX, null, $httpStatus)` anywhere in the domain — `ExceptionSubscriber` catches it and calls `$this->error()` automatically. All error codes and their Persian messages live in `src/Shared/Constant/ErrorCodes.php`.

**Critical pitfall — double-nested responses:**  
`$this->success(['data' => $rep->toArray()])` produces `{ data: { data: {...} } }`, so the frontend must extract with `data?.data?.data`. The `paginated()` helper does NOT nest — it returns `data` as a flat array.

### Frontend — `assets/admin/`

Single-page app mounted at `/admin/*`:

```
assets/admin/
  App.tsx                    # React Router routes
  pages/                     # one file per page
  components/
    ui/                      # DataTable, Modal, ConfirmDialog, PageHeader, StatusBadge, Pagination,
                             #   SearchableSelect, PersianDateInput, PersianCalendar, AppointmentStatusDropdown
    layout/                  # AdminLayout, Sidebar, Topbar
  hooks/                     # custom React hooks
  lib/api.ts                 # fetch wrapper (reads JWT from localStorage key: clinicpro-auth)
  lib/utils.ts               # formatRial, formatNumber, formatDate, formatDateTime
  types/index.ts             # all TypeScript interfaces
  stores/
    authStore.ts             # Zustand auth store (persisted to localStorage)
    uiStore.ts               # sidebar open/close state
```

**Data fetching pattern:** TanStack Query v5 (`useQuery` / `useMutation`). Query keys use `['resource-name', page, filters]`.

**API response types in `lib/api.ts`:**
- `ApiResponse<T>` — for single-resource responses: extract with `data?.data`
- `PaginatedResponse<T>` — for admin lists: items at `data?.data`, total at `data?.meta?.totalRecords`

**Forms:** React Hook Form + Zod resolver. Schema defined with `z.object()`, type inferred with `z.infer<typeof schema>`.

### Auth

- JWT stored in Zustand store → `localStorage['clinicpro-auth']` → `state.token`
- `api.ts` reads it automatically for every request
- Admin routes require `ROLE_ADMIN`. `#[IsGranted('ROLE_ADMIN')]` on controller class or method.
- Public endpoints listed in `config/packages/security.yaml` under `public_endpoints` firewall pattern

### Category / Bundle system

Categories are polymorphic via a `bundle` string field. Used for: `state`, `city`, `specially_doctor`, `doctor_services`, `insurance_type`, `supplementary_insurance`, `tag`.

City IDs (integer FK to `categories.id` where `bundle='city'`) are stored on entities like `Representation.cityId`. To get the city name, LEFT JOIN the categories table in DQL.

### Database

- MariaDB 11.8 via ddev
- Doctrine ORM with integer Unix timestamps (`createdAt`, `updatedAt`) — **not** DateTime objects
- All admin list queries in `AdminApiController` use DQL array hydration (`.getArrayResult()`) to avoid triggering non-existent getter errors on entities
- Migrations in `migrations/` — always run `doctrine:migrations:diff` after entity changes

---

## Key Patterns

**Adding a new admin list endpoint (backend):**
1. Add method to `src/Admin/Controller/AdminApiController.php`
2. Use `$this->em->createQueryBuilder()` with `->getArrayResult()` (never use entity getters in admin list queries)
3. Return `$this->paginated($items, $total, $page, $limit)`

**Adding a new admin page (frontend):**
1. Create `assets/admin/pages/XxxPage.tsx`
2. Use `PaginatedResponse<YourType>` with `useQuery`
3. Extract: `data?.data` for items, `data?.meta?.totalRecords` for total
4. Add route in `App.tsx`
5. Add UI components: `<DataTable>`, `<Pagination>`, `<Modal>`, `<ConfirmDialog>`

**Category API endpoint pattern:** `GET /api/v1/categorys/{bundle}` (note: typo `categorys` is intentional — existing route). Response is double-nested: extract array with `data?.data?.data ?? []`.

---

## Standing Rule — API Documentation

**Whenever any API endpoint is created or modified** (controller file, route, request/response structure, error code, permission), the corresponding file in `docs/api/` **must be updated in the same session**.

| Changed file | Doc to update |
|---|---|
| `src/Auth/*` | `docs/api/auth.md` |
| `src/Doctor/*` | `docs/api/doctor.md` |
| `src/Clinic/*` | `docs/api/clinic.md` + `docs/api/clinic-invitation.md` |
| `src/Appointment/Controller/AppointmentController.php` | `docs/api/appointment.md` |
| `src/Appointment/Controller/AppointmentSettings*` | `docs/api/appointment-settings.md` |
| `src/Payment/*` | `docs/api/payment.md` |
| `src/Settlement/*` | `docs/api/settlement.md` |
| `src/Rating/*` | `docs/api/rating.md` |
| `src/Secretary/*` | `docs/api/secretary.md` |
| `src/Representation/*` | `docs/api/representation.md` |
| `src/Sms/*` | `docs/api/sms.md` |
| `src/Blog/*` | `docs/api/blog.md` |
| `src/Admin/*` | `docs/api/admin.md` |
| Category/Province/City controllers | `docs/api/location.md`, `docs/api/specialty.md`, `docs/api/insurance.md`, `docs/api/doctor-service.md`, `docs/api/tag.md` |
