temporal.tsx 9,72 ko
Newer Older
Adrien Delmastro's avatar
Adrien Delmastro a validé
'use client'

import { useState } from 'react'
import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, CartesianGrid } from 'recharts'
import { getTimeSeries, type TimeSeriesData } from '../../services/temporal.services'
import { TAX_TYPES, YEARS, COLORS } from '../../constants'
import ErrorDiv from '../molecules/ErrorDiv'
import Loader from '../molecules/Loader'
interface TemporalProps {
    regions: string[]
}

export default function Temporal({ regions }: TemporalProps) {
    // Selected tax type filter (tfpb, tfpnb, th, cfe)
Adrien Delmastro's avatar
Adrien Delmastro a validé
    const [taxType, setTaxType] = useState<string>('tfpb')
    // Start year for the time range
Adrien Delmastro's avatar
Adrien Delmastro a validé
    const [startYear, setStartYear] = useState<number>(2019)
    // End year for the time range
Adrien Delmastro's avatar
Adrien Delmastro a validé
    const [endYear, setEndYear] = useState<number>(2022)
    // Time series data grouped by region { region: [{year, avg_rate}] }
Adrien Delmastro's avatar
Adrien Delmastro a validé
    const [data, setData] = useState<TimeSeriesData | null>(null)
    // API request loading state
Adrien Delmastro's avatar
Adrien Delmastro a validé
    const [loading, setLoading] = useState(false)
    // API error message
    const [error, setError] = useState<string | null>(null)
    // Regions currently selected for display (initialized from SSR props)
    const [selectedRegions, setSelectedRegions] = useState<string[]>(regions)
    // Toggles a region on/off in the selection
    const toggleRegion = (region: string) => {
        setSelectedRegions(prev => (prev.includes(region) ? prev.filter(r => r !== region) : [...prev, region]))
    }
    // Fetches time series data from API for all regions with selected tax type and year range
Adrien Delmastro's avatar
Adrien Delmastro a validé
    const handleSubmit = async () => {
        setLoading(true)
        setError(null)
        try {
            const result = await getTimeSeries([], taxType, startYear, endYear)
            setData(result)
        } catch (e) {
            setData(null)
            setError(e instanceof Error ? e.message : 'Une erreur est survenue')
        } finally {
            setLoading(false)
        }
    // Regions present in API response that are also selected by the user
    const visibleRegions = data ? Object.keys(data).filter(r => selectedRegions.includes(r)) : []

    // Pivoted data for Recharts: [{year, Bretagne: 1.2, Normandie: 1.5, ...}]
Adrien Delmastro's avatar
Adrien Delmastro a validé
    const chartData = data
        ? YEARS.filter(y => y >= startYear && y <= endYear).map(year => {
Adrien Delmastro's avatar
Adrien Delmastro a validé
              const point: Record<string, number | string> = { year }
              for (const region of visibleRegions) {
                  const entry = data[region]?.find(e => e.year === year)
Adrien Delmastro's avatar
Adrien Delmastro a validé
                  if (entry) point[region] = parseFloat(entry.avg_rate)
              }
              return point
          })
        : []

    return (
        <div className="p-6 text-white flex flex-col gap-6">
            <h2 className="text-lg font-semibold">Taux d&apos;imposition moyen par region</h2>
Adrien Delmastro's avatar
Adrien Delmastro a validé

            <div className="flex flex-wrap gap-4 items-end">
                <div className="flex flex-col gap-1">
                    <label className="text-xs text-[#b0afaf] uppercase">Taxe</label>
Adrien Delmastro's avatar
Adrien Delmastro a validé
                    <select
                        value={taxType}
                        onChange={e => setTaxType(e.target.value)}
Adrien Delmastro's avatar
Adrien Delmastro a validé
                        className="bg-[#212529] border border-[#3a3f44] rounded px-3 py-1.5 text-white text-sm"
                    >
                        {TAX_TYPES.map(t => (
Adrien Delmastro's avatar
Adrien Delmastro a validé
                            <option key={t} value={t}>
                                {t.toUpperCase()}
                            </option>
                        ))}
                    </select>
                </div>

                <div className="flex flex-col gap-1">
                    <label className="text-xs text-[#b0afaf] uppercase">De</label>
Adrien Delmastro's avatar
Adrien Delmastro a validé
                    <select
                        value={startYear}
                        onChange={e => setStartYear(Number(e.target.value))}
Adrien Delmastro's avatar
Adrien Delmastro a validé
                        className="bg-[#212529] border border-[#3a3f44] rounded px-3 py-1.5 text-white text-sm"
                    >
                        {YEARS.map(y => (
Adrien Delmastro's avatar
Adrien Delmastro a validé
                            <option key={y} value={y}>
                                {y}
                            </option>
                        ))}
                    </select>
                </div>

                <div className="flex flex-col gap-1">
                    <label className="text-xs text-[#b0afaf] uppercase">A</label>
Adrien Delmastro's avatar
Adrien Delmastro a validé
                    <select
                        value={endYear}
                        onChange={e => setEndYear(Number(e.target.value))}
Adrien Delmastro's avatar
Adrien Delmastro a validé
                        className="bg-[#212529] border border-[#3a3f44] rounded px-3 py-1.5 text-white text-sm"
                    >
                        {YEARS.map(y => (
Adrien Delmastro's avatar
Adrien Delmastro a validé
                            <option key={y} value={y}>
                                {y}
                            </option>
                        ))}
                    </select>
                </div>

                <button
                    onClick={handleSubmit}
                    disabled={loading}
                    className="bg-[#8884d8] hover:bg-[#7773c7] disabled:opacity-50 text-white text-sm px-4 py-1.5 rounded transition-colors"
                >
                    {loading ? 'Chargement...' : 'Afficher'}
                </button>
            </div>

            {regions.length > 0 && (
                <div className="flex flex-col gap-2">
                    <label className="text-xs text-[#b0afaf] uppercase">Régions {selectedRegions.length > 0 && `(${selectedRegions.length})`}</label>
                    <div className="flex flex-wrap gap-2">
                        <button
                            onClick={() => setSelectedRegions(regions)}
                            className="text-xs px-3 py-1 rounded-full border border-[#3a3f44] text-[#b0afaf] hover:text-white transition-colors"
                        >
                            Tous
                        </button>
                        <button
                            onClick={() => setSelectedRegions([])}
                            className="text-xs px-3 py-1 rounded-full border border-[#3a3f44] text-[#b0afaf] hover:text-white transition-colors"
                        >
                            Aucun
                        </button>
                        {regions.map(region => (
                            <button
                                key={region}
                                onClick={() => toggleRegion(region)}
                                className={`text-xs px-3 py-1 rounded-full border transition-colors ${
                                    selectedRegions.includes(region)
                                        ? 'bg-[#8884d8] border-[#8884d8] text-white'
                                        : 'border-[#3a3f44] text-[#b0afaf] hover:text-white'
                                }`}
                            >
                                {region}
                            </button>
                        ))}
                    </div>
                </div>
            )}

            {loading && <Loader />}

            {error && <ErrorDiv message={error} />}

            {!loading && !error && chartData.length > 0 && (
Adrien Delmastro's avatar
Adrien Delmastro a validé
                <ResponsiveContainer width="100%" height={400}>
                    <LineChart data={chartData}>
                        <CartesianGrid strokeDasharray="3 3" stroke="#3a3f44" />
                        <XAxis dataKey="year" stroke="#b0afaf" />
                        <YAxis stroke="#b0afaf" tickFormatter={(v: number) => `${v}%`} />
Adrien Delmastro's avatar
Adrien Delmastro a validé
                        <Tooltip
                            content={({ payload, label }) => {
                                if (!payload || payload.length === 0) return null
                                return (
                                    <div
                                        className="tooltip-scroll"
                                        style={{
                                            backgroundColor: '#212529',
                                            border: '1px solid #3a3f44',
                                            borderRadius: 8,
                                            padding: '8px 12px',
                                            maxHeight: 300,
                                            overflowY: 'auto',
                                        }}
                                    >
                                        <p style={{ margin: '0 0 4px', color: '#b0afaf' }}>{label}</p>
                                        {[...payload].sort((a, b) => Number(b.value) - Number(a.value)).map(item => (
                                            <p key={item.name} style={{ margin: '2px 0', color: item.color }}>
                                                {item.name} : {item.value}%
                                            </p>
                                        ))}
                                    </div>
                                )
Adrien Delmastro's avatar
Adrien Delmastro a validé
                            }}
                            wrapperStyle={{ zIndex: 10, pointerEvents: 'auto' }}
                        />
                        <Legend
                            wrapperStyle={{
                                paddingTop: '20px',
                            }}
Adrien Delmastro's avatar
Adrien Delmastro a validé
                        />
                        {visibleRegions.map(region => (
Adrien Delmastro's avatar
Adrien Delmastro a validé
                            <Line
                                key={region}
                                type="monotone"
                                dataKey={region}
                                stroke={COLORS[regions.indexOf(region) % COLORS.length]}
Adrien Delmastro's avatar
Adrien Delmastro a validé
                                strokeWidth={2}
                                dot={{ r: 4 }}
                                activeDot={{ r: 6 }}
                            />
                        ))}
                    </LineChart>
                </ResponsiveContainer>
            )}
        </div>
    )
}