- Introduced CLAUDE.md for internal guidance on project structure, commands, and architecture. - Created README.md with detailed project overview, technology stack, directory structure, setup instructions, API endpoints, authentication flow, and external services.
5.9 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.nodenative 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 withdata?.dataPaginatedResponse<T>— for admin lists: items atdata?.data, total atdata?.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.tsreads 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.yamlunderpublic_endpointsfirewall 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
AdminApiControlleruse DQL array hydration (.getArrayResult()) to avoid triggering non-existent getter errors on entities - Migrations in
migrations/— always rundoctrine:migrations:diffafter entity changes
Key Patterns
Adding a new admin list endpoint (backend):
- Add method to
src/Admin/Controller/AdminApiController.php - Use
$this->em->createQueryBuilder()with->getArrayResult()(never use entity getters in admin list queries) - Return
$this->paginated($items, $total, $page, $limit)
Adding a new admin page (frontend):
- Create
assets/admin/pages/XxxPage.tsx - Use
PaginatedResponse<YourType>withuseQuery - Extract:
data?.datafor items,data?.meta?.totalRecordsfor total - Add route in
App.tsx - 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 ?? [].