diff --git a/backend/prisma/migrations/20260628170000_remove_treatment_status/migration.sql b/backend/prisma/migrations/20260628170000_remove_treatment_status/migration.sql new file mode 100644 index 0000000..512b217 --- /dev/null +++ b/backend/prisma/migrations/20260628170000_remove_treatment_status/migration.sql @@ -0,0 +1,8 @@ +-- DropIndex +DROP INDEX IF EXISTS "treatments_organizationId_status_idx"; + +-- AlterTable +ALTER TABLE "treatments" DROP COLUMN "status"; + +-- DropEnum +DROP TYPE "TreatmentStatus"; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 41f23ec..b81fde5 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -114,11 +114,6 @@ model Appointment { @@map("appointments") } -enum TreatmentStatus { - DRAFT - COMPLETED -} - enum LabTaskStatus { PENDING IN_PROGRESS @@ -126,13 +121,12 @@ enum LabTaskStatus { } model Treatment { - id String @id @default(uuid()) + id String @id @default(uuid()) organizationId String patientId String - appointmentId String? @unique + appointmentId String? @unique providerUserId String title String - status TreatmentStatus @default(DRAFT) treatmentAt DateTime organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) @@ -145,7 +139,6 @@ model Treatment { updatedAt DateTime @updatedAt @@index([patientId, treatmentAt]) - @@index([organizationId, status]) @@map("treatments") } diff --git a/backend/src/modules/treatments/treatment.utils.ts b/backend/src/modules/treatments/treatment.utils.ts index 6943064..f2bfdfa 100644 --- a/backend/src/modules/treatments/treatment.utils.ts +++ b/backend/src/modules/treatments/treatment.utils.ts @@ -1,5 +1,3 @@ -import { TreatmentStatus } from '@prisma/client'; - const FDI_TOOTH_IDS = new Set([ '11', '12', '13', '14', '15', '16', '17', '18', '21', '22', '23', '24', '25', '26', '27', '28', @@ -39,7 +37,3 @@ export function generateTreatmentTitle( return parts.join(' · '); } - -export function mapTreatmentStatusForApi(status: TreatmentStatus): string { - return status === TreatmentStatus.DRAFT ? 'draft' : 'completed'; -} diff --git a/backend/src/modules/treatments/treatments.controller.ts b/backend/src/modules/treatments/treatments.controller.ts index 5f91bba..8a9eb85 100644 --- a/backend/src/modules/treatments/treatments.controller.ts +++ b/backend/src/modules/treatments/treatments.controller.ts @@ -40,7 +40,7 @@ export class TreatmentsController { } @Get('patients/:patientId/history') - @ApiOperation({ summary: 'List completed treatments for a patient (TAB_TREATMENT_READ)' }) + @ApiOperation({ summary: 'List treatments for a patient (draft and completed, TAB_TREATMENT_READ)' }) listPatientHistory( @Param('patientId') patientId: string, @Query('limit', new ParseIntPipe({ optional: true })) limit = 20, diff --git a/backend/src/modules/treatments/treatments.service.ts b/backend/src/modules/treatments/treatments.service.ts index 99b7846..51e96a7 100644 --- a/backend/src/modules/treatments/treatments.service.ts +++ b/backend/src/modules/treatments/treatments.service.ts @@ -4,7 +4,7 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; -import { LinkStatus, TreatmentStatus } from '@prisma/client'; +import { LinkStatus } from '@prisma/client'; import { createReadStream, existsSync, mkdirSync } from 'fs'; import { join } from 'path'; import { randomUUID } from 'crypto'; @@ -17,7 +17,6 @@ import { } from './dto/treatment.dto'; import { generateTreatmentTitle, - mapTreatmentStatusForApi, normalizeTeeth, } from './treatment.utils'; @@ -117,7 +116,7 @@ export class TreatmentsService { where: { patientId, organizationId, - status: TreatmentStatus.COMPLETED, + details: { some: {} }, }, include: treatmentInclude, orderBy: [{ treatmentAt: 'desc' }], @@ -144,7 +143,6 @@ export class TreatmentsService { where: { appointmentId: appointment.id, organizationId, - status: TreatmentStatus.DRAFT, }, include: treatmentInclude, }); @@ -196,7 +194,6 @@ export class TreatmentsService { treatmentAt: appointment.startAt, patientId: appointment.patientId, providerUserId: appointment.providerUserId, - status: TreatmentStatus.DRAFT, }, }) : await tx.treatment.create({ @@ -206,7 +203,6 @@ export class TreatmentsService { appointmentId: appointment.id, providerUserId: appointment.providerUserId, title, - status: TreatmentStatus.DRAFT, treatmentAt: appointment.startAt, }, }); @@ -314,7 +310,7 @@ export class TreatmentsService { ); const treatment = await this.prisma.treatment.findFirst({ - where: { appointmentId: appointment.id, organizationId, status: TreatmentStatus.DRAFT }, + where: { appointmentId: appointment.id, organizationId }, select: { id: true }, }); @@ -605,7 +601,6 @@ export class TreatmentsService { patientId: string; appointmentId: string | null; title: string; - status: TreatmentStatus; treatmentAt: Date; details: Array<{ id: string; @@ -660,7 +655,6 @@ export class TreatmentsService { appointmentId: treatment.appointmentId, title: treatment.title, treatmentAt: treatment.treatmentAt.toISOString(), - status: mapTreatmentStatusForApi(treatment.status), details: treatment.details.map((d) => this.mapDetail(d)), labCases: treatment.labCases.map((lc) => this.mapLabCase(lc)), documents, diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 5836698..138c125 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -416,7 +416,6 @@ "loadingAppointments": "Loading appointments…", "selectDayWithAppointment": "Select a day with at least one appointment.", "confirmDiscard": "You have unsaved changes. Discard them and continue?", - "successDraftSaved": "Treatment draft saved.", "errorChooseOrg": "Choose at least one active organization to send this case.", "successCaseSent": "Case sent to selected organizations.", "successFilesUploaded": "{count} file(s) uploaded successfully.", @@ -428,7 +427,7 @@ "errorSaveDraft": "Failed to save treatment draft.", "errorSendCase": "Failed to send case.", "errorCaseMustSave": "Case must be saved before sending.", - "draftTitle": "Draft · {patientName}", + "treatmentPlanTitle": "Treatment · {patientName}", "hiddenMessage": "Appointments are hidden.", "showAppointments": "Show appointments", "appointmentsTitle": "My appointments", @@ -483,36 +482,30 @@ "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", "saveStatusSaving": "Saving…", "saveStatusSaved": "All changes saved", "saveStatusError": "Could not save — check your connection", "sendSavesFirst": "Sending is per case and saves first automatically.", "historyTitle": "Previous treatments", - "historySubtitle": "Completed treatments for this patient. Each case is listed separately.", + "historySubtitle": "Click a treatment to preview it. Use Open in the preview card to load it in the workspace.", "loadingHistory": "Loading history…", - "historyEmpty": "No prior treatments for this patient.", - "statusLabel": "Status:", - "historyCaseLabel": "Case {n} · {type}", + "historyEmpty": "No other treatments recorded for this patient yet.", + "historyDetailLabel": "Detail {n} · {type}", + "previewTitle": "Treatment preview", + "openTreatment": "Open", + "selectAppointment": "Select an appointment to preview its treatment.", + "detailCount": "{n} detail(s)", + "detailSummary": "Detail {n}: {type}", + "detailAttachmentCount": "{n, plural, one {# file} other {# files}}", + "detailNotSentToLab": "Not sent to lab", + "detailPendingLabSend": "This lab detail has not been sent yet.", "teethLabel": "Teeth:", "teethNone": "None selected", - "reviewDetails": "Review details", - "previewTitle": "Treatment preview", - "previewDraft": "Preview current draft", - "selectAppointment": "Select an appointment to preview its draft.", - "caseCount": "{n} case(s)", - "attachmentCount": "{n} attachment(s)", - "caseSummary": "Case {n}: {type}", - "teethPrefix": "· Teeth", - "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.", + "historicalReadonlyNotice": "You are viewing a past treatment (read-only).", + "errorNoAppointmentForTreatment": "This treatment has no linked appointment and cannot be opened.", "noCases": "No cases in this treatment.", - "noDetails": "No treatment details in this draft.", + "noDetails": "No treatment details yet.", "typeLabel": "Type:", "commentsLabel": "Comments:", "commentsEmpty": "Comments: —", diff --git a/frontend/messages/fa.json b/frontend/messages/fa.json index c5560b2..d2a4b92 100644 --- a/frontend/messages/fa.json +++ b/frontend/messages/fa.json @@ -416,7 +416,6 @@ "loadingAppointments": "در حال بارگذاری نوبت‌ها...", "selectDayWithAppointment": "روزی را انتخاب کنید که حداقل یک نوبت داشته باشد.", "confirmDiscard": "تغییرات ذخیره‌نشده دارید. آنها را کنار بگذارید و ادامه دهید؟", - "successDraftSaved": "پیش‌نویس درمان ذخیره شد.", "errorChooseOrg": "حداقل یک سازمان فعال را برای ارسال این پرونده انتخاب کنید.", "successCaseSent": "پرونده به سازمان‌های انتخاب شده ارسال شد.", "successFilesUploaded": "{count} فایل با موفقیت بارگذاری شد.", @@ -428,7 +427,7 @@ "errorSaveDraft": "ذخیره پیش‌نویس درمان ناموفق بود.", "errorSendCase": "ارسال پرونده ناموفق بود.", "errorCaseMustSave": "پرونده باید قبل از ارسال ذخیره شود.", - "draftTitle": "پیش‌نویس · {patientName}", + "treatmentPlanTitle": "درمان · {patientName}", "hiddenMessage": "نوبت‌ها پنهان هستند.", "showAppointments": "نمایش نوبت‌ها", "appointmentsTitle": "نوبت‌های من", @@ -483,35 +482,29 @@ "successLabShipmentsSaved": "محموله‌های لاب ذخیره شد.", "errorSaveLabShipments": "ذخیره محموله‌های لاب ناموفق بود.", "errorLabCaseNeedsDetails": "حداقل یک جزئیات درمان برای این محموله انتخاب کنید.", - "saveDraft": "ذخیره پیش‌نویس درمان", "unsavedChanges": "تغییرات ذخیره‌نشده", - "draftSaved": "پیش‌نویس ذخیره شد", "saveStatusSaving": "در حال ذخیره…", "saveStatusSaved": "همه تغییرات ذخیره شد", "saveStatusError": "ذخیره ناموفق بود — اتصال را بررسی کنید", "sendSavesFirst": "ارسال برای هر پرونده به صورت جداگانه است و ابتدا به طور خودکار ذخیره می‌کند.", "historyTitle": "درمان‌های قبلی", - "historySubtitle": "درمان‌های تکمیل شده برای این بیمار. هر پرونده به طور جداگانه فهرست شده است.", + "historySubtitle": "برای پیش‌نمایش روی یک درمان کلیک کنید. از دکمه باز کردن در کارت پیش‌نمایش برای بارگذاری در فضای کاری استفاده کنید.", "loadingHistory": "در حال بارگذاری تاریخچه...", - "historyEmpty": "هیچ درمان قبلی برای این بیمار وجود ندارد.", - "statusLabel": "وضعیت:", - "historyCaseLabel": "پرونده {n} · {type}", + "historyEmpty": "هیچ درمان دیگری برای این بیمار ثبت نشده است.", + "historyDetailLabel": "جزئیات {n} · {type}", + "previewTitle": "پیش‌نمایش درمان", + "openTreatment": "باز کردن", + "selectAppointment": "یک نوبت را برای پیش‌نمایش درمان انتخاب کنید.", + "detailCount": "{n} جزئیات", + "detailSummary": "جزئیات {n}: {type}", + "detailAttachmentCount": "{n} فایل", + "detailNotSentToLab": "به لاب ارسال نشده", + "detailPendingLabSend": "این جزئیات لاب هنوز ارسال نشده است.", + "historicalReadonlyNotice": "در حال مشاهده یک درمان گذشته (فقط خواندنی) هستید.", + "errorNoAppointmentForTreatment": "این درمان نوبت مرتبطی ندارد و قابل باز کردن نیست.", "teethLabel": "دندان‌ها:", "teethNone": "هیچکدام انتخاب نشده", - "reviewDetails": "بررسی جزئیات", - "previewTitle": "پیش‌نمایش درمان", - "previewDraft": "پیش‌نمایش پیش‌نویس فعلی", - "selectAppointment": "یک نوبت را برای پیش‌نمایش پیش‌نویس آن انتخاب کنید.", - "caseCount": "{n} پرونده", - "attachmentCount": "{n} پیوست", - "caseSummary": "پرونده {n}: {type}", - "teethPrefix": "· دندان‌ها", - "moreCases": "+ {n} پرونده دیگر", - "previewDialogTitle": "پیش‌نمایش درمان", - "previewDialogSubtitle": "بررسی پرونده‌ها، پیوست‌ها و مقصدهای ارسال.", - "previewDialogSubtitlePhase4": "بررسی جزئیات درمان و پیوست‌ها.", - "previewLabDispatchHint": "برای ارسال کار به لابراتوار از بخش ارسال لاب در فضای کاری استفاده کنید.", - "noDetails": "جزئیات درمانی در این پیش‌نویس وجود ندارد.", + "noDetails": "هنوز جزئیات درمانی وجود ندارد.", "noCases": "هیچ پرونده‌ای در این درمان وجود ندارد.", "typeLabel": "نوع:", "commentsLabel": "نظرات:", diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json index 56a2e0f..714ffb5 100644 --- a/frontend/messages/nl.json +++ b/frontend/messages/nl.json @@ -416,7 +416,6 @@ "loadingAppointments": "Afspraken laden...", "selectDayWithAppointment": "Selecteer een dag met ten minste één afspraak.", "confirmDiscard": "U heeft niet-opgeslagen wijzigingen. Wilt u deze negeren en doorgaan?", - "successDraftSaved": "Behandelconcept opgeslagen.", "errorChooseOrg": "Kies ten minste één actieve organisatie om deze case te verzenden.", "successCaseSent": "Case verzonden naar geselecteerde organisaties.", "successFilesUploaded": "{count} bestand(en) succesvol geüpload.", @@ -428,7 +427,7 @@ "errorSaveDraft": "Behandelconcept opslaan mislukt.", "errorSendCase": "Case verzenden mislukt.", "errorCaseMustSave": "Case moet worden opgeslagen voor verzending.", - "draftTitle": "Concept · {patientName}", + "treatmentPlanTitle": "Behandeling · {patientName}", "hiddenMessage": "Afspraken zijn verborgen.", "showAppointments": "Afspraken tonen", "appointmentsTitle": "Mijn afspraken", @@ -483,36 +482,30 @@ "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", "saveStatusSaving": "Opslaan…", "saveStatusSaved": "Alle wijzigingen opgeslagen", "saveStatusError": "Opslaan mislukt — controleer uw verbinding", "sendSavesFirst": "Verzenden is per case en slaat eerst automatisch op.", "historyTitle": "Eerdere behandelingen", - "historySubtitle": "Voltooide behandelingen voor deze patiënt. Elke case wordt afzonderlijk weergegeven.", + "historySubtitle": "Klik op een behandeling om te bekijken. Gebruik Open in de voorbeeldkkaart om deze in de werkruimte te laden.", "loadingHistory": "Geschiedenis laden...", - "historyEmpty": "Geen eerdere behandelingen voor deze patiënt.", - "statusLabel": "Status:", - "historyCaseLabel": "Case {n} · {type}", + "historyEmpty": "Geen andere behandelingen voor deze patiënt geregistreerd.", + "historyDetailLabel": "Detail {n} · {type}", + "previewTitle": "Behandelvoorbeeld", + "openTreatment": "Openen", + "selectAppointment": "Selecteer een afspraak om de behandeling te bekijken.", + "detailCount": "{n} detail(s)", + "detailSummary": "Detail {n}: {type}", + "detailAttachmentCount": "{n, plural, one {# bestand} other {# bestanden}}", + "detailNotSentToLab": "Niet naar lab verzonden", + "detailPendingLabSend": "Dit labdetail is nog niet verzonden.", + "historicalReadonlyNotice": "U bekijkt een eerdere behandeling (alleen-lezen).", + "errorNoAppointmentForTreatment": "Deze behandeling heeft geen gekoppelde afspraak en kan niet worden geopend.", "teethLabel": "Tanden:", "teethNone": "Geen geselecteerd", - "reviewDetails": "Details bekijken", - "previewTitle": "Behandelvoorbeeld", - "previewDraft": "Bekijk huidig concept", - "selectAppointment": "Selecteer een afspraak om het concept te bekijken.", - "caseCount": "{n} case(s)", - "attachmentCount": "{n} bijlage(n)", - "caseSummary": "Case {n}: {type}", - "teethPrefix": "· Tanden", - "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.", + "noDetails": "Nog geen behandeldetails.", "typeLabel": "Type:", "commentsLabel": "Opmerkingen:", "commentsEmpty": "Opmerkingen: —", diff --git a/frontend/src/components/ui/treatment/DetailLabSendBadge.tsx b/frontend/src/components/ui/treatment/DetailLabSendBadge.tsx new file mode 100644 index 0000000..ddff044 --- /dev/null +++ b/frontend/src/components/ui/treatment/DetailLabSendBadge.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { useTranslations } from 'next-intl'; +import { CaseSentLabel } from '@/components/ui/treatment/CaseSentLabel'; +import { labNotSentBadgeClass, labSentBadgeClass } from '@/components/ui/treatment/treatmentStatusStyles'; +import type { LinkedOrganizationOption, PastTreatmentDetail } from '@/types/treatment'; + +interface DetailLabSendBadgeProps { + detail: Pick< + PastTreatmentDetail, + 'treatmentType' | 'sentAt' | 'sends' | 'destinationOrganizationId' + >; + labDependentCodes: Set; + orgs?: LinkedOrganizationOption[]; + className?: string; +} + +export function DetailLabSendBadge({ + detail, + labDependentCodes, + orgs, + className = '', +}: DetailLabSendBadgeProps) { + const t = useTranslations('treatment'); + + if (!labDependentCodes.has(detail.treatmentType)) { + return null; + } + + if (detail.sentAt) { + return ( + + ); + } + + return ( + {t('detailNotSentToLab')} + ); +} diff --git a/frontend/src/components/ui/treatment/PastTreatmentsPanel.tsx b/frontend/src/components/ui/treatment/PastTreatmentsPanel.tsx index e46997d..178f55f 100644 --- a/frontend/src/components/ui/treatment/PastTreatmentsPanel.tsx +++ b/frontend/src/components/ui/treatment/PastTreatmentsPanel.tsx @@ -1,39 +1,29 @@ 'use client'; import { useTranslations } from 'next-intl'; -import { FileText } from 'lucide-react'; +import { TreatmentHistoryDetailLine } from '@/components/ui/treatment/TreatmentHistoryDetailLine'; import type { PastTreatment } from '@/types/treatment'; -import { CaseSentLabel } from '@/components/ui/treatment/CaseSentLabel'; - -const TREATMENT_TYPE_KEYS = { - consultation: 'typeConsultation', - filling: 'typeFilling', - endo: 'typeEndo', - visit: 'typeVisit', - hygiene: 'typeHygiene', -} as const; interface PastTreatmentsPanelProps { items: PastTreatment[]; loading?: boolean; - onReviewTreatment?: (treatment: PastTreatment) => void; + selectedPreviewId?: string | null; + onSelectTreatment?: (treatment: PastTreatment) => void; } export function PastTreatmentsPanel({ items, loading, - onReviewTreatment, + selectedPreviewId, + onSelectTreatment, }: PastTreatmentsPanelProps) { const t = useTranslations('treatment'); - const tCommon = useTranslations('common'); return (

{t('historyTitle')}

-

- {t('historySubtitle')} -

+

{t('historySubtitle')}

{loading &&

{t('loadingHistory')}

} @@ -42,95 +32,60 @@ export function PastTreatmentsPanel({

{t('historyEmpty')}

)} -
- {items.map((treatment) => ( -
-
-
-

{treatment.title}

-

- {t('statusLabel')} {treatment.status} -

-
+
+ {items.map((treatment) => { + const isSelected = selectedPreviewId === treatment.id; + + return ( +
onSelectTreatment?.(treatment)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onSelectTreatment?.(treatment); + } + }} + className={` + border rounded-[var(--radius-sm)] px-2 py-1.5 cursor-pointer transition-colors + focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/45 + ${ + isSelected + ? 'border-primary bg-primary/5' + : 'border-border/60 bg-background-secondary/30 hover:border-border hover:bg-background-secondary/50' + } + `} + > -
-
- {treatment.details.map((c, idx) => { - const attachments = c.attachmentMetas ?? []; - const typeKey = TREATMENT_TYPE_KEYS[c.treatmentType as keyof typeof TREATMENT_TYPE_KEYS]; - const typeLabel = typeKey ? t(typeKey) : c.treatmentType; - return ( -
-
-

- {t('historyCaseLabel', { n: idx + 1, type: typeLabel })} -

- {c.sentAt && ( - - )} + {treatment.details.length === 0 ? ( +

{t('noDetails')}

+ ) : ( +
+ {treatment.details.map((detail, idx) => ( +
+
-

- {t('teethLabel')} {c.teeth.length ? [...c.teeth].sort().join(', ') : t('teethNone')} -

- {c.notes?.trim() && ( -

{c.notes}

- )} -
-

- {t('attachments')} -

- {attachments.length === 0 ? ( -

{tCommon('none')}

- ) : ( -
    - {attachments.map((doc) => ( -
  • - - {doc.fileName} - - {(doc.sizeBytes / 1024).toFixed(1)} KB - -
  • - ))} -
- )} -
-
- ); - })} -
- - {onReviewTreatment && ( -
- -
- )} -
- ))} + ))} +
+ )} + + ); + })}
); diff --git a/frontend/src/components/ui/treatment/TreatmentDetailSummaryRow.tsx b/frontend/src/components/ui/treatment/TreatmentDetailSummaryRow.tsx new file mode 100644 index 0000000..ea16c8a --- /dev/null +++ b/frontend/src/components/ui/treatment/TreatmentDetailSummaryRow.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { useTranslations } from 'next-intl'; +import { DetailLabSendBadge } from '@/components/ui/treatment/DetailLabSendBadge'; +import { TreatmentTypeBadge } from '@/components/ui/treatment/TreatmentTypeBadge'; +import type { LinkedOrganizationOption, PastTreatmentDetail } from '@/types/treatment'; + +interface TreatmentDetailSummaryRowProps { + detail: PastTreatmentDetail; + detailNumber: number; + labDependentCodes: Set; + orgs?: LinkedOrganizationOption[]; + compact?: boolean; +} + +export function TreatmentDetailSummaryRow({ + detail, + detailNumber, + labDependentCodes, + orgs, + compact = false, +}: TreatmentDetailSummaryRowProps) { + const t = useTranslations('treatment'); + const teeth = detail.teeth.length ? [...detail.teeth].sort().join(', ') : t('teethNone'); + const attachmentCount = detail.attachmentMetas?.length ?? 0; + + return ( +
+
+
+ + {t('detailLabel', { n: detailNumber })} + + +
+ +
+

+ {t('teethLabel')} {teeth} +

+ {attachmentCount > 0 && ( +

+ {t('detailAttachmentCount', { n: attachmentCount })} +

+ )} + {detail.notes?.trim() && ( +

+ {detail.notes} +

+ )} +
+ ); +} diff --git a/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx b/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx index b3693fb..ab4b388 100644 --- a/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx +++ b/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx @@ -4,6 +4,11 @@ import { useRef } from 'react'; import { useTranslations } from 'next-intl'; import { Button } from '@/components/ui/shared/Button'; import { Dropdown } from '@/components/ui/shared/Dropdown'; +import { + autosaveStatusClass, + labPendingBannerClass, + labSentBannerClass, +} from '@/components/ui/treatment/treatmentStatusStyles'; import type { TreatmentDetailDraft } from '@/types/treatment'; import { TREATMENT_TYPE_COLORS, treatmentTypeLabelKey } from '@/components/ui/treatment/treatmentTypeDisplay'; @@ -13,12 +18,12 @@ interface TreatmentDetailsEditorProps { onActiveDetailChange: (id: string) => void; onDetailsChange: (details: TreatmentDetailDraft[]) => void; isDetailLocked: (detail: TreatmentDetailDraft) => boolean; + labDependentCodes: Set; disabled: boolean; canEdit: boolean; saveStatus: 'idle' | 'dirty' | 'saving' | 'saved' | 'error'; uploadBusy: boolean; onAddDetail: () => void; - onPreview: () => void; onUploadFiles: (files: FileList | null) => void; } @@ -28,16 +33,15 @@ export function TreatmentDetailsEditor({ onActiveDetailChange, onDetailsChange, isDetailLocked, + labDependentCodes, disabled, canEdit, saveStatus, uploadBusy, onAddDetail, - onPreview, onUploadFiles, }: TreatmentDetailsEditorProps) { const t = useTranslations('treatment'); - const tCommon = useTranslations('common'); const attachmentInputRef = useRef(null); const activeDetail = details.find((d) => d.clientId === activeDetailId) ?? details[0]; @@ -46,6 +50,8 @@ export function TreatmentDetailsEditor({ const locked = isDetailLocked(activeDetail); const readOnly = disabled || locked; const treatmentTypeTextColor = TREATMENT_TYPE_COLORS[activeDetail.treatmentType]; + const isLabDependent = labDependentCodes.has(activeDetail.treatmentType); + const showPendingLabHint = isLabDependent && !locked && !readOnly; return (
@@ -54,14 +60,9 @@ export function TreatmentDetailsEditor({

{t('detailsTitle')}

{t('detailsSubtitle')}

-
- - -
+
@@ -88,9 +89,10 @@ export function TreatmentDetailsEditor({
{locked && ( -

- {t('detailLockedInShipment')} -

+

{t('detailLockedInShipment')}

+ )} + {showPendingLabHint && ( +

{t('detailPendingLabSend')}

)}