diff --git a/api/README.md b/api/README.md index f7a5db9769354470f3e4ee28140640c1b531faf8..8f901785b1ada8abdb9d1fd71859a873d4edffab 100755 --- a/api/README.md +++ b/api/README.md @@ -40,19 +40,17 @@ create index date_index on sale (date); ## Clear cache -```Shell -sudo docker exec -it php sh -```` -```sh -composer require --dev doctrine/doctrine-fixtures-bundle -``` - ```Shell sudo docker compose exec php bin/console cache:clear ``` ## Run tests +Install fixtures: +```shell +sudo docker compose exec php composer require --dev doctrine/doctrine-fixtures-bundle +``` + Create a test database: ```shell sudo docker compose exec php bin/console doctrine:database:create --env=test diff --git a/api/src/Service/SaleService.php b/api/src/Service/SaleService.php index cbd5d64ec2807d0ac1bc1d7169cfcad7c8779838..be0f1201a017e7275a811b22645d83b3fbcf548c 100755 --- a/api/src/Service/SaleService.php +++ b/api/src/Service/SaleService.php @@ -55,6 +55,8 @@ class SaleService $result = $queryBuilder ->select('MONTH(s.date) as month', 'YEAR(s.date) as year', 'AVG(CASE WHEN s.surface <> 0 THEN s.amount / s.surface ELSE 0 END) as average_price') + #->select('MONTH(s.date) as month', 'YEAR(s.date) as year', 'SUM(s.amount) as total_price', 'SUM(s.surface) as total_surface') + #->select('MONTH(s.date) as month', 'YEAR(s.date) as year', 'AVG(s.amount) as total_price', 'AVG(s.surface) as total_surface') ->from(Sale::class, 's') ->groupBy('year, month') ->orderBy('year, month') @@ -63,6 +65,7 @@ class SaleService $monthlyAveragePriceEvolutions = []; foreach ($result as $row) { + #$row['average_price'] = $row['total_surface'] != 0 ? $row['total_price'] / $row['total_surface'] : 0; $monthlyAveragePriceEvolutions[] = new MonthlyAveragePriceEvolution( (int)$row['month'], (int)$row['year'], @@ -115,7 +118,7 @@ class SaleService ->groupBy('year', 'month') ->orderBy('year') ->getQuery() - ->getResult(); + ->getResult(Query::HYDRATE_ARRAY); } else { switch ($input->granularity) { case 'day': @@ -141,7 +144,7 @@ class SaleService ->groupBy("{$groupByAlias}") ->orderBy("{$groupByAlias}") ->getQuery() - ->getResult(); + ->getResult(Query::HYDRATE_ARRAY); } foreach ($result as $row) { @@ -181,7 +184,7 @@ class SaleService ->setParameter('year', $year) ->groupBy('s.region') ->getQuery() - ->getResult(); + ->getResult(Query::HYDRATE_ARRAY); $data = []; foreach ($result as $row) { diff --git a/pwa/app/components/bar-chart/form.jsx b/pwa/app/components/bar-chart/form.jsx index 7c5b78c7ef5ecd3f462024fe5649a52bbd25bbef..21601c4ca599fbdf91727612de41325165af8712 100644 --- a/pwa/app/components/bar-chart/form.jsx +++ b/pwa/app/components/bar-chart/form.jsx @@ -1,10 +1,11 @@ import React, { useState } from 'react'; import '../../style/style.css'; +import constants from '../../../config.js'; const DataForm = ({ onSubmit }) => { - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); - const [granularity, setGranularity] = useState('month'); + const [startDate, setStartDate] = useState(`${constants.initialBarChartFormData.startDate}`); + const [endDate, setEndDate] = useState(`${constants.initialBarChartFormData.endDate}`); + const [granularity, setGranularity] = useState(`${constants.initialBarChartFormData.granularity}`); const handleSubmit = (event) => { event.preventDefault(); diff --git a/pwa/app/components/line_chart.jsx b/pwa/app/components/timeseries/timeseries.jsx similarity index 88% rename from pwa/app/components/line_chart.jsx rename to pwa/app/components/timeseries/timeseries.jsx index c32bd660d8efa418b28bef0192b622a6f3f70045..8cd5e098ab3dac8c9139d2b02c0841b8a0d0d8b7 100644 --- a/pwa/app/components/line_chart.jsx +++ b/pwa/app/components/timeseries/timeseries.jsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faSpinner } from "@fortawesome/free-solid-svg-icons"; import * as d3 from "d3"; -function LineChart({ data }) { +function Timeseries({ data }) { const chartRef = useRef(null); const isLoading = data.length === 0; @@ -16,14 +16,18 @@ function LineChart({ data }) { useEffect(() => { + if (data.length === 0) { + return; + } const x = d3.scaleTime() .domain(d3.extent(data, d => new Date(d.year, d.month - 1))) .range([marginLeft, width - marginRight]); - const y = d3.scaleLinear() - .domain([0, d3.max(data, d => d.averagePrice)]) + const y = d3.scaleLog() + .domain([d3.min(data, d => d.averagePrice), d3.max(data, d => d.averagePrice)]) .range([height - marginBottom, marginTop]); + const svg = d3.select(chartRef.current) .attr("width", width) .attr("height", height); @@ -68,4 +72,4 @@ function LineChart({ data }) { ); } -export default LineChart; +export default Timeseries; diff --git a/pwa/app/help/fetch-service.js b/pwa/app/help/fetch-service.js index 5fa318d2c81222b840c23035b4d02254a231cbea..e9b56d66f7ee7fc5d44c5baeed85a2497c66afd0 100644 --- a/pwa/app/help/fetch-service.js +++ b/pwa/app/help/fetch-service.js @@ -1,4 +1,4 @@ -import config from '../../config.js'; +import constants from '../../config.js'; /** * @param {number} year */ @@ -31,7 +31,7 @@ async function getDonutContent(year) { } const getTimesSeries = () => { - const url = `${config.apiUrl}/timeseries?page=1`; + const url = `${constants.config.apiUrl}/timeseries?page=1`; return fetch(url, { method: 'GET', headers: { @@ -41,10 +41,13 @@ const getTimesSeries = () => { } async function fetchBarChartData(formData) { - const url = `${config.apiUrl}/bar-chart/${formData.startDate}/${formData.endDate}/${formData.granularity}?page=1`; - const response = await fetch(url); - const data = await response.json(); - return data; + const url = `${constants.config.apiUrl}/bar-chart/${formData.startDate}/${formData.endDate}/${formData.granularity}?page=1`; + return fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); } export default { diff --git a/pwa/app/page.jsx b/pwa/app/page.jsx index e61855e7c2a3a6b46139de665b89cbdd7249b47c..1de97f99d28b04baefadcdf7b092c00a555820f6 100644 --- a/pwa/app/page.jsx +++ b/pwa/app/page.jsx @@ -1,14 +1,14 @@ "use client"; import React, {useEffect, useState} from "react"; -import config from '../config.js'; +import constants from '../config.js'; import FetchService from "./help/fetch-service"; import Donut from "./components/donut.jsx"; import DateInput from "./components/date-input"; import DataForm from "./components/bar-chart/form"; import BarChart from "./components/bar-chart/chart"; -import Line_Chart from "./components/line_chart.jsx"; +import Timeseries from "./components/timeseries/timeseries.jsx"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSpinner } from "@fortawesome/free-solid-svg-icons"; @@ -20,7 +20,11 @@ export default function Page() { const handleFormSubmit = async (formData) => { setIsLoading(true); - const data = await FetchService.fetchBarChartData(formData); + const response = await FetchService.fetchBarChartData(formData); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); setChartData(data); setIsLoading(false); }; @@ -29,11 +33,15 @@ export default function Page() { setIsLoading(true); try { const formData = { - startDate: '2020-01-01', - endDate: '2022-01-01', - granularity: 'month' + startDate: `${constants.initialBarChartFormData.startDate}`, + endDate: `${constants.initialBarChartFormData.endDate}`, + granularity: `${constants.initialBarChartFormData.granularity}` }; - const data = await FetchService.fetchBarChartData(formData); + const response = await FetchService.fetchBarChartData(formData); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); setChartData(data); } catch (error) { console.error('Error fetching bar chart data:', error); @@ -43,7 +51,10 @@ export default function Page() { useEffect(() => { (async () => { + // bar chart loadInitialBarChartData(); + + // time series try { const response = await FetchService.getTimesSeries(); if (!response.ok) { @@ -54,6 +65,8 @@ export default function Page() { } catch (error) { console.error('Error fetching time series data:', error); } + + // donut const data = await FetchService.getDonutContent(); const formattedData = data.map((x) => ({ id: x.region, @@ -68,7 +81,7 @@ export default function Page() {