espace-paie-odentas/components/staff/PayslipDetailsModal.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

874 lines
35 KiB
TypeScript

// components/staff/PayslipDetailsModal.tsx
"use client";
import React, { useState, useEffect, useRef, DragEvent } from "react";
import { X, ChevronLeft, ChevronRight, FileText, Loader, Edit, Save, Upload, CheckCircle2, Loader2, ExternalLink, Trash2, RefreshCw } from "lucide-react";
import { toast } from "sonner";
type PayslipDetails = {
id: string;
contract_id: string;
period_start?: string | null;
period_end?: string | null;
period_month?: string | null;
pay_number?: number | null;
pay_date?: string | null;
gross_amount?: string | number | null;
net_amount?: string | number | null;
net_after_withholding?: string | number | null;
employer_cost?: string | number | null;
processed?: boolean | null;
aem_status?: string | null;
transfer_done?: boolean | null;
organization_id?: string | null;
created_at?: string | null;
storage_path?: string | null;
cddu_contracts?: {
id: string;
contract_number?: string | null;
employee_name?: string | null;
employee_id?: string | null;
structure?: string | null;
type_de_contrat?: string | null;
org_id?: string | null;
salaries?: {
salarie?: string | null;
nom?: string | null;
prenom?: string | null;
} | null;
} | null;
};
type PayslipDetailsModalProps = {
isOpen: boolean;
onClose: () => void;
payslipIds: string[];
payslips: any[];
onPayslipUpdated?: (updatedPayslip: any) => void;
};
// Utility function to format dates as DD/MM/YYYY
function formatDate(dateString: string | null | undefined): string {
if (!dateString) return "—";
try {
const date = new Date(dateString);
return date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
} catch {
return "—";
}
}
// Utility function to format currency
function formatCurrency(value: string | number | null | undefined): string {
if (!value) return "—";
const num = typeof value === 'string' ? parseFloat(value) : value;
if (isNaN(num)) return "—";
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(num);
}
// Utility function to format employee name
function formatEmployeeName(payslip: PayslipDetails): string {
const contract = payslip.cddu_contracts;
if (!contract) return "—";
// Priorité : utiliser salaries.salarie si disponible
if (contract.salaries?.salarie) {
return contract.salaries.salarie;
}
// Construire depuis nom + prénom
if (contract.salaries?.nom || contract.salaries?.prenom) {
const nom = (contract.salaries.nom || '').toUpperCase().trim();
const prenom = (contract.salaries.prenom || '').trim();
return [nom, prenom].filter(Boolean).join(' ');
}
// Fallback : utiliser employee_name
if (contract.employee_name) {
const parts = contract.employee_name.trim().split(' ');
if (parts.length >= 2) {
const prenom = parts[0];
const nom = parts.slice(1).join(' ');
return `${nom.toUpperCase()} ${prenom}`;
}
return contract.employee_name;
}
return "—";
}
export default function PayslipDetailsModal({
isOpen,
onClose,
payslipIds,
payslips,
onPayslipUpdated
}: PayslipDetailsModalProps) {
const [currentIndex, setCurrentIndex] = useState(0);
const [payslipDetails, setPayslipDetails] = useState<PayslipDetails | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isEditMode, setIsEditMode] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [formData, setFormData] = useState({
gross_amount: "",
net_amount: "",
net_after_withholding: "",
employer_cost: "",
period_start: "",
period_end: "",
pay_date: "",
processed: false,
transfer_done: false,
aem_status: ""
});
// Document upload states
const [isDraggingDoc, setIsDraggingDoc] = useState(false);
const [isUploadingDoc, setIsUploadingDoc] = useState(false);
const [isOpeningDoc, setIsOpeningDoc] = useState(false);
const [isDeletingDoc, setIsDeletingDoc] = useState(false);
const fileInputRefDoc = useRef<HTMLInputElement | null>(null);
const currentPayslipId = payslipIds[currentIndex];
const currentPayslip = payslips.find(p => p.id === currentPayslipId);
// Fetch payslip details from API to get fresh data including storage_path
useEffect(() => {
if (!currentPayslipId) return;
const fetchPayslipDetails = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(`/api/staff/payslips/${currentPayslipId}`);
if (!response.ok) {
throw new Error('Erreur lors du chargement des détails');
}
const data = await response.json();
setPayslipDetails(data);
// Charger les données dans le formulaire
setFormData({
gross_amount: String(data.gross_amount || ""),
net_amount: String(data.net_amount || ""),
net_after_withholding: String(data.net_after_withholding || ""),
employer_cost: String(data.employer_cost || ""),
period_start: data.period_start?.slice(0, 10) || "",
period_end: data.period_end?.slice(0, 10) || "",
pay_date: data.pay_date?.slice(0, 10) || "",
processed: data.processed || false,
transfer_done: data.transfer_done || false,
aem_status: data.aem_status || ""
});
// Désactiver le mode édition lors du changement de paie
setIsEditMode(false);
} catch (err) {
console.error('Erreur fetch payslip:', err);
setError('Erreur lors du chargement des détails');
// Fallback sur les données des props si l'API échoue
if (currentPayslip) {
setPayslipDetails(currentPayslip);
setFormData({
gross_amount: String(currentPayslip.gross_amount || ""),
net_amount: String(currentPayslip.net_amount || ""),
net_after_withholding: String(currentPayslip.net_after_withholding || ""),
employer_cost: String(currentPayslip.employer_cost || ""),
period_start: currentPayslip.period_start?.slice(0, 10) || "",
period_end: currentPayslip.period_end?.slice(0, 10) || "",
pay_date: currentPayslip.pay_date?.slice(0, 10) || "",
processed: currentPayslip.processed || false,
transfer_done: currentPayslip.transfer_done || false,
aem_status: currentPayslip.aem_status || ""
});
setIsEditMode(false);
}
} finally {
setIsLoading(false);
}
};
fetchPayslipDetails();
}, [currentPayslipId, currentPayslip]);
// Fonction pour sauvegarder les modifications
const handleSave = async () => {
if (!payslipDetails) return;
setIsSaving(true);
try {
const response = await fetch(`/api/staff/payslips/${payslipDetails.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
gross_amount: parseFloat(formData.gross_amount) || null,
net_amount: parseFloat(formData.net_amount) || null,
net_after_withholding: parseFloat(formData.net_after_withholding) || null,
employer_cost: parseFloat(formData.employer_cost) || null,
period_start: formData.period_start || null,
period_end: formData.period_end || null,
pay_date: formData.pay_date || null,
processed: formData.processed,
transfer_done: formData.transfer_done,
aem_status: formData.aem_status || null
})
});
if (!response.ok) {
throw new Error('Erreur lors de la sauvegarde');
}
const updatedPayslip = await response.json();
// Mettre à jour les données affichées
setPayslipDetails(updatedPayslip);
setIsEditMode(false);
toast.success('Fiche de paie mise à jour avec succès');
// Notifier le parent pour mettre à jour la liste
if (onPayslipUpdated) {
onPayslipUpdated(updatedPayslip);
}
} catch (error) {
console.error('Erreur:', error);
toast.error('Erreur lors de la sauvegarde');
} finally {
setIsSaving(false);
}
};
// Document upload / open helpers
const handleDragEnterDoc = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingDoc(true);
};
const handleDragLeaveDoc = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingDoc(false);
};
const handleDragOverDoc = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
};
const handleDropDoc = async (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingDoc(false);
const files = Array.from(e.dataTransfer.files);
if (files.length === 0) return;
const file = files[0];
if (file.type !== 'application/pdf') {
toast.error('Seuls les fichiers PDF sont acceptés');
return;
}
await uploadDocFile(file);
};
const handleFileSelectDoc = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
const file = files[0];
if (file.type !== 'application/pdf') {
toast.error('Seuls les fichiers PDF sont acceptés');
return;
}
await uploadDocFile(file);
};
const uploadDocFile = async (file: File) => {
if (!payslipDetails) return;
setIsUploadingDoc(true);
try {
const formData = new FormData();
formData.append('file', file);
formData.append('contract_id', payslipDetails.cddu_contracts?.id || '');
formData.append('payslip_id', payslipDetails.id);
const response = await fetch('/api/staff/payslip-upload', {
method: 'POST',
body: formData,
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || 'Erreur lors de l\'upload');
}
const result = await response.json();
// Mettre à jour l'état local et notifier le parent
const updated = { ...payslipDetails, storage_path: result.s3_key };
setPayslipDetails(updated as PayslipDetails);
if (onPayslipUpdated) onPayslipUpdated(updated);
toast.success('Bulletin de paie uploadé avec succès !');
} catch (error) {
console.error('Erreur upload:', error);
toast.error(error instanceof Error ? error.message : 'Erreur lors de l\'upload');
} finally {
setIsUploadingDoc(false);
if (fileInputRefDoc.current) fileInputRefDoc.current.value = '';
}
};
const handleOpenPdfDoc = async (e?: React.MouseEvent) => {
if (e) { e.preventDefault(); e.stopPropagation(); }
if (!payslipDetails?.storage_path || isOpeningDoc) return;
setIsOpeningDoc(true);
try {
const res = await fetch('/api/s3-presigned', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: payslipDetails.storage_path })
});
if (!res.ok) throw new Error('Erreur lors de la génération de l\'URL');
const data = await res.json();
window.open(data.url, '_blank');
} catch (error) {
console.error('Erreur ouverture PDF:', error);
toast.error('Erreur lors de l\'accès au fichier');
} finally {
setIsOpeningDoc(false);
}
};
const handleDeleteDoc = async () => {
if (!payslipDetails?.storage_path || isDeletingDoc) return;
if (!confirm('Êtes-vous sûr de vouloir supprimer ce document ? Cette action est irréversible.')) {
return;
}
setIsDeletingDoc(true);
try {
// Mettre à jour la fiche pour enlever le storage_path
const response = await fetch(`/api/staff/payslips/${payslipDetails.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ storage_path: null })
});
if (!response.ok) {
throw new Error('Erreur lors de la suppression');
}
// Mettre à jour l'état local
const updated = { ...payslipDetails, storage_path: null };
setPayslipDetails(updated as PayslipDetails);
if (onPayslipUpdated) onPayslipUpdated(updated);
toast.success('Document supprimé avec succès');
} catch (error) {
console.error('Erreur suppression:', error);
toast.error(error instanceof Error ? error.message : 'Erreur lors de la suppression');
} finally {
setIsDeletingDoc(false);
}
};
const handleReplaceDoc = () => {
if (fileInputRefDoc.current) {
fileInputRefDoc.current.click();
}
};
// Reset index when payslips change
useEffect(() => {
setCurrentIndex(0);
}, [payslipIds]);
// Keyboard navigation
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "ArrowLeft" && currentIndex > 0) {
e.preventDefault();
setCurrentIndex(prev => prev - 1);
} else if (e.key === "ArrowRight" && currentIndex < payslipIds.length - 1) {
e.preventDefault();
setCurrentIndex(prev => prev + 1);
} else if (e.key === "Escape") {
e.preventDefault();
onClose();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isOpen, currentIndex, payslipIds.length, onClose]);
if (!isOpen) return null;
const handlePrevious = () => {
if (currentIndex > 0) {
setCurrentIndex(prev => prev - 1);
}
};
const handleNext = () => {
if (currentIndex < payslipIds.length - 1) {
setCurrentIndex(prev => prev + 1);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" onClick={onClose}>
<div
className="relative w-full max-w-3xl max-h-[90vh] overflow-y-auto bg-white rounded-xl shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-6 py-4">
<div className="flex items-center gap-3">
<FileText className="w-6 h-6 text-slate-600" />
<div>
<h2 className="text-xl font-semibold text-slate-900">
Détails de la fiche de paie
</h2>
<p className="text-sm text-slate-600">
{currentIndex + 1} sur {payslipIds.length}
</p>
</div>
</div>
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-600 transition-colors"
aria-label="Fermer"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Navigation */}
{payslipIds.length > 1 && (
<div className="flex items-center justify-between border-b bg-slate-50 px-6 py-3">
<button
onClick={handlePrevious}
disabled={currentIndex === 0}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-slate-700 bg-white rounded-lg border border-slate-300 hover:bg-slate-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronLeft className="w-4 h-4" />
Précédent
</button>
<span className="text-sm text-slate-600">
Navigation : /
</span>
<button
onClick={handleNext}
disabled={currentIndex === payslipIds.length - 1}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-slate-700 bg-white rounded-lg border border-slate-300 hover:bg-slate-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Suivant
<ChevronRight className="w-4 h-4" />
</button>
</div>
)}
{/* Content */}
<div className="p-6 space-y-6">
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader className="w-8 h-8 animate-spin text-indigo-600" />
</div>
) : error ? (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
{error}
</div>
) : payslipDetails ? (
<>
{/* Informations principales */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-slate-900 border-b pb-2">
Informations principales
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">
Salarié
</label>
<p className="text-base text-slate-900">
{formatEmployeeName(payslipDetails)}
</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">
N° Contrat
</label>
<p className="text-base text-slate-900">
{payslipDetails.cddu_contracts?.contract_number || "—"}
</p>
</div>
{!isEditMode ? (
<>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">
Période
</label>
<p className="text-base text-slate-900">
{formatDate(payslipDetails.period_start)} - {formatDate(payslipDetails.period_end)}
</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">
Date de paiement
</label>
<p className="text-base text-slate-900">
{formatDate(payslipDetails.pay_date)}
</p>
</div>
</>
) : (
<>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">
Date début période
</label>
<input
type="date"
value={formData.period_start}
onChange={(e) => setFormData({...formData, period_start: e.target.value})}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">
Date fin période
</label>
<input
type="date"
value={formData.period_end}
onChange={(e) => setFormData({...formData, period_end: e.target.value})}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">
Date de paiement
</label>
<input
type="date"
value={formData.pay_date}
onChange={(e) => setFormData({...formData, pay_date: e.target.value})}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
</>
)}
</div>
</div>
{/* Montants */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-slate-900 border-b pb-2">
Montants
</h3>
{!isEditMode ? (
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
<label className="block text-sm font-medium text-blue-800 mb-1">
Salaire brut
</label>
<p className="text-2xl font-bold text-blue-900">
{formatCurrency(payslipDetails.gross_amount)}
</p>
</div>
<div className="p-4 bg-orange-50 rounded-lg border border-orange-200">
<label className="block text-sm font-medium text-orange-800 mb-1">
Net avant PAS
</label>
<p className="text-2xl font-bold text-orange-900">
{formatCurrency(payslipDetails.net_amount)}
</p>
</div>
<div className="p-4 bg-green-50 rounded-lg border border-green-200">
<label className="block text-sm font-medium text-green-800 mb-1">
Net à payer
</label>
<p className="text-2xl font-bold text-green-900">
{formatCurrency(payslipDetails.net_after_withholding)}
</p>
</div>
<div className="p-4 bg-red-50 rounded-lg border border-red-200">
<label className="block text-sm font-medium text-red-800 mb-1">
Coût employeur
</label>
<p className="text-2xl font-bold text-red-900">
{formatCurrency(payslipDetails.employer_cost)}
</p>
</div>
</div>
) : (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">
Salaire brut
</label>
<input
type="number"
step="0.01"
value={formData.gross_amount}
onChange={(e) => setFormData({...formData, gross_amount: e.target.value})}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">
Net avant PAS
</label>
<input
type="number"
step="0.01"
value={formData.net_amount}
onChange={(e) => setFormData({...formData, net_amount: e.target.value})}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">
Net à payer
</label>
<input
type="number"
step="0.01"
value={formData.net_after_withholding}
onChange={(e) => setFormData({...formData, net_after_withholding: e.target.value})}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">
Coût employeur
</label>
<input
type="number"
step="0.01"
value={formData.employer_cost}
onChange={(e) => setFormData({...formData, employer_cost: e.target.value})}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
</div>
)}
</div>
{/* Documents */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-slate-900 border-b pb-2">Documents</h3>
{payslipDetails.storage_path ? (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="inline-flex items-center justify-center w-10 h-10 rounded-md bg-green-100 text-green-700">
<FileText className="w-5 h-5" />
</div>
<div>
<div className="text-sm font-medium text-slate-900">Bulletin de paie</div>
<div className="text-xs text-slate-600">Fichier disponible</div>
</div>
</div>
<button
onClick={handleOpenPdfDoc}
disabled={isOpeningDoc}
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-green-700 bg-white border border-green-200 rounded-lg hover:bg-green-50 transition-colors disabled:opacity-50"
>
{isOpeningDoc ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Ouverture...
</>
) : (
<>
<ExternalLink className="w-4 h-4" />
Ouvrir
</>
)}
</button>
</div>
{/* Actions : Remplacer et Supprimer */}
<div className="flex items-center gap-2 pt-2 border-t border-green-200">
<button
onClick={handleReplaceDoc}
disabled={isUploadingDoc || isDeletingDoc}
className="flex-1 inline-flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors disabled:opacity-50"
>
<RefreshCw className="w-4 h-4" />
Remplacer
</button>
<button
onClick={handleDeleteDoc}
disabled={isUploadingDoc || isDeletingDoc}
className="flex-1 inline-flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium text-red-700 bg-white border border-red-300 rounded-lg hover:bg-red-50 transition-colors disabled:opacity-50"
>
{isDeletingDoc ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Suppression...
</>
) : (
<>
<Trash2 className="w-4 h-4" />
Supprimer
</>
)}
</button>
</div>
{/* Hidden file input for replace */}
<input
ref={fileInputRefDoc}
type="file"
accept="application/pdf"
onChange={handleFileSelectDoc}
className="hidden"
/>
</div>
) : (
<div
onDragEnter={handleDragEnterDoc}
onDragLeave={handleDragLeaveDoc}
onDragOver={handleDragOverDoc}
onDrop={handleDropDoc}
className={`border-2 border-dashed rounded-xl p-4 text-center transition-all ${isDraggingDoc ? 'border-blue-500 bg-blue-50' : 'border-gray-300 bg-gray-50 hover:border-blue-400 hover:bg-blue-50'}`}
onClick={() => fileInputRefDoc.current?.click()}
>
{isUploadingDoc ? (
<div className="flex flex-col items-center gap-2">
<Loader2 className="w-6 h-6 text-blue-500 animate-spin" />
<p className="text-sm text-blue-600 font-medium">Upload en cours...</p>
</div>
) : (
<div className="flex flex-col items-center gap-2">
<Upload className="w-6 h-6 text-gray-400" />
<p className="text-sm text-gray-600">
<span className="font-medium text-blue-600">Cliquez</span> ou glissez un PDF ici
</p>
<p className="text-xs text-gray-500">Bulletin de paie au format PDF</p>
</div>
)}
<input
ref={fileInputRefDoc}
type="file"
accept="application/pdf"
onChange={handleFileSelectDoc}
className="hidden"
/>
</div>
)}
</div>
{/* Statuts */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-slate-900 border-b pb-2">
Statuts
</h3>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">
Traité
</label>
<p className={`text-base font-medium ${payslipDetails.processed ? 'text-green-600' : 'text-orange-600'}`}>
{payslipDetails.processed ? 'Oui' : 'Non'}
</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">
Virement effectué
</label>
<p className={`text-base font-medium ${payslipDetails.transfer_done ? 'text-green-600' : 'text-orange-600'}`}>
{payslipDetails.transfer_done ? 'Oui' : 'Non'}
</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">
Statut AEM
</label>
<p className="text-base text-slate-900">
{payslipDetails.aem_status || "—"}
</p>
</div>
</div>
</div>
{/* Actions */}
<div className="flex items-center justify-end gap-3 pt-4 border-t">
{!isEditMode ? (
<button
onClick={() => setIsEditMode(true)}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 transition-colors"
>
<Edit className="w-4 h-4" />
Modifier la fiche de paie
</button>
) : (
<>
<button
onClick={() => {
setIsEditMode(false);
// Restaurer les données originales
setFormData({
gross_amount: String(payslipDetails.gross_amount || ""),
net_amount: String(payslipDetails.net_amount || ""),
net_after_withholding: String(payslipDetails.net_after_withholding || ""),
employer_cost: String(payslipDetails.employer_cost || ""),
period_start: payslipDetails.period_start?.slice(0, 10) || "",
period_end: payslipDetails.period_end?.slice(0, 10) || "",
pay_date: payslipDetails.pay_date?.slice(0, 10) || "",
processed: payslipDetails.processed || false,
transfer_done: payslipDetails.transfer_done || false,
aem_status: payslipDetails.aem_status || ""
});
}}
disabled={isSaving}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors disabled:opacity-50"
>
Annuler
</button>
<button
onClick={handleSave}
disabled={isSaving}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50"
>
<Save className="w-4 h-4" />
{isSaving ? 'Sauvegarde...' : 'Sauvegarder'}
</button>
</>
)}
</div>
</>
) : (
<div className="text-center py-12 text-slate-500">
Aucune donnée disponible
</div>
)}
</div>
</div>
</div>
);
}