Files
clinicpro/CLAUDE.md
T

7.0 KiB

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.

Backend (PHP/Symfony)

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
ddev exec php vendor/bin/phpstan analyse

Frontend (React/TypeScript)

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 three 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

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
  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)

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