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

435 lines
18 KiB
TypeScript

'use client';
import React, { useState, useEffect } 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 { ProgressBar } from 'primereact/progressbar';
import { Timeline } from 'primereact/timeline';
import { Avatar } from 'primereact/avatar';
import type { Chantier } from '../../../../types/btp';
import type { User } from '../../../../types/auth';
import chantierService from '../../../../services/chantierService';
import { useAuth } from '../../../../contexts/AuthContext';
interface ClientStats {
chantiersTotal: number;
chantiersEnCours: number;
chantiersTermines: number;
prochaineCheance: Date | null;
}
interface Notification {
id: string;
type: 'info' | 'warning' | 'success';
titre: string;
message: string;
date: Date;
lu: boolean;
}
const ClientDashboard = () => {
const [stats, setStats] = useState<ClientStats>({
chantiersTotal: 0,
chantiersEnCours: 0,
chantiersTermines: 0,
prochaineCheance: null
});
const [mesChantiers, setMesChantiers] = useState<Chantier[]>([]);
const [notifications, setNotifications] = useState<Notification[]>([]);
const [loading, setLoading] = useState(true);
const { user: currentUser } = useAuth();
// Vérifier que l'utilisateur est connecté et est un client
if (!currentUser || !currentUser.roles?.includes('CLIENT')) {
return (
<div className="flex align-items-center justify-content-center min-h-screen">
<div className="text-center">
<i className="pi pi-lock text-6xl text-color-secondary mb-3"></i>
<h3 className="text-color">Accès non autorisé</h3>
<p className="text-color-secondary">Vous devez être connecté en tant que client.</p>
</div>
</div>
);
}
useEffect(() => {
loadClientData();
loadNotifications();
}, []);
const loadClientData = async () => {
try {
setLoading(true);
// Charger mes chantiers (en utilisant l'id utilisateur)
const chantiers = await chantierService.getByClient(currentUser.id as any);
setMesChantiers(chantiers);
// Calculer les statistiques
const chantiersEnCours = chantiers.filter(c => c.statut === 'EN_COURS').length;
const chantiersTermines = chantiers.filter(c => c.statut === 'TERMINE').length;
// Prochaine échéance (chantier avec dateFinPrevue la plus proche)
const chantiersAvecEcheance = chantiers
.filter(c => c.dateFinPrevue && c.statut === 'EN_COURS')
.sort((a, b) => new Date(a.dateFinPrevue!).getTime() - new Date(b.dateFinPrevue!).getTime());
const prochaineCheance = chantiersAvecEcheance.length > 0
? new Date(chantiersAvecEcheance[0].dateFinPrevue!)
: null;
setStats({
chantiersTotal: chantiers.length,
chantiersEnCours,
chantiersTermines,
prochaineCheance
});
} catch (error) {
console.error('Erreur lors du chargement des données client:', error);
} finally {
setLoading(false);
}
};
const loadNotifications = () => {
// Données mockées pour les notifications
const mockNotifications: Notification[] = [
{
id: '1',
type: 'info',
titre: 'Nouveau devis disponible',
message: 'Le devis pour votre extension cuisine est maintenant disponible',
date: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
lu: false
},
{
id: '2',
type: 'warning',
titre: 'Rendez-vous prévu',
message: 'Rendez-vous avec votre gestionnaire demain à 14h00',
date: new Date(Date.now() - 24 * 60 * 60 * 1000),
lu: false
},
{
id: '3',
type: 'success',
titre: 'Phase terminée',
message: 'La phase "Gros œuvre" de votre chantier a été terminée',
date: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000),
lu: true
}
];
setNotifications(mockNotifications);
};
const chantierStatutBodyTemplate = (rowData: Chantier) => {
return (
<Tag
value={chantierService.getStatutLabel(rowData.statut)}
style={{
backgroundColor: chantierService.getStatutColor(rowData.statut),
color: 'white'
}}
/>
);
};
const montantBodyTemplate = (rowData: Chantier) => {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR'
}).format(rowData.montantPrevu || 0);
};
const avancementBodyTemplate = (rowData: Chantier) => {
const avancement = chantierService.calculateAvancement(rowData);
return (
<div className="flex align-items-center">
<ProgressBar
value={avancement}
className="w-8 mr-2"
style={{ height: '8px' }}
/>
<span className="text-sm font-medium">{Math.round(avancement)}%</span>
</div>
);
};
const notificationTemplate = (item: Notification) => {
const getSeverityIcon = (type: string) => {
switch (type) {
case 'warning': return 'pi-exclamation-triangle';
case 'success': return 'pi-check-circle';
default: return 'pi-info-circle';
}
};
const getSeverityColor = (type: string) => {
switch (type) {
case 'warning': return 'orange';
case 'success': return 'green';
default: return 'blue';
}
};
return (
<div className="flex align-items-start">
<div className={`border-round-md p-2 mr-3 bg-${getSeverityColor(item.type)}-100`}>
<i className={`pi ${getSeverityIcon(item.type)} text-${getSeverityColor(item.type)}-500`}></i>
</div>
<div className="flex-1">
<div className="flex justify-content-between align-items-start mb-2">
<h6 className={`m-0 ${!item.lu ? 'font-bold' : ''}`}>{item.titre}</h6>
<small className="text-color-secondary">
{item.date.toLocaleDateString('fr-FR')}
</small>
</div>
<p className="text-color-secondary m-0 text-sm">{item.message}</p>
</div>
{!item.lu && (
<div className="border-circle bg-primary w-1rem h-1rem ml-2"></div>
)}
</div>
);
};
return (
<div className="grid">
{/* Header */}
<div className="col-12">
<div className="flex justify-content-between align-items-center mb-4">
<div>
<h2 className="text-primary m-0">Mon Espace Client</h2>
<p className="text-color-secondary m-0">
Bienvenue {currentUser.firstName} {currentUser.lastName}
</p>
</div>
<div className="flex align-items-center gap-2">
<Avatar
label={`${currentUser.firstName?.charAt(0) || ''}${currentUser.lastName?.charAt(0) || ''}`}
className="bg-primary text-white"
size="large"
/>
</div>
</div>
</div>
{/* Statistiques */}
<div className="col-12 lg:col-3 md:col-6">
<Card className="h-full">
<div className="flex justify-content-between align-items-start">
<div>
<div className="text-2xl font-bold text-blue-500">
{stats.chantiersTotal}
</div>
<div className="text-color-secondary font-medium">Mes projets</div>
</div>
<div className="border-round-md bg-blue-100 p-2">
<i className="pi pi-home 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 align-items-start">
<div>
<div className="text-2xl font-bold text-green-500">
{stats.chantiersEnCours}
</div>
<div className="text-color-secondary font-medium">En cours</div>
</div>
<div className="border-round-md bg-green-100 p-2">
<i className="pi pi-cog 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 align-items-start">
<div>
<div className="text-2xl font-bold text-purple-500">
{stats.chantiersTermines}
</div>
<div className="text-color-secondary font-medium">Terminés</div>
</div>
<div className="border-round-md bg-purple-100 p-2">
<i className="pi pi-check-circle text-purple-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 align-items-start">
<div>
<div className="text-lg font-bold text-orange-500">
{stats.prochaineCheance
? stats.prochaineCheance.toLocaleDateString('fr-FR')
: 'Aucune'
}
</div>
<div className="text-color-secondary font-medium">Prochaine échéance</div>
</div>
<div className="border-round-md bg-orange-100 p-2">
<i className="pi pi-calendar text-orange-500 text-xl"></i>
</div>
</div>
</Card>
</div>
{/* Notifications et projets */}
<div className="col-12 lg:col-4">
<Card title="Notifications récentes" className="h-full">
<div className="flex flex-column gap-4">
{notifications.slice(0, 4).map((notif) => (
<div key={notif.id}>
{notificationTemplate(notif)}
</div>
))}
</div>
<div className="mt-4 text-center">
<Button
label="Voir toutes les notifications"
className="p-button-text p-button-sm"
/>
</div>
</Card>
</div>
<div className="col-12 lg:col-8">
<Card title="Mes projets en cours" className="h-full">
<DataTable
value={mesChantiers.filter(c => c.statut === 'EN_COURS')}
paginator
rows={5}
dataKey="id"
emptyMessage="Aucun projet en cours"
loading={loading}
>
<Column
field="nom"
header="Projet"
style={{ minWidth: '12rem' }}
/>
<Column
header="Statut"
body={chantierStatutBodyTemplate}
style={{ minWidth: '8rem' }}
/>
<Column
field="dateDebut"
header="Début"
body={(rowData) => new Date(rowData.dateDebut).toLocaleDateString('fr-FR')}
style={{ minWidth: '8rem' }}
/>
<Column
field="dateFinPrevue"
header="Fin prévue"
body={(rowData) => rowData.dateFinPrevue ? new Date(rowData.dateFinPrevue).toLocaleDateString('fr-FR') : ''}
style={{ minWidth: '8rem' }}
/>
<Column
header="Avancement"
body={avancementBodyTemplate}
style={{ minWidth: '12rem' }}
/>
<Column
body={(rowData) => (
<Button
icon="pi pi-eye"
className="p-button-rounded p-button-text p-button-info"
tooltip="Voir le détail"
/>
)}
style={{ minWidth: '4rem' }}
/>
</DataTable>
</Card>
</div>
{/* Tous mes projets */}
<div className="col-12">
<Card title="Historique de mes projets">
<DataTable
value={mesChantiers}
paginator
rows={10}
dataKey="id"
emptyMessage="Aucun projet trouvé"
loading={loading}
sortMode="multiple"
>
<Column
field="nom"
header="Projet"
sortable
style={{ minWidth: '12rem' }}
/>
<Column
header="Statut"
body={chantierStatutBodyTemplate}
sortable
sortField="statut"
style={{ minWidth: '8rem' }}
/>
<Column
field="dateDebut"
header="Date début"
body={(rowData) => new Date(rowData.dateDebut).toLocaleDateString('fr-FR')}
sortable
style={{ minWidth: '10rem' }}
/>
<Column
field="dateFinPrevue"
header="Fin prévue"
body={(rowData) => rowData.dateFinPrevue ? new Date(rowData.dateFinPrevue).toLocaleDateString('fr-FR') : ''}
sortable
style={{ minWidth: '10rem' }}
/>
<Column
header="Montant"
body={montantBodyTemplate}
sortable
sortField="montantPrevu"
style={{ minWidth: '8rem', textAlign: 'right' }}
/>
<Column
header="Avancement"
body={avancementBodyTemplate}
style={{ minWidth: '12rem' }}
/>
<Column
body={(rowData) => (
<div className="flex gap-1">
<Button
icon="pi pi-eye"
className="p-button-rounded p-button-text p-button-info p-button-sm"
tooltip="Voir le détail"
/>
<Button
icon="pi pi-download"
className="p-button-rounded p-button-text p-button-help p-button-sm"
tooltip="Télécharger les documents"
/>
</div>
)}
style={{ minWidth: '6rem' }}
/>
</DataTable>
</Card>
</div>
</div>
);
};
export default ClientDashboard;