Initial commit
This commit is contained in:
435
app/(main)/client/dashboard/page.tsx
Normal file
435
app/(main)/client/dashboard/page.tsx
Normal file
@@ -0,0 +1,435 @@
|
||||
'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.role !== '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 le clientId)
|
||||
const chantiers = await chantierService.getByClient(currentUser.clientId!);
|
||||
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.prenom} {currentUser.nom}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex align-items-center gap-2">
|
||||
<Avatar
|
||||
label={`${currentUser.prenom.charAt(0)}${currentUser.nom.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;
|
||||
Reference in New Issue
Block a user