435 lines
18 KiB
TypeScript
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; |