- 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>
701 lines
29 KiB
TypeScript
701 lines
29 KiB
TypeScript
'use client';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Card } from 'primereact/card';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { Tag } from 'primereact/tag';
|
|
import { Button } from 'primereact/button';
|
|
import { Toast } from 'primereact/toast';
|
|
import { Dropdown } from 'primereact/dropdown';
|
|
import { Calendar } from 'primereact/calendar';
|
|
import { Badge } from 'primereact/badge';
|
|
import { Timeline } from 'primereact/timeline';
|
|
import { Message } from 'primereact/message';
|
|
import { Divider } from 'primereact/divider';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
interface Alerte {
|
|
id: string;
|
|
titre: string;
|
|
description: string;
|
|
type: string;
|
|
severite: string;
|
|
statut: string;
|
|
dateCreation: Date;
|
|
dateEcheance?: Date;
|
|
source: string;
|
|
entite: string;
|
|
entiteId: string;
|
|
actions: string[];
|
|
responsable?: string;
|
|
lu: boolean;
|
|
archive: boolean;
|
|
}
|
|
|
|
const DashboardAlertes = () => {
|
|
const toast = useRef<Toast>(null);
|
|
const router = useRouter();
|
|
|
|
const [alertes, setAlertes] = useState<Alerte[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [selectedType, setSelectedType] = useState('');
|
|
const [selectedSeverite, setSelectedSeverite] = useState('');
|
|
const [selectedStatut, setSelectedStatut] = useState('');
|
|
const [showArchived, setShowArchived] = useState(false);
|
|
|
|
const [timelineEvents, setTimelineEvents] = useState([]);
|
|
|
|
const typeOptions = [
|
|
{ label: 'Tous les types', value: '' },
|
|
{ label: 'Sécurité', value: 'SECURITE' },
|
|
{ label: 'Maintenance', value: 'MAINTENANCE' },
|
|
{ label: 'Budget', value: 'BUDGET' },
|
|
{ label: 'Planning', value: 'PLANNING' },
|
|
{ label: 'Qualité', value: 'QUALITE' },
|
|
{ label: 'Ressources', value: 'RESSOURCES' },
|
|
{ label: 'Conformité', value: 'CONFORMITE' },
|
|
{ label: 'Système', value: 'SYSTEME' }
|
|
];
|
|
|
|
const severiteOptions = [
|
|
{ label: 'Toutes les sévérités', value: '' },
|
|
{ label: 'Critique', value: 'CRITIQUE' },
|
|
{ label: 'Élevée', value: 'ELEVEE' },
|
|
{ label: 'Moyenne', value: 'MOYENNE' },
|
|
{ label: 'Faible', value: 'FAIBLE' },
|
|
{ label: 'Info', value: 'INFO' }
|
|
];
|
|
|
|
const statutOptions = [
|
|
{ label: 'Tous les statuts', value: '' },
|
|
{ label: 'Nouvelle', value: 'NOUVELLE' },
|
|
{ label: 'En cours', value: 'EN_COURS' },
|
|
{ label: 'Résolue', value: 'RESOLUE' },
|
|
{ label: 'Fermée', value: 'FERMEE' },
|
|
{ label: 'Ignorée', value: 'IGNOREE' }
|
|
];
|
|
|
|
useEffect(() => {
|
|
loadAlertes();
|
|
initTimeline();
|
|
}, [selectedType, selectedSeverite, selectedStatut, showArchived]);
|
|
|
|
const loadAlertes = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// TODO: Remplacer par un vrai appel API
|
|
// const response = await alerteService.getDashboardData({
|
|
// type: selectedType,
|
|
// severite: selectedSeverite,
|
|
// statut: selectedStatut,
|
|
// archived: showArchived
|
|
// });
|
|
|
|
// Données simulées pour la démonstration
|
|
const mockAlertes: Alerte[] = [
|
|
{
|
|
id: '1',
|
|
titre: 'Maintenance urgente requise',
|
|
description: 'La pelleteuse CAT 320D présente des signes de défaillance hydraulique',
|
|
type: 'MAINTENANCE',
|
|
severite: 'CRITIQUE',
|
|
statut: 'NOUVELLE',
|
|
dateCreation: new Date('2024-12-28T10:30:00'),
|
|
dateEcheance: new Date('2024-12-29T17:00:00'),
|
|
source: 'Système de monitoring',
|
|
entite: 'Matériel',
|
|
entiteId: 'MAT-002',
|
|
actions: ['Arrêt immédiat', 'Inspection technique', 'Réparation'],
|
|
responsable: 'Marie Martin',
|
|
lu: false,
|
|
archive: false
|
|
},
|
|
{
|
|
id: '2',
|
|
titre: 'Dépassement budget chantier',
|
|
description: 'Le chantier Résidence Les Jardins dépasse le budget prévu de 15%',
|
|
type: 'BUDGET',
|
|
severite: 'ELEVEE',
|
|
statut: 'EN_COURS',
|
|
dateCreation: new Date('2024-12-27T14:15:00'),
|
|
dateEcheance: new Date('2024-12-30T12:00:00'),
|
|
source: 'Contrôle de gestion',
|
|
entite: 'Chantier',
|
|
entiteId: 'CHT-001',
|
|
actions: ['Analyse des coûts', 'Révision budget', 'Validation client'],
|
|
responsable: 'Jean Dupont',
|
|
lu: true,
|
|
archive: false
|
|
},
|
|
{
|
|
id: '3',
|
|
titre: 'Retard de livraison matériaux',
|
|
description: 'Livraison d\'acier reportée de 3 jours pour le Centre Commercial Atlantis',
|
|
type: 'PLANNING',
|
|
severite: 'MOYENNE',
|
|
statut: 'EN_COURS',
|
|
dateCreation: new Date('2024-12-26T09:45:00'),
|
|
dateEcheance: new Date('2025-01-02T08:00:00'),
|
|
source: 'Fournisseur',
|
|
entite: 'Chantier',
|
|
entiteId: 'CHT-002',
|
|
actions: ['Replanification', 'Contact fournisseur', 'Solutions alternatives'],
|
|
responsable: 'Pierre Leroy',
|
|
lu: true,
|
|
archive: false
|
|
},
|
|
{
|
|
id: '4',
|
|
titre: 'Certification CACES expirée',
|
|
description: 'La certification CACES de Luc Bernard expire dans 7 jours',
|
|
type: 'CONFORMITE',
|
|
severite: 'ELEVEE',
|
|
statut: 'NOUVELLE',
|
|
dateCreation: new Date('2024-12-25T16:20:00'),
|
|
dateEcheance: new Date('2025-01-01T23:59:00'),
|
|
source: 'Gestion RH',
|
|
entite: 'Employé',
|
|
entiteId: 'EMP-004',
|
|
actions: ['Planifier formation', 'Restriction temporaire', 'Renouvellement'],
|
|
responsable: 'Sophie Dubois',
|
|
lu: false,
|
|
archive: false
|
|
},
|
|
{
|
|
id: '5',
|
|
titre: 'Incident sécurité mineur',
|
|
description: 'Chute d\'un ouvrier sans blessure grave sur le chantier Hôtel Luxe',
|
|
type: 'SECURITE',
|
|
severite: 'MOYENNE',
|
|
statut: 'RESOLUE',
|
|
dateCreation: new Date('2024-12-24T11:30:00'),
|
|
source: 'Chef de chantier',
|
|
entite: 'Chantier',
|
|
entiteId: 'CHT-003',
|
|
actions: ['Rapport incident', 'Analyse causes', 'Mesures préventives'],
|
|
responsable: 'Marc Rousseau',
|
|
lu: true,
|
|
archive: false
|
|
},
|
|
{
|
|
id: '6',
|
|
titre: 'Surcharge serveur de monitoring',
|
|
description: 'Le serveur de monitoring des équipements approche 90% de capacité',
|
|
type: 'SYSTEME',
|
|
severite: 'FAIBLE',
|
|
statut: 'EN_COURS',
|
|
dateCreation: new Date('2024-12-23T08:15:00'),
|
|
source: 'Système automatique',
|
|
entite: 'Infrastructure',
|
|
entiteId: 'SYS-001',
|
|
actions: ['Optimisation', 'Augmentation capacité', 'Surveillance'],
|
|
responsable: 'Admin Système',
|
|
lu: true,
|
|
archive: false
|
|
}
|
|
];
|
|
|
|
// Filtrer selon les critères sélectionnés
|
|
let filteredAlertes = mockAlertes;
|
|
|
|
if (selectedType) {
|
|
filteredAlertes = filteredAlertes.filter(a => a.type === selectedType);
|
|
}
|
|
|
|
if (selectedSeverite) {
|
|
filteredAlertes = filteredAlertes.filter(a => a.severite === selectedSeverite);
|
|
}
|
|
|
|
if (selectedStatut) {
|
|
filteredAlertes = filteredAlertes.filter(a => a.statut === selectedStatut);
|
|
}
|
|
|
|
if (!showArchived) {
|
|
filteredAlertes = filteredAlertes.filter(a => !a.archive);
|
|
}
|
|
|
|
setAlertes(filteredAlertes);
|
|
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement des alertes:', error);
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Impossible de charger les alertes'
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const initTimeline = () => {
|
|
const recentAlertes = alertes
|
|
.filter(a => a.severite === 'CRITIQUE' || a.severite === 'ELEVEE')
|
|
.sort((a, b) => b.dateCreation.getTime() - a.dateCreation.getTime())
|
|
.slice(0, 5)
|
|
.map(a => ({
|
|
status: a.severite === 'CRITIQUE' ? 'danger' : 'warning',
|
|
date: a.dateCreation.toLocaleDateString('fr-FR'),
|
|
time: a.dateCreation.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }),
|
|
icon: getTypeIcon(a.type),
|
|
color: getSeveriteColor(a.severite),
|
|
titre: a.titre,
|
|
description: a.description,
|
|
statut: a.statut
|
|
}));
|
|
|
|
setTimelineEvents(recentAlertes);
|
|
};
|
|
|
|
const getTypeIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'SECURITE': return 'pi pi-shield';
|
|
case 'MAINTENANCE': return 'pi pi-wrench';
|
|
case 'BUDGET': return 'pi pi-euro';
|
|
case 'PLANNING': return 'pi pi-calendar';
|
|
case 'QUALITE': return 'pi pi-star';
|
|
case 'RESSOURCES': return 'pi pi-users';
|
|
case 'CONFORMITE': return 'pi pi-verified';
|
|
case 'SYSTEME': return 'pi pi-server';
|
|
default: return 'pi pi-info-circle';
|
|
}
|
|
};
|
|
|
|
const getSeveriteColor = (severite: string) => {
|
|
switch (severite) {
|
|
case 'CRITIQUE': return '#dc2626';
|
|
case 'ELEVEE': return '#ea580c';
|
|
case 'MOYENNE': return '#d97706';
|
|
case 'FAIBLE': return '#65a30d';
|
|
case 'INFO': return '#2563eb';
|
|
default: return '#6b7280';
|
|
}
|
|
};
|
|
|
|
const getSeveriteSeverity = (severite: string) => {
|
|
switch (severite) {
|
|
case 'CRITIQUE': return 'danger';
|
|
case 'ELEVEE': return 'warning';
|
|
case 'MOYENNE': return 'info';
|
|
case 'FAIBLE': return 'success';
|
|
case 'INFO': return 'info';
|
|
default: return 'secondary';
|
|
}
|
|
};
|
|
|
|
const getStatutSeverity = (statut: string) => {
|
|
switch (statut) {
|
|
case 'NOUVELLE': return 'danger';
|
|
case 'EN_COURS': return 'warning';
|
|
case 'RESOLUE': return 'success';
|
|
case 'FERMEE': return 'secondary';
|
|
case 'IGNOREE': return 'secondary';
|
|
default: return 'info';
|
|
}
|
|
};
|
|
|
|
const getTypeSeverity = (type: string) => {
|
|
switch (type) {
|
|
case 'SECURITE': return 'danger';
|
|
case 'MAINTENANCE': return 'warning';
|
|
case 'BUDGET': return 'info';
|
|
case 'PLANNING': return 'info';
|
|
case 'QUALITE': return 'success';
|
|
case 'RESSOURCES': return 'info';
|
|
case 'CONFORMITE': return 'warning';
|
|
case 'SYSTEME': return 'secondary';
|
|
default: return 'info';
|
|
}
|
|
};
|
|
|
|
const severiteBodyTemplate = (rowData: Alerte) => (
|
|
<Tag value={rowData.severite} severity={getSeveriteSeverity(rowData.severite) as any} />
|
|
);
|
|
|
|
const statutBodyTemplate = (rowData: Alerte) => (
|
|
<Tag value={rowData.statut} severity={getStatutSeverity(rowData.statut) as any} />
|
|
);
|
|
|
|
const typeBodyTemplate = (rowData: Alerte) => (
|
|
<div className="flex align-items-center">
|
|
<i className={`${getTypeIcon(rowData.type)} mr-2`} style={{ color: getSeveriteColor(rowData.severite) }}></i>
|
|
<Tag value={rowData.type} severity={getTypeSeverity(rowData.type) as any} />
|
|
</div>
|
|
);
|
|
|
|
const titreBodyTemplate = (rowData: Alerte) => (
|
|
<div className="flex align-items-center">
|
|
{!rowData.lu && (
|
|
<div className="w-1rem h-1rem bg-primary border-circle mr-2"></div>
|
|
)}
|
|
<div>
|
|
<div className={`font-medium ${!rowData.lu ? 'font-bold' : ''}`}>
|
|
{rowData.titre}
|
|
</div>
|
|
<div className="text-sm text-500 mt-1">
|
|
{rowData.description.substring(0, 80)}...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const echeanceBodyTemplate = (rowData: Alerte) => {
|
|
if (!rowData.dateEcheance) return <span className="text-500">-</span>;
|
|
|
|
const now = new Date();
|
|
const echeance = rowData.dateEcheance;
|
|
const diffHours = (echeance.getTime() - now.getTime()) / (1000 * 60 * 60);
|
|
|
|
let severity = 'info';
|
|
if (diffHours < 0) severity = 'danger';
|
|
else if (diffHours < 24) severity = 'warning';
|
|
else if (diffHours < 72) severity = 'info';
|
|
|
|
return (
|
|
<div>
|
|
<div className="text-sm">{echeance.toLocaleDateString('fr-FR')}</div>
|
|
<Tag
|
|
value={diffHours < 0 ? 'Échue' : `${Math.ceil(diffHours)}h restantes`}
|
|
severity={severity as any}
|
|
className="text-xs mt-1"
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const actionsBodyTemplate = (rowData: Alerte) => (
|
|
<div className="flex flex-wrap gap-1">
|
|
{rowData.actions.slice(0, 2).map((action, index) => (
|
|
<Tag key={index} value={action} severity="info" className="text-xs" />
|
|
))}
|
|
{rowData.actions.length > 2 && (
|
|
<Tag value={`+${rowData.actions.length - 2}`} severity="info" className="text-xs" />
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const actionButtonsTemplate = (rowData: Alerte) => (
|
|
<div className="flex gap-2">
|
|
{!rowData.lu && (
|
|
<Button
|
|
icon="pi pi-eye"
|
|
className="p-button-text p-button-sm"
|
|
tooltip="Marquer comme lu"
|
|
onClick={() => handleMarkAsRead(rowData)}
|
|
/>
|
|
)}
|
|
<Button
|
|
icon="pi pi-pencil"
|
|
className="p-button-text p-button-sm"
|
|
tooltip="Traiter"
|
|
onClick={() => handleTreatAlert(rowData)}
|
|
/>
|
|
<Button
|
|
icon="pi pi-check"
|
|
className="p-button-text p-button-sm p-button-success"
|
|
tooltip="Résoudre"
|
|
onClick={() => handleResolveAlert(rowData)}
|
|
disabled={rowData.statut === 'RESOLUE' || rowData.statut === 'FERMEE'}
|
|
/>
|
|
<Button
|
|
icon="pi pi-times"
|
|
className="p-button-text p-button-sm p-button-danger"
|
|
tooltip="Ignorer"
|
|
onClick={() => handleIgnoreAlert(rowData)}
|
|
disabled={rowData.statut === 'IGNOREE'}
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
const handleMarkAsRead = (alerte: Alerte) => {
|
|
// TODO: Appel API pour marquer comme lu
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Alerte marquée comme lue',
|
|
detail: alerte.titre,
|
|
life: 3000
|
|
});
|
|
loadAlertes();
|
|
};
|
|
|
|
const handleTreatAlert = (alerte: Alerte) => {
|
|
router.push(`/alertes/${alerte.id}/traiter`);
|
|
};
|
|
|
|
const handleResolveAlert = (alerte: Alerte) => {
|
|
// TODO: Appel API pour résoudre
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Alerte résolue',
|
|
detail: alerte.titre,
|
|
life: 3000
|
|
});
|
|
loadAlertes();
|
|
};
|
|
|
|
const handleIgnoreAlert = (alerte: Alerte) => {
|
|
// TODO: Appel API pour ignorer
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Alerte ignorée',
|
|
detail: alerte.titre,
|
|
life: 3000
|
|
});
|
|
loadAlertes();
|
|
};
|
|
|
|
// Calculs des métriques
|
|
const alertesCritiques = alertes.filter(a => a.severite === 'CRITIQUE').length;
|
|
const alertesNonLues = alertes.filter(a => !a.lu).length;
|
|
const alertesEchues = alertes.filter(a => a.dateEcheance && a.dateEcheance < new Date()).length;
|
|
const alertesEnCours = alertes.filter(a => a.statut === 'EN_COURS').length;
|
|
|
|
return (
|
|
<div className="grid">
|
|
<Toast ref={toast} />
|
|
|
|
{/* En-tête avec filtres */}
|
|
<div className="col-12">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center mb-4">
|
|
<h2 className="text-2xl font-bold m-0">Dashboard Alertes</h2>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
label="Marquer tout comme lu"
|
|
icon="pi pi-check-circle"
|
|
className="p-button-outlined"
|
|
onClick={() => {
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Toutes les alertes marquées comme lues',
|
|
life: 3000
|
|
});
|
|
}}
|
|
/>
|
|
<Button
|
|
label="Nouvelle alerte"
|
|
icon="pi pi-plus"
|
|
onClick={() => router.push('/alertes/nouvelle')}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-3 align-items-center">
|
|
<div className="field">
|
|
<label htmlFor="type" className="font-semibold">Type</label>
|
|
<Dropdown
|
|
id="type"
|
|
value={selectedType}
|
|
options={typeOptions}
|
|
onChange={(e) => setSelectedType(e.value)}
|
|
className="w-full md:w-14rem"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field">
|
|
<label htmlFor="severite" className="font-semibold">Sévérité</label>
|
|
<Dropdown
|
|
id="severite"
|
|
value={selectedSeverite}
|
|
options={severiteOptions}
|
|
onChange={(e) => setSelectedSeverite(e.value)}
|
|
className="w-full md:w-14rem"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field">
|
|
<label htmlFor="statut" className="font-semibold">Statut</label>
|
|
<Dropdown
|
|
id="statut"
|
|
value={selectedStatut}
|
|
options={statutOptions}
|
|
onChange={(e) => setSelectedStatut(e.value)}
|
|
className="w-full md:w-14rem"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field">
|
|
<div className="flex align-items-center">
|
|
<input
|
|
type="checkbox"
|
|
id="archived"
|
|
checked={showArchived}
|
|
onChange={(e) => setShowArchived(e.target.checked)}
|
|
className="mr-2"
|
|
/>
|
|
<label htmlFor="archived" className="font-semibold">Inclure archivées</label>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
icon="pi pi-refresh"
|
|
className="p-button-outlined"
|
|
onClick={loadAlertes}
|
|
loading={loading}
|
|
tooltip="Actualiser"
|
|
/>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Métriques principales */}
|
|
<div className="col-12 lg:col-3 md:col-6">
|
|
<Card className="h-full">
|
|
<div className="flex justify-content-between mb-3">
|
|
<div>
|
|
<span className="block text-500 font-medium mb-3">Critiques</span>
|
|
<div className="text-900 font-medium text-xl">{alertesCritiques}</div>
|
|
</div>
|
|
<div className="flex align-items-center justify-content-center bg-red-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
|
<i className="pi pi-exclamation-triangle text-red-500 text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 lg:col-3 md:col-6">
|
|
<Card className="h-full">
|
|
<div className="flex justify-content-between mb-3">
|
|
<div>
|
|
<span className="block text-500 font-medium mb-3">Non lues</span>
|
|
<div className="text-900 font-medium text-xl">{alertesNonLues}</div>
|
|
</div>
|
|
<div className="flex align-items-center justify-content-center bg-blue-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
|
<i className="pi pi-envelope text-blue-500 text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 lg:col-3 md:col-6">
|
|
<Card className="h-full">
|
|
<div className="flex justify-content-between mb-3">
|
|
<div>
|
|
<span className="block text-500 font-medium mb-3">Échues</span>
|
|
<div className="text-900 font-medium text-xl">{alertesEchues}</div>
|
|
</div>
|
|
<div className="flex align-items-center justify-content-center bg-orange-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
|
<i className="pi pi-clock text-orange-500 text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 lg:col-3 md:col-6">
|
|
<Card className="h-full">
|
|
<div className="flex justify-content-between mb-3">
|
|
<div>
|
|
<span className="block text-500 font-medium mb-3">En cours</span>
|
|
<div className="text-900 font-medium text-xl">{alertesEnCours}</div>
|
|
</div>
|
|
<div className="flex align-items-center justify-content-center bg-yellow-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
|
<i className="pi pi-cog text-yellow-500 text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Alertes critiques en cours */}
|
|
{alertesCritiques > 0 && (
|
|
<div className="col-12">
|
|
<Message
|
|
severity="error"
|
|
text={`${alertesCritiques} alerte(s) critique(s) nécessitent une attention immédiate`}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Timeline des alertes récentes */}
|
|
<div className="col-12 lg:col-6">
|
|
<Card>
|
|
<h6>Alertes Récentes (Critiques & Élevées)</h6>
|
|
<Timeline
|
|
value={timelineEvents}
|
|
align="alternate"
|
|
className="customized-timeline"
|
|
marker={(item) => <span className={`flex w-2rem h-2rem align-items-center justify-content-center text-white border-circle z-1 shadow-1`} style={{ backgroundColor: item.color }}>
|
|
<i className={item.icon}></i>
|
|
</span>}
|
|
content={(item) => (
|
|
<Card title={item.titre} subTitle={`${item.date} à ${item.time}`}>
|
|
<p className="text-sm mb-2">{item.description}</p>
|
|
<Tag value={item.statut} severity={getStatutSeverity(item.statut) as any} />
|
|
</Card>
|
|
)}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Répartition par type */}
|
|
<div className="col-12 lg:col-6">
|
|
<Card>
|
|
<h6>Répartition par Type</h6>
|
|
<div className="grid">
|
|
{typeOptions.slice(1).map((type, index) => {
|
|
const count = alertes.filter(a => a.type === type.value).length;
|
|
const percentage = alertes.length > 0 ? (count / alertes.length) * 100 : 0;
|
|
|
|
return (
|
|
<div key={index} className="col-12 mb-3">
|
|
<div className="flex justify-content-between align-items-center mb-2">
|
|
<div className="flex align-items-center">
|
|
<i className={`${getTypeIcon(type.value)} mr-2`}></i>
|
|
<span className="font-medium">{type.label}</span>
|
|
</div>
|
|
<Badge value={count} />
|
|
</div>
|
|
<ProgressBar value={percentage} showValue={false} style={{ height: '6px' }} />
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Tableau des alertes */}
|
|
<div className="col-12">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center mb-4">
|
|
<h6>Liste des Alertes ({alertes.length})</h6>
|
|
<div className="flex gap-2">
|
|
<Badge value={`${alertesNonLues} non lues`} severity="danger" />
|
|
<Badge value={`${alertesEnCours} en cours`} severity="warning" />
|
|
</div>
|
|
</div>
|
|
|
|
<DataTable
|
|
value={alertes}
|
|
loading={loading}
|
|
responsiveLayout="scroll"
|
|
paginator
|
|
rows={10}
|
|
rowsPerPageOptions={[5, 10, 25]}
|
|
emptyMessage="Aucune alerte trouvée"
|
|
sortMode="multiple"
|
|
sortField="dateCreation"
|
|
sortOrder={-1}
|
|
>
|
|
<Column field="titre" header="Titre" body={titreBodyTemplate} sortable style={{ width: '25%' }} />
|
|
<Column field="type" header="Type" body={typeBodyTemplate} sortable />
|
|
<Column field="severite" header="Sévérité" body={severiteBodyTemplate} sortable />
|
|
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
|
|
<Column field="dateCreation" header="Créée le" body={(rowData) => rowData.dateCreation.toLocaleDateString('fr-FR')} sortable />
|
|
<Column field="dateEcheance" header="Échéance" body={echeanceBodyTemplate} sortable />
|
|
<Column field="responsable" header="Responsable" sortable />
|
|
<Column field="actions" header="Actions suggérées" body={actionsBodyTemplate} />
|
|
<Column header="Actions" body={actionButtonsTemplate} style={{ width: '150px' }} />
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DashboardAlertes;
|