improvement/v1-demo-improvements. #62
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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": "مشکلی در سرور رخ داد. لطفاً بعداً تلاش کنید."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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' && (
|
||||
|
||||
@@ -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 ?? [])}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user