11 KiB
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/.
What Dyolink is
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 |
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
- Read applicable rules in
.cursor/rules/(especiallydyolink-overviewand the file-scoped rule for the area you touch). - Match existing patterns in the nearest feature folder — do not invent parallel structures.
- Keep diffs small — one concern per change unless the user asks for a refactor.
- Verify:
npm run build(backend) andnpx 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
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 strip: ScheduleDayPicker. 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. - Sent-to-lab detail locks that line; Add detail still OK same day; Remove detail = trash icon on each detail chip (not in the wizard Content step) — only unsent and not the last line. Attachment upload blocked when sent (
TREATMENT_DETAIL_SENT). - Entry wizard:
WizardStepper— Teeth → Content → Lab; Lab step only when active detail type is lab-dependent (prosthesis); entering Lab auto-opens shipment draft (no Add-shipment CTA). Content notes field label is Notes (clinical). Detail chips ≠ wizard chrome. Switching detail chips resets wizard to Teeth unlesspendingEntryStepRefrequests Lab (e.g. opening a case from the lab shipments rail). - Tooth selection: Neighbor empty/filled circles between selected adjacent teeth connect/disconnect bridges; Shift+range selects only (empty circles; overlap absorbs as singles); midline 11–21 / 41–31 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 frommd:up (stacked on mobile).
Treatment lab rules (quick ref):
- Lab-dependent details (e.g. prosthesis) without teeth can save but cannot ship — show
LabShipmentBlockedNotice+ inline banner; toast on dispatch add. - Detail treatment type need not match appointment purpose — purpose only pre-fills new details.
- 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 entry wizard Lab step.
- 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.created→notifyTabBadgesChanged()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); passviewerSide.
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. See .cursor/rules/appointments.mdc.
Lab Tasks tab: Newest case first; steps ordered 1→N; case grouping when sorted by date; stepCompleted filter; 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). 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. 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=→storeAuthRedirectFromPaththenuseEnterAppWhenAuthenticated(consumeAuthRedirectonce after org ready — not insideuseAuth.login()/registerTrial). Trial register uses the same hook; staff/org invite accept thenlogin()+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, entry wizard, 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.