feat(clinic): implement doctor detachment functionality with API endpoint and confirmation dialog
This commit is contained in:
@@ -480,6 +480,7 @@ export default function ClinicDetailPage() {
|
|||||||
const [editingClinicAddr, setEditingClinicAddr] = useState<ClinicAddress | null>(null);
|
const [editingClinicAddr, setEditingClinicAddr] = useState<ClinicAddress | null>(null);
|
||||||
const [deleteAddrConfirm, setDeleteAddrConfirm] = useState<ClinicAddress | null>(null);
|
const [deleteAddrConfirm, setDeleteAddrConfirm] = useState<ClinicAddress | null>(null);
|
||||||
const [deleteImageConfirm, setDeleteImageConfirm] = useState<string | null>(null);
|
const [deleteImageConfirm, setDeleteImageConfirm] = useState<string | null>(null);
|
||||||
|
const [detachDoctorConfirm, setDetachDoctorConfirm] = useState<ClinicDoctorItem | null>(null);
|
||||||
|
|
||||||
const emptyAddrForm = { name: '', address: '', telephone: '', province_id: null as number | null, city_id: null as number | null, latitude: null as number | null, longitude: null as number | null };
|
const emptyAddrForm = { name: '', address: '', telephone: '', province_id: null as number | null, city_id: null as number | null, latitude: null as number | null, longitude: null as number | null };
|
||||||
const [addrForm, setAddrForm] = useState(emptyAddrForm);
|
const [addrForm, setAddrForm] = useState(emptyAddrForm);
|
||||||
@@ -587,6 +588,17 @@ export default function ClinicDetailPage() {
|
|||||||
onError: (e: Error) => toast.error(e.message),
|
onError: (e: Error) => toast.error(e.message),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const detachDoctorMut = useMutation({
|
||||||
|
mutationFn: (doctorUuid: string) =>
|
||||||
|
api.delete<ApiResponse<{ message: string }>>(`/api/v1/admin/clinic/${uuid}/doctor/${doctorUuid}`),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('پزشک از کلینیک جدا شد');
|
||||||
|
qc.invalidateQueries({ queryKey: ['clinic-doctors', uuid] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['clinic-detail', uuid] });
|
||||||
|
},
|
||||||
|
onError: (e: Error) => toast.error(e.message),
|
||||||
|
});
|
||||||
|
|
||||||
const handleLogoUpload = async (file: File) => {
|
const handleLogoUpload = async (file: File) => {
|
||||||
setLogoUploading(true);
|
setLogoUploading(true);
|
||||||
try {
|
try {
|
||||||
@@ -825,6 +837,13 @@ export default function ClinicDetailPage() {
|
|||||||
>
|
>
|
||||||
<EyeIcon style={{ width: 14, height: 14 }} />
|
<EyeIcon style={{ width: 14, height: 14 }} />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="mini-btn danger"
|
||||||
|
title="جداسازی از کلینیک"
|
||||||
|
onClick={() => setDetachDoctorConfirm(doc)}
|
||||||
|
>
|
||||||
|
<TrashIcon style={{ width: 14, height: 14 }} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1195,6 +1214,21 @@ export default function ClinicDetailPage() {
|
|||||||
onCancel={() => setDeleteImageConfirm(null)}
|
onCancel={() => setDeleteImageConfirm(null)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Detach Doctor From Clinic Confirm */}
|
||||||
|
<ConfirmDialog
|
||||||
|
open={detachDoctorConfirm !== null}
|
||||||
|
title="جداسازی پزشک از کلینیک"
|
||||||
|
message={`آیا پزشک "${detachDoctorConfirm?.name ?? ''}" از این کلینیک جدا شود؟ این کار فقط ارتباط پزشک با این کلینیک را حذف میکند.`}
|
||||||
|
confirmLabel="جداسازی"
|
||||||
|
danger
|
||||||
|
loading={detachDoctorMut.isPending}
|
||||||
|
onConfirm={() => {
|
||||||
|
if (detachDoctorConfirm) detachDoctorMut.mutate(detachDoctorConfirm.uuid);
|
||||||
|
setDetachDoctorConfirm(null);
|
||||||
|
}}
|
||||||
|
onCancel={() => setDetachDoctorConfirm(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Edit modal — portal to escape Leaflet transform context */}
|
{/* Edit modal — portal to escape Leaflet transform context */}
|
||||||
{editOpen && createPortal(
|
{editOpen && createPortal(
|
||||||
<EditModal
|
<EditModal
|
||||||
|
|||||||
@@ -303,6 +303,31 @@ Get doctors associated with a clinic.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## DELETE `/api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}`
|
||||||
|
|
||||||
|
Detach a doctor from a clinic. This removes the clinic↔doctor link only (the `clinic_doctors` association); it does **not** delete the doctor or change the doctor's own `active` appointment flag.
|
||||||
|
|
||||||
|
**Permission:** `ROLE_ADMIN`
|
||||||
|
|
||||||
|
### Path Parameters
|
||||||
|
| Param | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `clinicUuid` | string (UUID) | Clinic UUID |
|
||||||
|
| `doctorUuid` | string (UUID) | Doctor UUID |
|
||||||
|
|
||||||
|
### Response `200`
|
||||||
|
```json
|
||||||
|
{ "success": true, "data": { "message": "پزشک از کلینیک جدا شد" } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
| Code | HTTP | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `ERR_VALIDATION_002` | 404 | Clinic not found |
|
||||||
|
| `ERR_NOT_FOUND_001` | 404 | Doctor not found, or doctor not linked to this clinic |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## POST `/file/upload/clinic_pro/clinic/field_clinic_logo`
|
## POST `/file/upload/clinic_pro/clinic/field_clinic_logo`
|
||||||
|
|
||||||
Upload clinic logo.
|
Upload clinic logo.
|
||||||
|
|||||||
@@ -330,6 +330,43 @@ class ClinicController extends BaseController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Delete(
|
||||||
|
path: '/api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}',
|
||||||
|
summary: 'Detach a doctor from a clinic (admin only)',
|
||||||
|
security: [['bearerAuth' => []]],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(name: 'clinicUuid', in: 'path', required: true, schema: new OA\Schema(type: 'string', format: 'uuid')),
|
||||||
|
new OA\Parameter(name: 'doctorUuid', in: 'path', required: true, schema: new OA\Schema(type: 'string', format: 'uuid')),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(response: 200, description: 'Doctor detached from clinic'),
|
||||||
|
new OA\Response(response: 404, description: 'Clinic or doctor not found'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
#[Route('/api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}', methods: ['DELETE'])]
|
||||||
|
#[IsGranted('ROLE_ADMIN')]
|
||||||
|
public function detachDoctor(string $clinicUuid, string $doctorUuid): JsonResponse
|
||||||
|
{
|
||||||
|
$clinic = $this->clinicRepo->findByUuid($clinicUuid);
|
||||||
|
if ($clinic === null) {
|
||||||
|
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'کلینیک یافت نشد', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$doctor = $this->doctorRepo->findByUuid($doctorUuid);
|
||||||
|
if ($doctor === null) {
|
||||||
|
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پزشک یافت نشد', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$clinic->hasDoctor($doctor)) {
|
||||||
|
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'این پزشک به کلینیک متصل نیست', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$clinic->removeDoctor($doctor);
|
||||||
|
$this->clinicRepo->save($clinic);
|
||||||
|
|
||||||
|
return $this->success(['message' => 'پزشک از کلینیک جدا شد']);
|
||||||
|
}
|
||||||
|
|
||||||
#[OA\Post(
|
#[OA\Post(
|
||||||
path: '/file/upload/clinic_pro/clinic/field_image_clinic',
|
path: '/file/upload/clinic_pro/clinic/field_image_clinic',
|
||||||
summary: 'Upload a clinic gallery image',
|
summary: 'Upload a clinic gallery image',
|
||||||
|
|||||||
@@ -147,6 +147,16 @@ class Clinic
|
|||||||
public function getCreatedAt(): int { return $this->createdAt; }
|
public function getCreatedAt(): int { return $this->createdAt; }
|
||||||
public function getUpdatedAt(): int { return $this->updatedAt; }
|
public function getUpdatedAt(): int { return $this->updatedAt; }
|
||||||
public function getDoctors(): Collection { return $this->doctors; }
|
public function getDoctors(): Collection { return $this->doctors; }
|
||||||
|
|
||||||
|
public function hasDoctor(Doctor $doctor): bool { return $this->doctors->contains($doctor); }
|
||||||
|
|
||||||
|
public function removeDoctor(Doctor $doctor): self
|
||||||
|
{
|
||||||
|
$this->doctors->removeElement($doctor);
|
||||||
|
$this->touch();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function getSpecialties(): Collection { return $this->specialties; }
|
public function getSpecialties(): Collection { return $this->specialties; }
|
||||||
public function getServices(): Collection { return $this->services; }
|
public function getServices(): Collection { return $this->services; }
|
||||||
public function getInsurances(): Collection { return $this->insurances; }
|
public function getInsurances(): Collection { return $this->insurances; }
|
||||||
|
|||||||
Reference in New Issue
Block a user