8.7 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Read first
Project conventions already live in AGENTS.md (project map + per-feature quick-reference), .cursor/rules/*.mdc (short always-on / file-scoped rules), and .cursor/skills/*/SKILL.md (multi-step workflow playbooks). They are plain markdown — read the ones covering the area you touch before editing. This file covers only what those do not: commands and cross-cutting architecture.
Per .cursor/rules/maintain-agent-docs.mdc: when the user establishes a durable convention, update the matching .mdc rule or SKILL.md — not this file.
Commands
There is no root package.json. Every npm command runs inside backend/ or frontend/.
Backend (cd backend)
| Command | Purpose |
|---|---|
npm run start:dev |
API on http://localhost:3000/api; Swagger /api/docs; AdminJS /admin |
npm run build |
Verification gate for cross-cutting backend changes |
npm test |
Jest (src/**/*.spec.ts) |
npm test -- lab-case-task.generator |
Single suite by path fragment |
npm test -- -t "merges teeth" |
Single test by name |
npm run test:e2e |
Jest with test/jest-e2e.json |
npm run lint |
ESLint with --fix |
docker compose -f docker-compose.postgres.yml up -d |
Dev Postgres (host port from POSTGRES_PORT in .env) |
npm run prisma:generate / prisma:migrate / prisma:seed |
Client, dev migration, reference-data upsert (seed never wipes) |
npx prisma migrate reset |
Dev clean slate — drop, re-migrate, re-seed. Never against staging/prod |
npm run prisma:wipe-app-data / prisma:reset-treatment / prisma:regenerate-tasks |
Targeted dev data scripts |
DATABASE_URL must use localhost when Nest runs on the host and Postgres in Docker.
Frontend (cd frontend)
| Command | Purpose |
|---|---|
npm run dev |
Dev server on 3001 (3000 is the API) |
npx tsc --noEmit |
Verification gate for any type or cross-cutting frontend change |
npm run build |
Production build (output: 'standalone') |
npm run lint |
ESLint via Next |
NEXT_PUBLIC_* values are baked in at build time — restart npm run dev after changing .env.local.
Git
Do not commit, push, amend, force-push, or skip hooks unless the user explicitly asks.
Architecture
Dental clinic ↔ lab platform. Every user acts inside one Organization whose type is CLINIC (patients, appointments, treatment) or LAB (cases, tasks). Most features exist only for one side.
Request identity: cookie JWT carrying the selected org
There is no Authorization header. JwtStrategy reads the httpOnly accessToken cookie, and the JWT payload carries organizationId — the org the user currently acts as. POST /auth/select-organization re-issues the token with a different org, so switching orgs means a new token, and every service scopes queries by req.user.organizationId.
On 401 the axios interceptor (frontend/src/lib/api/client.ts) refreshes, re-selects the org from localStorage.currentOrganizationId, then retries the original request — skipping that dance for auth endpoints and public invitation routes. frontend/src/proxy.ts (the Next middleware, exported as proxy) is a separate, cookie-only route gate that redirects unauthenticated users to /{locale}/login?from=….
Permissions
TAB_*_READ / TAB_*_EDIT codes in backend/src/common/permissions.ts; EDIT implies READ. Owners get org-type defaults merged with stored grants — always resolve via hasEffectivePermission / getEffectivePermissionNames in common/membership-permissions.ts, never by reading membership.permissions directly. Controllers stack JwtAuthGuard + ClinicOrgGuard/LabOrgGuard; feature-specific checks belong in the service.
Error contract (spans 3 layers — change all of them)
AppException(ErrorCode.X) → HttpExceptionFilter → { success: false, error: { code } } → axios normalizes to ApiError → getUserFacingError(err, tErrors, fallback) resolves errors.X from the message files. Adding a user-facing failure means: a code in common/errors/error-codes.ts, the throw site, and an errors.X key in all three of frontend/messages/{en,fa,nl}.json. Never throw raw English Nest exceptions for user-facing failures.
The core domain pipeline
Appointment ─┐
├→ Treatment (patient + day) → TreatmentDetail (treatment type + selected teeth)
Walk-in ─────┘ │
│ "send to lab" (clinic side)
▼
LabCase + LabCaseToothProsthesis (per tooth, grouped by sourceKey)
│ generateLabCaseTasks()
▼
ProsthesisType → ProsthesisTypeStep → LabWorkflowStep ⇒ LabCaseTask rows
│
▼
LAB org: Cases tab + Tasks tab
backend/src/modules/cases/lab-case-task.generator.ts is the expansion point: it is idempotent (returns early if tasks exist) and drives the entire lab-side task list from catalog data. Teeth carry selectionGroupId so bridges/connected units survive into task grouping. A LabCase can also be lab-origin (LabCaseOrigin), created without any clinic treatment.
Clinics may only dispatch to labs they are linked to: OrganizationLink (A↔B, LinkStatus), plus OrganizationInvitation for counterparts not yet on the platform — the invite flow writes both rows in one transaction and stores only the token hash.
Catalog is code-based and DB-translated
TreatmentType, ProsthesisType, and LabWorkflowStep store a stable code and no label. Labels come from CatalogTranslation(entityKind, entityCode, locale) resolved by CatalogLabelService (falls back locale → en → humanized code). So: never hardcode a catalog label in backend code, and pass the actor's locale into anything that materializes labels (task generation does). Frontend colors/labels for these codes live in components/shared/treatmentTypeDisplay.ts and components/treatment/prosthesisTypeDisplay.ts.
Realtime and unread state
modules/notifications/user-notification.service.ts writes UserNotification rows and pushes them through the Socket.IO transport in backend/src/realtime/ (emitToUserOrg → notification.created). On the frontend a single notification.created event drives three things: the header bell inbox, sidebar tab badges, and a soft refresh of whatever list is currently open — soft meaning it must not remount components or clear an in-progress treatment draft. Unread is per-user cursor state (LabCaseUserReadState, LabCaseUserTabReadState) plus the LabCaseActivity log — badges clear on opening a case, not on visiting a tab.
Layout conventions worth knowing before you create a file
- Prisma lives outside
src/:backend/prisma/holdsschema.prisma, migrations, seeds andprisma.module.ts/prisma.service.ts— hence imports like../../../prisma/prisma.service. Register new Nest modules inapp.module.ts. - Frontend layering (
.cursor/rules/frontend-components.mdc):app/**/page.tsxis a thin wrapper only → route logic incomponents/ui/{feature}/{Feature}Page.tsx→ JSX incomponents/ui/**→ pure helpers incomponents/{feature}/orcomponents/shared/. No JSX outsideui/, no pure helpers inside it. - i18n is mandatory, not a follow-up: every user-visible string goes into
en.json,fa.json, andnl.json.fais RTL, so use logicaltext-start/text-end, nevertext-left/text-right. Dates/times/numbers go throughlib/i18n/format.ts; form dates useAppDateInput, never a native date input. - Treatment attachments are written to disk at
backend/uploads/treatmentsrelative toprocess.cwd().
Tests
Jest covers pure logic only — permission normalization, phone/timezone helpers, task generation, lab-send validation (7 suites in backend/src/**). There are no frontend tests; npx tsc --noEmit is the frontend gate.
Deployment
Images are built on a dev machine and pulled by the server; Compose files and scripts are in infrastructure/ (docker-compose.{prod,staging,registry}.yml). Full guide: infrastructure/DEPLOY.md. Root README.md covers the Docker Hub + Let's Encrypt path and the Gitea registry path. Frontend NEXT_PUBLIC_* are build args — changing the public domain requires rebuilding the frontend image.