Files
dyolink/AGENTS.md

14 KiB
Raw Permalink Blame History

Dyolink — Agent guide

This file orients Cursor agents at the start of a new chat. Project conventions live in .cursor/rules/ (auto-loaded). Workflow playbooks live in .cursor/skills/.

Dental clinic ↔ lab platform (monorepo):

Path Stack
backend/ NestJS, Prisma, PostgreSQL
frontend/ Next.js 16, React 19, next-intl, Tailwind
infrastructure/ Docker, nginx, deploy scripts — prod: DEPLOY.md, staging: STAGING-DEPLOY.md

Organization types: CLINIC (patients, appointments, treatment) and LAB (cases, tasks). Many features are org-type-specific. Permissions use TAB_*_READ / TAB_*_EDIT codes — see backend/src/common/permissions.ts.

Before you code

  1. Read applicable rules in .cursor/rules/ (especially dyolink-overview and the file-scoped rule for the area you touch).
  2. Match existing patterns in the nearest feature folder — do not invent parallel structures.
  3. Keep diffs small — one concern per change unless the user asks for a refactor.
  4. Verify: npm run build (backend) and npx tsc --noEmit (frontend) when you change types or cross-cutting code.

Frontend layout (critical)

frontend/src/
  app/                          → thin page.tsx only; compose from ui/
  components/
    ui/shared/                  → cross-feature UI (Button, Sidebar, …)
    ui/{feature}/               → feature UI (+ {Feature}Page.tsx for route logic)
    shared/                     → cross-feature non-UI (formatApiError, permissions, …)
    {feature}/                  → feature non-UI (helpers, config, pure functions)
  lib/                          → api clients, hooks
  types/                        → shared TS types
  assets/brand|fdi/             → brand + FDI SVG sources (not public/) — see `.cursor/rules/frontend-assets.mdc`
  messages/{en,fa,nl}.json      → all user-facing strings

Example thin page: app/.../treatment/page.tsx → imports TreatmentWorkspace from components/ui/treatment/.

i18n formatting: Display dates/times/numbers via lib/i18n/format.ts + useLocale(). Form dates: AppDateInput (all locales — same component, masked typing + calendar popup). Appointments / Treatment headers: ScheduleDayPicker (compact centers the date). Filter <select>s: FORM_SELECT_CLASS; data tables: Table with logical text-start/text-end (not text-left/text-right). Skill: .cursor/skills/i18n-formatting/SKILL.md.

Treatment tab: Preview and editable form are separate until the user clicks Load into workspace on a history item. See .cursor/skills/treatment-workspace/SKILL.md before changing that flow.

Treatment edit / details (quick ref):

  • Day/mode gate: editable only for live draft on today/future (canEditTreatmentForDay). Past day / historical load → read-only form. Patient search sits in the page header (workspace-wide). If the patient has a visit on the day strip, that visit opens; else the latest history plan loads. If neither exists, the editor shows an inline empty state (no dialog) pointing to New treatment in the rail — do not auto-create. New treatment is at the top of the left rail (selected-patient card below it) and opens a picker: Walk-in first, then a matching card for the current named patient (name + mobile/email), then search. It does not auto-copy the appointment cards patient. New treatment seeds one blank detail; a persisted empty plan loads as [] until Add.
  • Unscheduled strip cards use the same treatment-type banner as appointments from the first details type only (unscheduledStripColorCode; live draft for the open card). Empty first line → chip theming even if later lines are typed. Strip trash only when every line is blank (no type/teeth/notes/attachments, including []); typed cards need chip-delete first. Backend DELETE /treatments/:id is empty-only (TREATMENT_HAS_DETAILS).
  • Sent-to-lab detail locks that line; Add detail still OK same day; Remove detail = trash on chip (unsent, including last line; day/edit gates apply). Empty details persist as []. Attachment upload blocked when sent (TREATMENT_DETAIL_SENT).
  • Entry: Type dropdown + compact attachments strip (TreatmentDetailAttachmentsStrip) on one row, then bordered FDI chart (Cases chrome/scale), then full-width auto-growing Notes. No wizard for non-lab types. Prosthesis: WizardStepper Treatment → Lab with Back/Next. Detail chips show type + teeth; lab-dependent chips use colored sent/unsent text (sent date on Lab tab). Switching chips resets to the treatment form unless pendingEntryStepRef requests Lab (shipments rail). Lab dispatch keeps comments.
  • Tooth selection: Hit target is the unrotated cell (pointerdown only — do not also bind click). Glyph is pointer-events-none; nest hover/selected scale inside the rotate wrapper. Neighbor empty/filled circles between selected adjacent teeth connect/disconnect bridges; Shift+range selects only (empty circles; overlap absorbs as singles); midline 1121 / 4131 allowed. Plain click selects/deselects (deselect splits bridges). Never a 1-tooth connected. Helpers: toothSelectionGroups.ts. Connected label: ConnectedSelectionBadge. After send, Cases/Tasks merge teeth by prosthesis type.
  • Lab dispatch layout: due date beside title (justify-between, logical start/end for RTL); prosthesis type on same row as teeth from md: up (stacked on mobile).
  • Schedule date: Treatment strip and Appointments page headers use ScheduleDayPicker compact (date centered in a 3-col header; no “Schedule date” label; Today on the navigator).

Treatment lab rules (quick ref):

  • Lab-dependent details (e.g. prosthesis) without teeth can save but cannot ship — same inline amber banner (labShipmentBlockedBody) on Treatment and Lab steps; toast on dispatch add.
  • New prosthesis dispatch lines start empty (no default lab, no apply-all / per-tooth prosthesis type), even for siblings in the same plan. Recent-lab chips are the last 3 sent destinations — pick is explicit, never auto-selected.
  • Detail treatment type need not match appointment purpose — purpose only seeds the first line of an empty appointment draft (first open, and Add detail when the plan is []). Further Add detail starts with an empty type. Unscheduled / New treatment still seeds a blank first line.
  • History filters are client-side only (treatmentHistoryFilters.ts): “Not shipped to lab” + single date on already-fetched patient history; includes live current draft when filtering.
  • Lab shipments rail: unified list with scope toggle This patient vs All updates (unread across org for this clinician's cases only, includes patient name). Opening a case from the rail jumps to the Lab send sheet.
  • Unread semantics: Treatment tab badge = count of unread cases for the user's own treatment plans (per-case read cursor) and clears when a case is opened/marked read (not on tab visit).
  • Live lab rail: notification.creatednotifyTabBadgesChanged() silently refreshes patient lab cases + unread rail (does not clear draft/form state).
  • Lab shipment progress + comments: shown in Lab dispatch panel for the active shipment; expanding activity / opening comments marks that case read. Shared UI: LabCaseCommentsPanel — newest first; sent = start / received = end (text-start/justify-start, RTL-safe); pass viewerSide.

Appointments (quick ref): Do not delete (or change patient) when hasTreatment; codes APPOINTMENT_HAS_TREATMENT / APPOINTMENT_PATIENT_LOCKED. Past days: no new bookings; edit/delete OK without treatment; with treatment → toast. Appointment delete does not cascade-delete treatments. Working hours: client IANA timeZone on create/update — never Date#getHours()/getDay() on the UTC server. Logical API errors: AppException + errors.* (never Nest English throws). See .cursor/rules/appointments.mdc, .cursor/skills/api-errors/SKILL.md.

Lab Tasks tab: Newest case first; steps ordered 1→N; case grouping when sorted by date; stepCompleted filter; filter by case source (origin: received vs generated); prosthesis colors from catalog; task assignment in Cases (compact row: status + assignee + last update); on Tasks, all staff see every task but only assignee (or unassigned pool) can change status — others see “Assigned to {name}” instead of the status dropdown; case due dates set/edited in clinic Treatment lab dispatch, shown on lab Cases/Tasks with overdue filter + sort; completing intraoral_scan completes every scan task in that case (case-scoped; catalog first step for all prosthesis types); mobile: larger task status controls, sticky case header when grouped; tab badges: LabCaseActivity + GET /notifications/tab-counts (lab Cases/Tasks split, clinic Treatment) — live via inbox Socket.IO → notifyTabBadgesChanged() + soft list refresh — see .cursor/skills/lab-tasks/SKILL.md, .cursor/skills/tab-badges/SKILL.md, .cursor/skills/notifications-inbox/SKILL.md.

Lab Cases tab: Filter by prosthesis type (not treatment type); auto-select newest case on open; list 10 per page; left rail list fills column height (flex-1 overflow-y-auto); list cards use LabCaseProsthesisGroupsList (colored type + teeth, shared with Treatment rail) plus Received / Generated origin badges. Deep link: ?caseId=, ?clinicOrganizationId=. Share link: QR + URL on sent cases (attachment left, QR right); opens /lab-case/[token] focus page. Case Sheet PDF: client A4 (jspdf/html2canvas); hex-only print layout; optional externalCode replaces order number. Lab-origin Start has no clinic send; comments and mark-read still work (no clinic-visibility toggle). Unstarted generated drafts can be deleted (DELETE /cases/:id). Live: inbox Socket.IO → notifyTabBadgesChanged() soft-refreshes list + selected detail (no remount). See .cursor/skills/lab-cases/SKILL.md and .cursor/skills/lab-case-share-link/SKILL.md.

Lab case share link (quick ref):

  • Token on first ship → /{locale}/lab-case/{token} after login.
  • Lab: view/edit tasks (assignee rules), comments + visibility toggle.
  • Clinic: treatment provider only — read-only tasks, can comment.
  • Logged out → login with ?from=storeAuthRedirectFromPath then useEnterAppWhenAuthenticated (consumeAuthRedirect once after org ready — not inside useAuth.login() / registerTrial). Trial register uses the same hook; staff/org invite accept then login() + navigateIntoAppIfOrgSelected. See .cursor/rules/post-auth-navigation.mdc.

Today dashboard: KPIs + charts per org type/permissions; deep links via today-deep-links.ts (Tasks KPIs/charts, Staff highlight, case partners). See .cursor/skills/today-dashboard/SKILL.md.

Backend layout

backend/src/
  modules/{feature}/            → controller, service, dto, module
  common/                       → guards, permissions, errors, utils
  prisma/                       → schema, migrations, seed

Errors: AppException + ErrorCode → frontend getUserFacingError(). Never throw raw strings for user-facing failures.

Git & commits

  • Do not commit or push unless the user explicitly asks.
  • Do not amend commits, force-push, or skip hooks unless explicitly requested.

Skills (workflows)

Skill When to use
.cursor/skills/add-feature/ New tab, API module, or end-to-end feature
.cursor/skills/treatment-workspace/ Treatment tab: preview vs form, history, load flow, drafts, type-first entry, connected teeth
.cursor/skills/lab-tasks/ Lab Tasks tab: sort, case grouping, step-completed filter, prosthesis colors
.cursor/skills/lab-cases/ Lab Cases tab: prosthesis filter, auto-select, Case Sheet PDF, external code
.cursor/skills/lab-case-share-link/ Case QR share link: access token, focus page, auth redirect, access rules
.cursor/skills/tab-badges/ Sidebar tab badges: Cases/Tasks/Treatment, LabCaseActivity, tab-counts API, read cursors
.cursor/skills/notifications-inbox/ Header bell inbox: UserNotification fan-out, Socket.IO realtime
.cursor/skills/today-dashboard/ Today tab: KPIs, charts, deep links, gadget registry
.cursor/skills/frontend-structure/ Moving components, auditing folder layout
.cursor/skills/api-errors/ New backend errors + frontend translations
.cursor/skills/i18n-formatting/ Dates, times, numbers, Jalali picker, RTL formatting

Subagents (Task tool)

Use subagents to save context, not to avoid work:

Type Use for
explore Broad codebase search, unfamiliar areas
shell Git, npm, long command sequences
generalPurpose Multi-step research when parent context is large

Do not delegate the user's main task to a subagent and return its summary — implement in the parent unless the user asked for exploration only.

Improving this setup

When you and the user agree on a new convention, add or update a rule in .cursor/rules/ (keep each rule under ~50 lines, one topic). For multi-step workflows, extend .cursor/skills/.

To save a convention mid-task, say: "Remember this" or "Add to project rules" — the agent uses the capture-convention skill and updates the repo (commit with your code).

You say Agent does
"Remember this: …" Updates the right .mdc rule or skill
"Add a skill for …" Creates .cursor/skills/{name}/SKILL.md
"This rule is wrong" Edits the rule file; you commit

Rules/skills load automatically in new chats; they do not update themselves unless you ask.