215 lines
6.7 KiB
TypeScript
215 lines
6.7 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import {
|
|
FileText,
|
|
CheckCircle,
|
|
XCircle,
|
|
Clock,
|
|
TrendingUp,
|
|
DollarSign,
|
|
Euro,
|
|
Activity,
|
|
} from 'lucide-react';
|
|
|
|
interface DashboardStats {
|
|
totalBookings: number;
|
|
pendingBookings: number;
|
|
acceptedBookings: number;
|
|
rejectedBookings: number;
|
|
acceptanceRate: number;
|
|
totalRevenue: {
|
|
usd: number;
|
|
eur: number;
|
|
};
|
|
recentActivities: any[];
|
|
}
|
|
|
|
export default function CarrierDashboardPage() {
|
|
const router = useRouter();
|
|
const [stats, setStats] = useState<DashboardStats | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetchStats();
|
|
}, []);
|
|
|
|
const fetchStats = async () => {
|
|
try {
|
|
const token = localStorage.getItem('carrier_access_token');
|
|
const response = await fetch('http://localhost:4000/api/v1/carrier-dashboard/stats', {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to fetch stats');
|
|
|
|
const data = await response.json();
|
|
setStats(data);
|
|
} catch (error) {
|
|
console.error('Error fetching stats:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-96">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
<p className="text-gray-600">Chargement...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!stats) {
|
|
return <div>Erreur de chargement des statistiques</div>;
|
|
}
|
|
|
|
const statCards = [
|
|
{
|
|
title: 'Total Réservations',
|
|
value: stats.totalBookings,
|
|
icon: FileText,
|
|
color: 'blue',
|
|
},
|
|
{
|
|
title: 'En attente',
|
|
value: stats.pendingBookings,
|
|
icon: Clock,
|
|
color: 'yellow',
|
|
},
|
|
{
|
|
title: 'Acceptées',
|
|
value: stats.acceptedBookings,
|
|
icon: CheckCircle,
|
|
color: 'green',
|
|
},
|
|
{
|
|
title: 'Refusées',
|
|
value: stats.rejectedBookings,
|
|
icon: XCircle,
|
|
color: 'red',
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900">Tableau de bord</h1>
|
|
<p className="text-gray-600 mt-1">Vue d'ensemble de votre activité</p>
|
|
</div>
|
|
|
|
{/* Stats Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{statCards.map((card) => {
|
|
const Icon = card.icon;
|
|
return (
|
|
<div key={card.title} className="bg-white p-6 rounded-lg shadow-sm border">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<Icon className={`w-8 h-8 text-${card.color}-600`} />
|
|
</div>
|
|
<h3 className="text-gray-600 text-sm font-medium">{card.title}</h3>
|
|
<p className="text-3xl font-bold text-gray-900 mt-2">{card.value}</p>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Revenue & Acceptance Rate */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Revenue */}
|
|
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
|
<TrendingUp className="w-5 h-5 mr-2 text-green-600" />
|
|
Revenus totaux
|
|
</h2>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center">
|
|
<DollarSign className="w-5 h-5 text-green-600 mr-2" />
|
|
<span className="text-gray-700">USD</span>
|
|
</div>
|
|
<span className="text-2xl font-bold text-gray-900">
|
|
${stats.totalRevenue.usd.toLocaleString()}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center">
|
|
<Euro className="w-5 h-5 text-blue-600 mr-2" />
|
|
<span className="text-gray-700">EUR</span>
|
|
</div>
|
|
<span className="text-2xl font-bold text-gray-900">
|
|
€{stats.totalRevenue.eur.toLocaleString()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Acceptance Rate */}
|
|
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
|
<Activity className="w-5 h-5 mr-2 text-blue-600" />
|
|
Taux d'acceptation
|
|
</h2>
|
|
<div className="flex items-center justify-center h-32">
|
|
<div className="text-center">
|
|
<div className="text-5xl font-bold text-blue-600">
|
|
{stats.acceptanceRate.toFixed(1)}%
|
|
</div>
|
|
<p className="text-gray-600 mt-2">
|
|
{stats.acceptedBookings} acceptées / {stats.totalBookings} total
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Activities */}
|
|
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">Activité récente</h2>
|
|
{stats.recentActivities.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{stats.recentActivities.map((activity) => (
|
|
<div
|
|
key={activity.id}
|
|
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
|
>
|
|
<div>
|
|
<p className="text-gray-900 font-medium">{activity.description}</p>
|
|
<p className="text-gray-600 text-sm">
|
|
{new Date(activity.createdAt).toLocaleDateString('fr-FR', {
|
|
day: 'numeric',
|
|
month: 'long',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})}
|
|
</p>
|
|
</div>
|
|
<span
|
|
className={`px-3 py-1 rounded-full text-xs font-medium ${
|
|
activity.type === 'BOOKING_ACCEPTED'
|
|
? 'bg-green-100 text-green-800'
|
|
: activity.type === 'BOOKING_REJECTED'
|
|
? 'bg-red-100 text-red-800'
|
|
: 'bg-blue-100 text-blue-800'
|
|
}`}
|
|
>
|
|
{activity.type}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-gray-600 text-center py-8">Aucune activité récente</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|