From cf07b4d8a8ea0bf965e0d0084a4d4fa73cfbc117 Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 16 Jul 2026 22:45:37 +0330 Subject: [PATCH] improvement: delete action added for unsent treatment details. some edit controls added to details and shipments. --- backend/src/common/errors/error-codes.ts | 1 + .../modules/treatments/treatments.service.ts | 21 +++++++++++ frontend/messages/en.json | 2 ++ frontend/messages/fa.json | 2 ++ frontend/messages/nl.json | 2 ++ .../ui/treatment/TreatmentDetailsEditor.tsx | 19 ++++++++++ .../ui/treatment/TreatmentWorkspace.tsx | 36 ++++++++++++++++++- 7 files changed, 82 insertions(+), 1 deletion(-) diff --git a/backend/src/common/errors/error-codes.ts b/backend/src/common/errors/error-codes.ts index e1c6608..231de83 100644 --- a/backend/src/common/errors/error-codes.ts +++ b/backend/src/common/errors/error-codes.ts @@ -64,6 +64,7 @@ export const ErrorCode = { CONFLICT_FUTURE_APPOINTMENTS: 'CONFLICT_FUTURE_APPOINTMENTS', APPOINTMENT_PATIENT_LOCKED: 'APPOINTMENT_PATIENT_LOCKED', APPOINTMENT_HAS_TREATMENT: 'APPOINTMENT_HAS_TREATMENT', + TREATMENT_DETAIL_SENT: 'TREATMENT_DETAIL_SENT', BAD_REQUEST: 'BAD_REQUEST', INTERNAL_ERROR: 'INTERNAL_ERROR', } as const; diff --git a/backend/src/modules/treatments/treatments.service.ts b/backend/src/modules/treatments/treatments.service.ts index de5ba9a..2bf2de8 100644 --- a/backend/src/modules/treatments/treatments.service.ts +++ b/backend/src/modules/treatments/treatments.service.ts @@ -1,9 +1,11 @@ import { BadRequestException, ForbiddenException, + HttpStatus, Injectable, NotFoundException, } from '@nestjs/common'; +import { AppException, ErrorCode } from '../../common/errors'; import { LabCaseActivityType, LabTaskStatus, LinkStatus, Prisma } from '@prisma/client'; import { createReadStream, existsSync, mkdirSync } from 'fs'; import { join } from 'path'; @@ -908,6 +910,25 @@ export class TreatmentsService { throw new BadRequestException('At least one file is required'); } + const existingDetail = await this.prisma.treatmentDetail.findFirst({ + where: { + clientKey: detailClientKey, + treatment: { + appointmentId, + organizationId, + ...treatmentProviderScopeWhere(actorUserId), + }, + }, + select: { + labCaseLink: { + select: { labCase: { select: { sentAt: true } } }, + }, + }, + }); + if (existingDetail?.labCaseLink?.labCase.sentAt) { + throw new AppException(ErrorCode.TREATMENT_DETAIL_SENT, HttpStatus.CONFLICT); + } + const orgDir = join(this.uploadRoot, organizationId); mkdirSync(orgDir, { recursive: true }); diff --git a/frontend/messages/en.json b/frontend/messages/en.json index ae24ff6..a9fcf8c 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -660,6 +660,7 @@ "detailsTitle": "Treatment details", "detailsSubtitle": "Plan teeth, type, notes, and attachments for each detail line.", "addDetail": "Add detail", + "confirmRemoveDetail": "Remove this treatment detail?", "detailLabel": "Detail {n}", "detailSentBadge": "sent", "detailLockedInShipment": "This detail was sent to a lab and can no longer be edited.", @@ -1015,6 +1016,7 @@ "CONFLICT_FUTURE_APPOINTMENTS": "You cannot stop participating in treatments while you have future appointments. Reassign or cancel them first.", "APPOINTMENT_PATIENT_LOCKED": "Cannot change the patient while a treatment is linked to this appointment.", "APPOINTMENT_HAS_TREATMENT": "This appointment cannot be deleted because a treatment is linked to it.", + "TREATMENT_DETAIL_SENT": "This detail was sent to a lab and can no longer be changed.", "BAD_REQUEST": "The request could not be processed.", "INTERNAL_ERROR": "Something went wrong on our end. Please try again later." } diff --git a/frontend/messages/fa.json b/frontend/messages/fa.json index 0f0e74b..3b48635 100644 --- a/frontend/messages/fa.json +++ b/frontend/messages/fa.json @@ -661,6 +661,7 @@ "detailsTitle": "جزئیات درمان", "detailsSubtitle": "دندان‌ها، نوع، یادداشت و پیوست‌ها را برای هر خط جزئیات برنامه‌ریزی کنید.", "addDetail": "افزودن جزئیات", + "confirmRemoveDetail": "این جزئیات درمان حذف شود؟", "detailLabel": "جزئیات {n}", "detailSentBadge": "ارسال‌شده", "detailLockedInShipment": "این جزئیات به لابراتوار ارسال شده و دیگر قابل ویرایش نیست.", @@ -1016,6 +1017,7 @@ "CONFLICT_FUTURE_APPOINTMENTS": "تا وقتی نوبت‌های آینده دارید نمی‌توانید مشارکت در درمان را متوقف کنید. ابتدا آن‌ها را لغو یا واگذار کنید.", "APPOINTMENT_PATIENT_LOCKED": "تا وقتی درمانی به این نوبت متصل است، امکان تغییر بیمار وجود ندارد.", "APPOINTMENT_HAS_TREATMENT": "به‌دلیل وجود درمان مرتبط با این نوبت، امکان حذف آن وجود ندارد.", + "TREATMENT_DETAIL_SENT": "این جزئیات به لابراتوار ارسال شده و دیگر قابل تغییر نیست.", "BAD_REQUEST": "درخواست قابل پردازش نبود.", "INTERNAL_ERROR": "مشکلی در سرور رخ داد. لطفاً بعداً تلاش کنید." } diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json index d3f94cd..426e9b4 100644 --- a/frontend/messages/nl.json +++ b/frontend/messages/nl.json @@ -660,6 +660,7 @@ "detailsTitle": "Behandeldetails", "detailsSubtitle": "Plan tanden, type, notities en bijlagen per detailregel.", "addDetail": "Detail toevoegen", + "confirmRemoveDetail": "Dit behandelingsdetail verwijderen?", "detailLabel": "Detail {n}", "detailSentBadge": "verzonden", "detailLockedInShipment": "Dit detail is naar het lab verzonden en kan niet meer worden bewerkt.", @@ -1015,6 +1016,7 @@ "CONFLICT_FUTURE_APPOINTMENTS": "U kunt niet stoppen met deelnemen aan behandelingen zolang u toekomstige afspraken hebt. Wijs ze eerst opnieuw toe of annuleer ze.", "APPOINTMENT_PATIENT_LOCKED": "De patiënt kan niet worden gewijzigd zolang er een behandeling aan deze afspraak is gekoppeld.", "APPOINTMENT_HAS_TREATMENT": "Deze afspraak kan niet worden verwijderd omdat er een behandeling aan is gekoppeld.", + "TREATMENT_DETAIL_SENT": "Dit detail is naar het lab verzonden en kan niet meer worden gewijzigd.", "BAD_REQUEST": "Het verzoek kon niet worden verwerkt.", "INTERNAL_ERROR": "Er is iets misgegaan aan onze kant. Probeer het later opnieuw." } diff --git a/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx b/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx index f3206f9..81735bd 100644 --- a/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx +++ b/frontend/src/components/ui/treatment/TreatmentDetailsEditor.tsx @@ -32,6 +32,7 @@ interface TreatmentDetailsEditorProps { saveStatus: 'idle' | 'dirty' | 'saving' | 'saved' | 'error'; uploadBusy: boolean; onAddDetail: () => void; + onRemoveDetail: () => void; onUploadFiles: (files: FileList | null) => void; } @@ -48,9 +49,11 @@ export function TreatmentDetailsEditor({ saveStatus, uploadBusy, onAddDetail, + onRemoveDetail, onUploadFiles, }: TreatmentDetailsEditorProps) { const t = useTranslations('treatment'); + const tCommon = useTranslations('common'); const attachmentInputRef = useRef(null); const activeDetail = details.find((d) => d.clientId === activeDetailId) ?? details[0]; @@ -58,6 +61,7 @@ export function TreatmentDetailsEditor({ const locked = isDetailLocked(activeDetail); const readOnly = disabled || locked; + const canRemoveDetail = canEdit && !disabled && !locked && details.length > 1; const treatmentTypeTextColor = isDetailTypeSelected(activeDetail) ? treatmentTypeColor( activeDetail.treatmentType, @@ -206,6 +210,21 @@ export function TreatmentDetailsEditor({ )} + + {canRemoveDetail ? ( +
+ +
+ ) : null} {canEdit && saveStatus !== 'idle' && ( diff --git a/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx b/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx index 310252e..330e7a5 100644 --- a/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx +++ b/frontend/src/components/ui/treatment/TreatmentWorkspace.tsx @@ -1249,6 +1249,8 @@ export function TreatmentWorkspace({ const uploadForDetail = useCallback( async (detailClientId: string, files: FileList | File[]) => { if (!canEditTreatmentForDay || !selectedAppointment) return; + const target = detailsRef.current.find((d) => d.clientId === detailClientId); + if (target && isDetailLocked(target)) return; const list = files instanceof FileList ? Array.from(files) : files; if (!list.length) return; @@ -1273,7 +1275,7 @@ export function TreatmentWorkspace({ setUploadBusyDetailId(null); } }, - [canEditTreatmentForDay, selectedAppointment, showSuccess, showError, t], + [canEditTreatmentForDay, isDetailLocked, selectedAppointment, showSuccess, showError, t, tErrors], ); const persistLabCases = useCallback( @@ -1373,9 +1375,40 @@ export function TreatmentWorkspace({ selectedAppointment, showError, t, + tErrors, ], ); + const handleRemoveActiveDetail = useCallback(() => { + if (!canEditTreatmentForDay) return; + const idx = details.findIndex((d) => d.clientId === activeDetailId); + if (idx < 0) return; + const active = details[idx]; + if (!active || isDetailLocked(active) || details.length <= 1) return; + if (!window.confirm(t('confirmRemoveDetail'))) return; + + const removedId = active.clientId; + const nextDetails = details.filter((d) => d.clientId !== removedId); + const nextActive = + nextDetails[Math.min(idx, nextDetails.length - 1)]?.clientId ?? nextDetails[0]?.clientId; + setDetails(nextDetails); + if (nextActive) setActiveDetailId(nextActive); + + if (labCaseDrafts.some((lc) => lc.detailClientId === removedId)) { + handleLabCasesChange( + withoutEmptyLabCaseDrafts(labCaseDrafts.filter((lc) => lc.detailClientId !== removedId)), + ); + } + }, [ + activeDetailId, + canEditTreatmentForDay, + details, + handleLabCasesChange, + isDetailLocked, + labCaseDrafts, + t, + ]); + const handleAddLabCase = useCallback(async () => { if (!canEditTreatmentForDay || !selectedAppointment) return; @@ -1779,6 +1812,7 @@ export function TreatmentWorkspace({ setDetails((prev) => [...prev, next]); setActiveDetailId(next.clientId); }} + onRemoveDetail={handleRemoveActiveDetail} onUploadFiles={(files) => void uploadForDetail(activeDetailId, files ?? [])} />