fix: store appointment address from schedule and auto-add patient to clinic
Appointments now persist address_id resolved from the weekly-schedule session (location_id) across all booking paths (online, secretary, admin). On confirm, the patient is added to the clinic owning that address, or to the doctor's single clinic as fallback. Weekly-schedule create/update now requires location_id on every active session. PatientSession exposes doctor_uuid/doctor_name so clinic records show which doctor each visit is for. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ class AdminApiController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly \App\Appointment\Service\SlotCalculatorService $slotCalculator,
|
||||
) {}
|
||||
|
||||
// ── Users ─────────────────────────────────────────────────────────────────
|
||||
@@ -818,6 +819,8 @@ class AdminApiController extends BaseController
|
||||
|
||||
$appointment = new Appointment($doctor, $patient, $slotStart, $slotEnd);
|
||||
if (!empty($data['note'])) $appointment->setNote($data['note']);
|
||||
$locationId = $this->slotCalculator->resolveSlotLocationId($doctor, $slotStart);
|
||||
if ($locationId !== null) $appointment->setAddressId($locationId);
|
||||
|
||||
$this->em->persist($appointment);
|
||||
$this->em->flush();
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Appointment\Repository\AppointmentRepository;
|
||||
use App\Appointment\Repository\SlotTakenException;
|
||||
use App\Appointment\Repository\WeeklyScheduleRepository;
|
||||
use App\Appointment\Service\SlotCalculatorService;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Auth\Entity\User;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use App\Patient\Service\PatientService;
|
||||
@@ -236,6 +237,12 @@ class AppointmentController extends BaseController
|
||||
$appointment = new Appointment($doctor, $user, $slotStart, $slotEnd);
|
||||
if (isset($data['note'])) $appointment->setNote($data['note']);
|
||||
|
||||
// آدرس نوبت از روی session متناظر در برنامهی هفتگی تعیین میشود (location_id).
|
||||
$locationId = $this->resolveSlotLocationId($doctor, $slotStart);
|
||||
if ($locationId !== null) {
|
||||
$appointment->setAddressId($locationId);
|
||||
}
|
||||
|
||||
if ($forSelf) {
|
||||
$appointment->setPatientName($user->getRealName());
|
||||
$appointment->setPatientMobile($user->getMobileNumber());
|
||||
@@ -423,6 +430,11 @@ class AppointmentController extends BaseController
|
||||
|| $user->hasRole('ROLE_ADMIN');
|
||||
}
|
||||
|
||||
private function resolveSlotLocationId(Doctor $doctor, int $slotStart): ?int
|
||||
{
|
||||
return $this->slotCalculator->resolveSlotLocationId($doctor, $slotStart);
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
path: '/api/v1/appointment/{uuid}/status',
|
||||
summary: 'Update the status of an appointment',
|
||||
|
||||
@@ -53,6 +53,10 @@ class AppointmentSettingsController extends BaseController
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
|
||||
if (($err = $this->validateSessionsHaveLocation($data['schedule'] ?? [])) !== null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, $err, 422);
|
||||
}
|
||||
|
||||
// Only one schedule per doctor — upsert
|
||||
$schedule = $this->scheduleRepo->findByDoctor($doctor);
|
||||
if ($schedule !== null) {
|
||||
@@ -90,6 +94,9 @@ class AppointmentSettingsController extends BaseController
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
if (isset($data['schedule'])) {
|
||||
if (($err = $this->validateSessionsHaveLocation($data['schedule'])) !== null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, $err, 422);
|
||||
}
|
||||
$schedule->setSetting($data['schedule']);
|
||||
}
|
||||
if (isset($data['meta']) && is_array($data['meta'])) {
|
||||
@@ -356,4 +363,20 @@ class AppointmentSettingsController extends BaseController
|
||||
|
||||
return $this->success(['data' => $result]);
|
||||
}
|
||||
|
||||
/**
|
||||
* هر session فعال در برنامهی هفتگی باید آدرس (location_id) داشته باشد.
|
||||
* در صورت نقص، پیام خطا برمیگرداند؛ در غیر این صورت null.
|
||||
*/
|
||||
private function validateSessionsHaveLocation(array $schedule): ?string
|
||||
{
|
||||
foreach ($schedule as $day) {
|
||||
foreach (($day['sessions'] ?? []) as $session) {
|
||||
if (($session['active'] ?? false) && empty($session['location_id'])) {
|
||||
return 'برای هر شیفت فعال باید آدرس (مطب/کلینیک) انتخاب شود';
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Appointment\Controller;
|
||||
|
||||
use App\Appointment\Entity\Appointment;
|
||||
use App\Appointment\Service\SlotCalculatorService;
|
||||
use App\Auth\Entity\User;
|
||||
use App\Auth\Repository\UserActiveContextRepository;
|
||||
use App\Clinic\Repository\ClinicRepository;
|
||||
@@ -27,6 +28,7 @@ class MyAppointmentsController extends BaseController
|
||||
private readonly ClinicRepository $clinicRepo,
|
||||
private readonly DoctorSecretaryRepository $secretaryRepo,
|
||||
private readonly UserActiveContextRepository $contextRepo,
|
||||
private readonly SlotCalculatorService $slotCalculator,
|
||||
) {}
|
||||
|
||||
#[Route('/api/v1/my/appointment', methods: ['POST'])]
|
||||
@@ -79,6 +81,8 @@ class MyAppointmentsController extends BaseController
|
||||
|
||||
$appointment = new Appointment($doctor, $patient, $slotStart, $slotEnd);
|
||||
if (!empty($data['note'])) $appointment->setNote($data['note']);
|
||||
$locationId = $this->slotCalculator->resolveSlotLocationId($doctor, $slotStart);
|
||||
if ($locationId !== null) $appointment->setAddressId($locationId);
|
||||
$this->em->persist($appointment);
|
||||
$this->em->flush();
|
||||
|
||||
|
||||
@@ -83,6 +83,9 @@ class Appointment
|
||||
#[ORM\Column(name: 'patient_reason', type: 'text', nullable: true)]
|
||||
private ?string $patientReason = null;
|
||||
|
||||
#[ORM\Column(name: 'address_id', type: 'integer', nullable: true)]
|
||||
private ?int $addressId = null;
|
||||
|
||||
#[ORM\Column(name: 'created_at', type: 'integer')]
|
||||
private int $createdAt;
|
||||
|
||||
@@ -115,8 +118,10 @@ class Appointment
|
||||
public function getPatientNationalCode(): ?string { return $this->patientNationalCode; }
|
||||
public function getPatientGender(): ?string { return $this->patientGender; }
|
||||
public function getPatientReason(): ?string { return $this->patientReason; }
|
||||
public function getAddressId(): ?int { return $this->addressId; }
|
||||
|
||||
public function setNote(?string $v): self { $this->note = $v; return $this; }
|
||||
public function setAddressId(?int $v): self { $this->addressId = $v; return $this; }
|
||||
public function setPatientName(?string $v): self { $this->patientName = $v; return $this; }
|
||||
public function setPatientMobile(?string $v): self { $this->patientMobile = $v; return $this; }
|
||||
public function setPatientNationalCode(?string $v): self { $this->patientNationalCode = $v; return $this; }
|
||||
@@ -166,6 +171,7 @@ class Appointment
|
||||
),
|
||||
],
|
||||
'address' => $firstAddress?->toArray(),
|
||||
'address_id' => $this->addressId,
|
||||
'user' => [
|
||||
'uuid' => $this->user->getUuid(),
|
||||
'mobile' => $this->user->getMobileNumber(),
|
||||
|
||||
@@ -33,6 +33,24 @@ class SlotCalculatorService
|
||||
return $this->filterBookedSlots($doctor, $flat);
|
||||
}
|
||||
|
||||
/**
|
||||
* آدرس (location_id) متناظر با اسلاتِ شروعشده در تاریخ مشخص. اگر پیدا نشد null.
|
||||
*/
|
||||
public function resolveSlotLocationId(Doctor $doctor, int $slotStart): ?int
|
||||
{
|
||||
$date = date('Y-m-d', $slotStart);
|
||||
$sessions = $this->buildAllSessions($doctor, $date);
|
||||
foreach ($sessions as $session) {
|
||||
foreach (($session['slots'] ?? []) as $slot) {
|
||||
if ((int) ($slot['start'] ?? 0) === $slotStart) {
|
||||
$loc = $slot['location_id'] ?? null;
|
||||
return $loc !== null ? (int) $loc : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sessions grouped by shift, each slot tagged with is_available.
|
||||
* Used by the schedule view to show real shift boundaries.
|
||||
|
||||
@@ -108,6 +108,8 @@ class PatientSession
|
||||
'uuid' => $this->uuid,
|
||||
'record_uuid' => $this->record->getUuid(),
|
||||
'appointment_uuid' => $this->appointment?->getUuid(),
|
||||
'doctor_uuid' => $this->appointment?->getDoctor()->getUuid(),
|
||||
'doctor_name' => $this->appointment?->getDoctor()->getName(),
|
||||
'insurance_base_id' => $this->insuranceBaseId,
|
||||
'insurance_supplementary_id' => $this->insuranceSupplementaryId,
|
||||
'visit_price_rials' => $this->visitPriceRials,
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace App\Patient\Service;
|
||||
use App\Appointment\Entity\Appointment;
|
||||
use App\Auth\Repository\UserRepository;
|
||||
use App\ClinicService\Repository\ServiceItemRepository;
|
||||
use App\Clinic\Repository\ClinicRepository;
|
||||
use App\Doctor\Repository\DoctorAddressRepository;
|
||||
use App\Patient\Entity\PatientRecord;
|
||||
use App\Patient\Entity\PatientSession;
|
||||
use App\Patient\Entity\SessionService;
|
||||
@@ -24,6 +26,8 @@ class PatientService
|
||||
private readonly ClinicStaffRepository $staffRepo,
|
||||
private readonly UserRepository $userRepo,
|
||||
private readonly SubscriptionService $subscriptionService,
|
||||
private readonly DoctorAddressRepository $addressRepo,
|
||||
private readonly ClinicRepository $clinicRepo,
|
||||
) {}
|
||||
|
||||
public function calculateFinalPrice(int $visitPrice, float $baseDiscount, float $suppDiscount, array $serviceItems): array
|
||||
@@ -40,10 +44,31 @@ class PatientService
|
||||
|
||||
public function autoCreateOnAppointmentConfirm(Appointment $appointment): void
|
||||
{
|
||||
$doctor = $appointment->getDoctor();
|
||||
$entityType = 'doctor';
|
||||
$entityId = $doctor->getId();
|
||||
$doctor = $appointment->getDoctor();
|
||||
|
||||
// پروندهی پزشک
|
||||
$this->autoCreateForEntity('doctor', $doctor->getId(), $appointment, $doctor->getId());
|
||||
|
||||
// کلینیک نوبت را تعیین کن: اول از آدرس انتخابشده، وگرنه اگر دکتر فقط عضو یک کلینیک باشد.
|
||||
$clinicId = null;
|
||||
$addressId = $appointment->getAddressId();
|
||||
if ($addressId !== null) {
|
||||
$clinicId = $this->addressRepo->find($addressId)?->getClinicId();
|
||||
}
|
||||
if ($clinicId === null) {
|
||||
$clinics = $this->clinicRepo->findByDoctor($doctor);
|
||||
if (count($clinics) === 1) {
|
||||
$clinicId = $clinics[0]->getId();
|
||||
}
|
||||
}
|
||||
|
||||
if ($clinicId !== null) {
|
||||
$this->autoCreateForEntity('clinic', $clinicId, $appointment, $clinicId);
|
||||
}
|
||||
}
|
||||
|
||||
private function autoCreateForEntity(string $entityType, int $entityId, Appointment $appointment, int $createdById): void
|
||||
{
|
||||
if (!$this->subscriptionService->hasFeature($entityType, $entityId, 'patient_records')) {
|
||||
return;
|
||||
}
|
||||
@@ -52,7 +77,7 @@ class PatientService
|
||||
|
||||
$record = $this->recordRepo->findByEntityAndUser($entityType, $entityId, $patient);
|
||||
if ($record === null) {
|
||||
$record = new PatientRecord($entityType, $entityId, $patient, 'system', $doctor->getId());
|
||||
$record = new PatientRecord($entityType, $entityId, $patient, 'system', $createdById);
|
||||
$this->recordRepo->save($record);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user