Files
btpxpress-frontend/app/(main)/dashboard/temps-reel/page.tsx

472 lines
20 KiB
TypeScript
Executable File

'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;