diff --git a/api/README.md b/api/README.md index 861524933d63c913f2d0bc10302d96e2e0741dfb..4c45ccaea2d127e8d814eb2925935faee3cf4a72 100644 --- a/api/README.md +++ b/api/README.md @@ -5,11 +5,32 @@ The API will be here. Refer to the [Getting Started Guide](https://api-platform.com/docs/distribution) for more information. ## Migrations and database +To manage migrations and update the database, follow these steps: -```bash -docker compose exec php bin/console doctrine:migrations:diff -docker compose exec php bin/console doctrine:migrations:migrate -``` +1. Generate a new migration: + + ```bash + docker compose exec php bin/console doctrine:migrations:diff + ``` + +2. Modify the generated migration file: + Open the generated migration file in `api/migrations` and locate the `id` column for the `Sale` entity. Replace the line: + + ```php + $this->addSql('CREATE TABLE sale (id INT NOT NULL, ...)'); + ``` + + with: + + ```php + $this->addSql('CREATE TABLE sale (id INT NOT NULL DEFAULT nextval(\'sale_id_seq\'::regclass), ...)'); + ``` + +3. Apply the migration: + + ```bash + docker compose exec php bin/console doctrine:migrations:migrate + ``` (Optimisation) Indexation de la Colonne de Date, car toutes les routes font des opérations de filtrage basées sur cette colonne. diff --git a/api/src/Controller/ChartController.php b/api/src/Controller/ChartController.php deleted file mode 100644 index 68bd56e0da3c4598bfde985481f94a3c458a7d11..0000000000000000000000000000000000000000 --- a/api/src/Controller/ChartController.php +++ /dev/null @@ -1,66 +0,0 @@ -chartService = $chartService; - } - - #[Route('/api/bar-chart/{startDate}/{endDate}/{granularity}', name: 'bar-chart', requirements: [ - 'startDate' => '\d{4}-\d{2}-\d{2}', // YYYY-MM-DD - 'endDate' => '\d{4}-\d{2}-\d{2}', // YYYY-MM-DD - 'granularity' => 'day|month|year', // day, month, ou year - ])] - public function getChartData(string $startDate, string $endDate, string $granularity): JsonResponse - { - $input = new BarChartInput(); - $input->start = $startDate; - $input->end = $endDate; - $input->granularity = $granularity; - - $chartData = $this->chartService->getBarChartData($input); - $output = []; - foreach ($chartData as $data) { - $output[] = [ - 'date' => $data->date, - 'occurrences' => $data->occurrences, - ]; - } - return $this->json($output); - } - - #[Route('/api/donut-chart/{id}', name: 'donut-chart', requirements: [ - 'id' => '\d{4}', - ])] - public function getDonutChartData(string $id): JsonResponse - { - $validator = Validation::createValidator(); - $violations = $validator->validate($id, [ - new Assert\NotBlank(), - new Assert\Range(['min' => 2018, 'max' => 2023]), - ]); - - if (count($violations) > 0) { - return new JsonResponse(['error' => 'Not Found'], Response::HTTP_NOT_FOUND); - } - - $data = $this->chartService->getDonutChartData($id); - - return $this->json($data); - } -} diff --git a/api/src/Controller/SaleController.php b/api/src/Controller/SaleController.php index cad37dcdee8370f85b3b741bdba45f41e6ceb508..35b8dd1389f2b2f9d8e4b802005fea50c662f526 100644 --- a/api/src/Controller/SaleController.php +++ b/api/src/Controller/SaleController.php @@ -2,12 +2,16 @@ namespace App\Controller; -use ApiPlatform\Metadata\Get; +use App\Dto\BarChartInput; use App\Service\SaleService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Validation; + #[AsController] class SaleController extends AbstractController @@ -19,10 +23,54 @@ class SaleController extends AbstractController $this->saleService = $saleService; } - public function __invoke(): JsonResponse + #[Route('/api/timeseries', name: 'timeseries')] + public function getTimeSeries(): JsonResponse { - $result = $this->saleService->getMonthlyAveragePriceEvolution(); + $result = $this->saleService->getTimeSeries(); return $this->json($result); } + + #[Route('/api/bar-chart/{startDate}/{endDate}/{granularity}', name: 'bar-chart', requirements: [ + 'startDate' => '\d{4}-\d{2}-\d{2}', // YYYY-MM-DD + 'endDate' => '\d{4}-\d{2}-\d{2}', // YYYY-MM-DD + 'granularity' => 'day|month|year', // day, month, ou year + ])] + public function getChartData(string $startDate, string $endDate, string $granularity): JsonResponse + { + $input = new BarChartInput(); + $input->start = $startDate; + $input->end = $endDate; + $input->granularity = $granularity; + + $chartData = $this->saleService->getBarChartData($input); + $output = []; + foreach ($chartData as $data) { + $output[] = [ + 'date' => $data->date, + 'occurrences' => $data->occurrences, + ]; + } + return $this->json($output); + } + + #[Route('/api/donut-chart/{id}', name: 'donut-chart', requirements: [ + 'id' => '\d{4}', + ])] + public function getDonutChartData(string $id): JsonResponse + { + $validator = Validation::createValidator(); + $violations = $validator->validate($id, [ + new Assert\NotBlank(), + new Assert\Range(['min' => 2018, 'max' => 2023]), + ]); + + if (count($violations) > 0) { + return new JsonResponse(['error' => 'Not Found'], Response::HTTP_NOT_FOUND); + } + + $data = $this->saleService->getDonutChartData($id); + + return $this->json($data); + } } diff --git a/api/src/Dto/BarChart/BarChartInput.php b/api/src/Dto/BarChartInput.php similarity index 80% rename from api/src/Dto/BarChart/BarChartInput.php rename to api/src/Dto/BarChartInput.php index becb0bba555f6745930d8543bf7bbb6b0343b81a..bbec029a3c0d310e9d0fbe029c55ce2d4cae5741 100644 --- a/api/src/Dto/BarChart/BarChartInput.php +++ b/api/src/Dto/BarChartInput.php @@ -1,5 +1,5 @@ '\d{4}-\d{2}-\d{2}', 'granularity' => 'day|month|year', ], - controller: ChartController::class, + controller: SaleController::class, name: 'bar-chart' ), new GetCollection( @@ -26,13 +26,14 @@ use App\Controller\ChartController; requirements: [ 'id' => '\d{4}', ], - controller: ChartController::class, + controller: SaleController::class, name: 'donut-chart', ), new GetCollection( - uriTemplate: '/api/monthly_average_price_evolution', + uriTemplate: '/api/timeseries', + requirements: [], controller: SaleController::class, - name: 'monthly_average_price_evolution' + name: 'timeseries' ) ] )] diff --git a/api/src/Service/ChartService.php b/api/src/Service/ChartService.php deleted file mode 100644 index 811a1ab2a23ee6c0349d7696ba182904dbf1b315..0000000000000000000000000000000000000000 --- a/api/src/Service/ChartService.php +++ /dev/null @@ -1,104 +0,0 @@ -entityManager = $entityManager; - } - - public function getBarChartData(BarChartInput $input): array - { - $startDate = new \DateTime($input->start); - $endDate = new \DateTime($input->end); - - $queryBuilder = $this->entityManager->createQueryBuilder(); - $output = []; - - if ($input->granularity == 'month') { - $result = $queryBuilder - ->select('YEAR(s.date) as year', 'MONTH(s.date) as month', 'COUNT(s.id) as occurrences') - ->from(Sale::class, 's') - ->where('s.date BETWEEN :start AND :end') - ->setParameter('start', $startDate) - ->setParameter('end', $endDate) - ->groupBy('year', 'month') - ->orderBy('year') - ->getQuery() - ->getResult(); - } else { - switch ($input->granularity) { - case 'day': - $groupByExpression = 's.date'; - $groupByAlias = 'date'; - $dateFormat = 'Y-m-d'; - break; - case 'year': - $groupByExpression = 'YEAR(s.date)'; - $groupByAlias = 'year'; - $dateFormat = 'Y'; - break; - default: - throw new \InvalidArgumentException('Invalid granularity'); - } - - $result = $queryBuilder - ->select("{$groupByExpression} as {$groupByAlias}", 'COUNT(s.id) as occurrences') - ->from(Sale::class, 's') - ->where('s.date BETWEEN :start AND :end') - ->setParameter('start', $startDate) - ->setParameter('end', $endDate) - ->groupBy("{$groupByAlias}") - ->orderBy("{$groupByAlias}") - ->getQuery() - ->getResult(); - } - - foreach ($result as $row) { - if ($input->granularity == 'month') { - $month = $row['month']; - $year = $row['year']; - $dateString = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT); - } else { - $dateString = $row[$groupByAlias] instanceof \DateTimeInterface ? $row[$groupByAlias]->format($dateFormat) : $row[$groupByAlias]; - } - - $output[] = new BarChartOutput($dateString, (int)$row['occurrences']); - } - - return $output; - } - - public function getDonutChartData(string $year): array - { - $repository = $this->entityManager->getRepository(Sale::class); - - $result = $repository->createQueryBuilder('s') - ->select('s.region', 'COUNT(s.id) as occurrences') - ->andWhere('YEAR(s.date) = :year') - ->setParameter('year', $year) - ->groupBy('s.region') - ->getQuery() - ->getResult(); - - $data = []; - foreach ($result as $row) { - $data[] = [ - 'region' => $row['region'], - 'occurrences' => (int)$row['occurrences'], - ]; - } - - return $data; - } -} - diff --git a/api/src/Service/SaleService.php b/api/src/Service/SaleService.php index 6f2a07be684b1f9c3cae940052fa09ca794c205a..aeebcb09b5240feaedc089e8656142652ce8ea57 100644 --- a/api/src/Service/SaleService.php +++ b/api/src/Service/SaleService.php @@ -2,9 +2,11 @@ namespace App\Service; +use App\Dto\BarChartInput; +use App\Dto\BarChartOutput; +use App\Dto\MonthlyAveragePriceEvolution; use App\Entity\Sale; use Doctrine\ORM\EntityManagerInterface; -use App\Dto\MonthlyAveragePriceEvolutionDto; class SaleService { @@ -15,7 +17,7 @@ class SaleService $this->entityManager = $entityManager; } - public function getMonthlyAveragePriceEvolution(): array + public function getTimeSeries(): array { $queryBuilder = $this->entityManager->createQueryBuilder(); @@ -29,7 +31,7 @@ class SaleService $monthlyAveragePriceEvolutions = []; foreach ($result as $row) { - $monthlyAveragePriceEvolutions[] = new MonthlyAveragePriceEvolutionDto( + $monthlyAveragePriceEvolutions[] = new MonthlyAveragePriceEvolution( (int)$row['month'], (int)$row['year'], (float)$row['average_price'] @@ -38,4 +40,89 @@ class SaleService return $monthlyAveragePriceEvolutions; } + + public function getBarChartData(BarChartInput $input): array + { + $startDate = new \DateTime($input->start); + $endDate = new \DateTime($input->end); + + $queryBuilder = $this->entityManager->createQueryBuilder(); + $output = []; + + if ($input->granularity == 'month') { + $result = $queryBuilder + ->select('YEAR(s.date) as year', 'MONTH(s.date) as month', 'COUNT(s.id) as occurrences') + ->from(Sale::class, 's') + ->where('s.date BETWEEN :start AND :end') + ->setParameter('start', $startDate) + ->setParameter('end', $endDate) + ->groupBy('year', 'month') + ->orderBy('year') + ->getQuery() + ->getResult(); + } else { + switch ($input->granularity) { + case 'day': + $groupByExpression = 's.date'; + $groupByAlias = 'date'; + $dateFormat = 'Y-m-d'; + break; + case 'year': + $groupByExpression = 'YEAR(s.date)'; + $groupByAlias = 'year'; + $dateFormat = 'Y'; + break; + default: + throw new \InvalidArgumentException('Invalid granularity'); + } + + $result = $queryBuilder + ->select("{$groupByExpression} as {$groupByAlias}", 'COUNT(s.id) as occurrences') + ->from(Sale::class, 's') + ->where('s.date BETWEEN :start AND :end') + ->setParameter('start', $startDate) + ->setParameter('end', $endDate) + ->groupBy("{$groupByAlias}") + ->orderBy("{$groupByAlias}") + ->getQuery() + ->getResult(); + } + + foreach ($result as $row) { + if ($input->granularity == 'month') { + $month = $row['month']; + $year = $row['year']; + $dateString = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT); + } else { + $dateString = $row[$groupByAlias] instanceof \DateTimeInterface ? $row[$groupByAlias]->format($dateFormat) : $row[$groupByAlias]; + } + + $output[] = new BarChartOutput($dateString, (int)$row['occurrences']); + } + + return $output; + } + + public function getDonutChartData(string $year): array + { + $repository = $this->entityManager->getRepository(Sale::class); + + $result = $repository->createQueryBuilder('s') + ->select('s.region', 'COUNT(s.id) as occurrences') + ->andWhere('YEAR(s.date) = :year') + ->setParameter('year', $year) + ->groupBy('s.region') + ->getQuery() + ->getResult(); + + $data = []; + foreach ($result as $row) { + $data[] = [ + 'region' => $row['region'], + 'occurrences' => (int)$row['occurrences'], + ]; + } + + return $data; + } }