133 lines
12 KiB
Markdown
133 lines
12 KiB
Markdown
# 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
|
||
|
||
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 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. Empty **no-appointment** strip cards can be deleted (trash; no details). **New treatment** opens a patient picker (Walk-in always visible); it does not copy the selected appointment’s patient.
|
||
- 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 unless `pendingEntryStepRef` requests 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 from `md:` 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); 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). **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, 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.
|