Added more Responsive to Mobile Device
This commit is contained in:
@@ -2,6 +2,159 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getCurrentUser } from '@/lib/auth';
|
||||
|
||||
// Types de période: day | 7days | 30days | month | quarter | year
|
||||
// Ou plage personnalisée via from & to (format ISO)
|
||||
function getDateRange(period: string | null, from: string | null, to: string | null): {
|
||||
start: Date;
|
||||
end: Date;
|
||||
prevStart: Date;
|
||||
prevEnd: Date;
|
||||
periodLabel: string;
|
||||
} {
|
||||
const now = new Date();
|
||||
const endOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
|
||||
const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
||||
|
||||
if (from && to) {
|
||||
const start = new Date(from);
|
||||
const end = endOfDay(new Date(to));
|
||||
const diff = end.getTime() - start.getTime();
|
||||
const prevEnd = new Date(start.getTime() - 1);
|
||||
const prevStart = new Date(prevEnd.getTime() - diff);
|
||||
return {
|
||||
start: startOfDay(start),
|
||||
end,
|
||||
prevStart: startOfDay(prevStart),
|
||||
prevEnd: endOfDay(prevEnd),
|
||||
periodLabel: `${start.toLocaleDateString('fr-FR')} - ${new Date(to).toLocaleDateString('fr-FR')}`,
|
||||
};
|
||||
}
|
||||
|
||||
switch (period) {
|
||||
case 'day': {
|
||||
const start = startOfDay(now);
|
||||
const end = endOfDay(now);
|
||||
const yesterday = new Date(now);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
prevStart: startOfDay(yesterday),
|
||||
prevEnd: endOfDay(yesterday),
|
||||
periodLabel: "Aujourd'hui",
|
||||
};
|
||||
}
|
||||
case 'yesterday': {
|
||||
const yesterday = new Date(now);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const start = startOfDay(yesterday);
|
||||
const end = endOfDay(yesterday);
|
||||
const dayBefore = new Date(yesterday);
|
||||
dayBefore.setDate(dayBefore.getDate() - 1);
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
prevStart: startOfDay(dayBefore),
|
||||
prevEnd: endOfDay(dayBefore),
|
||||
periodLabel: 'Hier',
|
||||
};
|
||||
}
|
||||
case 'week': {
|
||||
// Cette semaine : lundi à aujourd'hui (ISO week, lundi = 1)
|
||||
const dayOfWeek = now.getDay();
|
||||
const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
|
||||
const startOfWeek = new Date(now);
|
||||
startOfWeek.setDate(startOfWeek.getDate() + mondayOffset);
|
||||
const start = startOfDay(startOfWeek);
|
||||
const end = endOfDay(now);
|
||||
const prevWeekStart = new Date(start);
|
||||
prevWeekStart.setDate(prevWeekStart.getDate() - 7);
|
||||
const prevWeekEnd = new Date(start);
|
||||
prevWeekEnd.setDate(prevWeekEnd.getDate() - 1);
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
prevStart: startOfDay(prevWeekStart),
|
||||
prevEnd: endOfDay(prevWeekEnd),
|
||||
periodLabel: 'Cette semaine',
|
||||
};
|
||||
}
|
||||
case '7days': {
|
||||
const start = new Date(now);
|
||||
start.setDate(start.getDate() - 6);
|
||||
const end = endOfDay(now);
|
||||
const prevEnd = new Date(start.getTime() - 1);
|
||||
const prevStart = new Date(prevEnd);
|
||||
prevStart.setDate(prevStart.getDate() - 6);
|
||||
return {
|
||||
start: startOfDay(start),
|
||||
end,
|
||||
prevStart: startOfDay(prevStart),
|
||||
prevEnd: endOfDay(prevEnd),
|
||||
periodLabel: '7 derniers jours',
|
||||
};
|
||||
}
|
||||
case '30days': {
|
||||
const start = new Date(now);
|
||||
start.setDate(start.getDate() - 29);
|
||||
const end = endOfDay(now);
|
||||
const prevEnd = new Date(start.getTime() - 1);
|
||||
const prevStart = new Date(prevEnd);
|
||||
prevStart.setDate(prevStart.getDate() - 29);
|
||||
return {
|
||||
start: startOfDay(start),
|
||||
end,
|
||||
prevStart: startOfDay(prevStart),
|
||||
prevEnd: endOfDay(prevEnd),
|
||||
periodLabel: '30 derniers jours',
|
||||
};
|
||||
}
|
||||
case 'quarter': {
|
||||
const q = Math.floor(now.getMonth() / 3) + 1;
|
||||
const startOfQuarter = new Date(now.getFullYear(), (q - 1) * 3, 1);
|
||||
const endOfQuarter = new Date(now.getFullYear(), q * 3, 0, 23, 59, 59, 999);
|
||||
const prevQuarter = q === 1 ? 4 : q - 1;
|
||||
const prevYear = q === 1 ? now.getFullYear() - 1 : now.getFullYear();
|
||||
const prevStart = new Date(prevYear, (prevQuarter - 1) * 3, 1);
|
||||
const prevEnd = new Date(prevYear, prevQuarter * 3, 0, 23, 59, 59, 999);
|
||||
return {
|
||||
start: startOfQuarter,
|
||||
end: endOfQuarter,
|
||||
prevStart: prevStart,
|
||||
prevEnd: prevEnd,
|
||||
periodLabel: `T${q} ${now.getFullYear()}`,
|
||||
};
|
||||
}
|
||||
case 'year': {
|
||||
const startOfYear = new Date(now.getFullYear(), 0, 1);
|
||||
const endOfYear = new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999);
|
||||
const prevStart = new Date(now.getFullYear() - 1, 0, 1);
|
||||
const prevEnd = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 999);
|
||||
return {
|
||||
start: startOfYear,
|
||||
end: endOfYear,
|
||||
prevStart: prevStart,
|
||||
prevEnd: prevEnd,
|
||||
periodLabel: `Année ${now.getFullYear()}`,
|
||||
};
|
||||
}
|
||||
case 'month':
|
||||
default: {
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||
const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
|
||||
return {
|
||||
start: startOfMonth,
|
||||
end: endOfMonth,
|
||||
prevStart: startOfLastMonth,
|
||||
prevEnd: endOfLastMonth,
|
||||
periodLabel: 'Mois en cours',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GET - Récupérer les statistiques du dashboard
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
@@ -10,26 +163,20 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const endOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
|
||||
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||
|
||||
const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
|
||||
|
||||
const startOfYesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
||||
const endOfYesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 23, 59, 59, 999);
|
||||
const { searchParams } = new URL(request.url);
|
||||
const period = searchParams.get('period') || 'month';
|
||||
const from = searchParams.get('from');
|
||||
const to = searchParams.get('to');
|
||||
|
||||
// 1. Participations du mois (trajets validés/terminés ce mois)
|
||||
const { start, end, prevStart, prevEnd, periodLabel } = getDateRange(period, from, to);
|
||||
|
||||
// 1. Participations sur la période (trajets validés/terminés)
|
||||
const participationsMoisData = await prisma.participationFinanciere.findMany({
|
||||
where: {
|
||||
trajet: {
|
||||
date: {
|
||||
gte: startOfMonth,
|
||||
lte: endOfMonth,
|
||||
gte: start,
|
||||
lte: end,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -44,57 +191,57 @@ export async function GET(request: NextRequest) {
|
||||
);
|
||||
const nombreFactures = participationsCeMois.length;
|
||||
|
||||
// 2. Trajets aujourd'hui
|
||||
const trajetsAujourdhui = await prisma.trajet.count({
|
||||
// 2. Trajets sur la période
|
||||
const trajetsPeriode = await prisma.trajet.count({
|
||||
where: {
|
||||
archived: false,
|
||||
date: {
|
||||
gte: startOfToday,
|
||||
lte: endOfToday,
|
||||
gte: start,
|
||||
lte: end,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Trajets hier pour comparaison
|
||||
const trajetsHier = await prisma.trajet.count({
|
||||
// Trajets période précédente pour comparaison
|
||||
const trajetsPeriodePrecedente = await prisma.trajet.count({
|
||||
where: {
|
||||
archived: false,
|
||||
date: {
|
||||
gte: startOfYesterday,
|
||||
lte: endOfYesterday,
|
||||
gte: prevStart,
|
||||
lte: prevEnd,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const differenceAujourdhui = trajetsAujourdhui - trajetsHier;
|
||||
const differenceTrajets = trajetsPeriode - trajetsPeriodePrecedente;
|
||||
|
||||
// 3. Trajets réalisés ce mois (terminés)
|
||||
const trajetsRealisesMois = await prisma.trajet.count({
|
||||
// 3. Trajets réalisés sur la période (terminés)
|
||||
const trajetsRealisesPeriode = await prisma.trajet.count({
|
||||
where: {
|
||||
archived: false,
|
||||
statut: 'Terminé',
|
||||
date: {
|
||||
gte: startOfMonth,
|
||||
lte: endOfMonth,
|
||||
gte: start,
|
||||
lte: end,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Trajets réalisés le mois dernier pour comparaison
|
||||
const trajetsRealisesMoisDernier = await prisma.trajet.count({
|
||||
// Trajets réalisés période précédente pour comparaison
|
||||
const trajetsRealisesPeriodePrecedente = await prisma.trajet.count({
|
||||
where: {
|
||||
archived: false,
|
||||
statut: 'Terminé',
|
||||
date: {
|
||||
gte: startOfLastMonth,
|
||||
lte: endOfLastMonth,
|
||||
gte: prevStart,
|
||||
lte: prevEnd,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const pourcentageEvolution = trajetsRealisesMoisDernier > 0
|
||||
? Math.round(((trajetsRealisesMois - trajetsRealisesMoisDernier) / trajetsRealisesMoisDernier) * 100)
|
||||
: trajetsRealisesMois > 0 ? 100 : 0;
|
||||
const pourcentageEvolution = trajetsRealisesPeriodePrecedente > 0
|
||||
? Math.round(((trajetsRealisesPeriode - trajetsRealisesPeriodePrecedente) / trajetsRealisesPeriodePrecedente) * 100)
|
||||
: trajetsRealisesPeriode > 0 ? 100 : 0;
|
||||
|
||||
// 4. Chauffeurs actifs (disponibles)
|
||||
const totalChauffeurs = await prisma.chauffeur.count();
|
||||
@@ -105,16 +252,17 @@ export async function GET(request: NextRequest) {
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
periodLabel,
|
||||
participationsMois: {
|
||||
montant: participationsMois,
|
||||
nombreFactures: nombreFactures,
|
||||
},
|
||||
trajetsAujourdhui: {
|
||||
nombre: trajetsAujourdhui,
|
||||
difference: differenceAujourdhui,
|
||||
nombre: trajetsPeriode,
|
||||
difference: differenceTrajets,
|
||||
},
|
||||
trajetsRealisesMois: {
|
||||
nombre: trajetsRealisesMois,
|
||||
nombre: trajetsRealisesPeriode,
|
||||
pourcentageEvolution: pourcentageEvolution,
|
||||
},
|
||||
chauffeursActifs: {
|
||||
|
||||
@@ -18,7 +18,7 @@ export default async function CalendrierPage() {
|
||||
|
||||
return (
|
||||
<DashboardLayout user={user}>
|
||||
<div className="p-4 sm:p-6 lg:p-8">
|
||||
<div className="px-3 py-4 sm:p-6 lg:p-8 min-w-0 overflow-x-hidden">
|
||||
<CalendrierPageContent />
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
|
||||
@@ -12,8 +12,8 @@ const poppins = Poppins({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Platform SaaS",
|
||||
description: "Plateforme SaaS",
|
||||
title: "MAD - BackOffice",
|
||||
description: "MAD",
|
||||
icons: {
|
||||
icon: "/logo.svg",
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function CalendrierPageContent() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 sm:gap-8">
|
||||
<div className="flex flex-col gap-6 sm:gap-8 min-w-0">
|
||||
{/* En-tête avec titre et bouton */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-2">
|
||||
<div>
|
||||
@@ -37,12 +37,12 @@ export default function CalendrierPageContent() {
|
||||
</div>
|
||||
|
||||
{/* Calendrier en haut */}
|
||||
<div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<CalendrierTrajets refreshTrigger={refreshTrigger} />
|
||||
</div>
|
||||
|
||||
{/* Liste des trajets en bas, triable par période */}
|
||||
<div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<ListeTrajets onTrajetCreated={handleTrajetCreated} hideNewTrajetButton />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -139,13 +139,13 @@ function DroppableDayCell({
|
||||
<button
|
||||
ref={setNodeRef}
|
||||
onClick={() => onDateClick(date)}
|
||||
className={`h-16 sm:h-20 md:h-24 p-1 sm:p-1.5 md:p-2 rounded-lg border-2 transition-all flex flex-col ${
|
||||
className={`h-12 sm:h-20 md:h-24 p-1 sm:p-1.5 md:p-2 rounded-md sm:rounded-lg border sm:border-2 transition-all flex flex-col items-center sm:items-stretch active:scale-95 sm:active:scale-100 ${
|
||||
isSelected
|
||||
? 'border-lblue bg-lblue/10'
|
||||
? 'border-lblue bg-lblue/10 sm:border-2'
|
||||
: isToday
|
||||
? 'border-lblue bg-lblue/5'
|
||||
? 'border-lblue bg-lblue/5 sm:border-2'
|
||||
: isOver
|
||||
? 'border-lgreen bg-lgreen/20 border-2'
|
||||
? 'border-lgreen bg-lgreen/20 sm:border-2'
|
||||
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
@@ -370,35 +370,47 @@ export default function CalendrierTrajets({ refreshTrigger }: CalendrierTrajetsP
|
||||
calendarDays.push(new Date(year, month, day));
|
||||
}
|
||||
|
||||
const mobileDays = ['L', 'M', 'M', 'J', 'V', 'S', 'D'];
|
||||
|
||||
const getStatutDotColor = (statut: string) => {
|
||||
switch (statut) {
|
||||
case 'Validé': return 'bg-purple-500';
|
||||
case 'Terminé': return 'bg-green-500';
|
||||
case 'En cours': return 'bg-blue-500';
|
||||
case 'Annulé': return 'bg-red-500';
|
||||
default: return 'bg-lblue';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm p-4 sm:p-6">
|
||||
<div className="bg-white rounded-lg shadow-sm p-3 sm:p-6 min-w-0 overflow-hidden">
|
||||
{/* En-tête du calendrier */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-0 mb-4 sm:mb-6">
|
||||
<h2 className="text-lg sm:text-xl font-semibold text-gray-900 capitalize">
|
||||
<div className="flex items-center justify-between mb-4 sm:mb-6">
|
||||
<h2 className="text-base sm:text-xl font-semibold text-gray-900 capitalize">
|
||||
{currentDate.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}
|
||||
</h2>
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<button
|
||||
onClick={goToPreviousMonth}
|
||||
className="p-1.5 sm:p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
className="p-2 rounded-lg hover:bg-gray-100 transition-colors active:bg-gray-200"
|
||||
title="Mois précédent"
|
||||
>
|
||||
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={goToToday}
|
||||
className="px-3 sm:px-4 py-1.5 sm:py-2 text-xs sm:text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors flex-1 sm:flex-initial"
|
||||
className="px-3 py-1.5 sm:px-4 sm:py-2 text-xs sm:text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors active:bg-gray-300"
|
||||
>
|
||||
Aujourd'hui
|
||||
Aujourd'hui
|
||||
</button>
|
||||
<button
|
||||
onClick={goToNextMonth}
|
||||
className="p-1.5 sm:p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
className="p-2 rounded-lg hover:bg-gray-100 transition-colors active:bg-gray-200"
|
||||
title="Mois suivant"
|
||||
>
|
||||
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -411,18 +423,19 @@ export default function CalendrierTrajets({ refreshTrigger }: CalendrierTrajetsP
|
||||
<>
|
||||
{/* Grille du calendrier avec drag and drop */}
|
||||
<DndContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<div className="grid grid-cols-7 gap-1 sm:gap-2 mb-4 sm:mb-6">
|
||||
{/* En-têtes des jours */}
|
||||
{days.map((day) => (
|
||||
<div key={day} className="text-center text-[10px] sm:text-xs md:text-sm font-semibold text-gray-600 py-1 sm:py-2">
|
||||
{day}
|
||||
<div className="grid grid-cols-7 gap-0.5 sm:gap-2 mb-4 sm:mb-6">
|
||||
{/* En-têtes des jours - version courte sur mobile */}
|
||||
{days.map((day, i) => (
|
||||
<div key={day} className="text-center text-xs sm:text-xs md:text-sm font-semibold text-gray-500 py-1 sm:py-2">
|
||||
<span className="sm:hidden">{mobileDays[i]}</span>
|
||||
<span className="hidden sm:inline">{day}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Jours du calendrier */}
|
||||
{calendarDays.map((date, index) => {
|
||||
if (!date) {
|
||||
return <div key={`empty-${index}`} className="h-24" />;
|
||||
return <div key={`empty-${index}`} className="h-12 sm:h-24" />;
|
||||
}
|
||||
|
||||
const trajetsDuJour = getTrajetsForDate(date);
|
||||
@@ -445,24 +458,45 @@ export default function CalendrierTrajets({ refreshTrigger }: CalendrierTrajetsP
|
||||
isSelected={isSelected}
|
||||
onDateClick={handleDateClick}
|
||||
>
|
||||
<div className="text-xs sm:text-sm font-medium text-gray-900 mb-0.5 sm:mb-1">
|
||||
{/* Numéro du jour */}
|
||||
<div className={`text-xs sm:text-sm font-medium mb-0.5 sm:mb-1 ${
|
||||
isToday ? 'text-lblue font-bold' : 'text-gray-900'
|
||||
}`}>
|
||||
{date.getDate()}
|
||||
</div>
|
||||
|
||||
{/* Mobile: indicateurs de points colorés */}
|
||||
{trajetsDuJour.length > 0 && (
|
||||
<div className="space-y-0.5 sm:space-y-1 flex-1 overflow-hidden">
|
||||
{trajetsDuJour.slice(0, 2).map((trajet) => (
|
||||
<DraggableTrajetEvent
|
||||
key={trajet.id}
|
||||
trajet={trajet}
|
||||
onClick={handleTrajetClick}
|
||||
/>
|
||||
))}
|
||||
{trajetsDuJour.length > 2 && (
|
||||
<div className="text-[9px] sm:text-[10px] text-gray-500 font-semibold text-center pt-0.5">
|
||||
+{trajetsDuJour.length - 2}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<div className="flex items-center justify-center gap-0.5 flex-wrap sm:hidden">
|
||||
{trajetsDuJour.length <= 3 ? (
|
||||
trajetsDuJour.map((t) => (
|
||||
<span key={t.id} className={`w-1.5 h-1.5 rounded-full ${getStatutDotColor(t.statut)}`} />
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${getStatutDotColor(trajetsDuJour[0].statut)}`} />
|
||||
<span className="text-[9px] font-bold text-gray-500 leading-none">{trajetsDuJour.length}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop: pastilles d'événements comme avant */}
|
||||
<div className="hidden sm:flex sm:flex-col space-y-0.5 sm:space-y-1 flex-1 overflow-hidden">
|
||||
{trajetsDuJour.slice(0, 2).map((trajet) => (
|
||||
<DraggableTrajetEvent
|
||||
key={trajet.id}
|
||||
trajet={trajet}
|
||||
onClick={handleTrajetClick}
|
||||
/>
|
||||
))}
|
||||
{trajetsDuJour.length > 2 && (
|
||||
<div className="text-[10px] text-gray-500 font-semibold text-center pt-0.5">
|
||||
+{trajetsDuJour.length - 2}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DroppableDayCell>
|
||||
);
|
||||
@@ -488,8 +522,12 @@ export default function CalendrierTrajets({ refreshTrigger }: CalendrierTrajetsP
|
||||
{/* Détails des trajets du jour sélectionné */}
|
||||
{selectedDate && selectedTrajets.length > 0 && (
|
||||
<div className="mt-4 sm:mt-6 pt-4 sm:pt-6 border-t border-gray-200">
|
||||
<h3 className="text-base sm:text-lg font-semibold text-gray-900 mb-3 sm:mb-4">
|
||||
Trajets du {formatDate(selectedDate)}
|
||||
<h3 className="text-sm sm:text-lg font-semibold text-gray-900 mb-3 sm:mb-4">
|
||||
Trajets du{' '}
|
||||
<span className="hidden sm:inline">{formatDate(selectedDate)}</span>
|
||||
<span className="sm:hidden">
|
||||
{selectedDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
|
||||
</span>
|
||||
</h3>
|
||||
<div className="space-y-2 sm:space-y-3">
|
||||
{selectedTrajets.map((trajet) => {
|
||||
@@ -512,60 +550,63 @@ export default function CalendrierTrajets({ refreshTrigger }: CalendrierTrajetsP
|
||||
<button
|
||||
key={trajet.id}
|
||||
onClick={() => setSelectedTrajet(trajet)}
|
||||
className="w-full p-3 sm:p-4 bg-gray-50 rounded-lg border border-gray-200 hover:border-gray-300 hover:shadow-sm transition-all text-left"
|
||||
className="w-full p-3 sm:p-4 bg-gray-50 rounded-lg border border-gray-200 hover:border-gray-300 hover:shadow-sm transition-all text-left active:bg-gray-100"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2 mb-2">
|
||||
<span className="text-xs sm:text-sm font-semibold text-gray-900">
|
||||
{/* Header: nom + heure + statut */}
|
||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
{trajet.adherent.prenom} {trajet.adherent.nom}
|
||||
</span>
|
||||
{trajet.participations?.[0] && (
|
||||
<span className="px-1.5 sm:px-2 py-0.5 text-[10px] sm:text-xs font-mono font-medium rounded bg-lblue/10 text-lblue" title="Référence de prescription">
|
||||
{getParticipationRef(trajet.participations[0].id)}
|
||||
</span>
|
||||
)}
|
||||
<span className="px-1.5 sm:px-2 py-0.5 text-[10px] sm:text-xs font-medium rounded bg-lblue/10 text-lblue">
|
||||
<span className="px-1.5 py-0.5 text-[10px] sm:text-xs font-medium rounded bg-lblue/10 text-lblue">
|
||||
{formatTime(trajet.date)}
|
||||
</span>
|
||||
<span
|
||||
className={`px-1.5 sm:px-2 py-0.5 text-[10px] sm:text-xs font-medium rounded border ${getStatutColor(trajet.statut)}`}
|
||||
className={`px-1.5 py-0.5 text-[10px] sm:text-xs font-medium rounded border ${getStatutColor(trajet.statut)}`}
|
||||
>
|
||||
{trajet.statut}
|
||||
</span>
|
||||
{trajet.participations?.[0] && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] sm:text-xs font-mono font-medium rounded bg-lblue/10 text-lblue" title="Référence de prescription">
|
||||
{getParticipationRef(trajet.participations[0].id)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Adresses */}
|
||||
<div className="text-xs sm:text-sm text-gray-600 mb-1 break-words">
|
||||
<span className="font-medium">Départ:</span> {trajet.adresseDepart}
|
||||
<span className="font-medium">De:</span> {trajet.adresseDepart}
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm text-gray-600 mb-2 break-words">
|
||||
<span className="font-medium">Arrivée:</span> {trajet.adresseArrivee}
|
||||
<div className="text-xs sm:text-sm text-gray-600 break-words">
|
||||
<span className="font-medium">A:</span> {trajet.adresseArrivee}
|
||||
</div>
|
||||
{/* Chauffeur */}
|
||||
{trajet.chauffeur ? (
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<svg className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17a2 2 0 11-4 0 2 2 0 014 0zM19 17a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
<div className="flex items-center gap-1.5 mt-2">
|
||||
<svg className="w-3.5 h-3.5 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<span className="text-xs sm:text-sm font-medium text-gray-900 truncate">
|
||||
Chauffeur: {trajet.chauffeur.prenom} {trajet.chauffeur.nom}
|
||||
<span className="text-xs font-medium text-gray-700">
|
||||
{trajet.chauffeur.prenom} {trajet.chauffeur.nom}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<svg className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-orange-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div className="flex items-center gap-1.5 mt-2">
|
||||
<svg className="w-3.5 h-3.5 text-orange-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span className="text-xs sm:text-sm text-orange-600 font-medium">
|
||||
Aucun chauffeur assigné
|
||||
<span className="text-xs text-orange-600 font-medium">
|
||||
Sans chauffeur
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{trajet.commentaire && (
|
||||
<div className="mt-2 text-xs sm:text-sm text-gray-500 italic line-clamp-2 break-words">
|
||||
<div className="mt-1.5 text-xs text-gray-500 italic line-clamp-2 break-words">
|
||||
{trajet.commentaire}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-gray-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4 text-gray-400 flex-shrink-0 mt-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
@@ -578,7 +619,11 @@ export default function CalendrierTrajets({ refreshTrigger }: CalendrierTrajetsP
|
||||
|
||||
{selectedDate && selectedTrajets.length === 0 && (
|
||||
<div className="mt-4 sm:mt-6 pt-4 sm:pt-6 border-t border-gray-200 text-center text-xs sm:text-sm text-gray-500">
|
||||
Aucun trajet prévu pour le {formatDate(selectedDate)}
|
||||
Aucun trajet prévu pour le{' '}
|
||||
<span className="hidden sm:inline">{formatDate(selectedDate)}</span>
|
||||
<span className="sm:hidden">
|
||||
{selectedDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import TrajetForm from './TrajetForm';
|
||||
import AdherentForm from './AdherentForm';
|
||||
import TrajetDetailModal from './TrajetDetailModal';
|
||||
import { getParticipationRef } from '@/lib/participation-ref';
|
||||
|
||||
type PeriodType = 'day' | 'yesterday' | 'week' | 'month' | 'quarter' | 'year' | 'custom';
|
||||
|
||||
interface Stats {
|
||||
periodLabel?: string;
|
||||
participationsMois: {
|
||||
montant: number;
|
||||
nombreFactures: number;
|
||||
@@ -53,23 +56,54 @@ interface DashboardContentProps {
|
||||
userName?: string | null;
|
||||
}
|
||||
|
||||
const PERIOD_OPTIONS: { value: PeriodType; label: string }[] = [
|
||||
{ value: 'day', label: "Aujourd'hui" },
|
||||
{ value: 'yesterday', label: 'Hier' },
|
||||
{ value: 'week', label: 'Cette semaine' },
|
||||
{ value: 'month', label: 'Ce mois-ci' },
|
||||
{ value: 'quarter', label: 'Ce trimestre' },
|
||||
{ value: 'year', label: 'Cette année' },
|
||||
{ value: 'custom', label: 'Personnalisé' },
|
||||
];
|
||||
|
||||
export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
const router = useRouter();
|
||||
const [stats, setStats] = useState<Stats | null>(null);
|
||||
const [trajetsRecents, setTrajetsRecents] = useState<Trajet[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [period, setPeriod] = useState<PeriodType>('month');
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [customFrom, setCustomFrom] = useState('');
|
||||
const [customTo, setCustomTo] = useState('');
|
||||
const [showTrajetForm, setShowTrajetForm] = useState(false);
|
||||
const [showAdherentForm, setShowAdherentForm] = useState(false);
|
||||
const [selectedTrajet, setSelectedTrajet] = useState<Trajet | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
if (period === 'custom' && customFrom && customTo) {
|
||||
fetchStatsCustom(customFrom, customTo);
|
||||
} else if (period !== 'custom') {
|
||||
fetchStats(period);
|
||||
}
|
||||
fetchTrajetsRecents();
|
||||
}, [period, customFrom, customTo]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const fetchStats = async () => {
|
||||
const fetchStats = async (p: PeriodType = period) => {
|
||||
if (p === 'custom') return;
|
||||
try {
|
||||
const response = await fetch('/api/dashboard/stats');
|
||||
const params = new URLSearchParams({ period: p });
|
||||
const response = await fetch(`/api/dashboard/stats?${params}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setStats(data);
|
||||
@@ -79,6 +113,27 @@ export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchStatsCustom = async (from: string, to: string) => {
|
||||
try {
|
||||
const params = new URLSearchParams({ from, to });
|
||||
const response = await fetch(`/api/dashboard/stats?${params}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setStats(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des statistiques:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshStats = () => {
|
||||
if (period === 'custom' && customFrom && customTo) {
|
||||
fetchStatsCustom(customFrom, customTo);
|
||||
} else if (period !== 'custom') {
|
||||
fetchStats(period);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTrajetsRecents = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -112,46 +167,6 @@ export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
return `${prenom.charAt(0)}${nom.charAt(0)}`.toUpperCase();
|
||||
};
|
||||
|
||||
const getStatusColor = (statut: string) => {
|
||||
switch (statut) {
|
||||
case 'Validé':
|
||||
return 'bg-green-100 text-green-700';
|
||||
case 'Terminé':
|
||||
return 'bg-green-100 text-green-700';
|
||||
case 'En cours':
|
||||
return 'bg-blue-100 text-blue-700';
|
||||
case 'Planifié':
|
||||
return 'bg-gray-100 text-gray-700';
|
||||
case 'Annulé':
|
||||
return 'bg-red-100 text-red-700';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-700';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusDotColor = (statut: string) => {
|
||||
switch (statut) {
|
||||
case 'Validé':
|
||||
case 'Terminé':
|
||||
return 'bg-green-500';
|
||||
case 'En cours':
|
||||
return 'bg-blue-500';
|
||||
case 'Planifié':
|
||||
return 'bg-yellow-500';
|
||||
case 'Annulé':
|
||||
return 'bg-red-500';
|
||||
default:
|
||||
return 'bg-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getChauffeurNumber = (chauffeur: { prenom: string; nom: string } | null | undefined) => {
|
||||
if (!chauffeur) return null;
|
||||
// Simple hash pour obtenir un numéro de chauffeur (1-5)
|
||||
const hash = (chauffeur.prenom + chauffeur.nom).split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
return (hash % 5) + 1;
|
||||
};
|
||||
|
||||
// Montant estimé par trajet (basé sur l'image)
|
||||
const getMontantTrajet = () => {
|
||||
return 6.80; // Montant moyen
|
||||
@@ -159,14 +174,122 @@ export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
|
||||
return (
|
||||
<div className="p-4 sm:p-6 space-y-6 sm:space-y-8">
|
||||
{/* En-tête */}
|
||||
{/* En-tête avec menu déroulant de période */}
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">
|
||||
Content de vous revoir <span className="text-dyellow">{userName || 'Utilisateur'}</span>
|
||||
</h1>
|
||||
<p className="text-xs sm:text-sm text-gray-600">
|
||||
Bienvenue sur votre tableau de bord.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4 mb-2">
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">
|
||||
Content de vous revoir <span className="text-dyellow">{userName || 'Utilisateur'}</span>
|
||||
</h1>
|
||||
<p className="text-xs sm:text-sm text-gray-600 mt-1">
|
||||
Bienvenue sur votre tableau de bord.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative flex-shrink-0" ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
className="inline-flex items-center gap-2 px-4 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-200/80 rounded-xl shadow-[0_1px_3px_rgba(0,0,0,0.06)] hover:shadow-[0_2px_8px_rgba(0,0,0,0.08)] hover:border-gray-200 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-dyellow/20 focus:border-dyellow"
|
||||
>
|
||||
<svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>
|
||||
{period === 'custom' && customFrom && customTo
|
||||
? `${customFrom.split('-').reverse().slice(0, 2).join('/')} → ${customTo.split('-').reverse().slice(0, 2).join('/')}`
|
||||
: PERIOD_OPTIONS.find((o) => o.value === period)?.label || 'Période'}
|
||||
</span>
|
||||
<svg className={`w-4 h-4 text-gray-400 transition-transform duration-200 ${dropdownOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{dropdownOpen && (
|
||||
<div className="absolute right-0 top-full mt-2 w-64 bg-white rounded-xl shadow-[0_4px_20px_rgba(0,0,0,0.08)] border border-gray-100 py-2 z-50 overflow-hidden">
|
||||
<div className="px-3 py-2 border-b border-gray-100">
|
||||
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Période des statistiques</p>
|
||||
</div>
|
||||
<div className="max-h-72 overflow-y-auto">
|
||||
{PERIOD_OPTIONS.filter((o) => o.value !== 'custom').map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPeriod(opt.value);
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
|
||||
period === opt.value
|
||||
? 'bg-dyellow/10 text-dyellow font-medium'
|
||||
: 'text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<span className="w-6 h-6 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
|
||||
<svg className="w-3.5 h-3.5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</span>
|
||||
{opt.label}
|
||||
{period === opt.value && (
|
||||
<svg className="w-4 h-4 ml-auto text-dyellow" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
<div className="border-t border-gray-100 my-2" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPeriod('custom')}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
|
||||
period === 'custom'
|
||||
? 'bg-dyellow/10 text-dyellow font-medium'
|
||||
: 'text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<span className="w-6 h-6 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
|
||||
<svg className="w-3.5 h-3.5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
Personnalisé
|
||||
{period === 'custom' && (
|
||||
<svg className="w-4 h-4 ml-auto text-dyellow" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
{period === 'custom' && (
|
||||
<div className="px-4 py-3 bg-gray-50/80 border-t border-gray-100 space-y-3">
|
||||
<p className="text-xs font-medium text-gray-600">Sélectionner une plage</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="date"
|
||||
value={customFrom}
|
||||
onChange={(e) => setCustomFrom(e.target.value)}
|
||||
className="flex-1 px-3 py-2 text-sm border border-gray-200 rounded-lg bg-white focus:ring-2 focus:ring-dyellow/30 focus:border-dyellow transition-shadow"
|
||||
/>
|
||||
<span className="text-gray-400 text-sm">→</span>
|
||||
<input
|
||||
type="date"
|
||||
value={customTo}
|
||||
onChange={(e) => setCustomTo(e.target.value)}
|
||||
className="flex-1 px-3 py-2 text-sm border border-gray-200 rounded-lg bg-white focus:ring-2 focus:ring-dyellow/30 focus:border-dyellow transition-shadow"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDropdownOpen(false)}
|
||||
className="w-full py-2.5 text-sm font-medium text-white bg-dyellow hover:bg-dyellow/90 rounded-lg transition-colors"
|
||||
>
|
||||
Appliquer
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistiques */}
|
||||
@@ -181,7 +304,9 @@ export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] sm:text-xs text-gray-600 mb-1.5 sm:mb-2 font-medium uppercase tracking-wide">Participations du mois</p>
|
||||
<p className="text-[10px] sm:text-xs text-gray-600 mb-1.5 sm:mb-2 font-medium uppercase tracking-wide">
|
||||
Participations {period === 'month' ? 'ce mois-ci' : 'sur la période'}
|
||||
</p>
|
||||
<p className="text-2xl sm:text-3xl font-bold text-gray-900 mb-1.5 sm:mb-2">
|
||||
{stats ? `${stats.participationsMois.montant.toFixed(2).replace('.', ',')}€` : '0,00€'}
|
||||
</p>
|
||||
@@ -201,13 +326,15 @@ export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] sm:text-xs text-gray-600 mb-1.5 sm:mb-2 font-medium uppercase tracking-wide">Trajets Aujourd'hui</p>
|
||||
<p className="text-[10px] sm:text-xs text-gray-600 mb-1.5 sm:mb-2 font-medium uppercase tracking-wide">
|
||||
Trajets {period === 'day' ? "aujourd'hui" : period === 'yesterday' ? 'hier' : 'sur la période'}
|
||||
</p>
|
||||
<p className="text-2xl sm:text-3xl font-bold text-gray-900 mb-1.5 sm:mb-2">
|
||||
{stats ? stats.trajetsAujourdhui.nombre : 0}
|
||||
</p>
|
||||
<p className="text-[10px] sm:text-xs text-gray-500 font-medium">
|
||||
{stats && stats.trajetsAujourdhui.difference !== 0
|
||||
? `${stats.trajetsAujourdhui.difference > 0 ? '+' : ''}${stats.trajetsAujourdhui.difference} vs hier`
|
||||
? `${stats.trajetsAujourdhui.difference > 0 ? '+' : ''}${stats.trajetsAujourdhui.difference} vs période précédente`
|
||||
: 'Aucun changement'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -223,13 +350,15 @@ export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] sm:text-xs text-gray-600 mb-1.5 sm:mb-2 font-medium uppercase tracking-wide">Trajets réalisés ce mois</p>
|
||||
<p className="text-[10px] sm:text-xs text-gray-600 mb-1.5 sm:mb-2 font-medium uppercase tracking-wide">
|
||||
Trajets réalisés {period === 'month' ? 'ce mois-ci' : 'sur la période'}
|
||||
</p>
|
||||
<p className="text-2xl sm:text-3xl font-bold text-gray-900 mb-1.5 sm:mb-2">
|
||||
{stats ? stats.trajetsRealisesMois.nombre : 0}
|
||||
</p>
|
||||
<p className="text-[10px] sm:text-xs text-gray-500 font-medium">
|
||||
{stats && stats.trajetsRealisesMois.pourcentageEvolution !== 0
|
||||
? `${stats.trajetsRealisesMois.pourcentageEvolution > 0 ? '+' : ''}${stats.trajetsRealisesMois.pourcentageEvolution}% vs mois dernier`
|
||||
? `${stats.trajetsRealisesMois.pourcentageEvolution > 0 ? '+' : ''}${stats.trajetsRealisesMois.pourcentageEvolution}% vs période précédente`
|
||||
: 'Aucun changement'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -261,7 +390,14 @@ export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
{/* Actions Rapides */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3 sm:mb-4">
|
||||
<h2 className="text-lg sm:text-xl font-bold text-gray-900">Actions Rapides</h2>
|
||||
<h2 className="text-lg sm:text-xl font-bold text-gray-900 flex items-center gap-2">
|
||||
<span className="w-8 h-8 rounded-lg bg-lblue/10 flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-lblue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</span>
|
||||
Actions Rapides
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 sm:gap-4">
|
||||
<button
|
||||
@@ -323,10 +459,17 @@ export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
{/* Trajets Récents */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3 sm:mb-4">
|
||||
<h2 className="text-lg sm:text-xl font-bold text-gray-900">Trajets Récents</h2>
|
||||
<h2 className="text-lg sm:text-xl font-bold text-gray-900 tracking-tight flex items-center gap-2">
|
||||
<span className="w-8 h-8 rounded-lg bg-lblue/10 flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-lblue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
|
||||
</svg>
|
||||
</span>
|
||||
Trajets Récents
|
||||
</h2>
|
||||
<button
|
||||
onClick={fetchTrajetsRecents}
|
||||
className="text-xs sm:text-sm text-lblue hover:text-dblue font-medium flex items-center gap-1 sm:gap-2"
|
||||
className="text-xs sm:text-sm text-gray-500 hover:text-lblue font-medium flex items-center gap-1.5 transition-colors"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
@@ -334,7 +477,7 @@ export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
<span className="hidden sm:inline">Actualiser</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div className="bg-white rounded-xl shadow-[0_1px_3px_rgba(0,0,0,0.05)] border border-gray-100/80 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="text-center py-6 sm:py-8 text-sm text-gray-500">Chargement...</div>
|
||||
) : trajetsRecents.length === 0 ? (
|
||||
@@ -342,111 +485,74 @@ export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
Aucun trajet créé récemment
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 sm:p-6 space-y-3">
|
||||
{trajetsRecents.map((trajet) => (
|
||||
<div
|
||||
key={trajet.id}
|
||||
onClick={() => setSelectedTrajet(trajet)}
|
||||
className="p-3 sm:p-4 bg-gray-50 rounded-lg border border-gray-200 hover:border-gray-300 hover:shadow-sm transition-all cursor-pointer"
|
||||
>
|
||||
<div className="flex items-start gap-3 sm:gap-4">
|
||||
{/* Avatar adhérent */}
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-full bg-lgreen flex items-center justify-center text-white text-xs sm:text-sm font-semibold flex-shrink-0">
|
||||
{getInitials(trajet.adherent.nom, trajet.adherent.prenom)}
|
||||
</div>
|
||||
|
||||
{/* Informations principales */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 sm:gap-4 mb-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-xs sm:text-sm font-semibold text-gray-900 truncate">
|
||||
{trajet.adherent.prenom} {trajet.adherent.nom}
|
||||
</h3>
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3 mt-1">
|
||||
{trajet.participations?.[0] && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] sm:text-xs font-mono font-medium rounded bg-lblue/10 text-lblue" title="Référence de prescription">
|
||||
{getParticipationRef(trajet.participations[0].id)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] sm:text-xs text-gray-500 flex items-center gap-1">
|
||||
<svg className="w-2.5 h-2.5 sm:w-3 sm:h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
{formatDate(trajet.date)}
|
||||
</span>
|
||||
<span className="text-[10px] sm:text-xs text-gray-500 flex items-center gap-1">
|
||||
<svg className="w-2.5 h-2.5 sm:w-3 sm:h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{formatTime(trajet.date)}
|
||||
</span>
|
||||
<span
|
||||
className={`px-1.5 sm:px-2 py-0.5 text-[10px] sm:text-xs font-medium rounded ${
|
||||
trajet.statut === 'Terminé'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: trajet.statut === 'En cours'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: trajet.statut === 'Annulé'
|
||||
? 'bg-red-100 text-red-700'
|
||||
: trajet.statut === 'Validé'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{trajet.statut}
|
||||
<div
|
||||
className="overflow-x-auto"
|
||||
style={{ WebkitOverflowScrolling: 'touch' } as React.CSSProperties}
|
||||
>
|
||||
<table className="w-full text-sm min-w-[420px]">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 bg-gray-50/80">
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-600">Adhérent</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-600">Statut</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-600">Fichier</th>
|
||||
<th className="w-10 px-4"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{trajetsRecents.map((trajet) => (
|
||||
<tr
|
||||
key={trajet.id}
|
||||
onClick={() => setSelectedTrajet(trajet)}
|
||||
className="border-b border-gray-100 last:border-b-0 hover:bg-gray-50/80 transition-colors cursor-pointer"
|
||||
>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-full bg-lgreen flex items-center justify-center text-white text-xs font-semibold flex-shrink-0 shadow-sm">
|
||||
{getInitials(trajet.adherent.nom, trajet.adherent.prenom)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-gray-900">
|
||||
{trajet.adherent.prenom} {trajet.adherent.nom}
|
||||
</span>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
{formatDate(trajet.date)} · {formatTime(trajet.date)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Adresses */}
|
||||
<div className="space-y-1 mb-2">
|
||||
<div className="text-xs sm:text-sm text-gray-600">
|
||||
<span className="font-medium">Départ:</span>{' '}
|
||||
<span className="text-gray-900 break-words">{trajet.adresseDepart}</span>
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm text-gray-600">
|
||||
<span className="font-medium">Arrivée:</span>{' '}
|
||||
<span className="text-gray-900 break-words">{trajet.adresseArrivee}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chauffeur */}
|
||||
<div className="flex items-center gap-2 sm:gap-4 mt-2 sm:mt-3">
|
||||
{trajet.chauffeur ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 sm:w-8 sm:h-8 rounded-full bg-lblue flex items-center justify-center text-white text-[10px] sm:text-xs font-semibold">
|
||||
{getInitials(trajet.chauffeur.nom, trajet.chauffeur.prenom)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[10px] sm:text-xs text-gray-500">Chauffeur</div>
|
||||
<div className="text-xs sm:text-sm font-medium text-gray-900">
|
||||
{trajet.chauffeur.prenom} {trajet.chauffeur.nom}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span
|
||||
className={`inline-flex px-2 py-0.5 text-[11px] font-medium rounded-md ${
|
||||
trajet.statut === 'Terminé' || trajet.statut === 'Validé'
|
||||
? 'bg-emerald-50 text-emerald-700'
|
||||
: trajet.statut === 'En cours'
|
||||
? 'bg-sky-50 text-sky-700'
|
||||
: trajet.statut === 'Annulé'
|
||||
? 'bg-red-50 text-red-700'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{trajet.statut}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{trajet.participations?.[0] ? (
|
||||
<span className="px-2 py-0.5 text-[11px] font-mono font-semibold rounded-md bg-lblue/15 text-lblue border border-lblue/20">
|
||||
{getParticipationRef(trajet.participations[0].id)}
|
||||
</span>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span className="text-xs sm:text-sm text-orange-600 font-medium">
|
||||
Aucun chauffeur assigné
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-gray-300">—</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Commentaire */}
|
||||
{trajet.commentaire && (
|
||||
<div className="mt-2 pt-2 border-t border-gray-200">
|
||||
<p className="text-[10px] sm:text-xs text-gray-500 italic break-words">{trajet.commentaire}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<svg className="w-4 h-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -460,7 +566,7 @@ export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
onSuccess={() => {
|
||||
setShowTrajetForm(false);
|
||||
fetchTrajetsRecents();
|
||||
fetchStats();
|
||||
refreshStats();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -470,7 +576,7 @@ export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
adherent={null}
|
||||
onClose={() => {
|
||||
setShowAdherentForm(false);
|
||||
fetchStats();
|
||||
refreshStats();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -481,7 +587,7 @@ export default function DashboardContent({ userName }: DashboardContentProps) {
|
||||
onClose={() => setSelectedTrajet(null)}
|
||||
onUpdate={() => {
|
||||
fetchTrajetsRecents();
|
||||
fetchStats();
|
||||
refreshStats();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -489,7 +489,7 @@ export default function DashboardLayout({ user, children }: DashboardLayoutProps
|
||||
</header>
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="flex-1 overflow-auto">
|
||||
<main className="flex-1 overflow-x-hidden overflow-y-auto min-w-0">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import TrajetForm from './TrajetForm';
|
||||
import TrajetDetailModal from './TrajetDetailModal';
|
||||
import { getParticipationRef } from '@/lib/participation-ref';
|
||||
|
||||
interface Trajet {
|
||||
@@ -54,6 +55,7 @@ export default function ListeTrajets({ onTrajetCreated, compact, hideNewTrajetBu
|
||||
const [startDate, setStartDate] = useState<string>('');
|
||||
const [endDate, setEndDate] = useState<string>('');
|
||||
const [showTrajetForm, setShowTrajetForm] = useState(false);
|
||||
const [selectedTrajet, setSelectedTrajet] = useState<Trajet | null>(null);
|
||||
|
||||
const getDateRange = (): { start: string; end: string } | null => {
|
||||
switch (filterPeriod) {
|
||||
@@ -174,7 +176,7 @@ export default function ListeTrajets({ onTrajetCreated, compact, hideNewTrajetBu
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-white rounded-lg shadow-sm overflow-hidden flex flex-col ${
|
||||
className={`bg-white rounded-lg shadow-sm overflow-hidden flex flex-col min-w-0 ${
|
||||
compact ? 'h-full min-h-0' : ''
|
||||
}`}
|
||||
>
|
||||
@@ -182,7 +184,7 @@ export default function ListeTrajets({ onTrajetCreated, compact, hideNewTrajetBu
|
||||
<div className={`border-b border-gray-200 flex-shrink-0 ${compact ? 'p-3' : 'p-4 sm:p-6'}`}>
|
||||
<div className="flex flex-col gap-2 sm:gap-3">
|
||||
{/* Barre de recherche */}
|
||||
<div className="relative">
|
||||
<div className="relative min-w-0">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
@@ -190,10 +192,10 @@ export default function ListeTrajets({ onTrajetCreated, compact, hideNewTrajetBu
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher (adhérent, adresse, référence PART-…)..."
|
||||
placeholder="Rechercher (adhérent, adresse, réf. PART-…)..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="block w-full pl-9 pr-3 py-2 text-sm border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-lblue transition-all"
|
||||
className="block w-full min-w-0 pl-9 pr-3 py-2 text-sm border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-lblue transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -333,143 +335,183 @@ export default function ListeTrajets({ onTrajetCreated, compact, hideNewTrajetBu
|
||||
|
||||
{/* Liste des trajets avec scrollbar */}
|
||||
<div
|
||||
className={`flex-1 min-h-0 overflow-y-auto ${compact ? 'p-3' : 'p-4 sm:p-6'}`}
|
||||
className={`flex-1 min-h-0 min-w-0 overflow-y-auto ${compact ? 'p-3' : 'p-4 sm:p-6'}`}
|
||||
style={{ scrollbarGutter: 'stable' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-base font-semibold text-gray-900">
|
||||
<div className="flex items-center justify-between mb-3 sm:mb-4">
|
||||
<h2 className="text-lg sm:text-xl font-bold text-gray-900 tracking-tight flex items-center gap-2">
|
||||
<span className="w-8 h-8 rounded-lg bg-lblue/10 flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-lblue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
|
||||
</svg>
|
||||
</span>
|
||||
{filterPeriod === 'derniers' ? 'Derniers trajets créés' : 'Trajets'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={fetchTrajets}
|
||||
className="text-xs text-lblue hover:text-dblue font-medium flex items-center gap-1"
|
||||
className="text-xs sm:text-sm text-gray-500 hover:text-lblue font-medium flex items-center gap-1.5 transition-colors"
|
||||
title="Actualiser"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-3.5 h-3.5 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Actualiser</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-6 text-sm text-gray-500">Chargement...</div>
|
||||
) : filteredTrajets.length === 0 ? (
|
||||
<div className="text-center py-6 text-xs text-gray-500">
|
||||
{trajets.length === 0
|
||||
? filterPeriod === 'derniers'
|
||||
? 'Aucun trajet créé récemment'
|
||||
: 'Aucun trajet pour cette période'
|
||||
: 'Aucun trajet ne correspond à votre recherche'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{filteredTrajets.map((trajet) => (
|
||||
<div
|
||||
key={trajet.id}
|
||||
className={`bg-gray-50 rounded-lg border border-gray-200 hover:border-gray-300 transition-colors ${
|
||||
compact ? 'p-2.5' : 'p-3 sm:p-4'
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-start ${compact ? 'gap-2' : 'gap-3 sm:gap-4'}`}>
|
||||
{/* Avatar adhérent */}
|
||||
<div className={`rounded-full bg-lgreen flex items-center justify-center text-white font-semibold flex-shrink-0 ${
|
||||
compact ? 'w-9 h-9 text-xs' : 'w-10 h-10 sm:w-12 sm:h-12 text-xs sm:text-sm'
|
||||
}`}>
|
||||
{getInitials(trajet.adherent.nom, trajet.adherent.prenom)}
|
||||
</div>
|
||||
|
||||
{/* Informations principales */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 sm:gap-4 mb-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-xs sm:text-sm font-semibold text-gray-900 truncate">
|
||||
{trajet.adherent.prenom} {trajet.adherent.nom}
|
||||
</h3>
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3 mt-1">
|
||||
{participationRef(trajet) && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] sm:text-xs font-mono font-medium rounded bg-lblue/10 text-lblue" title="Référence de prescription">
|
||||
{participationRef(trajet)}
|
||||
<div className="bg-white rounded-xl shadow-[0_1px_3px_rgba(0,0,0,0.05)] border border-gray-100/80 overflow-hidden min-w-0">
|
||||
{loading ? (
|
||||
<div className="text-center py-6 sm:py-8 text-sm text-gray-500">Chargement...</div>
|
||||
) : filteredTrajets.length === 0 ? (
|
||||
<div className="text-center py-6 sm:py-8 text-sm text-gray-500">
|
||||
{trajets.length === 0
|
||||
? filterPeriod === 'derniers'
|
||||
? 'Aucun trajet créé récemment'
|
||||
: 'Aucun trajet pour cette période'
|
||||
: 'Aucun trajet ne correspond à votre recherche'}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Mobile: layout en cartes */}
|
||||
<div className="sm:hidden divide-y divide-gray-100">
|
||||
{filteredTrajets.map((trajet) => (
|
||||
<button
|
||||
key={trajet.id}
|
||||
onClick={() => setSelectedTrajet(trajet)}
|
||||
className="w-full p-3 text-left hover:bg-gray-50 active:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-9 h-9 rounded-full bg-lgreen flex items-center justify-center text-white text-xs font-semibold flex-shrink-0 shadow-sm mt-0.5">
|
||||
{getInitials(trajet.adherent.nom, trajet.adherent.prenom)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-semibold text-gray-900 truncate">
|
||||
{trajet.adherent.prenom} {trajet.adherent.nom}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] sm:text-xs text-gray-500 flex items-center gap-1">
|
||||
<svg className="w-2.5 h-2.5 sm:w-3 sm:h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
<svg className="w-4 h-4 text-gray-300 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
{formatDate(trajet.date)}
|
||||
</span>
|
||||
<span className="text-[10px] sm:text-xs text-gray-500 flex items-center gap-1">
|
||||
<svg className="w-2.5 h-2.5 sm:w-3 sm:h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{formatTime(trajet.date)}
|
||||
</span>
|
||||
<span
|
||||
className={`px-1.5 sm:px-2 py-0.5 text-[10px] sm:text-xs font-medium rounded ${
|
||||
trajet.statut === 'Terminé'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: trajet.statut === 'En cours'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: trajet.statut === 'Annulé'
|
||||
? 'bg-red-100 text-red-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{trajet.statut}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
{formatDate(trajet.date)} · {formatTime(trajet.date)}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-1.5 mt-1.5">
|
||||
<span
|
||||
className={`inline-flex px-2 py-0.5 text-[10px] font-medium rounded-md ${
|
||||
trajet.statut === 'Terminé' || trajet.statut === 'Validé'
|
||||
? 'bg-emerald-50 text-emerald-700'
|
||||
: trajet.statut === 'En cours'
|
||||
? 'bg-sky-50 text-sky-700'
|
||||
: trajet.statut === 'Annulé'
|
||||
? 'bg-red-50 text-red-700'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{trajet.statut}
|
||||
</span>
|
||||
{trajet.participations?.[0] && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-mono font-semibold rounded-md bg-lblue/15 text-lblue border border-lblue/20">
|
||||
{getParticipationRef(trajet.participations[0].id)}
|
||||
</span>
|
||||
)}
|
||||
{trajet.chauffeur ? (
|
||||
<span className="text-[10px] text-gray-500">
|
||||
{trajet.chauffeur.prenom} {trajet.chauffeur.nom}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[10px] text-orange-500 font-medium">Sans chauffeur</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Adresses */}
|
||||
<div className="space-y-1 mb-2">
|
||||
<div className="text-xs sm:text-sm text-gray-600 break-words">
|
||||
<span className="font-medium">Départ:</span>{' '}
|
||||
<span className="text-gray-900">{trajet.adresseDepart}</span>
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm text-gray-600 break-words">
|
||||
<span className="font-medium">Arrivée:</span>{' '}
|
||||
<span className="text-gray-900">{trajet.adresseArrivee}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chauffeur */}
|
||||
<div className="flex items-center gap-2 sm:gap-4 mt-2 sm:mt-3">
|
||||
{trajet.chauffeur ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 sm:w-8 sm:h-8 rounded-full bg-lblue flex items-center justify-center text-white text-[10px] sm:text-xs font-semibold">
|
||||
{getInitials(trajet.chauffeur.nom, trajet.chauffeur.prenom)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[10px] sm:text-xs text-gray-500">Chauffeur</div>
|
||||
<div className="text-xs sm:text-sm font-medium text-gray-900 truncate">
|
||||
{trajet.chauffeur.prenom} {trajet.chauffeur.nom}
|
||||
{/* Desktop: tableau classique */}
|
||||
<div
|
||||
className="hidden sm:block overflow-x-auto min-w-0 w-full max-w-full"
|
||||
style={{ WebkitOverflowScrolling: 'touch' } as React.CSSProperties}
|
||||
>
|
||||
<table className="w-full text-sm min-w-[520px]">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 bg-gray-50/80">
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-600">Adhérent</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-600">Statut</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-600">Fichier</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-600">Chauffeur assigné</th>
|
||||
<th className="w-10 px-4"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredTrajets.map((trajet) => (
|
||||
<tr
|
||||
key={trajet.id}
|
||||
onClick={() => setSelectedTrajet(trajet)}
|
||||
className="border-b border-gray-100 last:border-b-0 hover:bg-gray-50/80 transition-colors cursor-pointer"
|
||||
>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-full bg-lgreen flex items-center justify-center text-white text-xs font-semibold flex-shrink-0 shadow-sm">
|
||||
{getInitials(trajet.adherent.nom, trajet.adherent.prenom)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-gray-900">
|
||||
{trajet.adherent.prenom} {trajet.adherent.nom}
|
||||
</span>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
{formatDate(trajet.date)} · {formatTime(trajet.date)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-orange-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span className="text-xs sm:text-sm text-orange-600 font-medium">
|
||||
Aucun chauffeur assigné
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Commentaire */}
|
||||
{trajet.commentaire && (
|
||||
<div className="mt-2 pt-2 border-t border-gray-200">
|
||||
<p className="text-[10px] sm:text-xs text-gray-500 italic break-words">{trajet.commentaire}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span
|
||||
className={`inline-flex px-2 py-0.5 text-[11px] font-medium rounded-md ${
|
||||
trajet.statut === 'Terminé' || trajet.statut === 'Validé'
|
||||
? 'bg-emerald-50 text-emerald-700'
|
||||
: trajet.statut === 'En cours'
|
||||
? 'bg-sky-50 text-sky-700'
|
||||
: trajet.statut === 'Annulé'
|
||||
? 'bg-red-50 text-red-700'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{trajet.statut}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{trajet.participations?.[0] ? (
|
||||
<span className="px-2 py-0.5 text-[11px] font-mono font-semibold rounded-md bg-lblue/15 text-lblue border border-lblue/20">
|
||||
{getParticipationRef(trajet.participations[0].id)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-300">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{trajet.chauffeur ? (
|
||||
<span className="text-sm text-gray-700">
|
||||
{trajet.chauffeur.prenom} {trajet.chauffeur.nom}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-300">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<svg className="w-4 h-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal formulaire trajet */}
|
||||
@@ -484,6 +526,18 @@ export default function ListeTrajets({ onTrajetCreated, compact, hideNewTrajetBu
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedTrajet && (
|
||||
<TrajetDetailModal
|
||||
trajet={selectedTrajet}
|
||||
onClose={() => setSelectedTrajet(null)}
|
||||
onUpdate={() => {
|
||||
fetchTrajets();
|
||||
setSelectedTrajet(null);
|
||||
if (onTrajetCreated) onTrajetCreated();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<NotificationContext.Provider value={{ showNotification }}>
|
||||
{children}
|
||||
<div className="fixed top-20 right-4 z-50 flex flex-col gap-2 max-w-md w-full">
|
||||
<div className="fixed top-20 right-4 z-50 flex flex-col gap-2 w-[calc(100vw-2rem)] sm:w-full sm:max-w-md">
|
||||
{notifications.map((notification) => (
|
||||
<NotificationToast
|
||||
key={notification.id}
|
||||
|
||||
BIN
prisma/dev.db
BIN
prisma/dev.db
Binary file not shown.
Reference in New Issue
Block a user