Files
btpxpress-frontend/app/(main)/dashboard/resume-quotidien/page.tsx
2025-10-13 05:29:32 +02:00

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;