improvement: delete action added for unsent treatment details. some edit controls added to details and shipments.

This commit is contained in:
2026-07-16 22:45:37 +03:30
parent 334b7841ae
commit cf07b4d8a8
7 changed files with 82 additions and 1 deletions

View File

@@ -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;

View File

@@ -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 });

View File

@@ -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."
}

View File

@@ -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": "مشکلی در سرور رخ داد. لطفاً بعداً تلاش کنید."
}

View File

@@ -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."
}

View File

@@ -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<HTMLInputElement>(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({
</ul>
)}
</div>
{canRemoveDetail ? (
<div className="pt-1">
<Button
type="button"
variant="danger"
onClick={onRemoveDetail}
disabled={uploadBusy}
fullWidth
className="sm:w-auto"
>
{tCommon('delete')}
</Button>
</div>
) : null}
</div>
{canEdit && saveStatus !== 'idle' && (

View File

@@ -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 ?? [])}
/>