feat(appointment-settings): let clinics manage each member doctor's booking
The API and React components were already parameterized by doctor uuid, but 14 copy-pasted identity checks limited every endpoint to "the doctor themselves or an admin", so a clinic owner could not touch a member doctor's booking setup. - Replaces those 14 checks with one denyDoctorAccess() that also admits the owner of a clinic the doctor belongs to, and a member doctor holding the clinic's appointment_settings permission (view for GET, update for writes). A doctor's own settings short-circuit before any permission lookup. - Moves ScheduleSection and its tabs out of DoctorDetailPage into components/schedule/ScheduleSection.tsx so the doctor panel and the new clinic page render the same module instead of one page importing another. Pure relocation — no logic changed. - Adds ClinicAppointmentSettingsPage: one tab per clinic doctor, each rendering that same section. The tab wrapper is keyed by doctor uuid so in-progress schedule edits cannot leak onto the wrong doctor. - insurance-pricing accepts an optional doctor_uuid (query on GET, body on PUT) under the same access rule, so the visit-price card works inside the clinic tabs. Fixes saveInsurancePricing calling getInsurancePricing with the wrong argument by extracting the shared pricingPayload(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,9 +42,42 @@ class InsuranceController extends BaseController
|
||||
private readonly TenantInsuranceService $tenantInsuranceService,
|
||||
private readonly ServiceItemRepository $serviceItemRepo,
|
||||
private readonly FileValidatorService $fileValidator,
|
||||
private readonly \App\Clinic\Security\ClinicDoctorPermissionChecker $permChecker,
|
||||
private readonly string $projectDir,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* وقتی doctor_uuid داده شود، قیمتگذاری همان پزشک هدف است — برای مدیریت پزشکان
|
||||
* کلینیک از پنل کلینیک. بدون آن، رفتار قبلی (موجودیتِ خودِ کاربر) حفظ میشود.
|
||||
*
|
||||
* @param 'view'|'update' $action
|
||||
* @return array{0: string, 1: int|null, 2: JsonResponse|null}
|
||||
*/
|
||||
private function resolveTargetEntity(User $user, ?string $doctorUuid, string $action): array
|
||||
{
|
||||
if ($doctorUuid === null || $doctorUuid === '') {
|
||||
[$type, $id] = $this->resolveEntity($user);
|
||||
return [$type, $id, null];
|
||||
}
|
||||
|
||||
$doctor = $this->doctorRepo->findByUuid($doctorUuid);
|
||||
if ($doctor === null) {
|
||||
return ['unknown', null, $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پزشک یافت نشد', 404)];
|
||||
}
|
||||
|
||||
if ($user->hasRole('ROLE_ADMIN') || $doctor->getUser()->getId() === $user->getId()) {
|
||||
return [EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), null];
|
||||
}
|
||||
|
||||
foreach ($this->clinicRepo->findByDoctor($doctor) as $clinic) {
|
||||
if ($this->permChecker->can($user, $clinic, 'services', $action)) {
|
||||
return [EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), null];
|
||||
}
|
||||
}
|
||||
|
||||
return ['unknown', null, $this->error(ErrorCodes::ERR_ACCESS_DENIED, 'دسترسی ممنوع', 403)];
|
||||
}
|
||||
|
||||
private function resolveEntity(User $user): array
|
||||
{
|
||||
if ($user->hasRole('ROLE_DOCTOR')) {
|
||||
@@ -219,13 +252,21 @@ class InsuranceController extends BaseController
|
||||
|
||||
#[Route('/api/v1/insurance-pricing', methods: ['GET'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function getInsurancePricing(#[CurrentUser] User $user): JsonResponse
|
||||
public function getInsurancePricing(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
[$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $request->query->get('doctor_uuid'), 'view');
|
||||
if ($err !== null) {
|
||||
return $err;
|
||||
}
|
||||
if ($entityId === null) {
|
||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
||||
}
|
||||
|
||||
return $this->success($this->pricingPayload($entityType, $entityId));
|
||||
}
|
||||
|
||||
private function pricingPayload(string $entityType, int $entityId): array
|
||||
{
|
||||
$rows = $this->pricingRepo->findByEntity($entityType, $entityId);
|
||||
|
||||
$freeVisitPriceRials = 0;
|
||||
@@ -249,26 +290,29 @@ class InsuranceController extends BaseController
|
||||
];
|
||||
}, $this->insuranceRepo->findActive(null));
|
||||
|
||||
return $this->success([
|
||||
return [
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => $entityId,
|
||||
'free_visit_price_rials' => $freeVisitPriceRials,
|
||||
'require_visit_price' => $requireVisitPrice,
|
||||
'insurances' => $insurances,
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
#[Route('/api/v1/insurance-pricing', methods: ['PUT'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function saveInsurancePricing(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
[$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $data['doctor_uuid'] ?? null, 'update');
|
||||
if ($err !== null) {
|
||||
return $err;
|
||||
}
|
||||
if ($entityId === null) {
|
||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
$freeVisitRow = $this->pricingRepo->findOneForInsurance($entityType, $entityId, null);
|
||||
|
||||
$requireVisitPrice = array_key_exists('require_visit_price', $data)
|
||||
@@ -306,7 +350,7 @@ class InsuranceController extends BaseController
|
||||
|
||||
$this->pricingRepo->getEntityManager()->flush();
|
||||
|
||||
return $this->getInsurancePricing($user);
|
||||
return $this->success($this->pricingPayload($entityType, $entityId));
|
||||
}
|
||||
|
||||
private function upsertPricing(string $entityType, int $entityId, ?int $insuranceId, int $shareRials): EntityInsurancePricing
|
||||
|
||||
Reference in New Issue
Block a user