Initial commit
This commit is contained in:
515
app/(main)/observatoire/page.tsx
Normal file
515
app/(main)/observatoire/page.tsx
Normal file
@@ -0,0 +1,515 @@
|
||||
'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;
|
||||
Reference in New Issue
Block a user