fix(services): forbid deleting service items/sections — deactivate only

Services are referenced by appointments, sessions, invoices and payment history,
so deleting one orphans/corrupts those records (deleting a section cascaded to
its services too). Make deletion impossible:

- Backend: DELETE /service-item/{uuid} and DELETE /service-section/{uuid} now
  always return 409 (ERR_SERVICE_ITEM_IN_USE) with a message pointing to
  deactivate; no rows are touched. Deactivate stays via PATCH active=false.
- Frontend: removed the section delete button, its confirm dialog, the delete
  mutation, and the now-unused delete state/flag/icon from ClinicServicesPage.
  Section and item deactivate toggles are unchanged.

Tests: ServiceItemDeleteCleanupTest rewritten — delete of item and section both
rejected (409) and the row survives. docs/api/clinic-services.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-23 19:20:40 +03:30
co-authored by Claude Opus 4.8
parent 48b0f684f5
commit aa842b883d
4 changed files with 58 additions and 93 deletions
+5 -36
View File
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
PlusIcon, PencilIcon, TrashIcon, WrenchScrewdriverIcon, BanknotesIcon,
PlusIcon, PencilIcon, WrenchScrewdriverIcon, BanknotesIcon,
ShieldCheckIcon, MagnifyingGlassIcon, EyeIcon, EyeSlashIcon,
EllipsisHorizontalIcon, CheckCircleIcon, XCircleIcon, ChevronRightIcon,
XMarkIcon, UsersIcon, ClockIcon,
@@ -48,12 +48,10 @@ function ClinicServicesPageInner() {
const { can } = usePermissions();
const canCreate = can('services', 'create');
const canUpdate = can('services', 'update');
const canDelete = can('services', 'delete');
const navigate = useNavigate();
const [selectedSection, setSelectedSection] = useState<ServiceSection | null>(null);
const [sectionModal, setSectionModal] = useState<'create' | ServiceSection | null>(null);
const [deleteSection, setDeleteSection] = useState<ServiceSection | null>(null);
const [itemModal, setItemModal] = useState<'create' | ServiceItem | null>(null);
const [toggleItem, setToggleItem] = useState<ServiceItem | null>(null);
const [tariffItem, setTariffItem] = useState<ServiceItem | null>(null);
@@ -98,17 +96,6 @@ function ClinicServicesPageInner() {
onError: (e: any) => toast.error(e.message),
});
const delSection = useMutation({
mutationFn: (uuid: string) => api.delete(`/api/v1/service-section/${uuid}`),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['service-sections'] });
if (selectedSection?.uuid === deleteSection?.uuid) setSelectedSection(null);
setDeleteSection(null);
toast.success('بخش حذف شد');
},
onError: (e: any) => { toast.error(e.message); setDeleteSection(null); },
});
const toggleSection = useMutation({
mutationFn: ({ uuid, active }: { uuid: string; active: boolean }) =>
api.patch(`/api/v1/service-section/${uuid}`, { active }),
@@ -188,18 +175,11 @@ function ClinicServicesPageInner() {
</span>
</div>
{(canUpdate || canDelete) && (
{canUpdate && (
<div style={{ display: 'flex', gap: 6, borderTop: '1px solid var(--border)', paddingTop: 10, marginTop: 8 }} onClick={(e) => e.stopPropagation()}>
{canUpdate && (
<button className="btn sm ghost" aria-label="ویرایش" style={{ display: 'flex', alignItems: 'center', gap: 4, color: 'var(--text-2)' }} onClick={() => openEditSection(s)}>
<PencilIcon style={{ width: 15 }} />
</button>
)}
{canDelete && (
<button className="btn sm ghost" aria-label="حذف" style={{ display: 'flex', alignItems: 'center', gap: 4, color: 'var(--danger)' }} onClick={() => setDeleteSection(s)}>
<TrashIcon style={{ width: 15 }} />
</button>
)}
<button className="btn sm ghost" aria-label="ویرایش" style={{ display: 'flex', alignItems: 'center', gap: 4, color: 'var(--text-2)' }} onClick={() => openEditSection(s)}>
<PencilIcon style={{ width: 15 }} />
</button>
</div>
)}
</div>
@@ -385,17 +365,6 @@ function ClinicServicesPageInner() {
<ServiceInsuranceModal item={insuranceItem} onClose={() => setInsuranceItem(null)} />
{/* Confirm حذف بخش */}
<ConfirmDialog
open={!!deleteSection}
title="حذف بخش"
message={`آیا مطمئن هستید که می‌خواهید بخش «${deleteSection?.name}» و همه‌ی سرویس‌های آن را حذف کنید؟`}
confirmLabel="حذف"
onConfirm={() => deleteSection && delSection.mutate(deleteSection.uuid)}
onCancel={() => setDeleteSection(null)}
loading={delSection.isPending}
/>
{/* Confirm فعال/غیرفعال سرویس */}
<ConfirmDialog
open={!!toggleItem}