- Correction des erreurs TypeScript dans userService.ts et workflowTester.ts - Ajout des propriétés manquantes aux objets User mockés - Conversion des dates de string vers objets Date - Correction des appels asynchrones et des types incompatibles - Ajout de dynamic rendering pour résoudre les erreurs useSearchParams - Enveloppement de useSearchParams dans Suspense boundary - Configuration de force-dynamic au niveau du layout principal Build réussi: 126 pages générées avec succès 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
517 lines
25 KiB
TypeScript
517 lines
25 KiB
TypeScript
'use client';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
|
|
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; |