- 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.
137 lines
5.9 KiB
Markdown
137 lines
5.9 KiB
Markdown
# 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)
|
|
```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
|
|
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 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 ?? []`.
|