diff --git a/composer.json b/composer.json index 08e725c..035d243 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,11 @@ "pagerfanta/doctrine-orm-adapter": "*", "pagerfanta/pagerfanta": "^4.6", "symfony/framework-bundle": "7.1.*", - "symfony/form": "7.1.*" + "symfony/form": "7.1.*", + "symfony/security-core": "7.1.*", + "symfony/validator": "7.1.*", + "symfony/http-client": "7.1.*", + "symfony/security-bundle": "7.1.*" }, "require-dev": { "phpstan/phpstan": "^1.10", diff --git a/src/Clients/Dto/AgendamentoRemoto.php b/src/Clients/Dto/AgendamentoRemoto.php new file mode 100644 index 0000000..b931c27 --- /dev/null +++ b/src/Clients/Dto/AgendamentoRemoto.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Clients\Dto; + +use DateTimeImmutable; + +/** + * AgendamentoRemoto + * + * @author Rogerio Lino + */ +class AgendamentoRemoto +{ + public function __construct( + public readonly int|string|null $id = null, + public readonly ?string $nome = null, + public readonly ?string $situacao = null, + public readonly ?DateTimeImmutable $dataCancelamento = null, + public readonly ?DateTimeImmutable $dataConfirmacao = null, + public readonly ?string $documento = null, + public readonly ?string $data = null, + public readonly ?string $horaInicio = null, + public readonly ?string $email = null, + public readonly ?string $telefone = null + ) { + } +} diff --git a/src/Clients/Dto/GetAgendamentosRequest.php b/src/Clients/Dto/GetAgendamentosRequest.php new file mode 100644 index 0000000..e98b1d6 --- /dev/null +++ b/src/Clients/Dto/GetAgendamentosRequest.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Clients\Dto; + +use DateTimeImmutable; + +/** + * GetAgendamentosRequest + * + * @author Rogerio Lino + */ +class GetAgendamentosRequest +{ + public function __construct( + public readonly string|int|null $servicoId = null, + public readonly string|int|null $unidadeId = null, + public readonly ?DateTimeImmutable $date = null, + public readonly int $page = 1 + ) { + } +} diff --git a/src/Clients/Dto/ServicoRemoto.php b/src/Clients/Dto/ServicoRemoto.php new file mode 100644 index 0000000..a369a5a --- /dev/null +++ b/src/Clients/Dto/ServicoRemoto.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Clients\Dto; + +/** + * ServicoRemoto + * + * @author Rogerio Lino + */ +class ServicoRemoto +{ + public function __construct( + public readonly int|string|null $id = null, + public readonly ?string $nome = null + ) { + } +} diff --git a/src/Clients/Dto/UnidadeRemota.php b/src/Clients/Dto/UnidadeRemota.php new file mode 100644 index 0000000..0a7bc16 --- /dev/null +++ b/src/Clients/Dto/UnidadeRemota.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Clients\Dto; + +/** + * UnidadeRemota + * + * @author Rogerio Lino + */ +class UnidadeRemota +{ + public function __construct( + public readonly int|string|null $id = null, + public readonly ?string $nome = null + ) { + } +} diff --git a/src/Clients/ExternalApiClientInterface.php b/src/Clients/ExternalApiClientInterface.php new file mode 100644 index 0000000..ee49479 --- /dev/null +++ b/src/Clients/ExternalApiClientInterface.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Clients; + +use Novosga\SchedulingBundle\Clients\Dto\AgendamentoRemoto; +use Novosga\SchedulingBundle\Clients\Dto\ServicoRemoto; +use Novosga\SchedulingBundle\Clients\Dto\UnidadeRemota; +use Novosga\SchedulingBundle\Clients\Dto\GetAgendamentosRequest; + +/** + * ExternalApiClientInterface + * + * @author Rogerio Lino + */ +interface ExternalApiClientInterface +{ + /** @return UnidadeRemota[] */ + public function getUnidades(): array; + + /** @return ServicoRemoto[] */ + public function getServicos(): array; + + /** @return AgendamentoRemoto[] */ + public function getAgendamentos(GetAgendamentosRequest $request): array; + + public function updateAgendamento(int|string $agendamentoId, string $situacao): bool; +} diff --git a/src/Clients/ExternalApiClientTrait.php b/src/Clients/ExternalApiClientTrait.php new file mode 100644 index 0000000..30d2361 --- /dev/null +++ b/src/Clients/ExternalApiClientTrait.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Clients; + +use Symfony\Contracts\HttpClient\ResponseInterface; +use Throwable; + +/** + * ExternalApiClientTrait + * + * @author Rogerio Lino + */ +trait ExternalApiClientTrait +{ + /** + * @param array $query + * @param array $body + */ + private function request(string $method, string $url, array $query = [], array $body = []): ?ResponseInterface + { + try { + $response = $this->client->request($method, $url, [ + 'headers' => [ + 'Authorization' => "Bearer {$this->apiToken}", + ], + 'query' => $query, + 'body' => $body, + ]); + //$statusCode = $response->getStatusCode(); + + return $response; + } catch (Throwable $ex) { + $this->logger->error('Error trying to access remote API: ' . $url); + $this->logger->error($ex->getMessage()); + } + + return null; + } +} diff --git a/src/Clients/MangatiAgendaApiClient.php b/src/Clients/MangatiAgendaApiClient.php new file mode 100644 index 0000000..cc5e020 --- /dev/null +++ b/src/Clients/MangatiAgendaApiClient.php @@ -0,0 +1,132 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Clients; + +use DateTime; +use Novosga\SchedulingBundle\Clients\Dto\AgendamentoRemoto; +use Novosga\SchedulingBundle\Clients\Dto\GetAgendamentosRequest; +use Novosga\SchedulingBundle\Clients\Dto\ServicoRemoto; +use Novosga\SchedulingBundle\Clients\Dto\UnidadeRemota; +use Psr\Log\LoggerInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * MangatiAgendaApiClient + * Classe cliente da API do sistema Mangati Agenda + * {@link https://agenda.mangati.com/} + * + * @author Rogerio Lino + */ +class MangatiAgendaApiClient implements ExternalApiClientInterface +{ + use ExternalApiClientTrait; + + /** @var array */ + private $statusDict; + + public function __construct( + private readonly HttpClientInterface $client, + private readonly LoggerInterface $logger, + private readonly string $apiToken, + private string $apiUrl, + ) { + if (!str_ends_with($this->apiUrl, '/')) { + $this->apiUrl .= '/'; + } + $this->statusDict = [ + 'scheduled' => 'agendado', + 'cancelled' => 'cancelado', + 'completed' => 'confirmado', + 'no_show' => 'nao_compareceu', + 'deleted' => 'excluido', + ]; + } + + /** @return UnidadeRemota[] */ + public function getUnidades(): array + { + $unidades = []; + $response = $this->request('GET', "{$this->apiUrl}locations.json"); + + if ($response) { + $unidades = array_map(function ($row) { + return new UnidadeRemota( + id: $row['id'], + nome: $row['name'], + ); + }, $response->toArray()); + } + + return $unidades; + } + + /** @return ServicoRemoto[] */ + public function getServicos(): array + { + $servicos = []; + $response = $this->request('GET', "{$this->apiUrl}resources.json"); + + if ($response) { + $servicos = array_map(function ($row) { + return new ServicoRemoto( + id: $row['id'], + nome: $row['name'], + ); + }, $response->toArray()); + } + + return $servicos; + } + + /** @return AgendamentoRemoto[] */ + public function getAgendamentos(GetAgendamentosRequest $request): array + { + $date = $request->date; + if (!$date) { + $date = new DateTime(); + } + $response = $this->request('GET', "{$this->apiUrl}appointments.json", [ + 'date' => $date->format('Y-m-d'), + 'page' => $request->page, + 'resource' => $request->servicoId, + 'location' => $request->unidadeId, + ]); + + $agendamentos = array_map(function ($row) { + $status = $this->statusDict[$row['status']] ?? $row['status']; + + return new AgendamentoRemoto( + id: $row['id'], + nome: $row['name'], + situacao: $status, + dataCancelamento: null, + dataConfirmacao: null, + documento: $row['documentId'] ?? '', + data: $row['date'], + horaInicio: $row['time'], + email: $row['email'] ?? '', + telefone: $row['phone'] ?? '', + ); + }, $response->toArray()); + + return $agendamentos; + } + + public function updateAgendamento(int|string $agendamentoId, string $situacao): bool + { + // TODO + $this->logger->info('updateAgendamento not implemented'); + return false; + } +} diff --git a/src/Command/DailyCommand.php b/src/Command/DailyCommand.php new file mode 100644 index 0000000..f6c2cba --- /dev/null +++ b/src/Command/DailyCommand.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Command; + +use DateTime; +use Doctrine\ORM\EntityManagerInterface; +use Novosga\Entity\AgendamentoInterface; +use Novosga\Repository\AgendamentoRepositoryInterface; +use Novosga\SchedulingBundle\Service\ConfigService; +use Novosga\SchedulingBundle\Service\ExternalApiClientFactory; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Throwable; + +/** + * DailyCommand + * + * @author Rogerio Lino + */ +#[AsCommand(name: 'novosga:scheduling:daily')] +class DailyCommand extends Command +{ + use HasClientsMapTrait; + + public function __construct( + private readonly EntityManagerInterface $em, + private readonly ConfigService $configService, + private readonly ExternalApiClientFactory $clientFactory, + private readonly AgendamentoRepositoryInterface $agendamentoRepository + ) { + parent::__construct(); + } + + protected function configure() + { + $this + ->setDescription('Atualiza a situação dos agendamentos como nao_compareceu'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->title('Novo SGA Scheduling Daily'); + + $today = new DateTime(); + $today->setTime(0, 0, 0, 0); + $limit = 100; + $offset = 0; + + $query = $this + ->agendamentoRepository + ->createQueryBuilder('e') + ->where('e.situacao = :situacao') + ->andWhere('e.data < :today') + ->setParameter('situacao', AgendamentoInterface::SITUACAO_AGENDADO) + ->setParameter('today', $today) + ->setMaxResults($limit) + ->getQuery(); + + do { + $agendamentos = $query + ->setFirstResult($offset) + ->getResult(); + + /** @var AgendamentoInterface $agendamento */ + foreach ($agendamentos as $agendamento) { + $io->text(sprintf( + "Updating schedule ID %s, date %s", + $agendamento->getId(), + $agendamento->getData()->format('Y-m-d'), + )); + + $agendamento->setSituacao(AgendamentoInterface::SITUACAO_NAO_COMPARECEU); + $this->em->persist($agendamento); + $this->em->flush(); + + $unidadeConfig = $this->configService->getUnidadeConfig($agendamento->getUnidade()); + if (!$unidadeConfig) { + continue; + } + $client = $this->getClient($agendamento->getUnidade(), $unidadeConfig); + + try { + $client->updateAgendamento( + $agendamento->getOid(), + AgendamentoInterface::SITUACAO_NAO_COMPARECEU + ); + } catch (Throwable $ex) { + $io->error(sprintf( + "Failed to update remove schedule (OID: %s): %s", + $agendamento->getOid(), + $ex->getMessage() + )); + } + } + + $offset += count($agendamentos); + } while (!empty($agendamentos)); + + return Command::SUCCESS; + } +} diff --git a/src/Command/HasClientsMapTrait.php b/src/Command/HasClientsMapTrait.php new file mode 100644 index 0000000..c5ef9a2 --- /dev/null +++ b/src/Command/HasClientsMapTrait.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Command; + +use Novosga\Entity\UnidadeInterface; +use Novosga\SchedulingBundle\Clients\ExternalApiClientInterface; +use Novosga\SchedulingBundle\ValueObject\UnidadeConfig; + +/** + * HasClientsMapTrait + * + * @author Rogerio Lino + */ +trait HasClientsMapTrait +{ + /** @var array */ + private array $clientsMap = []; + + private function getClient(UnidadeInterface $unidade, UnidadeConfig $unidadeConfig): ExternalApiClientInterface + { + $client = $this->clientsMap[$unidade->getId()] ?? null; + + if (!$client) { + $client = $this->clientFactory->create($unidadeConfig); + $this->clientsMap[$unidade->getId()] = $client; + } + + return $client; + } +} diff --git a/src/Command/SyncCommand.php b/src/Command/SyncCommand.php new file mode 100644 index 0000000..7d5a4db --- /dev/null +++ b/src/Command/SyncCommand.php @@ -0,0 +1,225 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Command; + +use DateInterval; +use DateTime; +use DateTimeImmutable; +use Novosga\Entity\AgendamentoInterface; +use Novosga\Entity\UnidadeInterface; +use Novosga\Repository\AgendamentoRepositoryInterface; +use Novosga\Repository\UnidadeRepositoryInterface; +use Novosga\SchedulingBundle\Clients\Dto\GetAgendamentosRequest; +use Novosga\SchedulingBundle\Service\AppointmentService; +use Novosga\SchedulingBundle\Service\ConfigService; +use Novosga\SchedulingBundle\Service\ExternalApiClientFactory; +use Novosga\SchedulingBundle\ValueObject\ServicoConfig; +use Novosga\SchedulingBundle\ValueObject\UnidadeConfig; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Throwable; + +/** + * SyncCommand + * + * @author Rogerio Lino + */ +#[AsCommand( + name: 'novosga:scheduling:sync', + description: 'Sincroniza os agendamentos online' +)] +class SyncCommand extends Command +{ + use HasClientsMapTrait; + + private const MAX_DAYS = 7; + + public function __construct( + private readonly ConfigService $configService, + private readonly AppointmentService $appointmentService, + private readonly ExternalApiClientFactory $clientFactory, + private readonly UnidadeRepositoryInterface $unidadeRepository, + private readonly AgendamentoRepositoryInterface $agendamentoRepository, + ) { + parent::__construct(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->title('Novo SGA Scheduling Sync'); + + $this->syncLocalToRemove($io); + $this->syncRemoteToLocal($io); + + return Command::SUCCESS; + } + + private function syncLocalToRemove(SymfonyStyle $io): void + { + $today = new DateTime(); + $today->setTime(0, 0, 0, 0); + $limit = 100; + $offset = 0; + + $query = $this + ->agendamentoRepository + ->createQueryBuilder('e') + ->where('e.situacao = :situacao') + ->andWhere('e.data >= :today') + ->setParameter('situacao', AgendamentoInterface::SITUACAO_CONFIRMADO) + ->setParameter('today', $today) + ->setMaxResults($limit) + ->getQuery(); + + do { + $agendamentos = $query + ->setFirstResult($offset) + ->getResult(); + + /** @var AgendamentoInterface $agendamento */ + foreach ($agendamentos as $agendamento) { + $io->text(sprintf( + "Updating schedule ID %s, date %s", + $agendamento->getId(), + $agendamento->getData()->format('Y-m-d'), + )); + + $unidadeConfig = $this->configService->getUnidadeConfig($agendamento->getUnidade()); + if (!$unidadeConfig) { + continue; + } + $client = $this->getClient($agendamento->getUnidade(), $unidadeConfig); + + try { + $client->updateAgendamento( + $agendamento->getOid(), + AgendamentoInterface::SITUACAO_CONFIRMADO + ); + } catch (Throwable $ex) { + $io->error(sprintf( + "Failed to update remove schedule (OID: %s): %s", + $agendamento->getOid(), + $ex->getMessage(), + )); + } + } + + $offset += count($agendamentos); + } while (!empty($agendamentos)); + } + + private function syncRemoteToLocal(SymfonyStyle $io): void + { + $unidades = $this->unidadeRepository->findAll(); + + /** @var UnidadeInterface $unidade */ + foreach ($unidades as $unidade) { + $unidadeConfig = $this->configService->getUnidadeConfig($unidade); + if ($unidadeConfig) { + $io->section("Config found for unity {$unidade->getNome()}"); + $servicoConfigs = $this->configService->getServicoConfigs($unidade); + if (!count($servicoConfigs)) { + $io->info("No services config found"); + } + foreach ($servicoConfigs as $servicoConfig) { + try { + $io->text("Syncing schedule for service {$servicoConfig->servicoLocal->getNome()} ... "); + $this->doSyncRemoteToLocal($io, $unidade, $unidadeConfig, $servicoConfig); + } catch (Throwable $e) { + $io->error($e->getMessage()); + } + } + } + } + } + + private function doSyncRemoteToLocal( + SymfonyStyle $io, + UnidadeInterface $unidade, + UnidadeConfig $unidadeConfig, + ServicoConfig $servicoConfig, + ): void { + $total = 0; + $totalSaved = 0; + $startDate = new DateTimeImmutable(); + $days = 0; + $client = $this->getClient($unidade, $unidadeConfig); + + while ($days <= self::MAX_DAYS) { + $date = $startDate; + if ($days > 0) { + $date = $date->add(new DateInterval("P{$days}D")); + } + + $page = 1; + + do { + $agendamentos = $client->getAgendamentos(new GetAgendamentosRequest( + servicoId: $servicoConfig->servicoRemoto, + unidadeId: $unidadeConfig->unidadeRemota, + date: $date, + page: $page, + )); + $totalDay = count($agendamentos); + $io->text("Records found for date {$date->format('Y-m-d')} (page={$page}): {$totalDay}"); + + $total += $totalDay; + $page++; + + foreach ($agendamentos as $remoto) { + $isAgendado = $remoto->situacao === 'agendado'; + $isCancelado = !!$remoto->dataCancelamento; + $isConfirmado = !!$remoto->dataConfirmacao; + $oid = $remoto->id; + $agendamento = $this->agendamentoRepository->findOneBy([ + 'oid' => $oid, + ]); + if ($isCancelado && $agendamento) { + $io->text("Cancelled record found. Removing it from local db."); + // remove agendamento que foi cancelado online + $this->appointmentService->remove($agendamento); + } + if ($isConfirmado && $agendamento) { + $io->text("Confirmed record found. Updating it on local db."); + + // atualiza agendamento confirmado + $this->appointmentService->markAsDone($agendamento, $remoto); + } + if (!$isAgendado) { + $io->text("Remote appoitment is already done. Skipping."); + // pula agendamento confirmado/cancelado + continue; + } + if ($agendamento) { + $io->text("Record already synced found. Updating customer info."); + // agendamento já sincronizado, atualiza cliente e pula + $this->appointmentService->updateCliente($agendamento->getCliente(), $remoto); + continue; + } + + $io->text("Persisting new record. Remote ID: {$oid}"); + $this->appointmentService->save($unidade, $servicoConfig->servicoLocal, $remoto); + $totalSaved++; + } + } while (count($agendamentos)); + $days++; + } + + $io->text("Sync done. Total items retrieved from API: {$total}. Total items saved: {$totalSaved}"); + } +} diff --git a/src/Controller/ConfigController.php b/src/Controller/ConfigController.php new file mode 100644 index 0000000..99e423e --- /dev/null +++ b/src/Controller/ConfigController.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Controller; + +use Exception; +use Novosga\Entity\ServicoInterface; +use Novosga\Entity\UsuarioInterface; +use Novosga\SchedulingBundle\Form\ServicoConfigType; +use Novosga\SchedulingBundle\Form\UnidadeConfigType; +use Novosga\SchedulingBundle\NovosgaSchedulingBundle; +use Novosga\SchedulingBundle\Service\ConfigService; +use Novosga\SchedulingBundle\Service\ExternalApiClientFactory; +use Novosga\SchedulingBundle\ValueObject\ServicoConfig; +use Novosga\SchedulingBundle\ValueObject\UnidadeConfig; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Contracts\Translation\TranslatorInterface; +use Throwable; + +/** + * Scheduling Config controller. + * + * @author Rogerio Lino + */ +#[Route("/config", name: "novosga_scheduling_config_")] +class ConfigController extends AbstractController +{ + #[Route("/", name: "index", methods: ["GET", "POST"])] + public function index( + Request $request, + ConfigService $service, + TranslatorInterface $translator, + ExternalApiClientFactory $clientFactory, + ): Response { + /** @var UsuarioInterface */ + $usuario = $this->getUser(); + $unidade = $usuario->getLotacao()->getUnidade(); + $unidadeConfig = $service->getUnidadeConfig($unidade); + $servicosRemotos = []; + + if (!$unidadeConfig) { + $unidadeConfig = new UnidadeConfig(); + } else { + try { + $servicosRemotos = $clientFactory->create($unidadeConfig)->getServicos(); + } catch (Throwable $ex) { + } + } + + $form = $this + ->createForm(UnidadeConfigType::class, $unidadeConfig) + ->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $service->setUnidadeConfig($unidade, $unidadeConfig); + + $this->addFlash('success', $translator->trans( + 'label.add_config_success', + [], + NovosgaSchedulingBundle::getDomain(), + )); + + return $this->redirectToRoute('novosga_scheduling_config_index'); + } + + $servicoConfigs = $service->getServicoConfigs($unidade); + + return $this->render('@NovosgaScheduling/config/index.html.twig', [ + 'unidade' => $unidade, + 'servicoConfigs' => $servicoConfigs, + 'servicosRemotos' => $servicosRemotos, + 'form' => $form->createView(), + ]); + } + + #[Route("/new", name: "new", methods: ["GET", "POST"])] + public function add( + Request $request, + TranslatorInterface $translator, + ConfigService $configService, + ): Response { + return $this->form($request, $translator, $configService, new ServicoConfig()); + } + + #[Route("/{id}", name: "edit", methods: ["GET", "POST"])] + public function edit( + Request $request, + TranslatorInterface $translator, + ConfigService $configService, + ServicoInterface $servico, + ): Response { + /** @var UsuarioInterface */ + $usuario = $this->getUser(); + $unidade = $usuario->getLotacao()->getUnidade(); + $config = $configService->getServicoConfig($unidade, $servico); + + if (!$config) { + return $this->redirectToRoute('novosga_scheduling_config_index'); + } + + return $this->form($request, $translator, $configService, $config); + } + + #[Route("/{id}", name: "delete", methods: ["DELETE"])] + public function delete(ConfigService $configService, ServicoInterface $servico): Response + { + /** @var UsuarioInterface */ + $usuario = $this->getUser(); + $unidade = $usuario->getLotacao()->getUnidade(); + $configService->removeServicoConfig($unidade, $servico); + + return $this->redirectToRoute('novosga_scheduling_config_index'); + } + + private function form( + Request $request, + TranslatorInterface $translator, + ConfigService $configService, + ServicoConfig $config + ): Response { + /** @var UsuarioInterface */ + $usuario = $this->getUser(); + $unidade = $usuario->getLotacao()->getUnidade(); + + $form = $this + ->createForm(ServicoConfigType::class, $config, []) + ->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + try { + $configService->setServicoConfig($unidade, $config); + + $this->addFlash('success', $translator->trans( + 'label.add_config_success', + [], + NovosgaSchedulingBundle::getDomain(), + )); + + return $this->redirectToRoute('novosga_scheduling_config_edit', [ + 'id' => $config->servicoLocal->getId(), + ]); + } catch (Exception $e) { + $this->addFlash('error', $e->getMessage()); + } + } + + return $this->render('@NovosgaScheduling/config/form.html.twig', [ + 'config' => $config, + 'form' => $form, + ]); + } +} diff --git a/src/Form/AgendamentoType.php b/src/Form/AgendamentoType.php index 9f1e850..9dc3070 100644 --- a/src/Form/AgendamentoType.php +++ b/src/Form/AgendamentoType.php @@ -26,6 +26,11 @@ use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Valid; +/** + * AgendamentoType + * + * @author Rogerio Lino + */ class AgendamentoType extends AbstractType { public function __construct( diff --git a/src/Form/ServicoConfigType.php b/src/Form/ServicoConfigType.php new file mode 100644 index 0000000..e280d38 --- /dev/null +++ b/src/Form/ServicoConfigType.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Form; + +use Novosga\Entity\ServicoUnidadeInterface; +use Novosga\Entity\UsuarioInterface; +use Novosga\SchedulingBundle\Clients\Dto\ServicoRemoto; +use Novosga\SchedulingBundle\ValueObject\ServicoConfig; +use Novosga\Repository\ServicoRepositoryInterface; +use Novosga\SchedulingBundle\Service\ConfigService; +use Novosga\SchedulingBundle\Service\ExternalApiClientFactory; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\NotNull; +use Throwable; + +/** + * ServicoConfigType + * + * @author Rogerio Lino + */ +class ServicoConfigType extends AbstractType +{ + public function __construct( + private readonly Security $security, + private readonly ConfigService $service, + private readonly ServicoRepositoryInterface $servicoRepository, + private readonly ExternalApiClientFactory $clientFactory + ) { + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + /** @var UsuarioInterface $usuario */ + $usuario = $this->security->getUser(); + $unidade = $usuario->getLotacao()->getUnidade(); + $unidadeConfig = $this->service->getUnidadeConfig($unidade); + + try { + $client = $this->clientFactory->create($unidadeConfig); + $servicos = $client->getServicos(); + } catch (Throwable $ex) { + $servicos = []; + } + + $builder + ->add('servicoLocal', ChoiceType::class, [ + 'constraints' => [ + new NotNull(), + ], + 'placeholder' => '', + 'choices' => $this->servicoRepository + ->createQueryBuilder('e') + ->join(ServicoUnidadeInterface::class, 'su', 'WITH', 'su.servico = e') + ->where('e.ativo = TRUE') + ->andWhere('su.ativo = TRUE') + ->andWhere('su.unidade = :unidade') + ->orderBy('e.nome', 'ASC') + ->setParameter('unidade', $unidade) + ->getQuery() + ->getResult(), + 'label' => 'label.local_service', + ]) + ->add('servicoRemoto', ChoiceType::class, [ + 'placeholder' => '', + 'choices' => $servicos, + 'choice_value' => function ($servico) { + if ($servico instanceof ServicoRemoto) { + return $servico->id; + } + return $servico; + }, + 'choice_label' => function ($servico) { + if ($servico instanceof ServicoRemoto) { + return $servico->nome; + } + return $servico; + }, + 'constraints' => [ + new NotNull(), + ], + 'label' => 'label.remote_service', + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => ServicoConfig::class, + 'translation_domain' => 'NovosgaSchedulingBundle', + ]); + } +} diff --git a/src/Form/UnidadeConfigType.php b/src/Form/UnidadeConfigType.php new file mode 100644 index 0000000..a0f4cda --- /dev/null +++ b/src/Form/UnidadeConfigType.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Form; + +use Novosga\SchedulingBundle\Clients\Dto\UnidadeRemota; +use Novosga\SchedulingBundle\Service\ExternalApiClientFactory; +use Novosga\SchedulingBundle\ValueObject\UnidadeConfig; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\UrlType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\NotNull; +use Throwable; + +/** + * UnidadeConfigType + * + * @author Rogerio Lino + */ +class UnidadeConfigType extends AbstractType +{ + public function __construct( + private readonly ExternalApiClientFactory $clientFactory, + ) { + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('apiUrl', UrlType::class, [ + 'constraints' => [ + new NotNull(), + ], + 'label' => 'label.url_api', + ]) + ->add('accessToken', TextType::class, [ + 'constraints' => [ + new NotNull(), + ], + 'label' => 'label.api_access_token', + ]) + ->add('provider', ChoiceType::class, [ + 'placeholder' => '', + 'choices' => [ + 'Agendamento Online' => 'agendamento.online', + 'Mangati Agenda' => 'mangati.agenda', + ], + 'constraints' => [ + new NotNull(), + ], + 'label' => 'label.api_provider', + ]); + + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { + $form = $event->getForm(); + /** @var UnidadeConfig */ + $data = $event->getData(); + $provider = $data->provider; + $apiUrl = $data->apiUrl; + $accessToken = $data->accessToken; + + $this->addUnidadeRemota($form, $provider, $apiUrl, $accessToken); + }); + + $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + $form = $event->getForm(); + $data = $event->getData(); + + $provider = $data['provider'] ?? ''; + $apiUrl = $data['apiUrl'] ?? ''; + $accessToken = $data['accessToken'] ?? ''; + + $this->addUnidadeRemota($form, $provider, $apiUrl, $accessToken); + }); + } + + /** + * @param OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => UnidadeConfig::class, + 'translation_domain' => 'NovosgaSchedulingBundle', + ]); + } + + private function addUnidadeRemota( + FormInterface $form, + ?string $provider, + ?string $apiUrl, + ?string $accessToken, + ): void { + $unidades = []; + try { + if ($provider && $apiUrl && $accessToken) { + $client = $this->clientFactory->createFromArgs( + $provider, + $apiUrl, + $accessToken, + ); + $unidades = $client->getUnidades(); + } + } catch (Throwable $ex) { + } + + $form + ->add('unidadeRemota', ChoiceType::class, [ + 'placeholder' => '', + 'choices' => $unidades, + 'choice_value' => function ($unidade) { + if ($unidade instanceof UnidadeRemota) { + return $unidade->id; + } + return $unidade; + }, + 'choice_label' => function ($unidade) { + if ($unidade instanceof UnidadeRemota) { + return $unidade->nome; + } + return $unidade; + }, + 'constraints' => [ + new NotNull(), + ], + 'label' => 'label.remote_unity', + ]); + } +} diff --git a/src/Mapper/AgendamentoMapper.php b/src/Mapper/AgendamentoMapper.php new file mode 100644 index 0000000..77796e8 --- /dev/null +++ b/src/Mapper/AgendamentoMapper.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Mapper; + +use DateTime; +use Novosga\Entity\AgendamentoInterface; +use Novosga\Entity\ServicoInterface; +use Novosga\Entity\UnidadeInterface; +use Novosga\SchedulingBundle\Clients\Dto\AgendamentoRemoto; +use Novosga\Service\AgendamentoServiceInterface; + +/** + * AgendamentoMapper + * + * @author Rogerio Lino + */ +class AgendamentoMapper +{ + public function __construct( + private readonly ClienteMapper $clienteMapper, + private readonly AgendamentoServiceInterface $agendamentoService, + ) { + } + + public function toAgendamento( + UnidadeInterface $unidade, + ServicoInterface $servico, + AgendamentoRemoto $agendamento + ): AgendamentoInterface { + // 'data' => $this->data->format('Y-m-d'), + $data = DateTime::createFromFormat('Y-m-d', $agendamento->data); + $hora = DateTime::createFromFormat('H:i', $agendamento->horaInicio); + + $oid = $agendamento->id; + $cliente = $this->clienteMapper->toCliente($agendamento); + + $agendamento = $this->agendamentoService->build(); + $agendamento + ->setOid($oid) + ->setServico($servico) + ->setUnidade($unidade) + ->setData($data) + ->setHora($hora) + ->setCliente($cliente); + + return $agendamento; + } +} diff --git a/src/Mapper/ClienteMapper.php b/src/Mapper/ClienteMapper.php new file mode 100644 index 0000000..eee28fb --- /dev/null +++ b/src/Mapper/ClienteMapper.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Mapper; + +use Novosga\Entity\ClienteInterface; +use Novosga\Repository\ClienteRepositoryInterface; +use Novosga\SchedulingBundle\Clients\Dto\AgendamentoRemoto; +use Novosga\Service\ClienteServiceInterface; + +/** + * ClienteMapper + * + * @author Rogerio Lino + */ +class ClienteMapper +{ + public function __construct( + private readonly ClienteServiceInterface $clienteService, + private readonly ClienteRepositoryInterface $clienteRepository, + ) { + } + + public function toCliente(AgendamentoRemoto $agendamento): ClienteInterface + { + $documento = $agendamento->documento ?? ''; + $clientes = $this->clienteRepository->findByDocumento($documento); + if (count($clientes)) { + return $this->updateCliente($clientes[0], $agendamento); + } + + return $this->clienteService + ->build() + ->setNome($agendamento->nome) + ->setDocumento($agendamento->documento) + ->setEmail($agendamento->email) + ->setTelefone($agendamento->telefone); + } + + public function updateCliente(ClienteInterface $cliente, AgendamentoRemoto $agendamento): ClienteInterface + { + return $cliente + ->setNome($agendamento->nome ?? $cliente->getNome()) + ->setDocumento($agendamento->documento ?? $cliente->getDocumento()) + ->setEmail($agendamento->email ?? $cliente->getEmail()) + ->setTelefone($agendamento->telefone ?? $cliente->getTelefone()); + } +} diff --git a/src/Mapper/ServicoConfigMapper.php b/src/Mapper/ServicoConfigMapper.php new file mode 100644 index 0000000..565dc81 --- /dev/null +++ b/src/Mapper/ServicoConfigMapper.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Mapper; + +use Novosga\Repository\ServicoRepositoryInterface; +use Novosga\SchedulingBundle\ValueObject\ServicoConfig; + +/** + * ServicoConfigMapper + * + * @author Rogerio Lino + */ +class ServicoConfigMapper +{ + public function __construct( + private readonly ServicoRepositoryInterface $servicoRepository, + ) { + } + + /** @param array $value */ + public function toServicoConfig(array $value): ServicoConfig + { + $servicoLocal = $this->servicoRepository->find($value['servicoLocal']); + + return new ServicoConfig( + servicoLocal: $servicoLocal, + servicoRemoto: $value['servicoRemoto'], + ); + } +} diff --git a/src/Mapper/UnidadeConfigMapper.php b/src/Mapper/UnidadeConfigMapper.php new file mode 100644 index 0000000..c2d0017 --- /dev/null +++ b/src/Mapper/UnidadeConfigMapper.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Mapper; + +use Novosga\SchedulingBundle\ValueObject\UnidadeConfig; + +/** + * UnidadeConfigMapper + * + * @author Rogerio Lino + */ +class UnidadeConfigMapper +{ + /** @param array $value */ + public function toUnidadeConfig(array $value): UnidadeConfig + { + return new UnidadeConfig( + unidadeRemota: $value['unidadeRemota'], + apiUrl: $value['apiUrl'], + provider: $value['provider'], + accessToken: $value['accessToken'], + ); + } +} diff --git a/src/NovosgaSchedulingBundle.php b/src/NovosgaSchedulingBundle.php index 5a4a52a..77fca44 100644 --- a/src/NovosgaSchedulingBundle.php +++ b/src/NovosgaSchedulingBundle.php @@ -15,6 +15,11 @@ use Novosga\Module\BaseModule; +/** + * NovosgaSchedulingBundle + * + * @author Rogerio Lino + */ class NovosgaSchedulingBundle extends BaseModule { public function getIconName(): string diff --git a/src/Resources/translations/NovosgaSchedulingBundle.pt_BR.xlf b/src/Resources/translations/NovosgaSchedulingBundle.pt_BR.xlf index 606929d..a925875 100644 --- a/src/Resources/translations/NovosgaSchedulingBundle.pt_BR.xlf +++ b/src/Resources/translations/NovosgaSchedulingBundle.pt_BR.xlf @@ -14,6 +14,14 @@ subtitle Gerencie os agendamentos de atendimento + + config_title + Configurações de Agendamento + + + config_subtitle + Integração com sistemas externos + label.name Nome @@ -114,10 +122,34 @@ label.notes Observação + + label.local_service + Serviço local + + + label.remote_service + Serviço remoto + + + button.new_config + Nova agenda + + + form.config.password + Senha + label.add_success Agendamento adicionado com successo + + label.add_config_success + Configuração atualizada com successo + + + label.remote_unity + Unidade remota + label.url_api Endereço da API @@ -126,6 +158,18 @@ label.service Serviço + + label.api_provider + Provedor + + + label.api_access_token + Access Token + + + label.service_configs + Agendas + label.status_agendado Agendado diff --git a/src/Resources/views/config/form.html.twig b/src/Resources/views/config/form.html.twig new file mode 100644 index 0000000..c575227 --- /dev/null +++ b/src/Resources/views/config/form.html.twig @@ -0,0 +1,48 @@ +{% extends "@NovosgaScheduling/base.html.twig" %} + +{% trans_default_domain 'NovosgaSchedulingBundle' %} + +{% block body %} +
+
+

+ {{ 'config_title'|trans }} +

+

{{ 'config_subtitle'|trans }}

+
+
+ + {{ form_start(form) }} + + {% include 'flashMessages.html.twig' %} + + {{ form_row(form.servicoLocal) }} + {{ form_row(form.servicoRemoto, { attr: { autocomplete: 'no' } }) }} + +
+ +
+ + + + {{ 'button.back'|trans }} + + + {% if config.servicoLocal %} + + {% endif %} +
+ + {{ form_end(form) }} +{% endblock %} + +{% block javascripts %} + {{ parent() }} + +{% endblock %} diff --git a/src/Resources/views/config/index.html.twig b/src/Resources/views/config/index.html.twig new file mode 100644 index 0000000..b17a05c --- /dev/null +++ b/src/Resources/views/config/index.html.twig @@ -0,0 +1,106 @@ +{% extends "@NovosgaScheduling/base.html.twig" %} + +{% trans_default_domain 'NovosgaSchedulingBundle' %} + +{% block body %} +
+
+

+ {{ 'config_title'|trans }} +

+

{{ 'config_subtitle'|trans }}

+
+
+ +
+

Configuração de agendamento para a unidade {{ unidade.nome }}

+
+ +
+ {{ form_start(form) }} + + {% include 'flashMessages.html.twig' %} + +
+
+ {{ form_row(form.provider) }} +
+
+ {{ form_row(form.apiUrl) }} +
+
+ {{ form_row(form.accessToken) }} +
+
+ {{ form_row(form.unidadeRemota) }} +
+
+ +
+ +
+ {{ form_end(form) }} +
+ +
+ +

{{ 'label.service_configs'|trans }}

+ + + + + + + + + + {% for config in servicoConfigs %} + + + + + + {% endfor %} + + + + + + + + +
{{ 'label.local_service'|trans }}{{ 'label.remote_service'|trans }}
{{ config.servicoLocal.nome }} + {% set nomeServicoRemoto = config.servicoRemoto %} + {% for servicoRemoto in servicosRemotos %} + {% if servicoRemoto.id == config.servicoRemoto %} + {% set nomeServicoRemoto = servicoRemoto.nome %} + {% endif %} + {% endfor %} + {{- nomeServicoRemoto -}} + + + + +
+ + + {{ 'button.new_config'|trans }} + +
+ + + +{% endblock %} + +{% block javascripts %} + {{ parent() }} + +{% endblock %} diff --git a/src/Service/AppointmentService.php b/src/Service/AppointmentService.php new file mode 100644 index 0000000..df8c4f0 --- /dev/null +++ b/src/Service/AppointmentService.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Service; + +use Doctrine\ORM\EntityManagerInterface; +use Novosga\Entity\AgendamentoInterface; +use Novosga\Entity\ClienteInterface; +use Novosga\Entity\ServicoInterface; +use Novosga\Entity\UnidadeInterface; +use Novosga\SchedulingBundle\Clients\Dto\AgendamentoRemoto; +use Novosga\SchedulingBundle\Mapper\AgendamentoMapper; +use Novosga\SchedulingBundle\Mapper\ClienteMapper; + +/** + * AppointmentService + * + * @author Rogerio Lino + */ +class AppointmentService +{ + public function __construct( + private readonly EntityManagerInterface $em, + private readonly AgendamentoMapper $agendamentoMapper, + private readonly ClienteMapper $clienteMapper, + ) { + } + + public function save(UnidadeInterface $unidade, ServicoInterface $servico, AgendamentoRemoto $remoto): void + { + $agendamento = $this->agendamentoMapper->toAgendamento($unidade, $servico, $remoto); + $this->em->persist($agendamento); + $this->em->flush(); + } + + public function remove(AgendamentoInterface $agendamento): void + { + $this->em->remove($agendamento); + $this->em->flush(); + } + + public function markAsDone(AgendamentoInterface $agendamento, AgendamentoRemoto $remoto): void + { + $agendamento->setDataConfirmacao($remoto->dataConfirmacao); + $this->em->persist($agendamento); + $this->em->flush(); + } + + public function updateCliente(ClienteInterface $cliente, AgendamentoRemoto $remoto): void + { + $cliente = $this->clienteMapper->updateCliente($cliente, $remoto); + $this->em->persist($cliente); + $this->em->flush(); + } +} diff --git a/src/Service/ConfigService.php b/src/Service/ConfigService.php new file mode 100644 index 0000000..8427774 --- /dev/null +++ b/src/Service/ConfigService.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Service; + +use Novosga\Entity\ServicoInterface; +use Novosga\Entity\UnidadeInterface; +use Novosga\Entity\EntityMetadataInterface as Metadata; +use Novosga\Repository\EntityMetadataRepositoryInterface as MetadataRepository; +use Novosga\SchedulingBundle\Mapper\ServicoConfigMapper; +use Novosga\SchedulingBundle\Mapper\UnidadeConfigMapper; +use Novosga\SchedulingBundle\ValueObject\ServicoConfig; +use Novosga\SchedulingBundle\ValueObject\UnidadeConfig; + +/** + * ConfigService + * + * @author Rogerio Lino + */ +class ConfigService +{ + private const METADATA_NAMESPACE = 'novosga.scheduling'; + private const METADATA_CONFIG_NAMESPACE = self::METADATA_NAMESPACE . '.config'; + private const METADATA_CONFIG_NAME_PREFIX = 'config_'; + private const METADATA_UNIDADE_NAME = 'unidade'; + + /** + * @param MetadataRepository,UnidadeInterface> $unidateMetadataRepository + */ + public function __construct( + private readonly MetadataRepository $unidateMetadataRepository, + private readonly UnidadeConfigMapper $unidadeConfigMapper, + private readonly ServicoConfigMapper $servicoConfigMapper, + ) { + } + + public function getUnidadeConfig(UnidadeInterface $unidade): ?UnidadeConfig + { + $metadata = $this + ->unidateMetadataRepository + ->get($unidade, self::METADATA_NAMESPACE, self::METADATA_UNIDADE_NAME); + + if (!$metadata) { + return null; + } + + $value = $metadata->getValue(); + return $this->unidadeConfigMapper->toUnidadeConfig($value); + } + + public function setUnidadeConfig(UnidadeInterface $unidade, UnidadeConfig $config): void + { + $this->unidateMetadataRepository->set( + $unidade, + self::METADATA_NAMESPACE, + self::METADATA_UNIDADE_NAME, + $config + ); + } + + /** @return ServicoConfig[] */ + public function getServicoConfigs(UnidadeInterface $unidade): array + { + $configs = []; + $configMetadata = $this + ->unidateMetadataRepository + ->findByNamespace($unidade, self::METADATA_CONFIG_NAMESPACE); + + foreach ($configMetadata as $meta) { + $configs[] = $this->servicoConfigMapper->toServicoConfig($meta->getValue()); + } + + return $configs; + } + + public function getServicoConfig(UnidadeInterface $unidade, ServicoInterface $servico): ?ServicoConfig + { + $name = $this->buildServicoConfigMetadataName($unidade, $servico); + $metadata = $this->unidateMetadataRepository->get($unidade, self::METADATA_CONFIG_NAMESPACE, $name); + + if (!$metadata) { + return null; + } + + $value = $metadata->getValue(); + return $this->servicoConfigMapper->toServicoConfig($value); + } + + public function removeServicoConfig(UnidadeInterface $unidade, ServicoInterface $servico): void + { + $name = $this->buildServicoConfigMetadataName($unidade, $servico); + $this->unidateMetadataRepository->remove($unidade, self::METADATA_CONFIG_NAMESPACE, $name); + } + + public function setServicoConfig(UnidadeInterface $unidade, ServicoConfig $config): void + { + $name = $this->buildServicoConfigMetadataName($unidade, $config->servicoLocal); + $this->unidateMetadataRepository->set($unidade, self::METADATA_CONFIG_NAMESPACE, $name, $config); + } + + public function buildServicoConfigMetadataName( + UnidadeInterface $unidade, + ServicoInterface $servico + ): string { + return self::METADATA_CONFIG_NAME_PREFIX . "{$unidade->getId()}_{$servico->getId()}"; + } +} diff --git a/src/Service/ExternalApiClientFactory.php b/src/Service/ExternalApiClientFactory.php new file mode 100644 index 0000000..48f357a --- /dev/null +++ b/src/Service/ExternalApiClientFactory.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\Service; + +use Exception; +use Novosga\SchedulingBundle\ValueObject\UnidadeConfig; +use Novosga\SchedulingBundle\Clients\ExternalApiClientInterface; +use Novosga\SchedulingBundle\Clients\MangatiAgendaApiClient; +use Psr\Log\LoggerInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +class ExternalApiClientFactory +{ + public function __construct( + private readonly HttpClientInterface $client, + private readonly LoggerInterface $logger, + ) { + } + + public function create(UnidadeConfig $config): ExternalApiClientInterface + { + return $this->createFromArgs( + $config->provider, + $config->apiUrl, + $config->accessToken, + ); + } + + public function createFromArgs(string $provider, string $apiUrl, string $accessToken): ExternalApiClientInterface + { + return match ($provider) { + "mangati.agenda" => new MangatiAgendaApiClient( + $this->client, + $this->logger, + $apiUrl, + $accessToken, + ), + default => throw new Exception('Invalid scheduling provider: ' . $provider), + }; + } +} diff --git a/src/ValueObject/ServicoConfig.php b/src/ValueObject/ServicoConfig.php new file mode 100644 index 0000000..c9e8133 --- /dev/null +++ b/src/ValueObject/ServicoConfig.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\ValueObject; + +use JsonSerializable; +use Novosga\Entity\ServicoInterface; + +/** + * ServicoConfig + * + * @author Rogerio Lino + */ +class ServicoConfig implements JsonSerializable +{ + public function __construct( + public ?ServicoInterface $servicoLocal = null, + public int|string|null $servicoRemoto = null + ) { + } + + /** @return array */ + public function jsonSerialize(): array + { + return [ + 'servicoLocal' => $this->servicoLocal?->getId(), + 'servicoRemoto' => $this->servicoRemoto, + ]; + } +} diff --git a/src/ValueObject/UnidadeConfig.php b/src/ValueObject/UnidadeConfig.php new file mode 100644 index 0000000..515db64 --- /dev/null +++ b/src/ValueObject/UnidadeConfig.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Novosga\SchedulingBundle\ValueObject; + +use JsonSerializable; + +/** + * UnidadeConfig + * + * @author Rogerio Lino + */ +class UnidadeConfig implements JsonSerializable +{ + public function __construct( + public int|string|null $unidadeRemota = null, + public ?string $apiUrl = null, + public ?string $accessToken = null, + public ?string $provider = null + ) { + } + + /** @return array */ + public function jsonSerialize(): array + { + return [ + 'unidadeRemota' => $this->unidadeRemota, + 'apiUrl' => $this->apiUrl, + 'accessToken' => $this->accessToken, + 'provider' => $this->provider, + ]; + } +}