651 lines
27 KiB
TypeScript
651 lines
27 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Card } from 'primereact/card';
|
|
import { Chart } from 'primereact/chart';
|
|
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 { Calendar } from 'primereact/calendar';
|
|
import { Badge } from 'primereact/badge';
|
|
import { Timeline } from 'primereact/timeline';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { Divider } from 'primereact/divider';
|
|
import { Panel } from 'primereact/panel';
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
interface ResumeQuotidien {
|
|
date: Date;
|
|
chantiers: {
|
|
actifs: number;
|
|
nouveaux: number;
|
|
termines: number;
|
|
retards: number;
|
|
};
|
|
employes: {
|
|
presents: number;
|
|
absents: number;
|
|
enMission: number;
|
|
disponibles: number;
|
|
};
|
|
materiels: {
|
|
utilises: number;
|
|
disponibles: number;
|
|
enMaintenance: number;
|
|
horsService: number;
|
|
};
|
|
finances: {
|
|
chiffreAffaires: number;
|
|
depenses: number;
|
|
benefice: number;
|
|
facturesEmises: number;
|
|
};
|
|
alertes: {
|
|
critiques: number;
|
|
elevees: number;
|
|
moyennes: number;
|
|
total: number;
|
|
};
|
|
evenements: EvenementQuotidien[];
|
|
objectifs: ObjectifQuotidien[];
|
|
}
|
|
|
|
interface EvenementQuotidien {
|
|
id: string;
|
|
heure: string;
|
|
type: string;
|
|
titre: string;
|
|
description: string;
|
|
statut: string;
|
|
importance: string;
|
|
}
|
|
|
|
interface ObjectifQuotidien {
|
|
id: string;
|
|
titre: string;
|
|
description: string;
|
|
progression: number;
|
|
echeance: Date;
|
|
responsable: string;
|
|
statut: string;
|
|
}
|
|
|
|
const DashboardResumeQuotidien = () => {
|
|
const toast = useRef<Toast>(null);
|
|
const router = useRouter();
|
|
|
|
const [resume, setResume] = useState<ResumeQuotidien | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
|
|
|
const [chartData, setChartData] = useState({});
|
|
const [chartOptions, setChartOptions] = useState({});
|
|
|
|
useEffect(() => {
|
|
loadResumeQuotidien();
|
|
initCharts();
|
|
}, [selectedDate]);
|
|
|
|
const loadResumeQuotidien = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// TODO: Remplacer par un vrai appel API
|
|
// const response = await dashboardService.getResumeQuotidien(selectedDate);
|
|
|
|
// Données simulées pour la démonstration
|
|
const mockResume: ResumeQuotidien = {
|
|
date: selectedDate,
|
|
chantiers: {
|
|
actifs: 8,
|
|
nouveaux: 2,
|
|
termines: 1,
|
|
retards: 3
|
|
},
|
|
employes: {
|
|
presents: 45,
|
|
absents: 5,
|
|
enMission: 38,
|
|
disponibles: 7
|
|
},
|
|
materiels: {
|
|
utilises: 28,
|
|
disponibles: 12,
|
|
enMaintenance: 4,
|
|
horsService: 2
|
|
},
|
|
finances: {
|
|
chiffreAffaires: 125000,
|
|
depenses: 89000,
|
|
benefice: 36000,
|
|
facturesEmises: 8
|
|
},
|
|
alertes: {
|
|
critiques: 2,
|
|
elevees: 5,
|
|
moyennes: 12,
|
|
total: 19
|
|
},
|
|
evenements: [
|
|
{
|
|
id: '1',
|
|
heure: '08:00',
|
|
type: 'REUNION',
|
|
titre: 'Briefing équipe Résidence Les Jardins',
|
|
description: 'Point sécurité et planning de la journée',
|
|
statut: 'TERMINE',
|
|
importance: 'HAUTE'
|
|
},
|
|
{
|
|
id: '2',
|
|
heure: '09:30',
|
|
type: 'LIVRAISON',
|
|
titre: 'Réception matériaux Centre Commercial',
|
|
description: 'Livraison acier pour structure',
|
|
statut: 'EN_COURS',
|
|
importance: 'HAUTE'
|
|
},
|
|
{
|
|
id: '3',
|
|
heure: '11:00',
|
|
type: 'MAINTENANCE',
|
|
titre: 'Révision pelleteuse CAT 320D',
|
|
description: 'Maintenance préventive système hydraulique',
|
|
statut: 'PLANIFIE',
|
|
importance: 'MOYENNE'
|
|
},
|
|
{
|
|
id: '4',
|
|
heure: '14:00',
|
|
titre: 'Coulage dalle béton',
|
|
type: 'CHANTIER',
|
|
description: 'Coulage niveau R+1 Résidence Les Jardins',
|
|
statut: 'PLANIFIE',
|
|
importance: 'HAUTE'
|
|
},
|
|
{
|
|
id: '5',
|
|
heure: '16:30',
|
|
type: 'REUNION',
|
|
titre: 'Point client Hôtel Luxe',
|
|
description: 'Validation avancement travaux',
|
|
statut: 'PLANIFIE',
|
|
importance: 'MOYENNE'
|
|
}
|
|
],
|
|
objectifs: [
|
|
{
|
|
id: '1',
|
|
titre: 'Finaliser gros œuvre Résidence',
|
|
description: 'Terminer le gros œuvre du bâtiment A',
|
|
progression: 85,
|
|
echeance: new Date('2025-01-15'),
|
|
responsable: 'Jean Dupont',
|
|
statut: 'EN_COURS'
|
|
},
|
|
{
|
|
id: '2',
|
|
titre: 'Livraison Centre Commercial',
|
|
description: 'Respecter la date de livraison prévue',
|
|
progression: 60,
|
|
echeance: new Date('2025-02-28'),
|
|
responsable: 'Marie Martin',
|
|
statut: 'EN_COURS'
|
|
},
|
|
{
|
|
id: '3',
|
|
titre: 'Formation équipe sécurité',
|
|
description: 'Former 100% de l\'équipe aux nouvelles normes',
|
|
progression: 40,
|
|
echeance: new Date('2025-01-31'),
|
|
responsable: 'Pierre Leroy',
|
|
statut: 'EN_COURS'
|
|
}
|
|
]
|
|
};
|
|
|
|
setResume(mockResume);
|
|
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement du résumé quotidien:', error);
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Impossible de charger le résumé quotidien'
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const initCharts = () => {
|
|
if (!resume) return;
|
|
|
|
const documentStyle = getComputedStyle(document.documentElement);
|
|
|
|
// Graphique de répartition des chantiers
|
|
const chantiersData = {
|
|
labels: ['Actifs', 'Nouveaux', 'Terminés', 'En retard'],
|
|
datasets: [
|
|
{
|
|
data: [
|
|
resume.chantiers.actifs,
|
|
resume.chantiers.nouveaux,
|
|
resume.chantiers.termines,
|
|
resume.chantiers.retards
|
|
],
|
|
backgroundColor: [
|
|
documentStyle.getPropertyValue('--green-500'),
|
|
documentStyle.getPropertyValue('--blue-500'),
|
|
documentStyle.getPropertyValue('--purple-500'),
|
|
documentStyle.getPropertyValue('--red-500')
|
|
]
|
|
}
|
|
]
|
|
};
|
|
|
|
const options = {
|
|
plugins: {
|
|
legend: {
|
|
labels: {
|
|
usePointStyle: true,
|
|
color: documentStyle.getPropertyValue('--text-color')
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
setChartData(chantiersData);
|
|
setChartOptions(options);
|
|
};
|
|
|
|
const getEvenementIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'REUNION': return 'pi pi-users';
|
|
case 'LIVRAISON': return 'pi pi-truck';
|
|
case 'MAINTENANCE': return 'pi pi-wrench';
|
|
case 'CHANTIER': return 'pi pi-building';
|
|
default: return 'pi pi-calendar';
|
|
}
|
|
};
|
|
|
|
const getEvenementColor = (importance: string) => {
|
|
switch (importance) {
|
|
case 'HAUTE': return '#dc2626';
|
|
case 'MOYENNE': return '#d97706';
|
|
case 'BASSE': return '#059669';
|
|
default: return '#6b7280';
|
|
}
|
|
};
|
|
|
|
const getStatutSeverity = (statut: string) => {
|
|
switch (statut) {
|
|
case 'TERMINE': return 'success';
|
|
case 'EN_COURS': return 'warning';
|
|
case 'PLANIFIE': return 'info';
|
|
case 'ANNULE': return 'danger';
|
|
default: return 'secondary';
|
|
}
|
|
};
|
|
|
|
const evenementStatutTemplate = (rowData: EvenementQuotidien) => (
|
|
<Tag value={rowData.statut} severity={getStatutSeverity(rowData.statut) as any} />
|
|
);
|
|
|
|
const evenementTypeTemplate = (rowData: EvenementQuotidien) => (
|
|
<div className="flex align-items-center">
|
|
<i className={`${getEvenementIcon(rowData.type)} mr-2`} style={{ color: getEvenementColor(rowData.importance) }}></i>
|
|
<span>{rowData.type}</span>
|
|
</div>
|
|
);
|
|
|
|
const objectifProgressionTemplate = (rowData: ObjectifQuotidien) => (
|
|
<div className="flex align-items-center">
|
|
<ProgressBar
|
|
value={rowData.progression}
|
|
style={{ width: '100px', marginRight: '8px' }}
|
|
showValue={false}
|
|
/>
|
|
<span className="text-sm font-semibold">{rowData.progression}%</span>
|
|
</div>
|
|
);
|
|
|
|
const objectifEcheanceTemplate = (rowData: ObjectifQuotidien) => {
|
|
const now = new Date();
|
|
const echeance = rowData.echeance;
|
|
const diffDays = Math.ceil((echeance.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
|
let severity = 'info';
|
|
if (diffDays < 0) severity = 'danger';
|
|
else if (diffDays < 7) severity = 'warning';
|
|
else if (diffDays < 30) severity = 'info';
|
|
|
|
return (
|
|
<div>
|
|
<div className="text-sm">{echeance.toLocaleDateString('fr-FR')}</div>
|
|
<Tag
|
|
value={diffDays < 0 ? 'Échue' : `${diffDays} jours`}
|
|
severity={severity as any}
|
|
className="text-xs mt-1"
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
if (!resume) {
|
|
return <div>Chargement...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="grid">
|
|
<Toast ref={toast} />
|
|
|
|
{/* En-tête avec sélection de date */}
|
|
<div className="col-12">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center mb-4">
|
|
<h2 className="text-2xl font-bold m-0">Résumé Quotidien</h2>
|
|
<div className="flex gap-3 align-items-center">
|
|
<Calendar
|
|
value={selectedDate}
|
|
onChange={(e) => setSelectedDate(e.value as Date)}
|
|
dateFormat="dd/mm/yy"
|
|
showIcon
|
|
placeholder="Sélectionner une date"
|
|
/>
|
|
<Button
|
|
icon="pi pi-refresh"
|
|
className="p-button-outlined"
|
|
onClick={loadResumeQuotidien}
|
|
loading={loading}
|
|
tooltip="Actualiser"
|
|
/>
|
|
<Button
|
|
label="Exporter PDF"
|
|
icon="pi pi-file-pdf"
|
|
className="p-button-outlined"
|
|
onClick={() => {
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Export en cours',
|
|
detail: 'Génération du rapport PDF...',
|
|
life: 3000
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-center">
|
|
<h3 className="text-xl font-semibold text-primary">
|
|
{selectedDate.toLocaleDateString('fr-FR', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
})}
|
|
</h3>
|
|
</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">Chantiers Actifs</span>
|
|
<div className="text-900 font-medium text-xl">{resume.chantiers.actifs}</div>
|
|
<div className="text-sm text-500">
|
|
{resume.chantiers.nouveaux} nouveaux, {resume.chantiers.retards} en retard
|
|
</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-building 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">Employés Présents</span>
|
|
<div className="text-900 font-medium text-xl">{resume.employes.presents}</div>
|
|
<div className="text-sm text-500">
|
|
{resume.employes.enMission} en mission, {resume.employes.disponibles} disponibles
|
|
</div>
|
|
</div>
|
|
<div className="flex align-items-center justify-content-center bg-green-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
|
<i className="pi pi-users text-green-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">Matériels Utilisés</span>
|
|
<div className="text-900 font-medium text-xl">{resume.materiels.utilises}</div>
|
|
<div className="text-sm text-500">
|
|
{resume.materiels.disponibles} disponibles, {resume.materiels.enMaintenance} en maintenance
|
|
</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-cog 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">CA du Jour</span>
|
|
<div className="text-900 font-medium text-xl">
|
|
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', notation: 'compact' }).format(resume.finances.chiffreAffaires)}
|
|
</div>
|
|
<div className="text-sm text-500">
|
|
Bénéfice: {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', notation: 'compact' }).format(resume.finances.benefice)}
|
|
</div>
|
|
</div>
|
|
<div className="flex align-items-center justify-content-center bg-purple-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
|
<i className="pi pi-euro text-purple-500 text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Alertes du jour */}
|
|
{resume.alertes.total > 0 && (
|
|
<div className="col-12">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center mb-3">
|
|
<h6>Alertes du Jour</h6>
|
|
<Badge value={`${resume.alertes.total} alertes`} severity="warning" />
|
|
</div>
|
|
<div className="grid">
|
|
<div className="col-12 md:col-4">
|
|
<div className="text-center p-3 border-round bg-red-50">
|
|
<div className="text-2xl font-bold text-red-500">{resume.alertes.critiques}</div>
|
|
<div className="text-sm text-red-600">Critiques</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-4">
|
|
<div className="text-center p-3 border-round bg-orange-50">
|
|
<div className="text-2xl font-bold text-orange-500">{resume.alertes.elevees}</div>
|
|
<div className="text-sm text-orange-600">Élevées</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-4">
|
|
<div className="text-center p-3 border-round bg-yellow-50">
|
|
<div className="text-2xl font-bold text-yellow-500">{resume.alertes.moyennes}</div>
|
|
<div className="text-sm text-yellow-600">Moyennes</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-center mt-3">
|
|
<Button
|
|
label="Voir toutes les alertes"
|
|
icon="pi pi-external-link"
|
|
className="p-button-outlined"
|
|
onClick={() => router.push('/dashboard/alertes')}
|
|
/>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{/* Graphique et indicateurs */}
|
|
<div className="col-12 lg:col-6">
|
|
<Card>
|
|
<h6>Répartition des Chantiers</h6>
|
|
<Chart type="doughnut" data={chartData} options={chartOptions} className="w-full md:w-30rem" />
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 lg:col-6">
|
|
<Card>
|
|
<h6>Indicateurs Financiers</h6>
|
|
<div className="grid">
|
|
<div className="col-6">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-green-500">
|
|
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', notation: 'compact' }).format(resume.finances.chiffreAffaires)}
|
|
</div>
|
|
<div className="text-sm text-500">Chiffre d'affaires</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-6">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-red-500">
|
|
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', notation: 'compact' }).format(resume.finances.depenses)}
|
|
</div>
|
|
<div className="text-sm text-500">Dépenses</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-6">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-blue-500">
|
|
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', notation: 'compact' }).format(resume.finances.benefice)}
|
|
</div>
|
|
<div className="text-sm text-500">Bénéfice</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-6">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-purple-500">{resume.finances.facturesEmises}</div>
|
|
<div className="text-sm text-500">Factures émises</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Planning de la journée */}
|
|
<div className="col-12 lg:col-8">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center mb-4">
|
|
<h6>Planning de la Journée</h6>
|
|
<Badge value={`${resume.evenements.length} événements`} />
|
|
</div>
|
|
|
|
<DataTable
|
|
value={resume.evenements}
|
|
responsiveLayout="scroll"
|
|
emptyMessage="Aucun événement planifié"
|
|
>
|
|
<Column field="heure" header="Heure" style={{ width: '80px' }} />
|
|
<Column field="type" header="Type" body={evenementTypeTemplate} />
|
|
<Column field="titre" header="Titre" />
|
|
<Column field="description" header="Description" />
|
|
<Column field="statut" header="Statut" body={evenementStatutTemplate} />
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Objectifs en cours */}
|
|
<div className="col-12 lg:col-4">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center mb-4">
|
|
<h6>Objectifs en Cours</h6>
|
|
<Badge value={`${resume.objectifs.length} objectifs`} severity="info" />
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{resume.objectifs.map((objectif, index) => (
|
|
<Panel key={index} header={objectif.titre} className="mb-3">
|
|
<p className="text-sm text-500 mb-3">{objectif.description}</p>
|
|
<div className="flex justify-content-between align-items-center mb-2">
|
|
<span className="text-sm font-medium">Progression</span>
|
|
<span className="text-sm font-bold">{objectif.progression}%</span>
|
|
</div>
|
|
<ProgressBar value={objectif.progression} className="mb-3" />
|
|
<div className="flex justify-content-between text-sm text-500">
|
|
<span>Responsable: {objectif.responsable}</span>
|
|
<span>Échéance: {objectif.echeance.toLocaleDateString('fr-FR')}</span>
|
|
</div>
|
|
</Panel>
|
|
))}
|
|
</div>
|
|
|
|
<div className="text-center mt-3">
|
|
<Button
|
|
label="Voir tous les objectifs"
|
|
icon="pi pi-external-link"
|
|
className="p-button-outlined"
|
|
onClick={() => router.push('/objectifs')}
|
|
/>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Actions rapides */}
|
|
<div className="col-12">
|
|
<Card>
|
|
<h6>Actions Rapides</h6>
|
|
<div className="flex flex-wrap gap-3">
|
|
<Button
|
|
label="Nouveau chantier"
|
|
icon="pi pi-plus"
|
|
onClick={() => router.push('/chantiers/nouveau')}
|
|
/>
|
|
<Button
|
|
label="Planifier événement"
|
|
icon="pi pi-calendar-plus"
|
|
className="p-button-outlined"
|
|
onClick={() => router.push('/planning/nouveau')}
|
|
/>
|
|
<Button
|
|
label="Créer facture"
|
|
icon="pi pi-file-plus"
|
|
className="p-button-outlined"
|
|
onClick={() => router.push('/factures/nouvelle')}
|
|
/>
|
|
<Button
|
|
label="Rapport incident"
|
|
icon="pi pi-exclamation-triangle"
|
|
className="p-button-outlined p-button-warning"
|
|
onClick={() => router.push('/incidents/nouveau')}
|
|
/>
|
|
<Button
|
|
label="Vue d'ensemble"
|
|
icon="pi pi-chart-line"
|
|
className="p-button-outlined p-button-info"
|
|
onClick={() => router.push('/dashboard')}
|
|
/>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DashboardResumeQuotidien;
|