- Ajout détails cachets/répétitions/heures au modal ContractDetails - Card verte avec validation quand tous les contrats ont une fiche de paie - Système complet de création de fiches de paie avec recherche et vérification - Modal liste des contrats sans paie avec création directe - Amélioration édition dates dans PayslipDetailsModal - Optimisation recherche contrats (ordre des filtres) - Augmentation limite pagination ContractsGrid à 200 - Ajout logs debug génération PDF logo - Script SQL vérification cohérence structure/organisation
530 lines
23 KiB
TypeScript
530 lines
23 KiB
TypeScript
// components/staff/ContractDetailsModal.tsx
|
|
"use client";
|
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
|
import { X, ChevronLeft, ChevronRight, FileText, AlertCircle, Loader } from "lucide-react";
|
|
|
|
type ContractDetails = {
|
|
id: string;
|
|
contract_number?: string;
|
|
employee_name?: string;
|
|
employee_id?: string;
|
|
structure?: string;
|
|
type_de_contrat?: string;
|
|
start_date?: string;
|
|
end_date?: string;
|
|
created_at?: string;
|
|
etat_de_la_demande?: string;
|
|
etat_de_la_paie?: string;
|
|
dpae?: string;
|
|
gross_pay?: number;
|
|
contrat_signe_par_employeur?: string;
|
|
contrat_signe?: string;
|
|
date_signature?: string;
|
|
notes?: string | null;
|
|
// Nouveaux champs pour détails du contrat
|
|
cachets_representations?: number | null;
|
|
services_repetitions?: number | null;
|
|
nombre_d_heures?: number | null;
|
|
nombre_d_heures_par_jour?: number | null;
|
|
minutes_total?: string | null;
|
|
jours_travail?: string | null;
|
|
jours_representations?: string | null;
|
|
jours_repetitions?: string | null;
|
|
precisions_salaire?: string | null;
|
|
profession?: string | null;
|
|
categorie_pro?: string | null;
|
|
categorie_professionnelle?: string | null;
|
|
heures_annexe_8?: number | null;
|
|
production_name?: string | null;
|
|
objet_spectacle?: string | null;
|
|
numero_objet?: string | null;
|
|
salaries?: {
|
|
salarie?: string;
|
|
nom?: string;
|
|
prenom?: string;
|
|
adresse_mail?: string;
|
|
};
|
|
};
|
|
|
|
type ContractDetailsModalProps = {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
contractIds: string[];
|
|
contracts: any[];
|
|
};
|
|
|
|
export default function ContractDetailsModal({
|
|
isOpen,
|
|
onClose,
|
|
contractIds,
|
|
contracts
|
|
}: ContractDetailsModalProps) {
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
const [contractDetails, setContractDetails] = useState<ContractDetails | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const currentContractId = contractIds[currentIndex];
|
|
const currentContract = contracts.find(c => c.id === currentContractId);
|
|
|
|
// Fetch contract details
|
|
useEffect(() => {
|
|
if (!currentContractId) return;
|
|
|
|
const fetchDetails = async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await fetch(`/api/staff/contrats/${currentContractId}`);
|
|
if (!response.ok) {
|
|
throw new Error('Impossible de charger les détails du contrat');
|
|
}
|
|
const data = await response.json();
|
|
setContractDetails(data);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Une erreur est survenue');
|
|
setContractDetails(null);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchDetails();
|
|
}, [currentContractId]);
|
|
|
|
// Reset index when contracts change
|
|
useEffect(() => {
|
|
setCurrentIndex(0);
|
|
}, [contractIds]);
|
|
|
|
// Keyboard navigation
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
goToPrevious();
|
|
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
goToNext();
|
|
} else if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
}, [isOpen, currentIndex, contractIds.length]);
|
|
|
|
const goToPrevious = useCallback(() => {
|
|
if (contractIds.length === 0) return;
|
|
setCurrentIndex(prev => prev > 0 ? prev - 1 : contractIds.length - 1);
|
|
}, [contractIds.length]);
|
|
|
|
const goToNext = useCallback(() => {
|
|
if (contractIds.length === 0) return;
|
|
setCurrentIndex(prev => prev < contractIds.length - 1 ? prev + 1 : 0);
|
|
}, [contractIds.length]);
|
|
|
|
const goToIndex = useCallback((index: number) => {
|
|
setCurrentIndex(index);
|
|
}, []);
|
|
|
|
const formatDate = (date: string | null | undefined) => {
|
|
if (!date) return '—';
|
|
try {
|
|
return new Date(date).toLocaleDateString('fr-FR');
|
|
} catch {
|
|
return '—';
|
|
}
|
|
};
|
|
|
|
const getSignatureStatus = (employeur?: string, salarie?: string) => {
|
|
if (employeur === 'Oui' && salarie === 'Oui') return '✓ Signé (les deux)';
|
|
if (employeur === 'Oui' && salarie === 'Non') return '✓ Employeur seulement';
|
|
if (employeur === 'Non') return '✗ Non signé (employeur)';
|
|
return '—';
|
|
};
|
|
|
|
const getStateColor = (value: string | null | undefined) => {
|
|
if (!value) return 'bg-slate-100 text-slate-700';
|
|
const lower = value.toLowerCase();
|
|
if (lower.includes('reçue') || lower.includes('recue')) return 'bg-sky-100 text-sky-800';
|
|
if (lower.includes('cours')) return 'bg-amber-100 text-amber-800';
|
|
if (lower.includes('traitée') || lower.includes('traitee')) return 'bg-emerald-100 text-emerald-800';
|
|
if (lower.includes('annulée') || lower.includes('annulee')) return 'bg-rose-100 text-rose-800';
|
|
if (lower.includes('à faire') || lower.includes('a faire')) return 'bg-orange-100 text-orange-800';
|
|
if (lower.includes('faite')) return 'bg-green-100 text-green-800';
|
|
return 'bg-slate-100 text-slate-700';
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const hasContracts = contractIds.length > 0;
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-lg w-full h-full max-w-7xl max-h-[95vh] flex flex-col">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-4 border-b bg-gray-50">
|
|
<div className="flex items-center gap-3">
|
|
<FileText className="size-5 text-blue-600" />
|
|
<div>
|
|
<h2 className="text-lg font-semibold">Détails des contrats</h2>
|
|
{hasContracts && (
|
|
<p className="text-sm text-gray-600">
|
|
{currentIndex + 1} sur {contractIds.length} contrats
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
|
>
|
|
<X className="size-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{/* Sidebar with contract list */}
|
|
<div className="w-80 border-r bg-gray-50 flex flex-col">
|
|
<div className="p-3 border-b bg-white">
|
|
<h3 className="font-medium text-sm text-gray-700">Liste des contrats</h3>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto">
|
|
{contractIds.length === 0 ? (
|
|
<div className="p-4 text-center text-gray-500">
|
|
<AlertCircle className="size-8 mx-auto mb-2 text-gray-400" />
|
|
<p className="text-sm">Aucun contrat sélectionné</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1 p-2">
|
|
{contractIds.map((contractId, index) => {
|
|
const contract = contracts.find(c => c.id === contractId);
|
|
return (
|
|
<button
|
|
key={contractId}
|
|
onClick={() => goToIndex(index)}
|
|
className={`w-full text-left p-3 rounded-lg transition-colors ${
|
|
index === currentIndex
|
|
? 'bg-blue-100 border border-blue-200'
|
|
: 'hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-medium text-sm truncate">
|
|
{contract?.contract_number || `Contrat ${index + 1}`}
|
|
</div>
|
|
<div className="text-xs text-gray-500 truncate">
|
|
{contract?.employee_name || 'Nom non disponible'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Details View */}
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
{isLoading ? (
|
|
<div className="flex-1 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<Loader className="size-12 mx-auto mb-4 text-blue-600 animate-spin" />
|
|
<p className="text-gray-600">Chargement des détails...</p>
|
|
</div>
|
|
</div>
|
|
) : !hasContracts ? (
|
|
<div className="flex-1 flex items-center justify-center">
|
|
<div className="text-center text-gray-500">
|
|
<FileText className="size-16 mx-auto mb-4 text-gray-300" />
|
|
<p className="text-lg font-medium mb-2">Aucun contrat sélectionné</p>
|
|
<p className="text-sm">Sélectionnez des contrats pour voir leurs détails</p>
|
|
</div>
|
|
</div>
|
|
) : error ? (
|
|
<div className="flex-1 flex items-center justify-center">
|
|
<div className="text-center text-red-500">
|
|
<AlertCircle className="size-16 mx-auto mb-4" />
|
|
<p className="text-lg font-medium mb-2">Erreur de chargement</p>
|
|
<p className="text-sm">{error}</p>
|
|
</div>
|
|
</div>
|
|
) : contractDetails ? (
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
<div className="grid grid-cols-2 gap-6">
|
|
{/* Colonne 1 */}
|
|
<div className="space-y-4">
|
|
{/* Production */}
|
|
{contractDetails.production_name && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Production</label>
|
|
<p className="text-sm text-gray-900">{contractDetails.production_name}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Numéro d'objet */}
|
|
{(contractDetails.objet_spectacle || contractDetails.numero_objet) && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">N° Objet</label>
|
|
<p className="text-sm text-gray-900">{contractDetails.objet_spectacle || contractDetails.numero_objet}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Numéro de contrat */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">N° Contrat</label>
|
|
<p className="text-sm text-gray-900">{contractDetails.contract_number || '—'}</p>
|
|
</div>
|
|
|
|
{/* Salarié */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Salarié</label>
|
|
<p className="text-sm text-gray-900">{contractDetails.employee_name || '—'}</p>
|
|
</div>
|
|
|
|
{/* Email salarié */}
|
|
{contractDetails.salaries?.adresse_mail && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Email</label>
|
|
<p className="text-sm text-gray-900">{contractDetails.salaries.adresse_mail}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Structure */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Structure</label>
|
|
<p className="text-sm text-gray-900">{contractDetails.structure || '—'}</p>
|
|
</div>
|
|
|
|
{/* Type de contrat */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Type de contrat</label>
|
|
<p className="text-sm text-gray-900">
|
|
{contractDetails.type_de_contrat === "CDD d'usage" ? "CDDU" : (contractDetails.type_de_contrat || '—')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Salaire brut */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Salaire brut</label>
|
|
<p className="text-sm text-gray-900">
|
|
{contractDetails.gross_pay ? `${contractDetails.gross_pay.toLocaleString('fr-FR', { minimumFractionDigits: 2 })} €` : '—'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Profession */}
|
|
{contractDetails.profession && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Profession</label>
|
|
<p className="text-sm text-gray-900">{contractDetails.profession}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Colonne 2 */}
|
|
<div className="space-y-4">
|
|
{/* Date de début */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Date de début</label>
|
|
<p className="text-sm text-gray-900">{formatDate(contractDetails.start_date)}</p>
|
|
</div>
|
|
|
|
{/* Date de fin */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Date de fin</label>
|
|
<p className="text-sm text-gray-900">{formatDate(contractDetails.end_date)}</p>
|
|
</div>
|
|
|
|
{/* DPAE */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">DPAE</label>
|
|
<div className={`inline-block px-2 py-1 rounded text-xs font-medium whitespace-nowrap ${getStateColor(contractDetails.dpae)}`}>
|
|
{contractDetails.dpae || '—'}
|
|
</div>
|
|
</div>
|
|
|
|
{/* État contrat */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">État contrat</label>
|
|
<div className={`inline-block px-2 py-1 rounded text-xs font-medium whitespace-nowrap ${getStateColor(contractDetails.etat_de_la_demande)}`}>
|
|
{contractDetails.etat_de_la_demande || '—'}
|
|
</div>
|
|
</div>
|
|
|
|
{/* État paie */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">État paie</label>
|
|
<div className={`inline-block px-2 py-1 rounded text-xs font-medium whitespace-nowrap ${getStateColor(contractDetails.etat_de_la_paie)}`}>
|
|
{contractDetails.etat_de_la_paie || '—'}
|
|
</div>
|
|
</div>
|
|
|
|
{/* État signature */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">État signature</label>
|
|
<p className="text-sm text-gray-900">
|
|
{getSignatureStatus(contractDetails.contrat_signe_par_employeur, contractDetails.contrat_signe)}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Annexe */}
|
|
{contractDetails.heures_annexe_8 !== null && contractDetails.heures_annexe_8 !== undefined && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Annexe</label>
|
|
<p className="text-sm text-gray-900">
|
|
{contractDetails.heures_annexe_8 ? 'Annexe 8 (Techniciens)' : 'Annexe 10 (Artistes)'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Date signature */}
|
|
{contractDetails.date_signature && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Date signature</label>
|
|
<p className="text-sm text-gray-900">{formatDate(contractDetails.date_signature)}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Deuxième grille: Détails des jours/heures/cachets */}
|
|
<div className="grid grid-cols-2 gap-6 mt-6 pt-6 border-t">
|
|
<h3 className="col-span-2 text-base font-semibold text-gray-800 mb-2">Détails du contrat</h3>
|
|
|
|
{/* Colonne 1 */}
|
|
<div className="space-y-4">
|
|
{/* Nombre de cachets (représentations) */}
|
|
{contractDetails.cachets_representations !== null && contractDetails.cachets_representations !== undefined && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Cachets (Représentations)</label>
|
|
<p className="text-sm text-gray-900">{contractDetails.cachets_representations}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Dates des représentations */}
|
|
{contractDetails.jours_representations && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Jours représentations</label>
|
|
<p className="text-sm text-gray-900 whitespace-pre-wrap">{contractDetails.jours_representations}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Nombre de services (répétitions) */}
|
|
{contractDetails.services_repetitions !== null && contractDetails.services_repetitions !== undefined && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Services (Répétitions)</label>
|
|
<p className="text-sm text-gray-900">{contractDetails.services_repetitions}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Dates des répétitions */}
|
|
{contractDetails.jours_repetitions && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Jours répétitions</label>
|
|
<p className="text-sm text-gray-900 whitespace-pre-wrap">{contractDetails.jours_repetitions}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Nombre d'heures total */}
|
|
{contractDetails.nombre_d_heures !== null && contractDetails.nombre_d_heures !== undefined && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Nombre d'heures total</label>
|
|
<p className="text-sm text-gray-900">
|
|
{contractDetails.nombre_d_heures}h
|
|
{contractDetails.minutes_total && contractDetails.minutes_total !== "0" && (
|
|
<span>{contractDetails.minutes_total}</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Nombre d'heures par jour */}
|
|
{contractDetails.nombre_d_heures_par_jour !== null && contractDetails.nombre_d_heures_par_jour !== undefined && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Heures par jour</label>
|
|
<p className="text-sm text-gray-900">{contractDetails.nombre_d_heures_par_jour}h</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Colonne 2 */}
|
|
<div className="space-y-4">
|
|
{/* Jours travail */}
|
|
{contractDetails.jours_travail && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Jours travaillés</label>
|
|
<p className="text-sm text-gray-900 whitespace-pre-wrap">{contractDetails.jours_travail}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Précisions salaire */}
|
|
{contractDetails.precisions_salaire && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Précisions salaire</label>
|
|
<p className="text-sm text-gray-900 whitespace-pre-wrap">{contractDetails.precisions_salaire}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Catégorie professionnelle */}
|
|
{(contractDetails.categorie_pro || contractDetails.categorie_professionnelle) && (
|
|
<div>
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Catégorie professionnelle</label>
|
|
<p className="text-sm text-gray-900">{contractDetails.categorie_pro || contractDetails.categorie_professionnelle}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section Notes */}
|
|
{contractDetails.notes && (
|
|
<div className="mt-6 pt-6 border-t">
|
|
<label className="text-xs font-semibold text-gray-600 uppercase">Notes</label>
|
|
<div className="mt-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
|
<p className="text-sm text-gray-900 whitespace-pre-wrap">{contractDetails.notes}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : null}
|
|
|
|
{/* Navigation */}
|
|
{hasContracts && (
|
|
<div className="flex items-center justify-center gap-4 p-4 border-t bg-gray-50">
|
|
<button
|
|
onClick={goToPrevious}
|
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
|
title="Contrat précédent (←)"
|
|
>
|
|
<ChevronLeft className="size-5" />
|
|
</button>
|
|
|
|
<span className="text-sm text-gray-600">
|
|
{currentIndex + 1} / {contractIds.length}
|
|
</span>
|
|
|
|
<button
|
|
onClick={goToNext}
|
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
|
title="Contrat suivant (→)"
|
|
>
|
|
<ChevronRight className="size-5" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|