From 7b19d6953c9a620d7a5a4bea0b929e805136e016 Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 28 Jun 2026 17:14:02 +0330 Subject: [PATCH] feature: Phase4 - Clinic two-step treatment UI --- .../scripts/clear-clinical-test-data.ts | 50 ++ frontend/messages/en.json | 31 +- frontend/messages/fa.json | 31 +- frontend/messages/nl.json | 31 +- .../src/app/[locale]/(dashboard)/layout.tsx | 4 +- .../src/components/ui/shared/Checkbox.tsx | 42 +- frontend/src/components/ui/shared/Input.tsx | 6 +- .../src/components/ui/shared/SearchBar.tsx | 37 +- .../ui/treatment/LabCasesDispatchPanel.tsx | 320 ++++++++++++ .../ui/treatment/TreatmentCasesEditor.tsx | 299 ----------- .../ui/treatment/TreatmentDetailsEditor.tsx | 196 ++++++++ .../ui/treatment/TreatmentPreviewDialog.tsx | 103 +--- .../ui/treatment/TreatmentWorkspace.tsx | 463 ++++++++++-------- .../ui/treatment/treatmentTypeDisplay.ts | 19 + frontend/src/lib/api/treatment-catalog.ts | 9 + frontend/src/types/treatment-catalog.ts | 6 + frontend/src/types/treatment.ts | 10 + 17 files changed, 1042 insertions(+), 615 deletions(-) create mode 100644 backend/prisma/scripts/clear-clinical-test-data.ts create mode 100644 frontend/src/components/ui/treatment/LabCasesDispatchPanel.tsx delete mode 100644 frontend/src/components/ui/treatment/TreatmentCasesEditor.tsx create mode 100644 frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx create mode 100644 frontend/src/components/ui/treatment/treatmentTypeDisplay.ts create mode 100644 frontend/src/lib/api/treatment-catalog.ts create mode 100644 frontend/src/types/treatment-catalog.ts diff --git a/backend/prisma/scripts/clear-clinical-test-data.ts b/backend/prisma/scripts/clear-clinical-test-data.ts new file mode 100644 index 0000000..91bf5ff --- /dev/null +++ b/backend/prisma/scripts/clear-clinical-test-data.ts @@ -0,0 +1,50 @@ +/** + * One-off cleanup: remove appointments, treatments, lab cases, and related rows. + * Keeps patients, organizations, users, and catalog data intact. + * + * Usage: npx ts-node prisma/scripts/clear-clinical-test-data.ts + */ +import { PrismaClient } from '@prisma/client'; +import { config } from 'dotenv'; +import path from 'path'; + +config({ path: path.join(__dirname, '..', '..', '.env') }); + +const prisma = new PrismaClient(); + +async function main() { + const counts = { + labCaseTasks: await prisma.labCaseTask.count(), + labCaseSends: await prisma.labCaseSend.count(), + labCaseDetails: await prisma.labCaseDetail.count(), + labCases: await prisma.labCase.count(), + attachments: await prisma.treatmentDetailAttachment.count(), + treatmentDetails: await prisma.treatmentDetail.count(), + treatments: await prisma.treatment.count(), + appointments: await prisma.appointment.count(), + }; + + console.log('Current row counts:', counts); + + await prisma.$transaction([ + prisma.labCaseTask.deleteMany(), + prisma.labCaseSend.deleteMany(), + prisma.labCaseDetail.deleteMany(), + prisma.labCase.deleteMany(), + prisma.treatmentDetailAttachment.deleteMany(), + prisma.treatmentDetail.deleteMany(), + prisma.treatment.deleteMany(), + prisma.appointment.deleteMany(), + ]); + + console.log('✅ Cleared appointments, treatments, lab cases, tasks, and attachments.'); +} + +main() + .catch((error) => { + console.error('❌ Cleanup failed:', error); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 05c96b0..3f889c3 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -396,6 +396,7 @@ "noPermissionBody": "You do not have permission to view the Treatment tab for this organization.", "title": "Treatment", "subtitleEdit": "Document cases for your appointments, save drafts, and send work to linked organizations.", + "subtitleEditPhase4": "Plan treatment details first, then group lab-dependent work into shipments in the lab dispatch panel.", "subtitleReadOnly": "View-only access — you can review appointments and treatment history but cannot edit.", "pastDayNotice": "Past days are view-only. You can review appointments and history, but treatment cases cannot be added or changed.", "selectedPatient": "Selected patient", @@ -423,6 +424,13 @@ "emptyDay": "No appointments assigned to you on this day.", "casesTitle": "Treatment cases", "casesSubtitle": "Each case has its own teeth, notes, attachments, and destinations for send.", + "detailsTitle": "Treatment details", + "detailsSubtitle": "Plan teeth, type, notes, and attachments for each detail line.", + "addDetail": "Add detail", + "detailLabel": "Detail {n}", + "detailSentBadge": "sent", + "detailLockedInShipment": "This detail was sent to a lab and can no longer be edited.", + "detailsSaveHint": "Lab dispatch is configured separately below.", "addCase": "Add case", "caseLabel": "Case {n}", "comments": "Comments", @@ -441,6 +449,24 @@ "recent": "Recent:", "noOrgMatch": "No active organization matches your search.", "sendThisCase": "Send this case", + "labDispatchTitle": "Lab dispatch", + "labDispatchSubtitle": "Group lab-dependent details into shipments and send them to linked labs.", + "addLabShipment": "Add lab shipment", + "labShipmentLabel": "Shipment {n}", + "includeDetails": "Include treatment details", + "labDetailLine": "Detail {n} · {type} · {teeth}", + "noLabDetails": "No lab-dependent treatment details yet. Add a lab type (e.g. endo) in treatment details above.", + "labDispatchEmpty": "Add a lab shipment to group details and send them to a lab.", + "labComment": "Message for the lab", + "labCommentPlaceholder": "Optional instructions for this shipment…", + "selectLab": "Destination lab", + "selectLabPlaceholder": "Choose a linked lab…", + "sendToLab": "Send to lab", + "saveLabShipments": "Save lab shipments", + "labDispatchSaveHint": "Saves shipment grouping without sending.", + "successLabShipmentsSaved": "Lab shipments saved.", + "errorSaveLabShipments": "Failed to save lab shipments.", + "errorLabCaseNeedsDetails": "Select at least one treatment detail for this shipment.", "saveDraft": "Save treatment draft", "unsavedChanges": "Unsaved changes", "draftSaved": "Draft saved", @@ -464,7 +490,10 @@ "moreCases": "+ {n} more case(s)", "previewDialogTitle": "Treatment preview", "previewDialogSubtitle": "Review cases, attachments, and send destinations.", + "previewDialogSubtitlePhase4": "Review treatment details and attachments.", + "previewLabDispatchHint": "Use the lab dispatch panel in the workspace to send work to labs.", "noCases": "No cases in this treatment.", + "noDetails": "No treatment details in this draft.", "typeLabel": "Type:", "commentsLabel": "Comments:", "commentsEmpty": "Comments: —", @@ -474,7 +503,7 @@ "noActiveOrgs": "No active linked organizations.", "confirmSend": "Confirm send", "toothChartTitle": "FDI tooth chart", - "toothChartHint": "Tap teeth to multi-select. Applies to the active case.", + "toothChartHint": "Tap teeth to multi-select. Applies to the active detail.", "selectedLabel": "Selected:", "selectedEmpty": "—", "upperArch": "Upper arch", diff --git a/frontend/messages/fa.json b/frontend/messages/fa.json index 4315638..c8394cb 100644 --- a/frontend/messages/fa.json +++ b/frontend/messages/fa.json @@ -396,6 +396,7 @@ "noPermissionBody": "شما مجوز مشاهده برگه درمان برای این سازمان را ندارید.", "title": "درمان", "subtitleEdit": "پرونده‌های نوبت‌های خود را مستند کنید، پیش‌نویس‌ها را ذخیره کنید و کار را به سازمان‌های مرتبط ارسال کنید.", + "subtitleEditPhase4": "ابتدا جزئیات درمان را برنامه‌ریزی کنید، سپس کار وابسته به لابراتوار را در بخش ارسال لاب گروه‌بندی کنید.", "subtitleReadOnly": "دسترسی فقط خواندنی — می‌توانید نوبت‌ها و تاریخچه درمان را بررسی کنید اما نمی‌توانید ویرایش کنید.", "pastDayNotice": "روزهای گذشته فقط قابل مشاهده هستند. می‌توانید نوبت‌ها و تاریخچه را بررسی کنید، اما پرونده‌های درمانی قابل اضافه یا تغییر نیستند.", "selectedPatient": "بیمار انتخاب شده", @@ -423,6 +424,13 @@ "emptyDay": "هیچ نوبتی به شما در این روز اختصاص داده نشده است.", "casesTitle": "پرونده‌های درمانی", "casesSubtitle": "هر پرونده دارای دندان‌ها، یادداشت‌ها، پیوست‌ها و مقصدهای ارسال خود است.", + "detailsTitle": "جزئیات درمان", + "detailsSubtitle": "دندان‌ها، نوع، یادداشت و پیوست‌ها را برای هر خط جزئیات برنامه‌ریزی کنید.", + "addDetail": "افزودن جزئیات", + "detailLabel": "جزئیات {n}", + "detailSentBadge": "ارسال‌شده", + "detailLockedInShipment": "این جزئیات به لابراتوار ارسال شده و دیگر قابل ویرایش نیست.", + "detailsSaveHint": "ارسال لاب در بخش جداگانه زیر پیکربندی می‌شود.", "addCase": "افزودن پرونده", "caseLabel": "پرونده {n}", "comments": "نظرات", @@ -441,6 +449,24 @@ "recent": "اخیر:", "noOrgMatch": "هیچ سازمان فعالی با جستجوی شما مطابقت ندارد.", "sendThisCase": "ارسال این پرونده", + "labDispatchTitle": "ارسال به لابراتوار", + "labDispatchSubtitle": "جزئیات وابسته به لاب را در محموله‌ها گروه‌بندی کرده و به لابراتوارهای متصل ارسال کنید.", + "addLabShipment": "افزودن محموله لاب", + "labShipmentLabel": "محموله {n}", + "includeDetails": "شامل جزئیات درمان", + "labDetailLine": "جزئیات {n} · {type} · {teeth}", + "noLabDetails": "هنوز جزئیات وابسته به لاب وجود ندارد. نوع لاب (مثلاً اندو) در جزئیات درمان بالا اضافه کنید.", + "labDispatchEmpty": "یک محموله لاب اضافه کنید تا جزئیات را گروه‌بندی و ارسال کنید.", + "labComment": "پیام برای لابراتوار", + "labCommentPlaceholder": "دستورالعمل اختیاری برای این محموله…", + "selectLab": "لابراتوار مقصد", + "selectLabPlaceholder": "یک لابراتوار متصل انتخاب کنید…", + "sendToLab": "ارسال به لابراتوار", + "saveLabShipments": "ذخیره محموله‌های لاب", + "labDispatchSaveHint": "گروه‌بندی محموله را بدون ارسال ذخیره می‌کند.", + "successLabShipmentsSaved": "محموله‌های لاب ذخیره شد.", + "errorSaveLabShipments": "ذخیره محموله‌های لاب ناموفق بود.", + "errorLabCaseNeedsDetails": "حداقل یک جزئیات درمان برای این محموله انتخاب کنید.", "saveDraft": "ذخیره پیش‌نویس درمان", "unsavedChanges": "تغییرات ذخیره‌نشده", "draftSaved": "پیش‌نویس ذخیره شد", @@ -464,6 +490,9 @@ "moreCases": "+ {n} پرونده دیگر", "previewDialogTitle": "پیش‌نمایش درمان", "previewDialogSubtitle": "بررسی پرونده‌ها، پیوست‌ها و مقصدهای ارسال.", + "previewDialogSubtitlePhase4": "بررسی جزئیات درمان و پیوست‌ها.", + "previewLabDispatchHint": "برای ارسال کار به لابراتوار از بخش ارسال لاب در فضای کاری استفاده کنید.", + "noDetails": "جزئیات درمانی در این پیش‌نویس وجود ندارد.", "noCases": "هیچ پرونده‌ای در این درمان وجود ندارد.", "typeLabel": "نوع:", "commentsLabel": "نظرات:", @@ -474,7 +503,7 @@ "noActiveOrgs": "هیچ سازمان مرتبط فعالی وجود ندارد.", "confirmSend": "تأیید ارسال", "toothChartTitle": "نمودار دندان‌ها FDI", - "toothChartHint": "برای انتخاب چندگانه روی دندان‌ها ضربه بزنید. برای پرونده فعال اعمال می‌شود.", + "toothChartHint": "برای انتخاب چندگانه روی دندان‌ها ضربه بزنید. برای جزئیات فعال اعمال می‌شود.", "selectedLabel": "انتخاب شده:", "selectedEmpty": "—", "upperArch": "قوس بالا", diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json index b2ae34e..16ea5b7 100644 --- a/frontend/messages/nl.json +++ b/frontend/messages/nl.json @@ -396,6 +396,7 @@ "noPermissionBody": "U heeft geen toestemming om het tabblad Behandeling voor deze organisatie te bekijken.", "title": "Behandeling", "subtitleEdit": "Documenteer casussen voor uw afspraken, sla concepten op en stuur werk naar gekoppelde organisaties.", + "subtitleEditPhase4": "Plan eerst behandeldetails, groepeer daarna lab-afhankelijk werk in het lab-dispatchpaneel.", "subtitleReadOnly": "Alleen-lezen toegang — u kunt afspraken en behandelgeschiedenis bekijken, maar niet bewerken.", "pastDayNotice": "Dagen in het verleden zijn alleen-lezen. U kunt afspraken en geschiedenis bekijken, maar behandelcasussen kunnen niet worden toegevoegd of gewijzigd.", "selectedPatient": "Geselecteerde patiënt", @@ -423,6 +424,13 @@ "emptyDay": "Geen afspraken aan u toegewezen op deze dag.", "casesTitle": "Behandelcasussen", "casesSubtitle": "Elke case heeft zijn eigen tanden, notities, bijlagen en verzendbestemmingen.", + "detailsTitle": "Behandeldetails", + "detailsSubtitle": "Plan tanden, type, notities en bijlagen per detailregel.", + "addDetail": "Detail toevoegen", + "detailLabel": "Detail {n}", + "detailSentBadge": "verzonden", + "detailLockedInShipment": "Dit detail is naar het lab verzonden en kan niet meer worden bewerkt.", + "detailsSaveHint": "Lab-dispatch wordt hieronder apart geconfigureerd.", "addCase": "Case toevoegen", "caseLabel": "Case {n}", "comments": "Opmerkingen", @@ -441,6 +449,24 @@ "recent": "Recent:", "noOrgMatch": "Geen actieve organisatie komt overeen met uw zoekopdracht.", "sendThisCase": "Verzend deze case", + "labDispatchTitle": "Lab-dispatch", + "labDispatchSubtitle": "Groepeer lab-afhankelijke details in zendingen en stuur ze naar gekoppelde labs.", + "addLabShipment": "Labzending toevoegen", + "labShipmentLabel": "Zending {n}", + "includeDetails": "Behandeldetails opnemen", + "labDetailLine": "Detail {n} · {type} · {teeth}", + "noLabDetails": "Nog geen lab-afhankelijke details. Voeg een labtype (bijv. endo) toe in de behandeldetails hierboven.", + "labDispatchEmpty": "Voeg een labzending toe om details te groeperen en naar een lab te sturen.", + "labComment": "Bericht voor het lab", + "labCommentPlaceholder": "Optionele instructies voor deze zending…", + "selectLab": "Bestemmingslab", + "selectLabPlaceholder": "Kies een gekoppeld lab…", + "sendToLab": "Versturen naar lab", + "saveLabShipments": "Labzendingen opslaan", + "labDispatchSaveHint": "Slaat groepering op zonder te verzenden.", + "successLabShipmentsSaved": "Labzendingen opgeslagen.", + "errorSaveLabShipments": "Labzendingen opslaan mislukt.", + "errorLabCaseNeedsDetails": "Selecteer minimaal één behandeldetail voor deze zending.", "saveDraft": "Behandelconcept opslaan", "unsavedChanges": "Niet-opgeslagen wijzigingen", "draftSaved": "Concept opgeslagen", @@ -464,7 +490,10 @@ "moreCases": "+ {n} meer case(s)", "previewDialogTitle": "Behandelvoorbeeld", "previewDialogSubtitle": "Bekijk casussen, bijlagen en verzendbestemmingen.", + "previewDialogSubtitlePhase4": "Bekijk behandeldetails en bijlagen.", + "previewLabDispatchHint": "Gebruik het lab-dispatchpaneel in de werkruimte om werk naar labs te sturen.", "noCases": "Geen casussen in deze behandeling.", + "noDetails": "Geen behandeldetails in dit concept.", "typeLabel": "Type:", "commentsLabel": "Opmerkingen:", "commentsEmpty": "Opmerkingen: —", @@ -474,7 +503,7 @@ "noActiveOrgs": "Geen actieve gekoppelde organisaties.", "confirmSend": "Bevestig verzending", "toothChartTitle": "FDI-tanddiagram", - "toothChartHint": "Tik op tanden om meerdere te selecteren. Geldt voor de actieve case.", + "toothChartHint": "Tik op tanden om meerdere te selecteren. Geldt voor het actieve detail.", "selectedLabel": "Geselecteerd:", "selectedEmpty": "—", "upperArch": "Bovenboog", diff --git a/frontend/src/app/[locale]/(dashboard)/layout.tsx b/frontend/src/app/[locale]/(dashboard)/layout.tsx index 246abcd..037e1c7 100644 --- a/frontend/src/app/[locale]/(dashboard)/layout.tsx +++ b/frontend/src/app/[locale]/(dashboard)/layout.tsx @@ -59,8 +59,8 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
-
-
+
+
{children}
diff --git a/frontend/src/components/ui/shared/Checkbox.tsx b/frontend/src/components/ui/shared/Checkbox.tsx index 2aa1114..453a12e 100644 --- a/frontend/src/components/ui/shared/Checkbox.tsx +++ b/frontend/src/components/ui/shared/Checkbox.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useId } from 'react'; +import { useId, type KeyboardEvent } from 'react'; import { Check } from 'lucide-react'; type CheckboxProps = { @@ -14,6 +14,7 @@ type CheckboxProps = { /** * App design-system checkbox: primary fill when checked, rounded, focus-visible ring. + * Uses a button-like label toggle so mouse clicks do not focus a hidden input (scroll jumps). */ export function Checkbox({ checked, @@ -26,25 +27,42 @@ export function Checkbox({ const genId = useId(); const inputId = id ?? genId; + const toggle = () => { + if (!disabled) onChange(!checked); + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (disabled) return; + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault(); + toggle(); + } + }; + return (