improvement: treatment exits contoll adde to appoinment remove action.

This commit is contained in:
2026-07-16 22:35:02 +03:30
parent e8e00e2911
commit 334b7841ae
7 changed files with 666 additions and 646 deletions

View File

@@ -63,6 +63,7 @@ export const ErrorCode = {
CONFLICT: 'CONFLICT', CONFLICT: 'CONFLICT',
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',
BAD_REQUEST: 'BAD_REQUEST', BAD_REQUEST: 'BAD_REQUEST',
INTERNAL_ERROR: 'INTERNAL_ERROR', INTERNAL_ERROR: 'INTERNAL_ERROR',
} as const; } as const;

View File

@@ -1,6 +1,7 @@
import { import {
BadRequestException, BadRequestException,
ForbiddenException, ForbiddenException,
HttpStatus,
Injectable, Injectable,
NotFoundException, NotFoundException,
} from '@nestjs/common'; } from '@nestjs/common';
@@ -16,6 +17,7 @@ import { ListAppointmentsDto } from './dto/list-appointments.dto';
import { UpdateAppointmentDto } from './dto/update-appointment.dto'; import { UpdateAppointmentDto } from './dto/update-appointment.dto';
import { TreatmentCatalogService } from '../treatment-catalog/treatment-catalog.service'; import { TreatmentCatalogService } from '../treatment-catalog/treatment-catalog.service';
import { hasEffectivePermission } from '../../common/membership-permissions'; import { hasEffectivePermission } from '../../common/membership-permissions';
import { AppException, ErrorCode } from '../../common/errors';
const MS_PER_DAY = 86_400_000; const MS_PER_DAY = 86_400_000;
@@ -215,9 +217,7 @@ export class AppointmentsService {
select: { id: true }, select: { id: true },
}); });
if (linkedTreatment) { if (linkedTreatment) {
throw new BadRequestException( throw new AppException(ErrorCode.APPOINTMENT_PATIENT_LOCKED, HttpStatus.CONFLICT);
'Cannot change the patient while a treatment is linked to this appointment',
);
} }
} }
@@ -256,13 +256,17 @@ export class AppointmentsService {
const existing = await this.prisma.appointment.findFirst({ const existing = await this.prisma.appointment.findFirst({
where: { id, organizationId }, where: { id, organizationId },
select: { id: true }, select: { id: true, treatment: { select: { id: true } } },
}); });
if (!existing) { if (!existing) {
throw new NotFoundException('Appointment not found'); throw new NotFoundException('Appointment not found');
} }
if (existing.treatment) {
throw new AppException(ErrorCode.APPOINTMENT_HAS_TREATMENT, HttpStatus.CONFLICT);
}
await this.prisma.appointment.delete({ await this.prisma.appointment.delete({
where: { id }, where: { id },
}); });

View File

@@ -582,6 +582,7 @@
"confirmRemove": "Remove this appointment?", "confirmRemove": "Remove this appointment?",
"successRemoved": "Appointment removed.", "successRemoved": "Appointment removed.",
"errorDelete": "Could not delete appointment.", "errorDelete": "Could not delete appointment.",
"deleteBlockedHint": "This appointment cannot be deleted because a treatment is linked to it.",
"errorLoadSchedule": "Failed to load schedule.", "errorLoadSchedule": "Failed to load schedule.",
"successPatientSaved": "Patient {firstName} {lastName} was saved.", "successPatientSaved": "Patient {firstName} {lastName} was saved.",
"searchPlaceholder": "Search existing patients", "searchPlaceholder": "Search existing patients",
@@ -1013,6 +1014,7 @@
"CONFLICT": "This action conflicts with existing data.", "CONFLICT": "This action conflicts with existing data.",
"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.",
"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."
} }

View File

@@ -583,6 +583,7 @@
"confirmRemove": "این نوبت حذف شود؟", "confirmRemove": "این نوبت حذف شود؟",
"successRemoved": "نوبت حذف شد.", "successRemoved": "نوبت حذف شد.",
"errorDelete": "حذف نوبت امکان‌پذیر نبود.", "errorDelete": "حذف نوبت امکان‌پذیر نبود.",
"deleteBlockedHint": "به‌دلیل وجود درمان مرتبط با این نوبت، امکان حذف آن وجود ندارد.",
"errorLoadSchedule": "بارگذاری برنامه ناموفق بود.", "errorLoadSchedule": "بارگذاری برنامه ناموفق بود.",
"successPatientSaved": "بیمار {firstName} {lastName} ذخیره شد.", "successPatientSaved": "بیمار {firstName} {lastName} ذخیره شد.",
"searchPlaceholder": "جستجوی بیماران موجود", "searchPlaceholder": "جستجوی بیماران موجود",
@@ -1014,6 +1015,7 @@
"CONFLICT": "این عمل با داده‌های موجود در تضاد است.", "CONFLICT": "این عمل با داده‌های موجود در تضاد است.",
"CONFLICT_FUTURE_APPOINTMENTS": "تا وقتی نوبت‌های آینده دارید نمی‌توانید مشارکت در درمان را متوقف کنید. ابتدا آن‌ها را لغو یا واگذار کنید.", "CONFLICT_FUTURE_APPOINTMENTS": "تا وقتی نوبت‌های آینده دارید نمی‌توانید مشارکت در درمان را متوقف کنید. ابتدا آن‌ها را لغو یا واگذار کنید.",
"APPOINTMENT_PATIENT_LOCKED": "تا وقتی درمانی به این نوبت متصل است، امکان تغییر بیمار وجود ندارد.", "APPOINTMENT_PATIENT_LOCKED": "تا وقتی درمانی به این نوبت متصل است، امکان تغییر بیمار وجود ندارد.",
"APPOINTMENT_HAS_TREATMENT": "به‌دلیل وجود درمان مرتبط با این نوبت، امکان حذف آن وجود ندارد.",
"BAD_REQUEST": "درخواست قابل پردازش نبود.", "BAD_REQUEST": "درخواست قابل پردازش نبود.",
"INTERNAL_ERROR": "مشکلی در سرور رخ داد. لطفاً بعداً تلاش کنید." "INTERNAL_ERROR": "مشکلی در سرور رخ داد. لطفاً بعداً تلاش کنید."
} }

View File

@@ -582,6 +582,7 @@
"confirmRemove": "Deze afspraak verwijderen?", "confirmRemove": "Deze afspraak verwijderen?",
"successRemoved": "Afspraak verwijderd.", "successRemoved": "Afspraak verwijderd.",
"errorDelete": "Kon afspraak niet verwijderen.", "errorDelete": "Kon afspraak niet verwijderen.",
"deleteBlockedHint": "Deze afspraak kan niet worden verwijderd omdat er een behandeling aan is gekoppeld.",
"errorLoadSchedule": "Rooster laden mislukt.", "errorLoadSchedule": "Rooster laden mislukt.",
"successPatientSaved": "Patiënt {firstName} {lastName} is opgeslagen.", "successPatientSaved": "Patiënt {firstName} {lastName} is opgeslagen.",
"searchPlaceholder": "Bestaande patiënten zoeken", "searchPlaceholder": "Bestaande patiënten zoeken",
@@ -1013,6 +1014,7 @@
"CONFLICT": "Deze actie conflicteert met bestaande gegevens.", "CONFLICT": "Deze actie conflicteert met bestaande gegevens.",
"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.",
"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."
} }

View File

@@ -310,6 +310,10 @@ export function AppointmentBookingModal({
{error && <p className="text-sm text-red-400">{error}</p>} {error && <p className="text-sm text-red-400">{error}</p>}
{editingAppointment?.hasTreatment ? (
<p className="text-sm text-text-secondary">{t('deleteBlockedHint')}</p>
) : null}
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between"> <div className="flex flex-col-reverse gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between">
{editingAppointment && canDelete && onDelete ? ( {editingAppointment && canDelete && onDelete ? (
<Button <Button

View File

@@ -279,7 +279,12 @@ export function AppointmentsPage() {
}} }}
onSubmit={handleSaveAppointment} onSubmit={handleSaveAppointment}
loading={savingAppointment} loading={savingAppointment}
canDelete={
canManageAppointments &&
!isViewingPastDay &&
!!activeEditingAppointment &&
!activeEditingAppointment.hasTreatment
}
onDelete={() => void handleDeleteEditingAppointment()} onDelete={() => void handleDeleteEditingAppointment()}
deleting={deletingAppointment} deleting={deletingAppointment}
/> />