From bd17ff824efd081611e59dac261a5c5c794a7407 Mon Sep 17 00:00:00 2001 From: Hugman Date: Wed, 11 Feb 2026 17:47:27 +0100 Subject: [PATCH] Branchement du front sur l'API --- api-platform/api/src/Entity/Commune.php | 3 +- api-platform/api/src/Entity/Departement.php | 5 +- api-platform/api/src/Entity/Region.php | 5 +- api-platform/api/src/Entity/Taxe.php | 2 +- api-platform/api/src/Entity/TypeTaxe.php | 3 +- front/src/components/PieChartPanel.tsx | 10 +- front/src/components/TimeSeriesChart.tsx | 6 +- front/src/pages/Dashboard.tsx | 503 ++++++++++++-------- 8 files changed, 332 insertions(+), 205 deletions(-) diff --git a/api-platform/api/src/Entity/Commune.php b/api-platform/api/src/Entity/Commune.php index a58e74e..ee12563 100644 --- a/api-platform/api/src/Entity/Commune.php +++ b/api-platform/api/src/Entity/Commune.php @@ -15,7 +15,8 @@ use Doctrine\ORM\Mapping as ORM; operations: [ new Get(), new GetCollection() - ] + ], + paginationClientEnabled: true, )] class Commune { diff --git a/api-platform/api/src/Entity/Departement.php b/api-platform/api/src/Entity/Departement.php index afb8d1c..db1d1bd 100644 --- a/api-platform/api/src/Entity/Departement.php +++ b/api-platform/api/src/Entity/Departement.php @@ -6,8 +6,6 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use App\Repository\DepartementRepository; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: DepartementRepository::class)] @@ -15,7 +13,8 @@ use Doctrine\ORM\Mapping as ORM; operations: [ new Get(), new GetCollection() - ] + ], + paginationClientEnabled: true, )] class Departement { diff --git a/api-platform/api/src/Entity/Region.php b/api-platform/api/src/Entity/Region.php index 3d64fd1..39fb3d1 100644 --- a/api-platform/api/src/Entity/Region.php +++ b/api-platform/api/src/Entity/Region.php @@ -6,8 +6,6 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use App\Repository\RegionRepository; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: RegionRepository::class)] @@ -15,7 +13,8 @@ use Doctrine\ORM\Mapping as ORM; operations: [ new Get(), new GetCollection() - ] + ], + paginationClientEnabled: true, )] class Region { diff --git a/api-platform/api/src/Entity/Taxe.php b/api-platform/api/src/Entity/Taxe.php index 1ccb6aa..d402c2a 100644 --- a/api-platform/api/src/Entity/Taxe.php +++ b/api-platform/api/src/Entity/Taxe.php @@ -17,7 +17,7 @@ use Doctrine\ORM\Mapping as ORM; new Get(), new GetCollection() ], - paginationClientEnabled: true + paginationClientEnabled: true, )] #[ApiFilter(SearchFilter::class, properties: [ 'type' => 'exact', diff --git a/api-platform/api/src/Entity/TypeTaxe.php b/api-platform/api/src/Entity/TypeTaxe.php index 9ac5ec3..d1cf3a0 100644 --- a/api-platform/api/src/Entity/TypeTaxe.php +++ b/api-platform/api/src/Entity/TypeTaxe.php @@ -15,7 +15,8 @@ use Doctrine\ORM\Mapping as ORM; operations: [ new Get(), new GetCollection() - ] + ], + paginationClientEnabled: true, )] class TypeTaxe { diff --git a/front/src/components/PieChartPanel.tsx b/front/src/components/PieChartPanel.tsx index 07e2f61..297a1c5 100644 --- a/front/src/components/PieChartPanel.tsx +++ b/front/src/components/PieChartPanel.tsx @@ -1,9 +1,7 @@ -import { PieChart, Pie } from "recharts"; +import { PieChart, Pie, Legend } from "recharts"; import { Card, CardContent } from "@/components/ui/card"; import { ChartContainer, - ChartLegend, - ChartLegendContent, ChartTooltip, ChartTooltipContent, type ChartConfig, @@ -56,11 +54,11 @@ export default function PieChartPanel({ - + } /> - } /> + } /> - } /> + {regions.map((r, i) => ( (2); const [pieYear, setPieYear] = useState(2022); - // Time series: for each year, for each region compute average taux of communes in region for selected type - const timeSeriesData = useMemo(() => { - const years: number[] = []; - for (let y = tsStartYear; y <= tsEndYear; y++) years.push(y); + // State for API data for Time Series + const [rawTimeSeries, setRawTimeSeries] = useState([]); + const [isLoadingTimeSeries, setIsLoadingTimeSeries] = useState(false); - const regions = mock.regions.map((r: any) => r.nom); + // State for API data for Scatter Plot + const [rawScatterData, setRawScatterData] = useState([]); + const [isLoadingScatter, setIsLoadingScatter] = useState(false); - const rows = years.map((y) => { - const row: any = { year: y }; - mock.regions.forEach((reg: any) => { - const communesInRegion = mock.communes.filter((c: any) => { - const dep = mock.departements.find( - (d: any) => d.id === c.departementId, + // State for API data for Pie Chart + const [rawPieData, setRawPieData] = useState([]); + const [isLoadingPie, setIsLoadingPie] = useState(false); + + // State for Lists (Departments, Tax Types) + const [listTaxTypes, setListTaxTypes] = useState([]); + const [listDepartements, setListDepartements] = useState([]); + + // Fetch Lists + useEffect(() => { + // Fetch Tax Types + fetch("https://localhost/type_taxes", { + headers: { Accept: "application/ld+json" }, + }) + .then((res) => res.json()) + .then((data) => { + if (data["member"]) { + setListTaxTypes(data["member"]); + // Default to first type if available and no selection + // or ensure defaults are valid + } + }) + .catch((err) => console.error("Failed to fetch tax types:", err)); + + // Fetch Departements + fetch("https://localhost/departements?pagination=false", { + headers: { Accept: "application/ld+json" }, + }) + .then((res) => res.json()) + .then((data) => { + if (data["member"]) { + setListDepartements( + data["member"].sort((a: any, b: any) => + a.code - b.code + ) ); - return dep && dep.regionId === reg.id; + } + }) + .catch((err) => console.error("Failed to fetch departements:", err)); + }, []); + + // Fetch Time Series data from API + useEffect(() => { + if (listTaxTypes.length === 0) return; + const typeObj = listTaxTypes.find((t: any) => t.id === tsTypeId); + if (!typeObj) return; + + const query = new URLSearchParams({ + startYear: String(tsStartYear), + endYear: String(tsEndYear), + typeCode: typeObj.code, + }); + + const fetchData = async () => { + setIsLoadingTimeSeries(true); + try { + const res = await fetch(`https://localhost/stats/average-tax-by-region?${query.toString()}`, { + headers: { + Accept: "application/ld+json", + }, }); - const taxes = mock.taxes.filter( - (t: any) => - t.typeId === tsTypeId && - t.annee === y && - communesInRegion.some((c: any) => c.id === t.communeId), + const data = await res.json(); + if (data["member"]) { + setRawTimeSeries(data["member"]); + } else { + setRawTimeSeries([]); + } + } catch (err) { + console.error("Failed to fetch time series:", err); + setRawTimeSeries([]); + } finally { + setIsLoadingTimeSeries(false); + } + }; + + fetchData(); + }, [tsStartYear, tsEndYear, tsTypeId, listTaxTypes]); + + // Fetch Scatter data from API + useEffect(() => { + if (!scDeptId || listTaxTypes.length === 0) return; + const typeObj = listTaxTypes.find((t: any) => t.id === scTypeId); + if (!typeObj) return; + + const query = new URLSearchParams({ + annee: String(scYear), + "commune.departement": `/api/departements/${scDeptId}`, + "type.code": typeObj.code, + pagination: "false", + }); + + const fetchData = async () => { + setIsLoadingScatter(true); + try { + const res = await fetch(`https://localhost/taxes?${query.toString()}`, { + headers: { + Accept: "application/ld+json", + }, + }); + const data = await res.json(); + if (data["member"]) { + setRawScatterData(data["member"]); + } else { + setRawScatterData([]); + } + } catch (err) { + console.error("Failed to fetch scatter data:", err); + setRawScatterData([]); + } finally { + setIsLoadingScatter(false); + } + }; + + fetchData(); + }, [scYear, scDeptId, scTypeId]); + + // Fetch Pie data from API + useEffect(() => { + if (listTaxTypes.length === 0) return; + const typeObj = listTaxTypes.find((t: any) => t.id === pieTypeId); + if (!typeObj) return; + + const query = new URLSearchParams({ + year: String(pieYear), + typeCode: typeObj.code, + }); + + const fetchData = async () => { + setIsLoadingPie(true); + try { + const res = await fetch(`https://localhost/stats/total-volume-by-region?${query.toString()}`, { + headers: { + Accept: "application/ld+json", + }, + }); + const data = await res.json(); + if (data["member"]) { + setRawPieData(data["member"]); + } else { + setRawPieData([]); + } + } catch (err) { + console.error("Failed to fetch pie data:", err); + setRawPieData([]); + } finally { + setIsLoadingPie(false); + } + }; + + fetchData(); + }, [pieYear, pieTypeId, listTaxTypes]); + + // Time series: transform API data for chart + const timeSeriesData = useMemo(() => { + if (!rawTimeSeries.length) return { rows: [], regions: [] }; + + const regions = Array.from( + new Set(rawTimeSeries.map((item: any) => item.region)), + ); + const availableYears = Array.from( + new Set(rawTimeSeries.map((item: any) => item.annee)), + ).sort((a: any, b: any) => a - b); + + const rows = availableYears.map((y: any) => { + const row: any = { year: y }; + regions.forEach((reg: any) => { + const entry = rawTimeSeries.find( + (item: any) => item.annee === y && item.region === reg, ); - const avg = taxes.length - ? taxes.reduce((s: number, t: any) => s + t.taux, 0) / - taxes.length - : null; - row[reg.nom] = avg; + row[reg] = entry ? entry.tauxMoyen : null; }); return row; }); return { rows, regions }; - }, [tsStartYear, tsEndYear, tsTypeId]); + }, [rawTimeSeries]); - // Scatter: communes in a departement for a given year + // Scatter: transform API data const scatterData = useMemo(() => { - if (!scDeptId) return []; - const communesInDept = mock.communes.filter( - (c: any) => c.departementId === scDeptId, - ); - const taxes = mock.taxes.filter( - (t: any) => - t.annee === scYear && - communesInDept.some((c: any) => c.id === t.communeId) && - t.typeId === scTypeId, - ); - return taxes.map((t: any) => { - const commune = mock.communes.find( - (c: any) => c.id === t.communeId, - ); + return rawScatterData.map((t: any) => { + let name = "Commune"; + if (t.commune && typeof t.commune === "object" && t.commune.nom) { + name = t.commune.nom; + } else if (typeof t.commune === "string") { + // Try to extract ID from IRI and match with mock if real name not available + const parts = t.commune.split("/"); + const id = parts[parts.length - 1]; + name = `Commune ${id}`; + } + return { - commune: commune?.nom || String(t.communeId), + commune: name, taux: t.taux, volume: t.volume, }; }); - }, [scDeptId, scYear, scTypeId]); + }, [rawScatterData]); - // Pie: sum volumes per region for given year and type + // Pie: transform API data const pieData = useMemo(() => { - const year = pieYear; - const regions = mock.regions.map((r: any) => { - const communesInRegion = mock.communes.filter((c: any) => { - const dep = mock.departements.find( - (d: any) => d.id === c.departementId, - ); - return dep && dep.regionId === r.id; - }); - const vols = mock.taxes.filter( - (t: any) => - t.annee === year && - t.typeId === pieTypeId && - communesInRegion.some((c: any) => c.id === t.communeId), - ); - const sum = vols.reduce((s: number, t: any) => s + t.volume, 0); - return { name: r.nom, value: sum }; - }); - return regions; - }, [pieYear, pieTypeId]); - - // KPIs: total volume for selected endYear and delta vs previous year - const kpis = useMemo(() => { - const year = tsEndYear; - const prev = year - 1; - const vols = mock.taxes - .filter((t: any) => t.annee === year && t.typeId === tsTypeId) - .reduce((s: number, t: any) => s + t.volume, 0); - const volsPrev = mock.taxes - .filter((t: any) => t.annee === prev && t.typeId === tsTypeId) - .reduce((s: number, t: any) => s + t.volume, 0); - const delta = volsPrev ? (vols - volsPrev) / volsPrev : 0; - const avgTaux = (() => { - const taxes = mock.taxes.filter( - (t: any) => t.annee === year && t.typeId === tsTypeId, - ); - return taxes.length - ? taxes.reduce((s: number, t: any) => s + t.taux, 0) / - taxes.length - : 0; - })(); - - return [ - { - label: "Volume collecté (" + year + ")", - value: Math.round(vols), - delta, - }, - { - label: "Taux moyen (" + year + ")", - value: Number((avgTaux * 100).toFixed(2)), - delta: 0, - }, - { label: "Régions", value: mock.regions.length }, - ]; - }, [tsEndYear, tsTypeId]); - - // available years for selectors (from mock data) - const years = useMemo(() => { - return Array.from(new Set(mock.taxes.map((t: any) => t.annee))).sort((a: number, b: number) => a - b); - }, []); + return rawPieData.map((d: any) => ({ + name: d.region, + value: d.totalVolume, + })); + }, [rawPieData]); - return ( -
-

- Tableau de bord fiscal -

- -
- - - - - - -
+ // available years for selectors (hardcoded per request) + const years = [2018, 2019, 2020, 2021, 2022]; -
- + return ( +
+

Tableau de bord fiscal

-

- Séries temporelles — taux moyen par région +
+

+ Séries temporelles - taux moyen par région

-
- + +
+
+ + +
+ +
+ + + - + +
+
+ +
+ {isLoadingTimeSeries ? ( +
+ + Chargement des données... +
+ ) : rawTimeSeries.length === 0 ? ( +
+ Aucune donnée disponible pour cette sélection. +
+ ) : ( + + )}

- Nuage de points — communes (département) + Nuage de points - communes (département)

@@ -236,9 +341,9 @@ export default function DashboardPage() { - {mock.typeTaxes.map((tt: any) => ( + {listTaxTypes.map((tt: any) => ( - {tt.code} — {tt.label} + {tt.code} - {tt.label} ))} @@ -249,9 +354,9 @@ export default function DashboardPage() { - {mock.departements.map((d: any) => ( + {listDepartements.map((d: any) => ( - {d.code} — {d.nom} + {d.code} - {d.nom} ))} @@ -268,14 +373,27 @@ export default function DashboardPage() {
-
- +
+ {isLoadingScatter ? ( +
+ + Chargement des données... +
+ ) : rawScatterData.length === 0 ? ( +
+ + Aucune donnée disponible pour cette sélection. + +
+ ) : ( + + )}

- Diagramme en secteurs — volume collecté par région (année) + Diagramme en secteurs - volume collecté par région (année)

@@ -284,9 +402,9 @@ export default function DashboardPage() { - {mock.typeTaxes.map((tt: any) => ( + {listTaxTypes.map((tt: any) => ( - {tt.code} — {tt.label} + {tt.code} - {tt.label} ))} @@ -304,7 +422,20 @@ export default function DashboardPage() {
- + {isLoadingPie ? ( +
+ + Chargement des données... +
+ ) : rawPieData.length === 0 ? ( +
+ + Aucune donnée disponible pour cette sélection. + +
+ ) : ( + + )}

-- GitLab