- Introduced clinic_id to weekly_schedules, date_overrides, and holidays to differentiate between personal and clinic schedules. - Updated unique constraints and indexes to accommodate the new clinic context. feat(command): create AssignScheduleClinicCommand to move schedules - Added a command to move a doctor's personal weekly schedule into a clinic context. - Implemented checks to ensure sessions align with the target clinic. feat(context): implement EntityContext and EntityContextResolver - Created EntityContext to represent the effective working environment of a request (doctor or clinic). - Developed EntityContextResolver to determine the execution context based on user roles and active contexts. test: add ServiceModeContextTest for appointment scheduling - Implemented tests to ensure service booking respects clinic and personal contexts. - Verified that financial data is omitted in clinic contexts in InvitedDoctorDashboardScopeTest.
113 lines
4.4 KiB
PHP
113 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Appointment\Command;
|
|
|
|
use App\Appointment\Repository\WeeklyScheduleRepository;
|
|
use App\Clinic\Repository\ClinicRepository;
|
|
use App\Doctor\Repository\DoctorAddressRepository;
|
|
use App\Doctor\Repository\DoctorRepository;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Component\Console\Attribute\AsCommand;
|
|
use Symfony\Component\Console\Command\Command;
|
|
use Symfony\Component\Console\Input\InputArgument;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Input\InputOption;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
|
|
/**
|
|
* Moves an existing weekly schedule from the personal context into a clinic.
|
|
*
|
|
* The clinic_id migration marks every pre-existing schedule as personal, because
|
|
* nothing in the data says otherwise. A schedule whose sessions actually point at
|
|
* a clinic address needs to be moved by hand — this command does that, and refuses
|
|
* when the sessions do not agree with the target clinic.
|
|
*/
|
|
#[AsCommand(name: 'app:schedule:assign-clinic', description: 'Move a doctor\'s personal weekly schedule into a clinic context')]
|
|
class AssignScheduleClinicCommand extends Command
|
|
{
|
|
public function __construct(
|
|
private readonly WeeklyScheduleRepository $scheduleRepo,
|
|
private readonly DoctorRepository $doctorRepo,
|
|
private readonly ClinicRepository $clinicRepo,
|
|
private readonly DoctorAddressRepository $addressRepo,
|
|
private readonly EntityManagerInterface $em,
|
|
) {
|
|
parent::__construct();
|
|
}
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this
|
|
->addArgument('doctor-uuid', InputArgument::REQUIRED, 'Doctor uuid')
|
|
->addArgument('clinic-uuid', InputArgument::REQUIRED, 'Target clinic uuid')
|
|
->addOption('force', null, InputOption::VALUE_NONE, 'Move even when some sessions use an address outside the clinic');
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
{
|
|
$io = new SymfonyStyle($input, $output);
|
|
|
|
$doctor = $this->doctorRepo->findByUuid((string) $input->getArgument('doctor-uuid'));
|
|
$clinic = $this->clinicRepo->findByUuid((string) $input->getArgument('clinic-uuid'));
|
|
|
|
if ($doctor === null || $clinic === null) {
|
|
$io->error('Doctor or clinic not found.');
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
if (!$clinic->hasDoctor($doctor)) {
|
|
$io->error('This doctor is not a member of that clinic.');
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
$schedule = $this->scheduleRepo->findByDoctorAndClinic($doctor, null);
|
|
if ($schedule === null) {
|
|
$io->warning('This doctor has no personal schedule to move.');
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
if ($this->scheduleRepo->findByDoctorAndClinic($doctor, $clinic) !== null) {
|
|
$io->error('A schedule already exists for this doctor in that clinic; merge it manually.');
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
$foreign = $this->sessionsOutsideClinic($schedule->getDaySchedule(), $doctor, $clinic->getId());
|
|
if ($foreign !== [] && !$input->getOption('force')) {
|
|
$io->error(sprintf(
|
|
'Sessions use address ids outside the clinic: %s. Re-run with --force to move anyway.',
|
|
implode(', ', $foreign)
|
|
));
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
$schedule->setClinic($clinic);
|
|
$this->em->flush();
|
|
|
|
$io->success(sprintf('Schedule %s moved to clinic "%s".', $schedule->getUuid(), $clinic->getName()));
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
/** @return int[] address ids referenced by the schedule that the clinic does not own */
|
|
private function sessionsOutsideClinic(array $daySchedule, \App\Doctor\Entity\Doctor $doctor, int $clinicId): array
|
|
{
|
|
$owned = [];
|
|
foreach ($this->addressRepo->findForContext($doctor, $clinicId) as $address) {
|
|
$owned[(int) $address->getId()] = true;
|
|
}
|
|
|
|
$foreign = [];
|
|
foreach ($daySchedule as $day) {
|
|
foreach (($day['sessions'] ?? []) as $session) {
|
|
$id = (int) ($session['location_id'] ?? 0);
|
|
if ($id > 0 && !isset($owned[$id])) {
|
|
$foreign[$id] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return array_keys($foreign);
|
|
}
|
|
}
|