Added few functions
This commit is contained in:
@@ -39,6 +39,9 @@ export async function GET(
|
|||||||
email: true,
|
email: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
participations: {
|
||||||
|
select: { id: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ export async function GET(request: NextRequest) {
|
|||||||
telephone: true,
|
telephone: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
participations: {
|
||||||
|
select: { id: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
updatedAt: 'desc' as const,
|
updatedAt: 'desc' as const,
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ export async function GET(request: NextRequest) {
|
|||||||
telephone: true,
|
telephone: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
participations: {
|
||||||
|
select: { id: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy,
|
orderBy,
|
||||||
take: limit ? parseInt(limit) : undefined,
|
take: limit ? parseInt(limit) : undefined,
|
||||||
|
|||||||
@@ -19,13 +19,6 @@ export default async function CalendrierPage() {
|
|||||||
return (
|
return (
|
||||||
<DashboardLayout user={user}>
|
<DashboardLayout user={user}>
|
||||||
<div className="p-4 sm:p-6 lg:p-8">
|
<div className="p-4 sm:p-6 lg:p-8">
|
||||||
<h1 className="text-2xl sm:text-3xl font-semibold text-cblack mb-2">
|
|
||||||
Calendrier
|
|
||||||
</h1>
|
|
||||||
<p className="text-xs sm:text-sm text-cgray mb-6 sm:mb-8">
|
|
||||||
Gestion des trajets et planning des chauffeurs
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<CalendrierPageContent />
|
<CalendrierPageContent />
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import AlertModal from './AlertModal';
|
import AlertModal from './AlertModal';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
|
|
||||||
interface Adherent {
|
interface Adherent {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -26,6 +27,7 @@ interface AdherentFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AdherentForm({ adherent, onClose }: AdherentFormProps) {
|
export default function AdherentForm({ adherent, onClose }: AdherentFormProps) {
|
||||||
|
useBodyScrollLock(true);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
const [alertModal, setAlertModal] = useState<{
|
const [alertModal, setAlertModal] = useState<{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
import AdherentForm from './AdherentForm';
|
import AdherentForm from './AdherentForm';
|
||||||
import ConfirmModal from './ConfirmModal';
|
import ConfirmModal from './ConfirmModal';
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ export default function AdherentsTable() {
|
|||||||
show: boolean;
|
show: boolean;
|
||||||
id: string | null;
|
id: string | null;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
useBodyScrollLock(showImportModal || !!(resultModal?.show) || !!viewingAdherent);
|
||||||
|
|
||||||
const fetchAdherents = async (searchTerm: string = '') => {
|
const fetchAdherents = async (searchTerm: string = '') => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
|
|
||||||
interface AlertModalProps {
|
interface AlertModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
type: 'success' | 'error' | 'info' | 'warning';
|
type: 'success' | 'error' | 'info' | 'warning';
|
||||||
@@ -15,6 +17,7 @@ export default function AlertModal({
|
|||||||
message,
|
message,
|
||||||
onClose,
|
onClose,
|
||||||
}: AlertModalProps) {
|
}: AlertModalProps) {
|
||||||
|
useBodyScrollLock(isOpen);
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const getStyles = () => {
|
const getStyles = () => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useNotification } from './NotificationProvider';
|
import { useNotification } from './NotificationProvider';
|
||||||
import ConfirmModal from './ConfirmModal';
|
import ConfirmModal from './ConfirmModal';
|
||||||
|
import { getParticipationRef } from '@/lib/participation-ref';
|
||||||
|
|
||||||
interface Trajet {
|
interface Trajet {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -12,6 +13,7 @@ interface Trajet {
|
|||||||
commentaire?: string | null;
|
commentaire?: string | null;
|
||||||
statut: string;
|
statut: string;
|
||||||
archived: boolean;
|
archived: boolean;
|
||||||
|
participations?: { id: string }[];
|
||||||
adherent: {
|
adherent: {
|
||||||
id: string;
|
id: string;
|
||||||
nom: string;
|
nom: string;
|
||||||
@@ -117,14 +119,21 @@ export default function ArchivesTrajets() {
|
|||||||
return `${prenom.charAt(0)}${nom.charAt(0)}`.toUpperCase();
|
return `${prenom.charAt(0)}${nom.charAt(0)}`.toUpperCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const participationRef = (t: Trajet) => {
|
||||||
|
const p = t.participations?.[0];
|
||||||
|
return p ? getParticipationRef(p.id) : null;
|
||||||
|
};
|
||||||
|
|
||||||
const filteredTrajets = trajets.filter((trajet) => {
|
const filteredTrajets = trajets.filter((trajet) => {
|
||||||
|
const ref = participationRef(trajet);
|
||||||
const searchLower = searchTerm.toLowerCase();
|
const searchLower = searchTerm.toLowerCase();
|
||||||
return (
|
return (
|
||||||
trajet.adherent.nom.toLowerCase().includes(searchLower) ||
|
trajet.adherent.nom.toLowerCase().includes(searchLower) ||
|
||||||
trajet.adherent.prenom.toLowerCase().includes(searchLower) ||
|
trajet.adherent.prenom.toLowerCase().includes(searchLower) ||
|
||||||
trajet.adresseDepart.toLowerCase().includes(searchLower) ||
|
trajet.adresseDepart.toLowerCase().includes(searchLower) ||
|
||||||
trajet.adresseArrivee.toLowerCase().includes(searchLower) ||
|
trajet.adresseArrivee.toLowerCase().includes(searchLower) ||
|
||||||
(trajet.chauffeur && `${trajet.chauffeur.prenom} ${trajet.chauffeur.nom}`.toLowerCase().includes(searchLower))
|
(trajet.chauffeur && `${trajet.chauffeur.prenom} ${trajet.chauffeur.nom}`.toLowerCase().includes(searchLower)) ||
|
||||||
|
(ref && ref.toLowerCase().includes(searchLower))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -144,7 +153,7 @@ export default function ArchivesTrajets() {
|
|||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Rechercher dans les archives..."
|
placeholder="Rechercher (adhérent, adresse, référence PART-…)..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="w-full px-3 md:px-4 py-2 md:py-2.5 pl-9 md:pl-10 text-sm md:text-base border border-gray-300 rounded-lg focus:ring-2 focus:ring-lblue focus:border-transparent"
|
className="w-full px-3 md:px-4 py-2 md:py-2.5 pl-9 md:pl-10 text-sm md:text-base border border-gray-300 rounded-lg focus:ring-2 focus:ring-lblue focus:border-transparent"
|
||||||
@@ -188,6 +197,11 @@ export default function ArchivesTrajets() {
|
|||||||
{formatDate(trajet.date)} à {formatTime(trajet.date)}
|
{formatDate(trajet.date)} à {formatTime(trajet.date)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{participationRef(trajet) && (
|
||||||
|
<span className="px-1.5 py-0.5 text-xs font-mono font-medium rounded bg-lblue/10 text-lblue flex-shrink-0" title="Référence de prescription">
|
||||||
|
{participationRef(trajet)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className={`px-2 py-1 text-xs font-medium rounded border ${getStatutColor(trajet.statut)} flex-shrink-0`}>
|
<span className={`px-2 py-1 text-xs font-medium rounded border ${getStatutColor(trajet.statut)} flex-shrink-0`}>
|
||||||
{trajet.statut}
|
{trajet.statut}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNotification } from './NotificationProvider';
|
import { useNotification } from './NotificationProvider';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { getParticipationRef } from '@/lib/participation-ref';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
|
|
||||||
interface HistoriqueItem {
|
interface HistoriqueItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -41,6 +43,7 @@ export default function BudgetContent() {
|
|||||||
const [rectifierBudget, setRectifierBudget] = useState('');
|
const [rectifierBudget, setRectifierBudget] = useState('');
|
||||||
const [rectifierAjustement, setRectifierAjustement] = useState('');
|
const [rectifierAjustement, setRectifierAjustement] = useState('');
|
||||||
const [expandedPrescripteur, setExpandedPrescripteur] = useState<string | null>(null);
|
const [expandedPrescripteur, setExpandedPrescripteur] = useState<string | null>(null);
|
||||||
|
useBodyScrollLock(!!addModalPrescripteur || !!rectifierModalPrescripteur);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchBudgets();
|
fetchBudgets();
|
||||||
@@ -332,6 +335,7 @@ export default function BudgetContent() {
|
|||||||
<thead className="bg-gray-100">
|
<thead className="bg-gray-100">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500">Date</th>
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500">Date</th>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500">Référence</th>
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500">Adhérent</th>
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500">Adhérent</th>
|
||||||
<th className="px-4 py-2 text-right text-xs font-medium text-gray-500">Montant</th>
|
<th className="px-4 py-2 text-right text-xs font-medium text-gray-500">Montant</th>
|
||||||
<th className="px-4 py-2 text-right text-xs font-medium text-gray-500 w-24">Lien</th>
|
<th className="px-4 py-2 text-right text-xs font-medium text-gray-500 w-24">Lien</th>
|
||||||
@@ -341,6 +345,11 @@ export default function BudgetContent() {
|
|||||||
{item.historique.map((h) => (
|
{item.historique.map((h) => (
|
||||||
<tr key={h.id}>
|
<tr key={h.id}>
|
||||||
<td className="px-4 py-2 text-gray-700">{formatDate(h.date)}</td>
|
<td className="px-4 py-2 text-gray-700">{formatDate(h.date)}</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<span className="font-mono text-xs font-medium text-lblue" title="Référence de prescription">
|
||||||
|
{getParticipationRef(h.id)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td className="px-4 py-2 text-gray-700">{h.adherentNom}</td>
|
<td className="px-4 py-2 text-gray-700">{h.adherentNom}</td>
|
||||||
<td className="px-4 py-2 text-right text-orange-600 font-medium">
|
<td className="px-4 py-2 text-right text-orange-600 font-medium">
|
||||||
{formatEuro(h.montant)}
|
{formatEuro(h.montant)}
|
||||||
@@ -455,6 +464,7 @@ export default function BudgetContent() {
|
|||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="font-medium text-gray-900 truncate">{h.adherentNom}</p>
|
<p className="font-medium text-gray-900 truncate">{h.adherentNom}</p>
|
||||||
<p className="text-xs text-gray-500">{formatDate(h.date)}</p>
|
<p className="text-xs text-gray-500">{formatDate(h.date)}</p>
|
||||||
|
<p className="text-xs font-mono text-lblue mt-0.5">{getParticipationRef(h.id)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
<span className="text-orange-600 font-medium">{formatEuro(h.montant)}</span>
|
<span className="text-orange-600 font-medium">{formatEuro(h.montant)}</span>
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import CalendrierTrajets from './CalendrierTrajets';
|
import CalendrierTrajets from './CalendrierTrajets';
|
||||||
import ListeTrajets from './ListeTrajets';
|
import ListeTrajets from './ListeTrajets';
|
||||||
|
import TrajetForm from './TrajetForm';
|
||||||
|
|
||||||
export default function CalendrierPageContent() {
|
export default function CalendrierPageContent() {
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||||
|
const [showTrajetForm, setShowTrajetForm] = useState(false);
|
||||||
|
|
||||||
const handleTrajetCreated = () => {
|
const handleTrajetCreated = () => {
|
||||||
setRefreshTrigger((prev) => prev + 1);
|
setRefreshTrigger((prev) => prev + 1);
|
||||||
@@ -13,6 +15,27 @@ export default function CalendrierPageContent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6 sm:gap-8">
|
<div className="flex flex-col gap-6 sm:gap-8">
|
||||||
|
{/* 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>
|
||||||
|
<h1 className="text-2xl sm:text-3xl font-semibold text-cblack">
|
||||||
|
Calendrier
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs sm:text-sm text-cgray mt-1">
|
||||||
|
Gestion des trajets et planning des chauffeurs
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowTrajetForm(true)}
|
||||||
|
className="flex items-center justify-center gap-2 px-4 py-2.5 bg-lgreen text-white text-sm font-medium rounded-lg hover:bg-dgreen transition-colors self-start sm:self-center shrink-0"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
Nouveau trajet
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Calendrier en haut */}
|
{/* Calendrier en haut */}
|
||||||
<div>
|
<div>
|
||||||
<CalendrierTrajets refreshTrigger={refreshTrigger} />
|
<CalendrierTrajets refreshTrigger={refreshTrigger} />
|
||||||
@@ -20,8 +43,19 @@ export default function CalendrierPageContent() {
|
|||||||
|
|
||||||
{/* Liste des trajets en bas, triable par période */}
|
{/* Liste des trajets en bas, triable par période */}
|
||||||
<div>
|
<div>
|
||||||
<ListeTrajets onTrajetCreated={handleTrajetCreated} />
|
<ListeTrajets onTrajetCreated={handleTrajetCreated} hideNewTrajetButton />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Modal formulaire trajet */}
|
||||||
|
{showTrajetForm && (
|
||||||
|
<TrajetForm
|
||||||
|
onClose={() => setShowTrajetForm(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
handleTrajetCreated();
|
||||||
|
setShowTrajetForm(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, useDraggable, us
|
|||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import TrajetDetailModal from './TrajetDetailModal';
|
import TrajetDetailModal from './TrajetDetailModal';
|
||||||
import { useNotification } from './NotificationProvider';
|
import { useNotification } from './NotificationProvider';
|
||||||
|
import { getParticipationRef } from '@/lib/participation-ref';
|
||||||
|
|
||||||
interface Trajet {
|
interface Trajet {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -13,6 +14,7 @@ interface Trajet {
|
|||||||
adresseArrivee: string;
|
adresseArrivee: string;
|
||||||
commentaire?: string | null;
|
commentaire?: string | null;
|
||||||
statut: string;
|
statut: string;
|
||||||
|
participations?: { id: string }[];
|
||||||
adherent: {
|
adherent: {
|
||||||
id: string;
|
id: string;
|
||||||
nom: string;
|
nom: string;
|
||||||
@@ -518,6 +520,11 @@ export default function CalendrierTrajets({ refreshTrigger }: CalendrierTrajetsP
|
|||||||
<span className="text-xs sm:text-sm font-semibold text-gray-900">
|
<span className="text-xs sm:text-sm font-semibold text-gray-900">
|
||||||
{trajet.adherent.prenom} {trajet.adherent.nom}
|
{trajet.adherent.prenom} {trajet.adherent.nom}
|
||||||
</span>
|
</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 sm:px-2 py-0.5 text-[10px] sm:text-xs font-medium rounded bg-lblue/10 text-lblue">
|
||||||
{formatTime(trajet.date)}
|
{formatTime(trajet.date)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import ChauffeurForm from './ChauffeurForm';
|
import ChauffeurForm from './ChauffeurForm';
|
||||||
import ConfirmModal from './ConfirmModal';
|
import ConfirmModal from './ConfirmModal';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
|
|
||||||
interface Chauffeur {
|
interface Chauffeur {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -39,6 +40,7 @@ export default function ChauffeursTable() {
|
|||||||
show: boolean;
|
show: boolean;
|
||||||
id: string | null;
|
id: string | null;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
useBodyScrollLock(showImportModal || !!(resultModal?.show) || !!viewingChauffeur);
|
||||||
|
|
||||||
const fetchChauffeurs = async (searchTerm: string = '') => {
|
const fetchChauffeurs = async (searchTerm: string = '') => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { useNotification } from './NotificationProvider';
|
import { useNotification } from './NotificationProvider';
|
||||||
import { AVAILABLE_PAGES } from '@/lib/pages';
|
import { AVAILABLE_PAGES } from '@/lib/pages';
|
||||||
import ConfirmModal from './ConfirmModal';
|
import ConfirmModal from './ConfirmModal';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
|
|
||||||
interface AdherentOption {
|
interface AdherentOption {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -200,6 +201,8 @@ export default function ConfigurationContent() {
|
|||||||
roleId: string | null;
|
roleId: string | null;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
useBodyScrollLock(isMobile);
|
||||||
|
|
||||||
const fetchOptions = useCallback(async () => {
|
const fetchOptions = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -373,6 +376,7 @@ export default function ConfigurationContent() {
|
|||||||
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
||||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||||
const [newPassword, setNewPassword] = useState<string | null>(null);
|
const [newPassword, setNewPassword] = useState<string | null>(null);
|
||||||
|
useBodyScrollLock(showPasswordModal && !!newPassword);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
@@ -688,6 +692,7 @@ export default function ConfigurationContent() {
|
|||||||
description: '',
|
description: '',
|
||||||
pageRoutes: [] as string[],
|
pageRoutes: [] as string[],
|
||||||
});
|
});
|
||||||
|
useBodyScrollLock(showRoleForm);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRoles();
|
fetchRoles();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
|
|
||||||
interface ConfirmModalProps {
|
interface ConfirmModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -21,6 +23,7 @@ export default function ConfirmModal({
|
|||||||
onConfirm,
|
onConfirm,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: ConfirmModalProps) {
|
}: ConfirmModalProps) {
|
||||||
|
useBodyScrollLock(isOpen);
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const getConfirmButtonStyle = () => {
|
const getConfirmButtonStyle = () => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
|
|||||||
import TrajetForm from './TrajetForm';
|
import TrajetForm from './TrajetForm';
|
||||||
import AdherentForm from './AdherentForm';
|
import AdherentForm from './AdherentForm';
|
||||||
import TrajetDetailModal from './TrajetDetailModal';
|
import TrajetDetailModal from './TrajetDetailModal';
|
||||||
|
import { getParticipationRef } from '@/lib/participation-ref';
|
||||||
|
|
||||||
interface Stats {
|
interface Stats {
|
||||||
participationsMois: {
|
participationsMois: {
|
||||||
@@ -32,6 +33,7 @@ interface Trajet {
|
|||||||
adresseArrivee: string;
|
adresseArrivee: string;
|
||||||
commentaire?: string | null;
|
commentaire?: string | null;
|
||||||
statut: string;
|
statut: string;
|
||||||
|
participations?: { id: string }[];
|
||||||
adherent: {
|
adherent: {
|
||||||
id: string;
|
id: string;
|
||||||
nom: string;
|
nom: string;
|
||||||
@@ -361,6 +363,11 @@ export default function DashboardContent({ userName }: DashboardContentProps) {
|
|||||||
{trajet.adherent.prenom} {trajet.adherent.nom}
|
{trajet.adherent.prenom} {trajet.adherent.nom}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3 mt-1">
|
<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">
|
<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">
|
<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" />
|
<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" />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ export default function DashboardLayout({ user, children }: DashboardLayoutProps
|
|||||||
const [showNotifications, setShowNotifications] = useState(false);
|
const [showNotifications, setShowNotifications] = useState(false);
|
||||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
useBodyScrollLock(sidebarOpen);
|
||||||
|
|
||||||
// Récupérer les conversations pour compter les messages non lus
|
// Récupérer les conversations pour compter les messages non lus
|
||||||
const { data: conversations } = useSWR<Array<{ unreadCount: number }>>(
|
const { data: conversations } = useSWR<Array<{ unreadCount: number }>>(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import AlertModal from './AlertModal';
|
import AlertModal from './AlertModal';
|
||||||
import ConfirmModal from './ConfirmModal';
|
import ConfirmModal from './ConfirmModal';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -38,6 +39,7 @@ export default function GroupSettingsModal({
|
|||||||
onClose,
|
onClose,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
}: GroupSettingsModalProps) {
|
}: GroupSettingsModalProps) {
|
||||||
|
useBodyScrollLock(true);
|
||||||
const [groupName, setGroupName] = useState(conversation.name || '');
|
const [groupName, setGroupName] = useState(conversation.name || '');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import TrajetForm from './TrajetForm';
|
import TrajetForm from './TrajetForm';
|
||||||
|
import { getParticipationRef } from '@/lib/participation-ref';
|
||||||
|
|
||||||
interface Trajet {
|
interface Trajet {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -10,6 +11,7 @@ interface Trajet {
|
|||||||
adresseArrivee: string;
|
adresseArrivee: string;
|
||||||
commentaire?: string | null;
|
commentaire?: string | null;
|
||||||
statut: string;
|
statut: string;
|
||||||
|
participations?: { id: string }[];
|
||||||
adherent: {
|
adherent: {
|
||||||
id: string;
|
id: string;
|
||||||
nom: string;
|
nom: string;
|
||||||
@@ -30,9 +32,10 @@ type FilterPeriod = 'derniers' | 'jour' | 'mois' | 'an' | 'personnalise';
|
|||||||
interface ListeTrajetsProps {
|
interface ListeTrajetsProps {
|
||||||
onTrajetCreated?: () => void;
|
onTrajetCreated?: () => void;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
|
hideNewTrajetButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ListeTrajets({ onTrajetCreated, compact }: ListeTrajetsProps) {
|
export default function ListeTrajets({ onTrajetCreated, compact, hideNewTrajetButton }: ListeTrajetsProps) {
|
||||||
const [trajets, setTrajets] = useState<Trajet[]>([]);
|
const [trajets, setTrajets] = useState<Trajet[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
@@ -119,7 +122,13 @@ export default function ListeTrajets({ onTrajetCreated, compact }: ListeTrajetsP
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const participationRef = (t: Trajet) => {
|
||||||
|
const p = t.participations?.[0];
|
||||||
|
return p ? getParticipationRef(p.id) : null;
|
||||||
|
};
|
||||||
|
|
||||||
const filteredTrajets = trajets.filter((trajet) => {
|
const filteredTrajets = trajets.filter((trajet) => {
|
||||||
|
const ref = participationRef(trajet);
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
!search ||
|
!search ||
|
||||||
trajet.adherent.nom.toLowerCase().includes(search.toLowerCase()) ||
|
trajet.adherent.nom.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
@@ -129,7 +138,8 @@ export default function ListeTrajets({ onTrajetCreated, compact }: ListeTrajetsP
|
|||||||
(trajet.chauffeur &&
|
(trajet.chauffeur &&
|
||||||
`${trajet.chauffeur.prenom} ${trajet.chauffeur.nom}`
|
`${trajet.chauffeur.prenom} ${trajet.chauffeur.nom}`
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(search.toLowerCase()));
|
.includes(search.toLowerCase())) ||
|
||||||
|
(ref && ref.toLowerCase().includes(search.toLowerCase()));
|
||||||
|
|
||||||
const matchesStatut = !filterStatut || trajet.statut === filterStatut;
|
const matchesStatut = !filterStatut || trajet.statut === filterStatut;
|
||||||
|
|
||||||
@@ -180,7 +190,7 @@ export default function ListeTrajets({ onTrajetCreated, compact }: ListeTrajetsP
|
|||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Rechercher..."
|
placeholder="Rechercher (adhérent, adresse, référence PART-…)..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
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 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"
|
||||||
@@ -272,6 +282,7 @@ export default function ListeTrajets({ onTrajetCreated, compact }: ListeTrajetsP
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex items-center gap-2 pt-1">
|
<div className="flex items-center gap-2 pt-1">
|
||||||
|
{!hideNewTrajetButton && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowTrajetForm(true)}
|
onClick={() => setShowTrajetForm(true)}
|
||||||
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 bg-lgreen text-white text-xs font-medium rounded-lg hover:bg-dgreen transition-colors"
|
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 bg-lgreen text-white text-xs font-medium rounded-lg hover:bg-dgreen transition-colors"
|
||||||
@@ -281,6 +292,7 @@ export default function ListeTrajets({ onTrajetCreated, compact }: ListeTrajetsP
|
|||||||
</svg>
|
</svg>
|
||||||
Nouveau trajet
|
Nouveau trajet
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowFilters(!showFilters)}
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
className={`px-3 py-2 text-xs font-medium rounded-lg transition-colors flex items-center gap-1.5 ${
|
className={`px-3 py-2 text-xs font-medium rounded-lg transition-colors flex items-center gap-1.5 ${
|
||||||
@@ -374,6 +386,11 @@ export default function ListeTrajets({ onTrajetCreated, compact }: ListeTrajetsP
|
|||||||
{trajet.adherent.prenom} {trajet.adherent.nom}
|
{trajet.adherent.prenom} {trajet.adherent.nom}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3 mt-1">
|
<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)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="text-[10px] sm:text-xs text-gray-500 flex items-center gap-1">
|
<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">
|
<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" />
|
<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" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
import ChatWindow from './ChatWindow';
|
import ChatWindow from './ChatWindow';
|
||||||
import NewConversationModal from './NewConversationModal';
|
import NewConversationModal from './NewConversationModal';
|
||||||
import GroupSettingsModal from './GroupSettingsModal';
|
import GroupSettingsModal from './GroupSettingsModal';
|
||||||
@@ -46,6 +47,7 @@ export default function Messagerie() {
|
|||||||
const [showNewConversation, setShowNewConversation] = useState(false);
|
const [showNewConversation, setShowNewConversation] = useState(false);
|
||||||
const [showGroupSettings, setShowGroupSettings] = useState(false);
|
const [showGroupSettings, setShowGroupSettings] = useState(false);
|
||||||
const [showSidebar, setShowSidebar] = useState(false);
|
const [showSidebar, setShowSidebar] = useState(false);
|
||||||
|
useBodyScrollLock(showSidebar);
|
||||||
|
|
||||||
const { data: conversations, error, mutate } = useSWR<Conversation[]>(
|
const { data: conversations, error, mutate } = useSWR<Conversation[]>(
|
||||||
'/api/conversations',
|
'/api/conversations',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import AlertModal from './AlertModal';
|
import AlertModal from './AlertModal';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -21,6 +22,7 @@ export default function NewConversationModal({
|
|||||||
onClose,
|
onClose,
|
||||||
onConversationCreated,
|
onConversationCreated,
|
||||||
}: NewConversationModalProps) {
|
}: NewConversationModalProps) {
|
||||||
|
useBodyScrollLock(true);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
||||||
const [conversationType, setConversationType] = useState<'direct' | 'group'>('direct');
|
const [conversationType, setConversationType] = useState<'direct' | 'group'>('direct');
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useNotification } from './NotificationProvider';
|
import { useNotification } from './NotificationProvider';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
|
|
||||||
interface Participation {
|
interface Participation {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -26,6 +27,7 @@ export default function ParticipationEditModal({
|
|||||||
onSuccess,
|
onSuccess,
|
||||||
}: ParticipationEditModalProps) {
|
}: ParticipationEditModalProps) {
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
|
useBodyScrollLock(true);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
destinataireEmail: '',
|
destinataireEmail: '',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { createPortal } from 'react-dom';
|
|||||||
import { useNotification } from './NotificationProvider';
|
import { useNotification } from './NotificationProvider';
|
||||||
import ConfirmModal from './ConfirmModal';
|
import ConfirmModal from './ConfirmModal';
|
||||||
import ParticipationEditModal from './ParticipationEditModal';
|
import ParticipationEditModal from './ParticipationEditModal';
|
||||||
|
import { getParticipationRef } from '@/lib/participation-ref';
|
||||||
|
|
||||||
interface Participation {
|
interface Participation {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -59,9 +60,6 @@ const STATUT_CONFIG: Record<string, { label: string; className: string; dot: str
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function getRefNum(id: string) {
|
|
||||||
return `PART-${id.slice(-8).toUpperCase()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ParticipationFinanciereList() {
|
export default function ParticipationFinanciereList() {
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
@@ -204,7 +202,7 @@ export default function ParticipationFinanciereList() {
|
|||||||
|
|
||||||
const filteredParticipations = participations.filter((p) => {
|
const filteredParticipations = participations.filter((p) => {
|
||||||
const search = searchTerm.toLowerCase();
|
const search = searchTerm.toLowerCase();
|
||||||
const ref = getRefNum(p.id).toLowerCase();
|
const ref = getParticipationRef(p.id).toLowerCase();
|
||||||
const chauffeurName = p.trajet.chauffeur
|
const chauffeurName = p.trajet.chauffeur
|
||||||
? `${p.trajet.chauffeur.prenom} ${p.trajet.chauffeur.nom}`.toLowerCase()
|
? `${p.trajet.chauffeur.prenom} ${p.trajet.chauffeur.nom}`.toLowerCase()
|
||||||
: '';
|
: '';
|
||||||
@@ -500,7 +498,7 @@ export default function ParticipationFinanciereList() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{filteredParticipations.map((p) => {
|
{filteredParticipations.map((p) => {
|
||||||
const ref = getRefNum(p.id);
|
const ref = getParticipationRef(p.id);
|
||||||
const chauffeur = p.trajet.chauffeur;
|
const chauffeur = p.trajet.chauffeur;
|
||||||
const montant = p.montant != null ? `${p.montant.toFixed(2).replace('.', ',')} €` : '—';
|
const montant = p.montant != null ? `${p.montant.toFixed(2).replace('.', ',')} €` : '—';
|
||||||
const statutConfig = STATUT_CONFIG[p.statut] || STATUT_CONFIG.en_attente;
|
const statutConfig = STATUT_CONFIG[p.statut] || STATUT_CONFIG.en_attente;
|
||||||
@@ -619,7 +617,7 @@ export default function ParticipationFinanciereList() {
|
|||||||
{/* Vue mobile - Cartes */}
|
{/* Vue mobile - Cartes */}
|
||||||
<div className="md:hidden divide-y divide-gray-200">
|
<div className="md:hidden divide-y divide-gray-200">
|
||||||
{filteredParticipations.map((p) => {
|
{filteredParticipations.map((p) => {
|
||||||
const ref = getRefNum(p.id);
|
const ref = getParticipationRef(p.id);
|
||||||
const chauffeur = p.trajet.chauffeur;
|
const chauffeur = p.trajet.chauffeur;
|
||||||
const montant = p.montant != null ? `${p.montant.toFixed(2).replace('.', ',')} €` : '—';
|
const montant = p.montant != null ? `${p.montant.toFixed(2).replace('.', ',')} €` : '—';
|
||||||
const statutConfig = STATUT_CONFIG[p.statut] || STATUT_CONFIG.en_attente;
|
const statutConfig = STATUT_CONFIG[p.statut] || STATUT_CONFIG.en_attente;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import TrajetForm from './TrajetForm';
|
import TrajetForm from './TrajetForm';
|
||||||
|
import { getParticipationRef } from '@/lib/participation-ref';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
import ValidationModal from './ValidationModal';
|
import ValidationModal from './ValidationModal';
|
||||||
import ConfirmModal from './ConfirmModal';
|
import ConfirmModal from './ConfirmModal';
|
||||||
import { useNotification } from './NotificationProvider';
|
import { useNotification } from './NotificationProvider';
|
||||||
@@ -25,6 +27,7 @@ interface Trajet {
|
|||||||
commentaire?: string | null;
|
commentaire?: string | null;
|
||||||
instructions?: string | null;
|
instructions?: string | null;
|
||||||
statut: string;
|
statut: string;
|
||||||
|
participations?: { id: string }[];
|
||||||
adherent: {
|
adherent: {
|
||||||
id: string;
|
id: string;
|
||||||
nom: string;
|
nom: string;
|
||||||
@@ -50,6 +53,7 @@ interface TrajetDetailModalProps {
|
|||||||
|
|
||||||
export default function TrajetDetailModal({ trajet, onClose, onUpdate }: TrajetDetailModalProps) {
|
export default function TrajetDetailModal({ trajet, onClose, onUpdate }: TrajetDetailModalProps) {
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
|
useBodyScrollLock(true);
|
||||||
const [showEditForm, setShowEditForm] = useState(false);
|
const [showEditForm, setShowEditForm] = useState(false);
|
||||||
const [showValidationModal, setShowValidationModal] = useState(false);
|
const [showValidationModal, setShowValidationModal] = useState(false);
|
||||||
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
|
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
|
||||||
@@ -219,6 +223,11 @@ export default function TrajetDetailModal({ trajet, onClose, onUpdate }: TrajetD
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-3 mb-1">
|
<div className="flex items-center gap-3 mb-1">
|
||||||
<h2 className="text-xl sm:text-2xl font-semibold text-gray-900">Détails du trajet</h2>
|
<h2 className="text-xl sm:text-2xl font-semibold text-gray-900">Détails du trajet</h2>
|
||||||
|
{trajet.participations?.[0] && (
|
||||||
|
<span className="px-2 py-1 text-xs font-mono font-medium rounded-lg bg-lblue/10 text-lblue border border-lblue/20 flex-shrink-0" title="Référence de prescription">
|
||||||
|
{getParticipationRef(trajet.participations[0].id)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className={`px-2.5 sm:px-3 py-1 sm:py-1.5 text-xs sm:text-sm font-semibold rounded-lg border flex-shrink-0 ${getStatutColor(trajet.statut)}`}>
|
<span className={`px-2.5 sm:px-3 py-1 sm:py-1.5 text-xs sm:text-sm font-semibold rounded-lg border flex-shrink-0 ${getStatutColor(trajet.statut)}`}>
|
||||||
{trajet.statut}
|
{trajet.statut}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { useState, useEffect, useRef } from 'react';
|
|||||||
import TrajetMap from './TrajetMap';
|
import TrajetMap from './TrajetMap';
|
||||||
import AddressAutocomplete from './AddressAutocomplete';
|
import AddressAutocomplete from './AddressAutocomplete';
|
||||||
import { useNotification } from './NotificationProvider';
|
import { useNotification } from './NotificationProvider';
|
||||||
|
import { calculerDureeTrajet } from '@/lib/trajet-duree';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
|
|
||||||
interface Adherent {
|
interface Adherent {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -27,6 +29,8 @@ interface Chauffeur {
|
|||||||
prenom: string;
|
prenom: string;
|
||||||
telephone: string;
|
telephone: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
heuresRestantes?: number;
|
||||||
|
heuresContrat?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TrajetFormProps {
|
interface TrajetFormProps {
|
||||||
@@ -47,6 +51,7 @@ interface TrajetFormProps {
|
|||||||
|
|
||||||
export default function TrajetForm({ onClose, onSuccess, trajetToEdit }: TrajetFormProps) {
|
export default function TrajetForm({ onClose, onSuccess, trajetToEdit }: TrajetFormProps) {
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
|
useBodyScrollLock(true); // TrajetForm est toujours affiché en modal quand monté
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [adherents, setAdherents] = useState<Adherent[]>([]);
|
const [adherents, setAdherents] = useState<Adherent[]>([]);
|
||||||
const [chauffeurs, setChauffeurs] = useState<Chauffeur[]>([]);
|
const [chauffeurs, setChauffeurs] = useState<Chauffeur[]>([]);
|
||||||
@@ -56,6 +61,7 @@ export default function TrajetForm({ onClose, onSuccess, trajetToEdit }: TrajetF
|
|||||||
const [showChauffeurDropdown, setShowChauffeurDropdown] = useState(false);
|
const [showChauffeurDropdown, setShowChauffeurDropdown] = useState(false);
|
||||||
const adherentDropdownRef = useRef<HTMLDivElement>(null);
|
const adherentDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const chauffeurDropdownRef = useRef<HTMLDivElement>(null);
|
const chauffeurDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [dureeEstimee, setDureeEstimee] = useState<number | null>(null);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
adherentId: trajetToEdit?.adherentId || '',
|
adherentId: trajetToEdit?.adherentId || '',
|
||||||
@@ -83,6 +89,50 @@ export default function TrajetForm({ onClose, onSuccess, trajetToEdit }: TrajetF
|
|||||||
fetchChauffeurs();
|
fetchChauffeurs();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Heures minimales requises pour ce trajet (arrondi au supérieur pour être conservateur)
|
||||||
|
const heuresRequerues = dureeEstimee != null ? Math.ceil(dureeEstimee) : 0;
|
||||||
|
|
||||||
|
// Calculer la durée du trajet quand les adresses sont remplies (pour filtrer les chauffeurs)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!formData.adresseDepart?.trim() || !formData.adresseArrivee?.trim()) {
|
||||||
|
setDureeEstimee(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cancelled = false;
|
||||||
|
calculerDureeTrajet(formData.adresseDepart, formData.adresseArrivee).then((duree) => {
|
||||||
|
if (!cancelled && duree != null) {
|
||||||
|
setDureeEstimee(duree);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [formData.adresseDepart, formData.adresseArrivee]);
|
||||||
|
|
||||||
|
// Si la durée estimée exclut le chauffeur sélectionné, le désélectionner
|
||||||
|
useEffect(() => {
|
||||||
|
if (heuresRequerues > 0 && formData.chauffeurId) {
|
||||||
|
const selectedChauffeur = chauffeurs.find((c) => c.id === formData.chauffeurId);
|
||||||
|
if (selectedChauffeur) {
|
||||||
|
const heuresDispo = selectedChauffeur.heuresRestantes ?? selectedChauffeur.heuresContrat ?? 35;
|
||||||
|
if (heuresDispo < heuresRequerues) {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
chauffeurId: '',
|
||||||
|
chauffeurNom: '',
|
||||||
|
chauffeurPrenom: '',
|
||||||
|
chauffeurTelephone: '',
|
||||||
|
}));
|
||||||
|
setSearchChauffeur('');
|
||||||
|
showNotification(
|
||||||
|
'warning',
|
||||||
|
`${selectedChauffeur.prenom} ${selectedChauffeur.nom} n'a que ${heuresDispo}h disponibles (trajet estimé ~${dureeEstimee}h)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [heuresRequerues, dureeEstimee, formData.chauffeurId, chauffeurs]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Si on modifie un trajet, charger les données de l'adhérent et du chauffeur
|
// Si on modifie un trajet, charger les données de l'adhérent et du chauffeur
|
||||||
if (trajetToEdit) {
|
if (trajetToEdit) {
|
||||||
@@ -276,13 +326,20 @@ export default function TrajetForm({ onClose, onSuccess, trajetToEdit }: TrajetF
|
|||||||
a.telephone.includes(searchAdherent)
|
a.telephone.includes(searchAdherent)
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredChauffeurs = chauffeurs.filter(
|
const filteredChauffeurs = chauffeurs
|
||||||
(c) =>
|
.filter((c) => {
|
||||||
|
// Exclure les chauffeurs qui n'ont pas assez d'heures restantes
|
||||||
|
if (heuresRequerues > 0) {
|
||||||
|
const heuresDispo = c.heuresRestantes ?? c.heuresContrat ?? 35;
|
||||||
|
if (heuresDispo < heuresRequerues) return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
!searchChauffeur ||
|
!searchChauffeur ||
|
||||||
`${c.prenom} ${c.nom}`.toLowerCase().includes(searchChauffeur.toLowerCase()) ||
|
`${c.prenom} ${c.nom}`.toLowerCase().includes(searchChauffeur.toLowerCase()) ||
|
||||||
c.email.toLowerCase().includes(searchChauffeur.toLowerCase()) ||
|
c.email.toLowerCase().includes(searchChauffeur.toLowerCase()) ||
|
||||||
c.telephone.includes(searchChauffeur)
|
c.telephone.includes(searchChauffeur)
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -460,6 +517,11 @@ export default function TrajetForm({ onClose, onSuccess, trajetToEdit }: TrajetF
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-gray-900 mb-2">
|
<label className="block text-sm font-semibold text-gray-900 mb-2">
|
||||||
Chauffeur
|
Chauffeur
|
||||||
|
{dureeEstimee != null && (
|
||||||
|
<span className="ml-2 text-xs font-normal text-gray-500">
|
||||||
|
(trajet ~{dureeEstimee}h — chauffeurs avec <{heuresRequerues}h restantes masqués)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative" ref={chauffeurDropdownRef}>
|
<div className="relative" ref={chauffeurDropdownRef}>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import AlertModal from './AlertModal';
|
import AlertModal from './AlertModal';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
|
|
||||||
interface UniversPro {
|
interface UniversPro {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,6 +20,7 @@ interface UniversProFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function UniversProForm({ contact, onClose }: UniversProFormProps) {
|
export default function UniversProForm({ contact, onClose }: UniversProFormProps) {
|
||||||
|
useBodyScrollLock(true);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
const [alertModal, setAlertModal] = useState<{
|
const [alertModal, setAlertModal] = useState<{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
import UniversProForm from './UniversProForm';
|
import UniversProForm from './UniversProForm';
|
||||||
import ConfirmModal from './ConfirmModal';
|
import ConfirmModal from './ConfirmModal';
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ export default function UniversProTable() {
|
|||||||
show: boolean;
|
show: boolean;
|
||||||
id: string | null;
|
id: string | null;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
useBodyScrollLock(showImportModal || !!(resultModal?.show) || !!viewingContact);
|
||||||
|
|
||||||
const fetchContacts = async (searchTerm: string = '') => {
|
const fetchContacts = async (searchTerm: string = '') => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useNotification } from './NotificationProvider';
|
import { useNotification } from './NotificationProvider';
|
||||||
|
import { useBodyScrollLock } from '@/lib/body-scroll-lock';
|
||||||
|
|
||||||
interface Trajet {
|
interface Trajet {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -23,6 +24,7 @@ interface ValidationModalProps {
|
|||||||
|
|
||||||
export default function ValidationModal({ trajet, onClose, onSuccess }: ValidationModalProps) {
|
export default function ValidationModal({ trajet, onClose, onSuccess }: ValidationModalProps) {
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
|
useBodyScrollLock(true);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [dureeTrajet, setDureeTrajet] = useState<number | null>(null);
|
const [dureeTrajet, setDureeTrajet] = useState<number | null>(null);
|
||||||
|
|
||||||
|
|||||||
45
lib/body-scroll-lock.ts
Normal file
45
lib/body-scroll-lock.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère le verrouillage du scroll du body quand des modales sont ouvertes.
|
||||||
|
* Gère plusieurs modales empilées via un compteur.
|
||||||
|
*/
|
||||||
|
let lockCount = 0;
|
||||||
|
let savedScrollY = 0;
|
||||||
|
|
||||||
|
export function lockBodyScroll() {
|
||||||
|
lockCount++;
|
||||||
|
if (lockCount === 1) {
|
||||||
|
savedScrollY = window.scrollY;
|
||||||
|
document.documentElement.style.overflow = 'hidden';
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
document.body.style.position = 'fixed';
|
||||||
|
document.body.style.top = `-${savedScrollY}px`;
|
||||||
|
document.body.style.left = '0';
|
||||||
|
document.body.style.right = '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unlockBodyScroll() {
|
||||||
|
if (lockCount > 0) lockCount--;
|
||||||
|
if (lockCount === 0) {
|
||||||
|
document.documentElement.style.overflow = '';
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
document.body.style.position = '';
|
||||||
|
document.body.style.top = '';
|
||||||
|
document.body.style.left = '';
|
||||||
|
document.body.style.right = '';
|
||||||
|
window.scrollTo(0, savedScrollY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useBodyScrollLock(isOpen: boolean) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
lockBodyScroll();
|
||||||
|
return () => unlockBodyScroll();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
}
|
||||||
7
lib/participation-ref.ts
Normal file
7
lib/participation-ref.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Génère la référence de prescription (ex: PART-XXXXXXXX)
|
||||||
|
* utilisée dans les participations financières et affichée sur les trajets.
|
||||||
|
*/
|
||||||
|
export function getParticipationRef(participationId: string): string {
|
||||||
|
return `PART-${participationId.slice(-8).toUpperCase()}`;
|
||||||
|
}
|
||||||
72
lib/trajet-duree.ts
Normal file
72
lib/trajet-duree.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Calcule la durée estimée d'un trajet en heures à partir des adresses.
|
||||||
|
* Utilise Nominatim pour géocoder et la formule de Haversine pour la distance.
|
||||||
|
*/
|
||||||
|
export async function calculerDureeTrajet(
|
||||||
|
adresseDepart: string,
|
||||||
|
adresseArrivee: string
|
||||||
|
): Promise<number | null> {
|
||||||
|
if (!adresseDepart?.trim() || !adresseArrivee?.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const departResponse = await fetch(
|
||||||
|
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(adresseDepart.trim())}&limit=1&countrycodes=fr`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'MAD Platform',
|
||||||
|
'Accept-Language': 'fr-FR,fr;q=0.9',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!departResponse.ok) return null;
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
const arriveeResponse = await fetch(
|
||||||
|
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(adresseArrivee.trim())}&limit=1&countrycodes=fr`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'MAD Platform',
|
||||||
|
'Accept-Language': 'fr-FR,fr;q=0.9',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!arriveeResponse.ok) return null;
|
||||||
|
|
||||||
|
const [departData, arriveeData] = await Promise.all([
|
||||||
|
departResponse.json(),
|
||||||
|
arriveeResponse.json(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!departData?.length || !arriveeData?.length) return null;
|
||||||
|
|
||||||
|
const lat1 = parseFloat(departData[0].lat);
|
||||||
|
const lon1 = parseFloat(departData[0].lon);
|
||||||
|
const lat2 = parseFloat(arriveeData[0].lat);
|
||||||
|
const lon2 = parseFloat(arriveeData[0].lon);
|
||||||
|
|
||||||
|
const R = 6371; // Rayon de la Terre en km
|
||||||
|
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
||||||
|
const dLon = ((lon2 - lon1) * Math.PI) / 180;
|
||||||
|
const a =
|
||||||
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
|
Math.cos((lat1 * Math.PI) / 180) *
|
||||||
|
Math.cos((lat2 * Math.PI) / 180) *
|
||||||
|
Math.sin(dLon / 2) *
|
||||||
|
Math.sin(dLon / 2);
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
const distance = R * c;
|
||||||
|
|
||||||
|
const distanceWithDetour = distance * 1.3;
|
||||||
|
const vitesseMoyenne = 50; // km/h
|
||||||
|
const dureeEnHeures = distanceWithDetour / vitesseMoyenne;
|
||||||
|
|
||||||
|
return Math.round(dureeEnHeures * 10) / 10;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
prisma/dev.db
BIN
prisma/dev.db
Binary file not shown.
Reference in New Issue
Block a user