472 lines
20 KiB
TypeScript
472 lines
20 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Card } from 'primereact/card';
|
|
import { Chart } from 'primereact/chart';
|
|
import { Badge } from 'primereact/badge';
|
|
import { Tag } from 'primereact/tag';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { Button } from 'primereact/button';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { Knob } from 'primereact/knob';
|
|
import { Timeline } from 'primereact/timeline';
|
|
import { Divider } from 'primereact/divider';
|
|
import { Toast } from 'primereact/toast';
|
|
|
|
/**
|
|
* Dashboard Temps Réel BTP Express
|
|
* Surveillance en temps réel des chantiers, équipes et performances
|
|
*/
|
|
const DashboardTempsReel = () => {
|
|
const [donneesTR, setDonneesTR] = useState<any>({});
|
|
const [derniereMiseAJour, setDerniereMiseAJour] = useState<Date>(new Date());
|
|
const [alertesActives, setAlertesActives] = useState<any[]>([]);
|
|
const [evolutionCA, setEvolutionCA] = useState<any>({});
|
|
const [interventionsUrgentes, setInterventionsUrgentes] = useState<any[]>([]);
|
|
const toast = useRef<Toast>(null);
|
|
|
|
useEffect(() => {
|
|
// Simulation données temps réel
|
|
chargerDonneesTempsReel();
|
|
|
|
// Mise à jour automatique toutes les 30 secondes
|
|
const interval = setInterval(() => {
|
|
chargerDonneesTempsReel();
|
|
}, 30000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const chargerDonneesTempsReel = () => {
|
|
// TODO: Remplacer par des appels API réels pour les données temps réel
|
|
// const donnees = await dashboardService.getDonneesTempsReel();
|
|
|
|
// Initialisation avec des données vides plutôt que des données fictives
|
|
const donnees = {
|
|
chantiersEnCours: {
|
|
total: 0,
|
|
nouveauxAujourdhui: 0,
|
|
terminesAujourdhui: 0,
|
|
enRetard: 0
|
|
},
|
|
equipes: {
|
|
totalActives: 0,
|
|
surSite: 0,
|
|
disponibles: 0,
|
|
enDeplacementProchain: 0
|
|
},
|
|
materiel: {
|
|
enUtilisation: 0,
|
|
disponible: 0,
|
|
enMaintenance: 0,
|
|
alertesStock: 0
|
|
},
|
|
financier: {
|
|
caJournalier: 0,
|
|
objectifJour: 0,
|
|
margeJour: 0,
|
|
facturations: 0
|
|
},
|
|
securite: {
|
|
accidentsJour: 0,
|
|
joursDepuisDernierAccident: 0,
|
|
controlsSecurite: 0,
|
|
epiOk: 0
|
|
}
|
|
};
|
|
|
|
setDonneesTR(donnees);
|
|
setDerniereMiseAJour(new Date());
|
|
|
|
// Alertes vides jusqu'à ce que l'API fournisse les vraies données
|
|
setAlertesActives([]);
|
|
|
|
// Graphique avec données vides
|
|
setEvolutionCA({
|
|
labels: ['08h', '10h', '12h', '14h', '16h', '18h'],
|
|
datasets: [
|
|
{
|
|
label: 'CA Réalisé',
|
|
data: new Array(6).fill(0),
|
|
borderColor: '#4BC0C0',
|
|
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
|
tension: 0.4,
|
|
fill: true
|
|
},
|
|
{
|
|
label: 'Objectif',
|
|
data: new Array(6).fill(0),
|
|
borderColor: '#FF6B6B',
|
|
backgroundColor: 'rgba(255, 107, 107, 0.1)',
|
|
borderDash: [5, 5],
|
|
tension: 0.4,
|
|
fill: false
|
|
}
|
|
]
|
|
});
|
|
|
|
// Interventions vides jusqu'à ce que l'API fournisse les vraies données
|
|
setInterventionsUrgentes([]);
|
|
};
|
|
|
|
const chartOptions = {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top' as const,
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: 'Évolution CA Journalier'
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: (value: any) => `${value}€`
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const getAlerteSeverityConfig = (severity: string) => {
|
|
switch (severity) {
|
|
case 'high':
|
|
return { color: 'danger', icon: 'pi-exclamation-triangle' };
|
|
case 'medium':
|
|
return { color: 'warning', icon: 'pi-info-circle' };
|
|
case 'low':
|
|
return { color: 'info', icon: 'pi-check-circle' };
|
|
default:
|
|
return { color: 'secondary', icon: 'pi-info' };
|
|
}
|
|
};
|
|
|
|
const getUrgenceConfig = (urgence: string) => {
|
|
switch (urgence) {
|
|
case 'CRITIQUE':
|
|
return { color: 'danger', label: 'CRITIQUE' };
|
|
case 'HAUTE':
|
|
return { color: 'warning', label: 'HAUTE' };
|
|
case 'MOYENNE':
|
|
return { color: 'info', label: 'MOYENNE' };
|
|
default:
|
|
return { color: 'secondary', label: urgence };
|
|
}
|
|
};
|
|
|
|
const actualiserDonnees = () => {
|
|
chargerDonneesTempsReel();
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Données actualisées',
|
|
detail: 'Mise à jour terminée',
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="grid">
|
|
<Toast ref={toast} />
|
|
|
|
{/* En-tête avec actualisation */}
|
|
<div className="col-12">
|
|
<div className="flex justify-content-between align-items-center mb-4">
|
|
<h2 className="text-2xl font-bold m-0">
|
|
<i className="pi pi-radar mr-2 text-primary" />
|
|
Dashboard Temps Réel BTP
|
|
</h2>
|
|
|
|
<div className="flex align-items-center gap-3">
|
|
<span className="text-sm text-color-secondary">
|
|
Dernière MAJ: {derniereMiseAJour.toLocaleTimeString('fr-FR')}
|
|
</span>
|
|
<Button
|
|
icon="pi pi-refresh"
|
|
label="Actualiser"
|
|
size="small"
|
|
onClick={actualiserDonnees}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* KPIs Temps Réel */}
|
|
<div className="col-12 md:col-6 lg:col-3">
|
|
<Card className="h-full">
|
|
<div className="flex justify-content-between align-items-start">
|
|
<div>
|
|
<div className="text-2xl font-bold text-blue-500">
|
|
{donneesTR.chantiersEnCours?.total || 0}
|
|
</div>
|
|
<div className="text-color-secondary mb-2">Chantiers actifs</div>
|
|
<div className="flex gap-2">
|
|
<Badge value={`+${donneesTR.chantiersEnCours?.nouveauxAujourdhui || 0}`} severity="success" />
|
|
<Badge value={`${donneesTR.chantiersEnCours?.enRetard || 0} retard`} severity="danger" />
|
|
</div>
|
|
</div>
|
|
<i className="pi pi-map text-blue-500 text-3xl" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-6 lg:col-3">
|
|
<Card className="h-full">
|
|
<div className="flex justify-content-between align-items-start">
|
|
<div>
|
|
<div className="text-2xl font-bold text-green-500">
|
|
{donneesTR.equipes?.surSite || 0}
|
|
</div>
|
|
<div className="text-color-secondary mb-2">Personnes sur site</div>
|
|
<div className="flex gap-2">
|
|
<Badge value={`${donneesTR.equipes?.totalActives || 0} équipes`} severity="info" />
|
|
<Badge value={`${donneesTR.equipes?.disponibles || 0} dispo`} severity="success" />
|
|
</div>
|
|
</div>
|
|
<i className="pi pi-users text-green-500 text-3xl" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-6 lg:col-3">
|
|
<Card className="h-full">
|
|
<div className="flex justify-content-between align-items-start">
|
|
<div>
|
|
<div className="text-2xl font-bold text-purple-500">
|
|
{new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
notation: 'compact'
|
|
}).format(donneesTR.financier?.caJournalier || 0)}
|
|
</div>
|
|
<div className="text-color-secondary mb-2">CA Journalier</div>
|
|
<ProgressBar
|
|
value={((donneesTR.financier?.caJournalier || 0) / (donneesTR.financier?.objectifJour || 1)) * 100}
|
|
style={{ height: '8px' }}
|
|
color="#8B5CF6"
|
|
/>
|
|
</div>
|
|
<i className="pi pi-euro text-purple-500 text-3xl" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-6 lg:col-3">
|
|
<Card className="h-full">
|
|
<div className="flex justify-content-between align-items-start">
|
|
<div>
|
|
<div className="text-center">
|
|
<Knob
|
|
value={donneesTR.securite?.epiOk || 0}
|
|
size={60}
|
|
strokeWidth={8}
|
|
valueColor="#10B981"
|
|
rangeColor="#E5E7EB"
|
|
/>
|
|
</div>
|
|
<div className="text-color-secondary text-center mt-2">EPI Conformes</div>
|
|
<div className="text-center">
|
|
<Badge value={`${donneesTR.securite?.joursDepuisDernierAccident || 0} jours`} severity="success" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Graphique évolution CA */}
|
|
<div className="col-12 lg:col-8">
|
|
<Card title="Évolution CA Temps Réel">
|
|
<Chart type="line" data={evolutionCA} options={chartOptions} height="300px" />
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Alertes en cours */}
|
|
<div className="col-12 lg:col-4">
|
|
<Card title="Alertes Actives" className="h-full">
|
|
<div className="flex flex-column gap-3">
|
|
{alertesActives.map((alerte) => {
|
|
const config = getAlerteSeverityConfig(alerte.severity);
|
|
return (
|
|
<div key={alerte.id} className="border-1 border-round p-3 surface-border">
|
|
<div className="flex align-items-start gap-2">
|
|
<i className={`pi ${config.icon} text-${config.color} mt-1`} />
|
|
<div className="flex-1">
|
|
<div className="text-sm font-medium">{alerte.message}</div>
|
|
<div className="text-xs text-color-secondary mt-1">
|
|
{alerte.heure.toLocaleTimeString('fr-FR')}
|
|
</div>
|
|
<Tag
|
|
value={alerte.type}
|
|
severity={config.color as any}
|
|
className="mt-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{alertesActives.length === 0 && (
|
|
<div className="text-center text-color-secondary p-4">
|
|
<i className="pi pi-check-circle text-green-500 text-3xl mb-2" />
|
|
<div>Aucune alerte active</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Interventions urgentes */}
|
|
<div className="col-12">
|
|
<Card title="Interventions Urgentes en Cours">
|
|
<DataTable
|
|
value={interventionsUrgentes}
|
|
emptyMessage="Aucune intervention urgente"
|
|
responsiveLayout="scroll"
|
|
>
|
|
<Column
|
|
header="Type"
|
|
body={(rowData) => (
|
|
<Tag
|
|
value={rowData.type.replace('_', ' ')}
|
|
severity="warning"
|
|
icon="pi pi-exclamation-triangle"
|
|
/>
|
|
)}
|
|
/>
|
|
|
|
<Column field="chantier" header="Chantier" />
|
|
|
|
<Column field="description" header="Description" />
|
|
|
|
<Column
|
|
header="Urgence"
|
|
body={(rowData) => {
|
|
const config = getUrgenceConfig(rowData.urgence);
|
|
return <Tag value={config.label} severity={config.color as any} />;
|
|
}}
|
|
/>
|
|
|
|
<Column
|
|
header="Temps écoulé"
|
|
body={(rowData) => (
|
|
<Badge value={rowData.tempsEcoule} severity="danger" />
|
|
)}
|
|
/>
|
|
|
|
<Column field="technicien" header="Technicien assigné" />
|
|
|
|
<Column
|
|
body={(rowData) => (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
icon="pi pi-phone"
|
|
size="small"
|
|
severity="success"
|
|
tooltip="Appeler"
|
|
/>
|
|
<Button
|
|
icon="pi pi-map-marker"
|
|
size="small"
|
|
severity="info"
|
|
tooltip="Localiser"
|
|
/>
|
|
<Button
|
|
icon="pi pi-check"
|
|
size="small"
|
|
severity="warning"
|
|
tooltip="Marquer résolu"
|
|
/>
|
|
</div>
|
|
)}
|
|
/>
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Matériel et ressources */}
|
|
<div className="col-12 md:col-6">
|
|
<Card title="État Matériel">
|
|
<div className="grid">
|
|
<div className="col-6">
|
|
<div className="text-center">
|
|
<div className="text-xl font-bold text-green-500">
|
|
{donneesTR.materiel?.enUtilisation || 0}
|
|
</div>
|
|
<div className="text-sm text-color-secondary">En utilisation</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-6">
|
|
<div className="text-center">
|
|
<div className="text-xl font-bold text-blue-500">
|
|
{donneesTR.materiel?.disponible || 0}
|
|
</div>
|
|
<div className="text-sm text-color-secondary">Disponible</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-6">
|
|
<div className="text-center">
|
|
<div className="text-xl font-bold text-orange-500">
|
|
{donneesTR.materiel?.enMaintenance || 0}
|
|
</div>
|
|
<div className="text-sm text-color-secondary">En maintenance</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-6">
|
|
<div className="text-center">
|
|
<div className="text-xl font-bold text-red-500">
|
|
{donneesTR.materiel?.alertesStock || 0}
|
|
</div>
|
|
<div className="text-sm text-color-secondary">Alertes stock</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Performances financières */}
|
|
<div className="col-12 md:col-6">
|
|
<Card title="Performances Financières">
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<div className="flex justify-content-between align-items-center mb-3">
|
|
<span>Objectif journalier</span>
|
|
<span className="font-bold">
|
|
{((donneesTR.financier?.caJournalier || 0) / (donneesTR.financier?.objectifJour || 1) * 100).toFixed(1)}%
|
|
</span>
|
|
</div>
|
|
<ProgressBar
|
|
value={((donneesTR.financier?.caJournalier || 0) / (donneesTR.financier?.objectifJour || 1)) * 100}
|
|
style={{ height: '12px' }}
|
|
/>
|
|
</div>
|
|
|
|
<div className="col-6">
|
|
<div className="text-center">
|
|
<div className="text-xl font-bold text-green-500">
|
|
{donneesTR.financier?.margeJour || 0}%
|
|
</div>
|
|
<div className="text-sm text-color-secondary">Marge journalière</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-6">
|
|
<div className="text-center">
|
|
<div className="text-xl font-bold text-cyan-500">
|
|
{donneesTR.financier?.facturations || 0}
|
|
</div>
|
|
<div className="text-sm text-color-secondary">Facturations</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DashboardTempsReel;
|
|
|