improvement/v1-demo-improvements. #62
@@ -64,6 +64,7 @@ export const ErrorCode = {
|
|||||||
CONFLICT_FUTURE_APPOINTMENTS: 'CONFLICT_FUTURE_APPOINTMENTS',
|
CONFLICT_FUTURE_APPOINTMENTS: 'CONFLICT_FUTURE_APPOINTMENTS',
|
||||||
APPOINTMENT_PATIENT_LOCKED: 'APPOINTMENT_PATIENT_LOCKED',
|
APPOINTMENT_PATIENT_LOCKED: 'APPOINTMENT_PATIENT_LOCKED',
|
||||||
APPOINTMENT_HAS_TREATMENT: 'APPOINTMENT_HAS_TREATMENT',
|
APPOINTMENT_HAS_TREATMENT: 'APPOINTMENT_HAS_TREATMENT',
|
||||||
|
TREATMENT_DETAIL_SENT: 'TREATMENT_DETAIL_SENT',
|
||||||
BAD_REQUEST: 'BAD_REQUEST',
|
BAD_REQUEST: 'BAD_REQUEST',
|
||||||
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
ForbiddenException,
|
ForbiddenException,
|
||||||
|
HttpStatus,
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { AppException, ErrorCode } from '../../common/errors';
|
||||||
import { LabCaseActivityType, LabTaskStatus, LinkStatus, Prisma } from '@prisma/client';
|
import { LabCaseActivityType, LabTaskStatus, LinkStatus, Prisma } from '@prisma/client';
|
||||||
import { createReadStream, existsSync, mkdirSync } from 'fs';
|
import { createReadStream, existsSync, mkdirSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
@@ -908,6 +910,25 @@ export class TreatmentsService {
|
|||||||
throw new BadRequestException('At least one file is required');
|
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);
|
const orgDir = join(this.uploadRoot, organizationId);
|
||||||
mkdirSync(orgDir, { recursive: true });
|
mkdirSync(orgDir, { recursive: true });
|
||||||
|
|
||||||
|
|||||||
@@ -660,6 +660,7 @@
|
|||||||
"detailsTitle": "Treatment details",
|
"detailsTitle": "Treatment details",
|
||||||
"detailsSubtitle": "Plan teeth, type, notes, and attachments for each detail line.",
|
"detailsSubtitle": "Plan teeth, type, notes, and attachments for each detail line.",
|
||||||
"addDetail": "Add detail",
|
"addDetail": "Add detail",
|
||||||
|
"confirmRemoveDetail": "Remove this treatment detail?",
|
||||||
"detailLabel": "Detail {n}",
|
"detailLabel": "Detail {n}",
|
||||||
"detailSentBadge": "sent",
|
"detailSentBadge": "sent",
|
||||||
"detailLockedInShipment": "This detail was sent to a lab and can no longer be edited.",
|
"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.",
|
"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_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.",
|
"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.",
|
"BAD_REQUEST": "The request could not be processed.",
|
||||||
"INTERNAL_ERROR": "Something went wrong on our end. Please try again later."
|
"INTERNAL_ERROR": "Something went wrong on our end. Please try again later."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -661,6 +661,7 @@
|
|||||||
"detailsTitle": "جزئیات درمان",
|
"detailsTitle": "جزئیات درمان",
|
||||||
"detailsSubtitle": "دندانها، نوع، یادداشت و پیوستها را برای هر خط جزئیات برنامهریزی کنید.",
|
"detailsSubtitle": "دندانها، نوع، یادداشت و پیوستها را برای هر خط جزئیات برنامهریزی کنید.",
|
||||||
"addDetail": "افزودن جزئیات",
|
"addDetail": "افزودن جزئیات",
|
||||||
|
"confirmRemoveDetail": "این جزئیات درمان حذف شود؟",
|
||||||
"detailLabel": "جزئیات {n}",
|
"detailLabel": "جزئیات {n}",
|
||||||
"detailSentBadge": "ارسالشده",
|
"detailSentBadge": "ارسالشده",
|
||||||
"detailLockedInShipment": "این جزئیات به لابراتوار ارسال شده و دیگر قابل ویرایش نیست.",
|
"detailLockedInShipment": "این جزئیات به لابراتوار ارسال شده و دیگر قابل ویرایش نیست.",
|
||||||
@@ -1016,6 +1017,7 @@
|
|||||||
"CONFLICT_FUTURE_APPOINTMENTS": "تا وقتی نوبتهای آینده دارید نمیتوانید مشارکت در درمان را متوقف کنید. ابتدا آنها را لغو یا واگذار کنید.",
|
"CONFLICT_FUTURE_APPOINTMENTS": "تا وقتی نوبتهای آینده دارید نمیتوانید مشارکت در درمان را متوقف کنید. ابتدا آنها را لغو یا واگذار کنید.",
|
||||||
"APPOINTMENT_PATIENT_LOCKED": "تا وقتی درمانی به این نوبت متصل است، امکان تغییر بیمار وجود ندارد.",
|
"APPOINTMENT_PATIENT_LOCKED": "تا وقتی درمانی به این نوبت متصل است، امکان تغییر بیمار وجود ندارد.",
|
||||||
"APPOINTMENT_HAS_TREATMENT": "بهدلیل وجود درمان مرتبط با این نوبت، امکان حذف آن وجود ندارد.",
|
"APPOINTMENT_HAS_TREATMENT": "بهدلیل وجود درمان مرتبط با این نوبت، امکان حذف آن وجود ندارد.",
|
||||||
|
"TREATMENT_DETAIL_SENT": "این جزئیات به لابراتوار ارسال شده و دیگر قابل تغییر نیست.",
|
||||||
"BAD_REQUEST": "درخواست قابل پردازش نبود.",
|
"BAD_REQUEST": "درخواست قابل پردازش نبود.",
|
||||||
"INTERNAL_ERROR": "مشکلی در سرور رخ داد. لطفاً بعداً تلاش کنید."
|
"INTERNAL_ERROR": "مشکلی در سرور رخ داد. لطفاً بعداً تلاش کنید."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -660,6 +660,7 @@
|
|||||||
"detailsTitle": "Behandeldetails",
|
"detailsTitle": "Behandeldetails",
|
||||||
"detailsSubtitle": "Plan tanden, type, notities en bijlagen per detailregel.",
|
"detailsSubtitle": "Plan tanden, type, notities en bijlagen per detailregel.",
|
||||||
"addDetail": "Detail toevoegen",
|
"addDetail": "Detail toevoegen",
|
||||||
|
"confirmRemoveDetail": "Dit behandelingsdetail verwijderen?",
|
||||||
"detailLabel": "Detail {n}",
|
"detailLabel": "Detail {n}",
|
||||||
"detailSentBadge": "verzonden",
|
"detailSentBadge": "verzonden",
|
||||||
"detailLockedInShipment": "Dit detail is naar het lab verzonden en kan niet meer worden bewerkt.",
|
"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.",
|
"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_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.",
|
"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.",
|
"BAD_REQUEST": "Het verzoek kon niet worden verwerkt.",
|
||||||
"INTERNAL_ERROR": "Er is iets misgegaan aan onze kant. Probeer het later opnieuw."
|
"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';
|
saveStatus: 'idle' | 'dirty' | 'saving' | 'saved' | 'error';
|
||||||
uploadBusy: boolean;
|
uploadBusy: boolean;
|
||||||
onAddDetail: () => void;
|
onAddDetail: () => void;
|
||||||
|
onRemoveDetail: () => void;
|
||||||
onUploadFiles: (files: FileList | null) => void;
|
onUploadFiles: (files: FileList | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,9 +49,11 @@ export function TreatmentDetailsEditor({
|
|||||||
saveStatus,
|
saveStatus,
|
||||||
uploadBusy,
|
uploadBusy,
|
||||||
onAddDetail,
|
onAddDetail,
|
||||||
|
onRemoveDetail,
|
||||||
onUploadFiles,
|
onUploadFiles,
|
||||||
}: TreatmentDetailsEditorProps) {
|
}: TreatmentDetailsEditorProps) {
|
||||||
const t = useTranslations('treatment');
|
const t = useTranslations('treatment');
|
||||||
|
const tCommon = useTranslations('common');
|
||||||
const attachmentInputRef = useRef<HTMLInputElement>(null);
|
const attachmentInputRef = useRef<HTMLInputElement>(null);
|
||||||
const activeDetail = details.find((d) => d.clientId === activeDetailId) ?? details[0];
|
const activeDetail = details.find((d) => d.clientId === activeDetailId) ?? details[0];
|
||||||
|
|
||||||
@@ -58,6 +61,7 @@ export function TreatmentDetailsEditor({
|
|||||||
|
|
||||||
const locked = isDetailLocked(activeDetail);
|
const locked = isDetailLocked(activeDetail);
|
||||||
const readOnly = disabled || locked;
|
const readOnly = disabled || locked;
|
||||||
|
const canRemoveDetail = canEdit && !disabled && !locked && details.length > 1;
|
||||||
const treatmentTypeTextColor = isDetailTypeSelected(activeDetail)
|
const treatmentTypeTextColor = isDetailTypeSelected(activeDetail)
|
||||||
? treatmentTypeColor(
|
? treatmentTypeColor(
|
||||||
activeDetail.treatmentType,
|
activeDetail.treatmentType,
|
||||||
@@ -206,6 +210,21 @@ export function TreatmentDetailsEditor({
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{canEdit && saveStatus !== 'idle' && (
|
{canEdit && saveStatus !== 'idle' && (
|
||||||
|
|||||||
@@ -1249,6 +1249,8 @@ export function TreatmentWorkspace({
|
|||||||
const uploadForDetail = useCallback(
|
const uploadForDetail = useCallback(
|
||||||
async (detailClientId: string, files: FileList | File[]) => {
|
async (detailClientId: string, files: FileList | File[]) => {
|
||||||
if (!canEditTreatmentForDay || !selectedAppointment) return;
|
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;
|
const list = files instanceof FileList ? Array.from(files) : files;
|
||||||
if (!list.length) return;
|
if (!list.length) return;
|
||||||
|
|
||||||
@@ -1273,7 +1275,7 @@ export function TreatmentWorkspace({
|
|||||||
setUploadBusyDetailId(null);
|
setUploadBusyDetailId(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[canEditTreatmentForDay, selectedAppointment, showSuccess, showError, t],
|
[canEditTreatmentForDay, isDetailLocked, selectedAppointment, showSuccess, showError, t, tErrors],
|
||||||
);
|
);
|
||||||
|
|
||||||
const persistLabCases = useCallback(
|
const persistLabCases = useCallback(
|
||||||
@@ -1373,9 +1375,40 @@ export function TreatmentWorkspace({
|
|||||||
selectedAppointment,
|
selectedAppointment,
|
||||||
showError,
|
showError,
|
||||||
t,
|
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 () => {
|
const handleAddLabCase = useCallback(async () => {
|
||||||
if (!canEditTreatmentForDay || !selectedAppointment) return;
|
if (!canEditTreatmentForDay || !selectedAppointment) return;
|
||||||
|
|
||||||
@@ -1779,6 +1812,7 @@ export function TreatmentWorkspace({
|
|||||||
setDetails((prev) => [...prev, next]);
|
setDetails((prev) => [...prev, next]);
|
||||||
setActiveDetailId(next.clientId);
|
setActiveDetailId(next.clientId);
|
||||||
}}
|
}}
|
||||||
|
onRemoveDetail={handleRemoveActiveDetail}
|
||||||
onUploadFiles={(files) => void uploadForDetail(activeDetailId, files ?? [])}
|
onUploadFiles={(files) => void uploadForDetail(activeDetailId, files ?? [])}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user