points.tsx 8,06 ko
Newer Older
'use client'

import { useEffect, useRef, useState } from 'react'
import { Scatter } from 'react-chartjs-2'
import { Chart as ChartJS, LinearScale, PointElement, Tooltip, Legend } from 'chart.js'

ChartJS.register(LinearScale, PointElement, Tooltip, Legend)
import { searchDepartments, getCorrelation, type Department, type CorrelationPoint } from '../../services/points.services'
import { TAX_TYPES, YEARS } from '../../constants'
import ErrorDiv from '../molecules/ErrorDiv'

export default function Points() {
    const [results, setResults] = useState<Department[]>([])
    const [departmentCode, setDepartmentCode] = useState<string>('')
    const [search, setSearch] = useState('')
    const [showDropdown, setShowDropdown] = useState(false)
    const [taxType, setTaxType] = useState<string>('th')
    const [year, setYear] = useState<number>(2019)
    const [data, setData] = useState<CorrelationPoint[]>([])
    const [loading, setLoading] = useState(false)
    const [error, setError] = useState<string | null>(null)
    const dropdownRef = useRef<HTMLDivElement>(null)
    const debounceRef = useRef<ReturnType<typeof setTimeout>>(null)
    const [selectedDept, setSelectedDept] = useState<Department | null>(null)

    useEffect(() => {
        const handleClickOutside = (e: MouseEvent) => {
            if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
                setShowDropdown(false)
            }
        }
        document.addEventListener('mousedown', handleClickOutside)
        return () => document.removeEventListener('mousedown', handleClickOutside)
    }, [])

    const handleSearchChange = (value: string) => {
        setSearch(value)
        setDepartmentCode('')
        setShowDropdown(true)
        if (debounceRef.current) clearTimeout(debounceRef.current)
        if (value.length < 2) {
            setResults([])
            return
        }
        debounceRef.current = setTimeout(() => {
            searchDepartments(value).then(setResults)
        }, 300)
    }

    const selectDepartment = (dept: Department) => {
        setDepartmentCode(dept.departmentCode)
        setSelectedDept(dept)
        setSearch(`${dept.departmentCode} - ${dept.departmentName}`)
        setShowDropdown(false)
    }

    const handleSubmit = async () => {
        if (!departmentCode) return
        setLoading(true)
        setError(null)
        try {
            const result = await getCorrelation(departmentCode, taxType, year)
            setData(result)
        } catch (e) {
            setData([])
            setError(e instanceof Error ? e.message : 'Une erreur est survenue')
        } finally {
            setLoading(false)
        }
    }

    const chartConfig = {
        datasets: [
            {
                label: 'Communes',
                data: data.map(p => ({ x: p.rate, y: p.amount / 1_000_000, commune: p.commune_name })),
                borderColor: 'rgba(136, 132, 216, 0.8)',
                backgroundColor: 'rgba(136, 132, 216, 0.8)',
                pointStyle: 'cross' as const,
                pointRadius: 4,
                borderWidth: 2,
            },
        ],
    }

    const chartOptions = {
        responsive: true,
        maintainAspectRatio: false,
        scales: {
            x: {
                title: { display: true, text: 'Taux (%)', color: '#b0afaf' },
                ticks: { color: '#b0afaf' },
                grid: { color: '#3a3f44' },
            },
            y: {
                title: { display: true, text: 'Volume (M€)', color: '#b0afaf' },
                ticks: { color: '#b0afaf' },
                grid: { color: '#3a3f44' },
            },
        },
        plugins: {
            legend: { display: false },
            tooltip: {
                backgroundColor: '#212529',
                borderColor: '#3a3f44',
                borderWidth: 1,
                callbacks: {
                    label: (ctx: { raw: unknown; parsed: { x: number | null; y: number | null } }) => {
                        const raw = ctx.raw as { commune?: string } | undefined
                        const name = raw?.commune ?? ''
                        return [`${name}`, `Taux: ${ctx.parsed.x}%`, `Volume: ${(ctx.parsed.y ?? 0).toFixed(3)} M€`]
                    },
                },
            },
        },
    }

    return (
        <div className="p-6 text-white flex flex-col gap-6">
            <h2 className="text-lg font-semibold">Relation taux d&apos;imposition / volume collecté</h2>

            <div className="flex flex-wrap gap-4 items-end">
                <div className="flex flex-col gap-1 relative" ref={dropdownRef}>
                    <label className="text-xs text-[#b0afaf] uppercase">Département</label>
                    <input
                        value={search}
                        onChange={e => handleSearchChange(e.target.value)}
                        onFocus={() => search.length >= 2 && setShowDropdown(true)}
                        placeholder="Code ou nom..."
                        className="bg-[#212529] border border-[#3a3f44] rounded px-3 py-1.5 text-white text-sm w-64"
                    />
                    {showDropdown && results.length > 0 && (
                        <div className="absolute top-full mt-1 w-64 max-h-60 overflow-auto bg-[#212529] border border-[#3a3f44] rounded z-10">
                            {results.map(d => (
                                <button
                                    key={d.departmentCode}
                                    onClick={() => selectDepartment(d)}
                                    className="w-full text-left px-3 py-1.5 text-sm text-[#b0afaf] hover:bg-[#181C1F] hover:text-white transition-colors"
                                >
                                    {d.departmentCode} - {d.departmentName}
                                </button>
                            ))}
                        </div>
                    )}
                </div>

                <div className="flex flex-col gap-1">
                    <label className="text-xs text-[#b0afaf] uppercase">Taxe</label>
                    <select
                        value={taxType}
                        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 => (
                            <option key={t} value={t}>
                                {t.toUpperCase()}
                            </option>
                        ))}
                    </select>
                </div>

                <div className="flex flex-col gap-1">
                    <label className="text-xs text-[#b0afaf] uppercase">Année</label>
                    <select
                        value={year}
                        onChange={e => setYear(Number(e.target.value))}
                        className="bg-[#212529] border border-[#3a3f44] rounded px-3 py-1.5 text-white text-sm"
                    >
                        {YEARS.map(y => (
                            <option key={y} value={y}>
                                {y}
                            </option>
                        ))}
                    </select>
                </div>

                <button
                    onClick={handleSubmit}
                    disabled={loading || !departmentCode}
                    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>

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

            {!error && data.length > 0 && (
                <>
                    <p className="text-sm text-[#b0afaf]">
                        {selectedDept?.departmentName}{data.length} communes
                    </p>
                    <div className="h-[450px]">
                        <Scatter data={chartConfig} options={chartOptions} />
                    </div>
                </>
            )}
        </div>
    )
}