espace-paie-odentas/components/staff/ContractDetailsModal.tsx
odentas dd570d4509 feat: Améliorations majeures des contrats et fiches de paie
- 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
2025-11-27 20:31:11 +01:00

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>
);
}