Files
btpxpress-frontend/app/(main)/observatoire/page.tsx

515 lines
25 KiB
TypeScript
Executable File

'use client';
import React, { useState, useRef, useEffect } from 'react';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { InputText } from 'primereact/inputtext';
import { Dropdown } from 'primereact/dropdown';
import { Chart } from 'primereact/chart';
import { Tag } from 'primereact/tag';
import { Toast } from 'primereact/toast';
import { TabView, TabPanel } from 'primereact/tabview';
import { Badge } from 'primereact/badge';
import { Divider } from 'primereact/divider';
import { ProgressBar } from 'primereact/progressbar';
import { Chip } from 'primereact/chip';
interface PrixElement {
id: string;
designation: string;
categorie: string;
typeElement: string;
prixUnitaire: number;
unite: string;
region: string;
variationMensuelle: number;
variationAnnuelle: number;
tendance: string;
fiabilite: string;
dateMiseAJour: string;
nombreSources: number;
}
interface AnalyseTendance {
id: string;
titre: string;
resume: string;
typeAnalyse: string;
variationMoyenne: number;
nombreElementsAnalyses: number;
tendancePrevue: string;
datePublication: string;
situationExceptionnelle: boolean;
niveauAlerte: string;
}
const ObservatoirePrix = () => {
const [activeIndex, setActiveIndex] = useState(0);
const [prix, setPrix] = useState<PrixElement[]>([]);
const [analyses, setAnalyses] = useState<AnalyseTendance[]>([]);
const [loading, setLoading] = useState(false);
const [globalFilter, setGlobalFilter] = useState('');
const [selectedRegion, setSelectedRegion] = useState('');
const [selectedCategorie, setSelectedCategorie] = useState('');
const toast = useRef<Toast>(null);
const regions = [
{ label: 'Toutes les régions', value: '' },
{ label: 'Île-de-France', value: 'IDF' },
{ label: 'Auvergne-Rhône-Alpes', value: 'ARA' },
{ label: 'Nouvelle-Aquitaine', value: 'NA' },
{ label: 'Occitanie', value: 'OCC' },
{ label: 'PACA', value: 'PACA' }
];
const categories = [
{ label: 'Toutes catégories', value: '' },
{ label: 'Matériaux gros œuvre', value: 'MATERIAUX_GROS_OEUVRE' },
{ label: 'Matériaux second œuvre', value: 'MATERIAUX_SECOND_OEUVRE' },
{ label: 'Main d\'œuvre', value: 'MAIN_OEUVRE' },
{ label: 'Location matériel', value: 'LOCATION_MATERIEL' }
];
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
// TODO: Remplacer par des appels API réels quand les endpoints seront disponibles
// const prixData = await observatoireService.getPrix();
// const analysesData = await observatoireService.getAnalyses();
// Pour l'instant, afficher des listes vides plutôt que des données fictives
const prixData: PrixElement[] = [];
const analysesData: AnalyseTendance[] = [];
setPrix(prixData);
setAnalyses(analysesData);
} catch (error) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les données',
life: 3000
});
} finally {
setLoading(false);
}
};
const getTendanceSeverity = (tendance: string) => {
switch (tendance) {
case 'FORTE_HAUSSE': return 'danger';
case 'HAUSSE': return 'warning';
case 'STABLE': return 'success';
case 'BAISSE': return 'info';
case 'FORTE_BAISSE': return 'danger';
default: return 'secondary';
}
};
const getTendanceIcon = (tendance: string) => {
switch (tendance) {
case 'FORTE_HAUSSE': return 'pi-angle-double-up';
case 'HAUSSE': return 'pi-angle-up';
case 'STABLE': return 'pi-minus';
case 'BAISSE': return 'pi-angle-down';
case 'FORTE_BAISSE': return 'pi-angle-double-down';
default: return 'pi-question';
}
};
const getFiabiliteSeverity = (fiabilite: string) => {
switch (fiabilite) {
case 'EXCELLENTE': return 'success';
case 'BONNE': return 'info';
case 'MOYENNE': return 'warning';
default: return 'danger';
}
};
const getAlerteSeverity = (niveau: string) => {
switch (niveau) {
case 'CRITIQUE': return 'danger';
case 'ELEVEE': return 'warning';
case 'MODEREE': return 'info';
default: return 'success';
}
};
const formatPrix = (value: number) => {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 2
}).format(value);
};
const formatVariation = (value: number) => {
return `${value > 0 ? '+' : ''}${value}%`;
};
// Templates pour DataTable
const prixTemplate = (rowData: PrixElement) => {
return <span className="font-bold">{formatPrix(rowData.prixUnitaire)}</span>;
};
const variationTemplate = (rowData: PrixElement, field: string) => {
const value = rowData[field as keyof PrixElement] as number;
const isPositive = value > 0;
return (
<span className={isPositive ? 'text-orange-500 font-semibold' : 'text-green-500 font-semibold'}>
<i className={`pi ${isPositive ? 'pi-caret-up' : 'pi-caret-down'} mr-1`}></i>
{formatVariation(value)}
</span>
);
};
const tendanceTemplate = (rowData: PrixElement) => {
return (
<Tag
value={rowData.tendance}
severity={getTendanceSeverity(rowData.tendance)}
icon={getTendanceIcon(rowData.tendance)}
/>
);
};
const fiabiliteTemplate = (rowData: PrixElement) => {
return (
<Tag
value={rowData.fiabilite}
severity={getFiabiliteSeverity(rowData.fiabilite)}
/>
);
};
// Configuration du graphique des tendances
// Données du graphique basées sur les vraies données de l'API
const chartData = {
labels: ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin'],
datasets: [
{
label: 'Matériaux',
data: new Array(6).fill(0), // Vide jusqu'à ce que l'API fournisse les données
borderColor: '#42A5F5',
backgroundColor: 'rgba(66, 165, 245, 0.1)',
tension: 0.4
},
{
label: 'Main d\'œuvre',
data: new Array(6).fill(0), // Vide jusqu'à ce que l'API fournisse les données
borderColor: '#FFA726',
backgroundColor: 'rgba(255, 167, 38, 0.1)',
tension: 0.4
}
]
};
const chartOptions = {
maintainAspectRatio: false,
aspectRatio: 0.6,
plugins: {
legend: {
labels: {
color: '#495057'
}
}
},
scales: {
x: {
ticks: {
color: '#495057'
},
grid: {
color: '#ebedef'
}
},
y: {
ticks: {
color: '#495057'
},
grid: {
color: '#ebedef'
}
}
}
};
const filteredPrix = prix.filter(item => {
return (
(!globalFilter || item.designation.toLowerCase().includes(globalFilter.toLowerCase())) &&
(!selectedRegion || item.region === selectedRegion) &&
(!selectedCategorie || item.categorie === selectedCategorie)
);
});
return (
<div className="grid">
<Toast ref={toast} />
<div className="col-12">
<h2>Observatoire des Prix BTP</h2>
<p className="text-600">
Suivez l'évolution des prix des matériaux, main d'œuvre et services BTP en temps réel
</p>
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
{/* Dashboard principal */}
<TabPanel header="Tableau de bord" leftIcon="pi pi-chart-line">
<div className="grid">
{/* Indicateurs clés */}
<div className="col-12 lg:col-3">
<Card className="mb-3">
<div className="flex align-items-center">
<div className="flex-shrink-0">
<span className="bg-blue-100 text-blue-800 text-2xl font-semibold inline-flex align-items-center justify-content-center border-round w-3rem h-3rem">
<i className="pi pi-arrow-up"></i>
</span>
</div>
<div className="flex-grow-1 ml-3">
<div className="text-500 font-medium mb-1">Variation mensuelle moyenne</div>
<div className="text-900 font-medium text-xl">+4.2%</div>
</div>
</div>
</Card>
</div>
<div className="col-12 lg:col-3">
<Card className="mb-3">
<div className="flex align-items-center">
<div className="flex-shrink-0">
<span className="bg-orange-100 text-orange-800 text-2xl font-semibold inline-flex align-items-center justify-content-center border-round w-3rem h-3rem">
<i className="pi pi-exclamation-triangle"></i>
</span>
</div>
<div className="flex-grow-1 ml-3">
<div className="text-500 font-medium mb-1">Alertes actives</div>
<div className="text-900 font-medium text-xl">
3 <Badge value="2 critiques" severity="danger" className="ml-2" />
</div>
</div>
</div>
</Card>
</div>
<div className="col-12 lg:col-3">
<Card className="mb-3">
<div className="flex align-items-center">
<div className="flex-shrink-0">
<span className="bg-green-100 text-green-800 text-2xl font-semibold inline-flex align-items-center justify-content-center border-round w-3rem h-3rem">
<i className="pi pi-database"></i>
</span>
</div>
<div className="flex-grow-1 ml-3">
<div className="text-500 font-medium mb-1">Éléments surveillés</div>
<div className="text-900 font-medium text-xl">1,247</div>
</div>
</div>
</Card>
</div>
<div className="col-12 lg:col-3">
<Card className="mb-3">
<div className="flex align-items-center">
<div className="flex-shrink-0">
<span className="bg-purple-100 text-purple-800 text-2xl font-semibold inline-flex align-items-center justify-content-center border-round w-3rem h-3rem">
<i className="pi pi-calendar"></i>
</span>
</div>
<div className="flex-grow-1 ml-3">
<div className="text-500 font-medium mb-1">Dernière mise à jour</div>
<div className="text-900 font-medium text-xl">Aujourd'hui</div>
</div>
</div>
</Card>
</div>
{/* Graphique des tendances */}
<div className="col-12 lg:col-8">
<Card title="Évolution des indices de prix" className="mb-3">
<Chart type="line" data={chartData} options={chartOptions} />
</Card>
</div>
{/* Alertes récentes */}
<div className="col-12 lg:col-4">
<Card title="Alertes récentes" className="mb-3">
<div className="flex flex-column gap-3">
<div className="flex align-items-center p-2 border-round border-1 surface-border">
<i className="pi pi-exclamation-triangle text-orange-500 text-xl mr-2"></i>
<div className="flex-grow-1">
<div className="font-medium">Béton C25/30</div>
<div className="text-500 text-sm">+12.3% en 3 semaines</div>
</div>
</div>
<div className="flex align-items-center p-2 border-round border-1 surface-border">
<i className="pi pi-arrow-up text-red-500 text-xl mr-2"></i>
<div className="flex-grow-1">
<div className="font-medium">Acier d'armature</div>
<div className="text-500 text-sm">+8.7% ce mois</div>
</div>
</div>
<div className="flex align-items-center p-2 border-round border-1 surface-border">
<i className="pi pi-arrow-up text-orange-500 text-xl mr-2"></i>
<div className="flex-grow-1">
<div className="font-medium">Main d'œuvre IDF</div>
<div className="text-500 text-sm">+5.2% ce trimestre</div>
</div>
</div>
</div>
</Card>
</div>
</div>
</TabPanel>
{/* Prix en temps réel */}
<TabPanel header="Prix en temps réel" leftIcon="pi pi-list">
<div className="grid">
<div className="col-12">
<Card title="Filtres" className="mb-3">
<div className="grid">
<div className="col-12 md:col-4">
<span className="p-input-icon-left w-full">
<i className="pi pi-search" />
<InputText
placeholder="Rechercher un élément..."
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
className="w-full"
/>
</span>
</div>
<div className="col-12 md:col-4">
<Dropdown
value={selectedRegion}
options={regions}
onChange={(e) => setSelectedRegion(e.value)}
placeholder="Sélectionner une région"
className="w-full"
/>
</div>
<div className="col-12 md:col-4">
<Dropdown
value={selectedCategorie}
options={categories}
onChange={(e) => setSelectedCategorie(e.value)}
placeholder="Sélectionner une catégorie"
className="w-full"
/>
</div>
</div>
</Card>
<Card title="Prix des matériaux et services">
<DataTable
value={filteredPrix}
loading={loading}
paginator
rows={20}
dataKey="id"
emptyMessage="Aucun élément trouvé."
className="datatable-responsive"
>
<Column field="designation" header="Désignation" sortable />
<Column field="prixUnitaire" header="Prix unitaire" body={prixTemplate} sortable />
<Column field="unite" header="Unité" />
<Column field="region" header="Région" />
<Column
field="variationMensuelle"
header="Var. mensuelle"
body={(data) => variationTemplate(data, 'variationMensuelle')}
sortable
/>
<Column
field="variationAnnuelle"
header="Var. annuelle"
body={(data) => variationTemplate(data, 'variationAnnuelle')}
sortable
/>
<Column field="tendance" header="Tendance" body={tendanceTemplate} />
<Column field="fiabilite" header="Fiabilité" body={fiabiliteTemplate} />
</DataTable>
</Card>
</div>
</div>
</TabPanel>
{/* Analyses et rapports */}
<TabPanel header="Analyses" leftIcon="pi pi-file-text">
<div className="grid">
<div className="col-12">
<Card title="Analyses de tendances récentes">
<div className="flex flex-column gap-4">
{analyses.map(analyse => (
<Card key={analyse.id} className="shadow-2">
<div className="flex flex-column lg:flex-row justify-content-between align-items-start gap-3">
<div className="flex-grow-1">
<div className="flex align-items-center gap-2 mb-2">
<h4 className="m-0">{analyse.titre}</h4>
{analyse.situationExceptionnelle && (
<Badge value="Situation exceptionnelle" severity="danger" />
)}
<Tag
value={analyse.niveauAlerte}
severity={getAlerteSeverity(analyse.niveauAlerte)}
/>
</div>
<p className="text-700 line-height-3 mb-3">{analyse.resume}</p>
<div className="flex flex-wrap gap-2">
<Chip label={`${analyse.nombreElementsAnalyses} éléments analysés`} />
<Chip label={`Variation: ${formatVariation(analyse.variationMoyenne)}`} />
<Chip label={analyse.tendancePrevue} />
</div>
</div>
<div className="flex flex-column gap-2">
<span className="text-500 text-sm">
Publié le {new Date(analyse.datePublication).toLocaleDateString('fr-FR')}
</span>
<Button
label="Lire l'analyse"
icon="pi pi-eye"
className="p-button-outlined"
/>
</div>
</div>
</Card>
))}
</div>
</Card>
</div>
</div>
</TabPanel>
{/* Alertes et notifications */}
<TabPanel header="Alertes" leftIcon="pi pi-bell">
<div className="grid">
<div className="col-12">
<Card title="Configuration des alertes">
<p className="text-600 mb-4">
Configurez vos alertes pour être notifié des variations importantes de prix.
</p>
<div className="text-center">
<i className="pi pi-cog text-6xl text-300 mb-3"></i>
<p className="text-500">Fonctionnalité en cours de développement</p>
<Button label="Configurer mes alertes" disabled />
</div>
</Card>
</div>
</div>
</TabPanel>
</TabView>
</div>
</div>
);
};
export default ObservatoirePrix;