diff --git a/pwa/app/components/molecules/RegionSelector.tsx b/pwa/app/components/molecules/RegionSelector.tsx new file mode 100644 index 0000000000000000000000000000000000000000..71862c9c0e523e7cf814b3cb8b6f502ce6b1a588 --- /dev/null +++ b/pwa/app/components/molecules/RegionSelector.tsx @@ -0,0 +1,47 @@ +'use client' + +interface RegionSelectorProps { + regions: string[] + selectedRegions: string[] + onSelectionChange: (regions: string[]) => void +} + +export default function RegionSelector({ regions, selectedRegions, onSelectionChange }: RegionSelectorProps) { + const toggleRegion = (region: string) => { + const newSelection = selectedRegions.includes(region) ? selectedRegions.filter(r => r !== region) : [...selectedRegions, region] + onSelectionChange(newSelection) + } + + if (regions.length === 0) return null + + return ( +
+ +
+ + + {regions.map(region => ( + + ))} +
+
+ ) +} diff --git a/pwa/app/components/organisms/Dashboard.tsx b/pwa/app/components/organisms/Dashboard.tsx index 84656879cc18abbe2fff507b05472bd26f6f75eb..6bf2c1ef0a0cab241588b31c3bad72a58813f1c7 100644 --- a/pwa/app/components/organisms/Dashboard.tsx +++ b/pwa/app/components/organisms/Dashboard.tsx @@ -11,8 +11,6 @@ interface DashboardProps { regions: string[] } -const tabKeys = ['temporal', 'points', 'diagram'] as const - export default function Dashboard({ regions }: DashboardProps) { const [activeTab, setActiveTab] = useState('temporal') const [sidebarOpen, setSidebarOpen] = useState(false) @@ -20,19 +18,14 @@ export default function Dashboard({ regions }: DashboardProps) { return (
- {sidebarOpen && ( -
setSidebarOpen(false)} - /> - )} + {sidebarOpen &&
setSidebarOpen(false)} />}
setSidebarOpen(prev => !prev)} />
{activeTab === 'temporal' && } {activeTab === 'points' && } - {activeTab === 'diagram' && } + {activeTab === 'diagram' && }
diff --git a/pwa/app/components/tabs/diagram.tsx b/pwa/app/components/tabs/diagram.tsx index c9df2e01c20bdb5663ef46c64bdf11df03aae6e7..7485ff3d9d05243aa4cfe9ccd9458319d096902e 100644 --- a/pwa/app/components/tabs/diagram.tsx +++ b/pwa/app/components/tabs/diagram.tsx @@ -1,120 +1,163 @@ 'use client' import { useState, useEffect } from 'react' -import { Doughnut } from 'react-chartjs-2' -import { Chart as ChartJS, ArcElement, Tooltip, Legend, TooltipItem } from 'chart.js' -import ChartDataLabels from 'chartjs-plugin-datalabels' +import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts' import { TAX_TYPES, YEARS, COLORS } from '../../constants' -import { getRegionDistribution, prepareChartData, RegionDistribution } from '../../services/diagram.services' +import { getRegionDistribution, RegionDistribution } from '../../services/diagram.services' import ErrorDiv from '../molecules/ErrorDiv' +import RegionSelector from '../molecules/RegionSelector' -ChartJS.register(ArcElement, Tooltip, Legend, ChartDataLabels) +interface DiagramProps { + regions: string[] +} -export default function Diagram() { - const [taxType, setTaxType] = useState('th') - const [year, setYear] = useState(2019) - const [topCount, setTopCount] = useState(9) +export default function Diagram({ regions }: DiagramProps) { + const [taxType, setTaxType] = useState('') + const [year, setYear] = useState(null) const [data, setData] = useState([]) - const [maxRegions, setMaxRegions] = useState(20) + const [rawData, setRawData] = useState([]) // Store fetched data const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + // Regions currently selected for display (all selected by default) + const [selectedRegions, setSelectedRegions] = useState([]) + const [isInitialized, setIsInitialized] = useState(false) - // Load number of regions on mount + // Initialize selectedRegions with all regions on first render useEffect(() => { - const loadMaxRegions = async () => { + if (!isInitialized && regions.length > 0) { + setSelectedRegions(regions) + setIsInitialized(true) + } + }, [regions, isInitialized]) + + // Fetch data only when tax type or year changes + useEffect(() => { + const fetchData = async () => { + if (!taxType || !year) { + setRawData([]) + setData([]) + return + } + setLoading(true) + setError(null) try { const result = await getRegionDistribution(taxType, year) - setMaxRegions(result.length) - } catch { - // Keep default value on error + setRawData(result) + } catch (e) { + setRawData([]) + setData([]) + setError(e instanceof Error ? e.message : 'Une erreur est survenue') + } finally { + setLoading(false) } } - loadMaxRegions() + + fetchData() }, [taxType, year]) - const handleSubmit = async () => { - setLoading(true) - setError(null) - try { - const result = await getRegionDistribution(taxType, year) - setMaxRegions(result.length) - const chartData = prepareChartData(result, topCount) - setData(chartData) - } catch (e) { + // Filter and process data when selectedRegions or rawData changes + useEffect(() => { + if (rawData.length === 0 || selectedRegions.length === 0) { setData([]) - setError(e instanceof Error ? e.message : 'Une erreur est survenue') - } finally { - setLoading(false) + return } - } - const chartConfig = { - labels: data.map(d => d.region), - datasets: [ - { - data: data.map(d => d.total_amount), - backgroundColor: COLORS, - borderColor: '#1a1d21', - borderWidth: 2, - }, - ], + // Calculate total for percentages + const totalAmount = rawData.reduce((sum, item) => sum + item.total_amount, 0) + + // Separate selected and non-selected regions + const selected = rawData.filter(r => selectedRegions.includes(r.region)) + const notSelected = rawData.filter(r => !selectedRegions.includes(r.region)) + + // Filter selected regions: keep only those >= 0.5%, others go to "Autres" + const significantRegions: RegionDistribution[] = [] + const smallRegions: RegionDistribution[] = [] + + selected.forEach(item => { + const percentage = totalAmount > 0 ? (item.total_amount / totalAmount) * 100 : 0 + if (percentage >= 0.5) { + significantRegions.push({ + region: item.region, + total_amount: item.total_amount, + percentage: percentage, + }) + } else { + smallRegions.push(item) + } + }) + + // Combine small selected regions with non-selected regions for "Autres" + const othersRegions = [...smallRegions, ...notSelected] + + // Add "Autres" if there are regions to group + if (othersRegions.length > 0) { + const othersTotal = othersRegions.reduce((sum, item) => sum + item.total_amount, 0) + const othersPercentage = totalAmount > 0 ? (othersTotal / totalAmount) * 100 : 0 + significantRegions.push({ + region: 'Autres', + total_amount: othersTotal, + percentage: othersPercentage, + otherRegionsSmall: smallRegions.map(item => ({ + name: item.region, + percentage: totalAmount > 0 ? (item.total_amount / totalAmount) * 100 : 0, + })), + otherRegionsNotSelected: notSelected.map(item => ({ + name: item.region, + percentage: totalAmount > 0 ? (item.total_amount / totalAmount) * 100 : 0, + })), + }) + } + + setData(significantRegions) + }, [rawData, selectedRegions]) + + // Custom label to display percentage on slices + const renderLabel = (props: { payload?: RegionDistribution; percent?: number }) => { + const entry = props.payload + if (!entry || !entry.percentage || entry.percentage < 3) return '' + return `${entry.percentage.toFixed(1)}%` } - const chartOptions = { - responsive: true, - maintainAspectRatio: true, - aspectRatio: 1.5, - plugins: { - legend: { - position: 'right' as const, - labels: { - color: '#b0afaf', - padding: 25, - font: { size: 18 }, - boxWidth: 30, - boxHeight: 30, - }, - }, - tooltip: { - backgroundColor: '#212529', - borderColor: '#3a3f44', - borderWidth: 1, - titleFont: { size: 18 }, - bodyFont: { size: 16 }, - padding: 12, - callbacks: { - label: (ctx: TooltipItem<'doughnut'>) => { - const region = data[ctx.dataIndex] - if (!region) return '' - const amount = region.total_amount ? (region.total_amount / 1_000_000).toFixed(2) : '0.00' - const percentage = region.percentage ? region.percentage.toFixed(2) : '0.00' - - const lines = [`${region.region}`, `Volume: ${amount} M€`, `Part: ${percentage}%`] - - // If it's "Others", add the list of regions - if (region.region === 'Autres' && region.otherRegions) { - lines.push('', 'Régions incluses:') - region.otherRegions.forEach((r: string) => lines.push(` • ${r}`)) - } - - return lines - }, - }, - }, - datalabels: { - color: '#fff', - font: { - weight: 'bold' as const, - size: 24, - }, - formatter: (_value: unknown, ctx: { dataIndex: number }) => { - const region = data[ctx.dataIndex] - if (!region || !region.percentage) return '' - // Only display percentage if greater than 3% - return region.percentage >= 3 ? `${region.percentage.toFixed(1)}%` : '' - }, - }, - }, + // Custom tooltip + const CustomTooltip = ({ active, payload }: { active?: boolean; payload?: { payload: RegionDistribution }[] }) => { + if (active && payload && payload.length && payload[0]) { + const region = payload[0].payload + const amount = region.total_amount ? (region.total_amount / 1_000_000).toFixed(2) : '0.00' + const percentage = region.percentage ? region.percentage.toFixed(2) : '0.00' + + return ( +
+

{region.region}

+

Volume: {amount} M€

+

Part: {percentage}%

+ {region.region === 'Autres' && ( + <> + {region.otherRegionsSmall && region.otherRegionsSmall.length > 0 && ( + <> +

Régions sélectionnées (< 0.5%):

+ {region.otherRegionsSmall.map((r: { name: string; percentage: number }) => ( +

+ • {r.name} ({r.percentage < 0.01 ? '<0.01%' : `${r.percentage.toFixed(2)}%`}) +

+ ))} + + )} + {region.otherRegionsNotSelected && region.otherRegionsNotSelected.length > 0 && ( + <> +

Régions non sélectionnées:

+ {region.otherRegionsNotSelected.map((r: { name: string; percentage: number }) => ( +

+ • {r.name} ({r.percentage < 0.01 ? '<0.01%' : `${r.percentage.toFixed(2)}%`}) +

+ ))} + + )} + + )} +
+ ) + } + return null } return ( @@ -129,6 +172,7 @@ export default function Diagram() { onChange={e => setTaxType(e.target.value)} className="bg-[#212529] border border-[#3a3f44] rounded px-3 py-1.5 text-white text-sm" > + {TAX_TYPES.map(t => (