diff --git a/migrations/Version20260610110921.php b/migrations/Version20260610110921.php new file mode 100644 index 00000000..b3bb9cf5 --- /dev/null +++ b/migrations/Version20260610110921.php @@ -0,0 +1,37 @@ +addSql('ALTER TABLE doctor_addresses ADD city_id INT DEFAULT NULL, ADD province_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE doctor_addresses ADD CONSTRAINT FK_doctor_addresses_city FOREIGN KEY (city_id) REFERENCES cities (id) ON DELETE SET NULL'); + $this->addSql('ALTER TABLE doctor_addresses ADD CONSTRAINT FK_doctor_addresses_province FOREIGN KEY (province_id) REFERENCES provinces (id) ON DELETE SET NULL'); + $this->addSql('CREATE INDEX IDX_doctor_addresses_city ON doctor_addresses (city_id)'); + $this->addSql('CREATE INDEX IDX_doctor_addresses_province ON doctor_addresses (province_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE doctor_addresses DROP FOREIGN KEY FK_doctor_addresses_city'); + $this->addSql('ALTER TABLE doctor_addresses DROP FOREIGN KEY FK_doctor_addresses_province'); + $this->addSql('DROP INDEX IDX_doctor_addresses_city ON doctor_addresses'); + $this->addSql('DROP INDEX IDX_doctor_addresses_province ON doctor_addresses'); + $this->addSql('ALTER TABLE doctor_addresses DROP city_id, DROP province_id'); + } +} diff --git a/migrations/Version20260610111254.php b/migrations/Version20260610111254.php new file mode 100644 index 00000000..214307b2 --- /dev/null +++ b/migrations/Version20260610111254.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE doctor_addresses RENAME INDEX idx_doctor_addresses_city TO IDX_A0D814FD8BAC62AF'); + $this->addSql('ALTER TABLE doctor_addresses RENAME INDEX idx_doctor_addresses_province TO IDX_A0D814FDE946114A'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE doctor_addresses RENAME INDEX idx_a0d814fd8bac62af TO IDX_doctor_addresses_city'); + $this->addSql('ALTER TABLE doctor_addresses RENAME INDEX idx_a0d814fde946114a TO IDX_doctor_addresses_province'); + } +} diff --git a/src/Clinic/Repository/ClinicRepository.php b/src/Clinic/Repository/ClinicRepository.php index aee8d881..1cf65316 100644 --- a/src/Clinic/Repository/ClinicRepository.php +++ b/src/Clinic/Repository/ClinicRepository.php @@ -4,6 +4,7 @@ namespace App\Clinic\Repository; use App\Auth\Entity\User; use App\Clinic\Entity\Clinic; +use App\Doctor\Entity\Doctor; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\Tools\Pagination\Paginator; use Doctrine\Persistence\ManagerRegistry; @@ -20,6 +21,17 @@ class ClinicRepository extends ServiceEntityRepository return $this->findOneBy(['uuid' => $uuid]); } + /** @return Clinic[] */ + public function findByDoctor(Doctor $doctor): array + { + return $this->createQueryBuilder('c') + ->innerJoin('c.doctors', 'd') + ->where('d.id = :doctorId') + ->setParameter('doctorId', $doctor->getId()) + ->getQuery() + ->getResult(); + } + public function findByUser(User $user): ?Clinic { return $this->findOneBy(['user' => $user]); diff --git a/src/Doctor/Controller/DoctorController.php b/src/Doctor/Controller/DoctorController.php index 6021639b..5bc5c90c 100644 --- a/src/Doctor/Controller/DoctorController.php +++ b/src/Doctor/Controller/DoctorController.php @@ -4,6 +4,8 @@ namespace App\Doctor\Controller; use App\Auth\Entity\User; use App\Auth\Repository\UserRepository; +use App\Clinic\Entity\Clinic; +use App\Clinic\Repository\ClinicRepository; use App\DoctorService\Repository\DoctorServiceRepository; use App\Doctor\Entity\Doctor; use App\Doctor\Entity\DoctorAddress; @@ -28,6 +30,7 @@ class DoctorController extends BaseController public function __construct( private readonly DoctorRepository $doctorRepo, private readonly DoctorAddressRepository $addressRepo, + private readonly ClinicRepository $clinicRepo, private readonly SpecialtyRepository $specialtyRepo, private readonly DoctorServiceRepository $serviceRepo, private readonly ProvinceRepository $provinceRepo, @@ -142,7 +145,22 @@ class DoctorController extends BaseController return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404); } - return $this->success(['data' => $doctor->toDetailArray()]); + $clinics = $this->clinicRepo->findByDoctor($doctor); + $clinicData = array_map(fn(Clinic $c) => [ + 'id' => (string) $c->getId(), + 'uuid' => $c->getUuid(), + 'name' => $c->getName(), + 'address' => $c->getAddress(), + 'telephone' => $c->getTelephone(), + 'city_id' => $c->getCityId(), + 'province_id' => $c->getProvinceId(), + 'map' => [ + 'latitude' => $c->getLatitude() !== null ? (string) $c->getLatitude() : null, + 'longitude' => $c->getLongitude() !== null ? (string) $c->getLongitude() : null, + ], + ], $clinics); + + return $this->success(['data' => array_merge($doctor->toDetailArray(), ['clinics' => $clinicData])]); } #[OA\Get( @@ -693,8 +711,65 @@ class DoctorController extends BaseController if (isset($data['map']['latitude'])) $address->setLatitude((float) $data['map']['latitude']); if (isset($data['map']['longitude'])) $address->setLongitude((float) $data['map']['longitude']); - // Also support flat keys if (array_key_exists('latitude', $data)) $address->setLatitude((float) $data['latitude']); if (array_key_exists('longitude', $data)) $address->setLongitude((float) $data['longitude']); + + if (array_key_exists('city_id', $data)) { + $city = $data['city_id'] !== null ? $this->cityRepo->find((int) $data['city_id']) : null; + $address->setCity($city); + } + if (array_key_exists('province_id', $data)) { + $province = $data['province_id'] !== null ? $this->provinceRepo->find((int) $data['province_id']) : null; + $address->setProvince($province); + } + } + + #[Route('/api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}', methods: ['POST'])] + #[IsGranted('IS_AUTHENTICATED_FULLY')] + public function createAddressFromClinic(string $clinicUuid, #[CurrentUser] User $user): JsonResponse + { + $doctor = $this->doctorRepo->findByUser($user); + if ($doctor === null) { + return $this->error(ErrorCodes::ERR_AUTH_006, 'فقط دکتر می‌تواند آدرس اضافه کند', 403); + } + + $clinic = $this->clinicRepo->findByUuid($clinicUuid); + if ($clinic === null) { + return $this->error(ErrorCodes::ERR_VALIDATION_002, 'کلینیک یافت نشد', 404); + } + + // Verify doctor belongs to this clinic + $clinics = $this->clinicRepo->findByDoctor($doctor); + $belongs = array_filter($clinics, fn(Clinic $c) => $c->getUuid() === $clinicUuid); + if (empty($belongs)) { + return $this->error(ErrorCodes::ERR_AUTH_006, 'این دکتر عضو این کلینیک نیست', 403); + } + + // Prevent duplicate: if this doctor already has an address for this clinic (same name), skip + foreach ($doctor->getAddresses() as $existing) { + if ($existing->getName() === $clinic->getName()) { + return $this->success(['data' => $existing->toArray()]); + } + } + + $address = new DoctorAddress($doctor); + $address->setName($clinic->getName()); + $address->setAddress($clinic->getAddress()); + $address->setTelephone($clinic->getTelephone()); + $address->setLatitude($clinic->getLatitude()); + $address->setLongitude($clinic->getLongitude()); + + if ($clinic->getCityId() !== null) { + $city = $this->cityRepo->find($clinic->getCityId()); + $address->setCity($city); + } + if ($clinic->getProvinceId() !== null) { + $province = $this->provinceRepo->find($clinic->getProvinceId()); + $address->setProvince($province); + } + + $this->addressRepo->save($address); + + return $this->success(['data' => $address->toArray()], 201); } } diff --git a/src/Doctor/Entity/DoctorAddress.php b/src/Doctor/Entity/DoctorAddress.php index e892ab6c..798c5e69 100644 --- a/src/Doctor/Entity/DoctorAddress.php +++ b/src/Doctor/Entity/DoctorAddress.php @@ -2,6 +2,8 @@ namespace App\Doctor\Entity; +use App\Location\Entity\City; +use App\Location\Entity\Province; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Uid\Uuid; @@ -37,6 +39,14 @@ class DoctorAddress #[ORM\Column(type: 'float', nullable: true)] private ?float $longitude = null; + #[ORM\ManyToOne(targetEntity: City::class)] + #[ORM\JoinColumn(name: 'city_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] + private ?City $city = null; + + #[ORM\ManyToOne(targetEntity: Province::class)] + #[ORM\JoinColumn(name: 'province_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] + private ?Province $province = null; + #[ORM\Column(name: 'created_at', type: 'integer')] private int $createdAt; @@ -57,14 +67,18 @@ class DoctorAddress public function getName(): ?string { return $this->name; } public function getAddress(): ?string { return $this->address; } public function getTelephone(): ?string { return $this->telephone; } - public function getLatitude(): ?float { return $this->latitude; } + public function getLatitude(): ?float { return $this->latitude; } public function getLongitude(): ?float { return $this->longitude; } + public function getCity(): ?City { return $this->city; } + public function getProvince(): ?Province { return $this->province; } public function setName(?string $v): self { $this->name = $v; return $this; } public function setAddress(?string $v): self { $this->address = $v; $this->touch(); return $this; } public function setTelephone(?string $v): self { $this->telephone = $v; $this->touch(); return $this; } public function setLatitude(?float $v): self { $this->latitude = $v; $this->touch(); return $this; } public function setLongitude(?float $v): self { $this->longitude = $v; $this->touch(); return $this; } + public function setCity(?City $v): self { $this->city = $v; $this->touch(); return $this; } + public function setProvince(?Province $v): self { $this->province = $v; $this->touch(); return $this; } private function touch(): void { $this->updatedAt = time(); } @@ -80,6 +94,14 @@ class DoctorAddress ], 'address' => $this->address, 'telephone' => $this->telephone, + 'city' => $this->city !== null ? [ + 'id' => (string) $this->city->getId(), + 'name' => $this->city->getName(), + ] : null, + 'province' => $this->province !== null ? [ + 'id' => (string) $this->province->getId(), + 'name' => $this->province->getName(), + ] : null, ]; } }