✨ Nouvelles fonctionnalités - Page de gestion des avenants (/staff/avenants) - Page de détail d'un avenant (/staff/avenants/[id]) - Création d'avenants (objet, durée, rémunération) - Génération automatique de PDF d'avenant - Signature électronique via DocuSeal (employeur puis salarié) - Changement manuel du statut d'un avenant - Suppression d'avenants 🔧 Routes API - POST /api/staff/amendments/create - Créer un avenant - POST /api/staff/amendments/generate-pdf - Générer le PDF - POST /api/staff/amendments/[id]/send-signature - Envoyer en signature - POST /api/staff/amendments/[id]/change-status - Changer le statut - POST /api/webhooks/docuseal-amendment - Webhook après signature employeur - GET /api/signatures-electroniques/avenants - Liste des avenants en signature 📧 Système email universel v2 - Migration vers le système universel v2 pour les emails d'avenants - Template 'signature-request-employee-amendment' pour salariés - Insertion automatique dans DynamoDB pour la Lambda - Mise à jour automatique du statut dans Supabase 🗄️ Base de données - Table 'avenants' avec tous les champs (objet, durée, rémunération) - Colonnes de notification (last_employer_notification_at, last_employee_notification_at) - Liaison avec cddu_contracts 🎨 Composants - AvenantDetailPageClient - Détail complet d'un avenant - ChangeStatusModal - Changement de statut manuel - SendSignatureModal - Envoi en signature - DeleteAvenantModal - Suppression avec confirmation - AvenantSuccessModal - Confirmation de création 📚 Documentation - AVENANT_EMAIL_SYSTEM_MIGRATION.md - Guide complet de migration 🐛 Corrections - Fix parsing défensif dans Lambda AWS - Fix récupération des données depuis DynamoDB - Fix statut MFA !== 'verified' au lieu de === 'unverified'
685 lines
28 KiB
TypeScript
685 lines
28 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import { ArrowLeft, Calendar, FileText, User, Building2, Download, Trash2, RefreshCw, Send, Check, X, Clock, Edit3 } from "lucide-react";
|
|
import { useEffect, useState } from "react";
|
|
import DeleteAvenantModal from "@/components/staff/amendments/DeleteAvenantModal";
|
|
import SendSignatureModal from "@/components/staff/amendments/SendSignatureModal";
|
|
import ChangeStatusModal from "@/components/staff/amendments/ChangeStatusModal";
|
|
|
|
interface AvenantDetailPageClientProps {
|
|
avenant: any;
|
|
}
|
|
|
|
export default function AvenantDetailPageClient({ avenant }: AvenantDetailPageClientProps) {
|
|
const router = useRouter();
|
|
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
|
const [loadingPdf, setLoadingPdf] = useState(false);
|
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
const [isRegeneratingPdf, setIsRegeneratingPdf] = useState(false);
|
|
const [isSendingSignature, setIsSendingSignature] = useState(false);
|
|
const [showSendSignatureModal, setShowSendSignatureModal] = useState(false);
|
|
const [sendSignatureSuccess, setSendSignatureSuccess] = useState(false);
|
|
const [showChangeStatusModal, setShowChangeStatusModal] = useState(false);
|
|
const [isChangingStatus, setIsChangingStatus] = useState(false);
|
|
|
|
// Charger l'URL du PDF si la clé S3 existe
|
|
useEffect(() => {
|
|
if (avenant.pdf_s3_key) {
|
|
setLoadingPdf(true);
|
|
fetch(`/api/staff/amendments/${avenant.id}/pdf-url`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.presignedUrl) {
|
|
setPdfUrl(data.presignedUrl);
|
|
}
|
|
})
|
|
.catch(err => console.error("Erreur chargement URL PDF:", err))
|
|
.finally(() => setLoadingPdf(false));
|
|
}
|
|
}, [avenant.id, avenant.pdf_s3_key]);
|
|
|
|
const handleDelete = async () => {
|
|
setIsDeleting(true);
|
|
try {
|
|
const response = await fetch(`/api/staff/amendments/${avenant.id}`, {
|
|
method: "DELETE",
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || "Erreur lors de la suppression");
|
|
}
|
|
|
|
// Rediriger vers la liste des avenants
|
|
router.push("/staff/avenants");
|
|
} catch (error: any) {
|
|
console.error("Erreur suppression avenant:", error);
|
|
alert("Erreur lors de la suppression de l'avenant: " + error.message);
|
|
setIsDeleting(false);
|
|
setShowDeleteModal(false);
|
|
}
|
|
};
|
|
|
|
const handleRegeneratePdf = async () => {
|
|
setIsRegeneratingPdf(true);
|
|
try {
|
|
// Préparer les données de l'avenant pour la génération
|
|
const amendmentData = {
|
|
contract_id: avenant.contract_id,
|
|
date_effet: avenant.date_effet,
|
|
date_signature: avenant.date_avenant,
|
|
elements: avenant.elements_avenantes,
|
|
objet_data: avenant.objet_data,
|
|
duree_data: avenant.duree_data,
|
|
lieu_horaire_data: avenant.lieu_horaire_data,
|
|
remuneration_data: avenant.remuneration_data,
|
|
};
|
|
|
|
// Générer le PDF
|
|
const generateResponse = await fetch("/api/staff/amendments/generate-pdf", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
contractId: avenant.contract_id,
|
|
amendmentData,
|
|
}),
|
|
});
|
|
|
|
if (!generateResponse.ok) {
|
|
const error = await generateResponse.json();
|
|
throw new Error(error.error || "Erreur lors de la génération du PDF");
|
|
}
|
|
|
|
const { s3Key, presignedUrl } = await generateResponse.json();
|
|
|
|
// Mettre à jour l'avenant avec la nouvelle clé S3
|
|
const updateResponse = await fetch(`/api/staff/amendments/${avenant.id}/update-pdf`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
pdf_s3_key: s3Key,
|
|
}),
|
|
});
|
|
|
|
if (!updateResponse.ok) {
|
|
const error = await updateResponse.json();
|
|
throw new Error(error.error || "Erreur lors de la mise à jour");
|
|
}
|
|
|
|
// Mettre à jour l'URL affichée
|
|
setPdfUrl(presignedUrl);
|
|
alert("PDF régénéré avec succès !");
|
|
|
|
// Recharger la page pour avoir les nouvelles données
|
|
router.refresh();
|
|
} catch (error: any) {
|
|
console.error("Erreur regénération PDF:", error);
|
|
alert("Erreur lors de la regénération du PDF: " + error.message);
|
|
} finally {
|
|
setIsRegeneratingPdf(false);
|
|
}
|
|
};
|
|
|
|
const handleSendForSignature = async () => {
|
|
if (!avenant.pdf_s3_key) {
|
|
alert("Vous devez d'abord générer le PDF avant de l'envoyer en signature.");
|
|
return;
|
|
}
|
|
|
|
setIsSendingSignature(true);
|
|
setSendSignatureSuccess(false);
|
|
try {
|
|
const response = await fetch(`/api/staff/amendments/${avenant.id}/send-signature`, {
|
|
method: "POST",
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || "Erreur lors de l'envoi");
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Afficher le succès dans le modal
|
|
setSendSignatureSuccess(true);
|
|
|
|
// Recharger la page après 2 secondes pour voir le nouveau statut
|
|
setTimeout(() => {
|
|
router.refresh();
|
|
}, 2000);
|
|
} catch (error: any) {
|
|
console.error("Erreur envoi signature:", error);
|
|
alert("Erreur lors de l'envoi en signature: " + error.message);
|
|
setShowSendSignatureModal(false);
|
|
} finally {
|
|
setIsSendingSignature(false);
|
|
}
|
|
};
|
|
|
|
const handleChangeStatus = async (newStatus: string) => {
|
|
setIsChangingStatus(true);
|
|
try {
|
|
const response = await fetch(`/api/staff/amendments/${avenant.id}/change-status`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ status: newStatus }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || "Erreur lors du changement de statut");
|
|
}
|
|
|
|
// Fermer le modal et recharger la page
|
|
setShowChangeStatusModal(false);
|
|
router.refresh();
|
|
} catch (error: any) {
|
|
console.error("Erreur changement statut:", error);
|
|
alert("Erreur lors du changement de statut: " + error.message);
|
|
} finally {
|
|
setIsChangingStatus(false);
|
|
}
|
|
};
|
|
|
|
const contract = avenant.cddu_contracts;
|
|
|
|
const formatDate = (dateStr?: string) => {
|
|
if (!dateStr) return "-";
|
|
const [y, m, d] = dateStr.split("-");
|
|
return `${d}/${m}/${y}`;
|
|
};
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
const badges = {
|
|
draft: "bg-slate-100 text-slate-700",
|
|
pending: "bg-orange-100 text-orange-700",
|
|
signed: "bg-green-100 text-green-700",
|
|
cancelled: "bg-red-100 text-red-700",
|
|
};
|
|
const labels = {
|
|
draft: "Brouillon",
|
|
pending: "En attente",
|
|
signed: "Signé",
|
|
cancelled: "Annulé",
|
|
};
|
|
return (
|
|
<span className={`px-3 py-1 text-sm font-medium rounded-full ${badges[status as keyof typeof badges] || badges.draft}`}>
|
|
{labels[status as keyof typeof labels] || status}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const getTypeBadge = (type: string) => {
|
|
const badges = {
|
|
modification: "bg-blue-100 text-blue-700",
|
|
annulation: "bg-red-100 text-red-700",
|
|
};
|
|
const labels = {
|
|
modification: "Modification",
|
|
annulation: "Annulation",
|
|
};
|
|
return (
|
|
<span className={`px-3 py-1 text-sm font-medium rounded-full ${badges[type as keyof typeof badges] || badges.modification}`}>
|
|
{labels[type as keyof typeof labels] || type}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const getElementLabel = (element: string) => {
|
|
const labels: Record<string, string> = {
|
|
objet: "Objet (profession, production)",
|
|
duree: "Durée de l'engagement",
|
|
lieu_horaire: "Lieu et horaires",
|
|
remuneration: "Rémunération",
|
|
};
|
|
return labels[element] || element;
|
|
};
|
|
|
|
const getSignatureStatusBadge = (status?: string) => {
|
|
if (!status || status === 'not_sent') {
|
|
return (
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-slate-100 text-slate-700 rounded-full text-sm font-medium">
|
|
<Clock className="w-4 h-4" />
|
|
Non envoyé
|
|
</span>
|
|
);
|
|
}
|
|
if (status === 'pending_employer') {
|
|
return (
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-orange-100 text-orange-700 rounded-full text-sm font-medium">
|
|
<Clock className="w-4 h-4" />
|
|
En attente employeur
|
|
</span>
|
|
);
|
|
}
|
|
if (status === 'pending_employee') {
|
|
return (
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-blue-100 text-blue-700 rounded-full text-sm font-medium">
|
|
<Clock className="w-4 h-4" />
|
|
En attente salarié
|
|
</span>
|
|
);
|
|
}
|
|
if (status === 'signed') {
|
|
return (
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-green-100 text-green-700 rounded-full text-sm font-medium">
|
|
<Check className="w-4 h-4" />
|
|
Signé
|
|
</span>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-5xl mx-auto space-y-6">
|
|
{/* Header avec bouton retour */}
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={() => router.push("/staff/avenants")}
|
|
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
|
|
>
|
|
<ArrowLeft className="h-5 w-5 text-slate-600" />
|
|
</button>
|
|
<div className="flex-1">
|
|
<h1 className="text-2xl font-bold text-slate-900">
|
|
Avenant {avenant.numero_avenant}
|
|
</h1>
|
|
<p className="text-sm text-slate-600 mt-1">
|
|
Contrat {contract?.contract_number}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{getStatusBadge(avenant.statut)}
|
|
{getTypeBadge(avenant.type_avenant)}
|
|
<button
|
|
onClick={() => setShowChangeStatusModal(true)}
|
|
className="inline-flex items-center gap-2 px-4 py-2 border border-indigo-300 text-indigo-700 rounded-lg hover:bg-indigo-50 transition-colors"
|
|
title="Changer le statut manuellement"
|
|
>
|
|
<Edit3 className="h-4 w-4" />
|
|
Changer statut
|
|
</button>
|
|
<button
|
|
onClick={() => setShowDeleteModal(true)}
|
|
className="inline-flex items-center gap-2 px-4 py-2 border border-red-300 text-red-700 rounded-lg hover:bg-red-50 transition-colors"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
Supprimer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Informations générales */}
|
|
<div className="bg-white rounded-xl border shadow-sm p-6">
|
|
<h2 className="font-semibold text-slate-900 mb-4 flex items-center gap-2">
|
|
<FileText className="h-5 w-5" />
|
|
Informations générales
|
|
</h2>
|
|
<div className="grid grid-cols-2 gap-6">
|
|
<div>
|
|
<label className="text-sm font-medium text-slate-500">Date d'effet</label>
|
|
<div className="mt-1 text-slate-900">{formatDate(avenant.date_effet)}</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-slate-500">Date de signature</label>
|
|
<div className="mt-1 text-slate-900">{formatDate(avenant.date_avenant)}</div>
|
|
</div>
|
|
{avenant.motif_avenant && (
|
|
<div className="col-span-2">
|
|
<label className="text-sm font-medium text-slate-500">Motif</label>
|
|
<div className="mt-1 text-slate-900">{avenant.motif_avenant}</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Informations du contrat */}
|
|
<div className="bg-white rounded-xl border shadow-sm p-6">
|
|
<h2 className="font-semibold text-slate-900 mb-4 flex items-center gap-2">
|
|
<User className="h-5 w-5" />
|
|
Contrat concerné
|
|
</h2>
|
|
<div className="grid grid-cols-2 gap-6">
|
|
<div>
|
|
<label className="text-sm font-medium text-slate-500">Salarié</label>
|
|
<div className="mt-1 text-slate-900">
|
|
{contract?.employee_name}
|
|
{contract?.employee_matricule && (
|
|
<span className="text-slate-500 ml-2">({contract.employee_matricule})</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-slate-500">Organisation</label>
|
|
<div className="mt-1 text-slate-900">{contract?.structure}</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-slate-500">Type de contrat</label>
|
|
<div className="mt-1 text-slate-900">{contract?.type_de_contrat}</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-slate-500">Période</label>
|
|
<div className="mt-1 text-slate-900">
|
|
{formatDate(contract?.start_date)} - {formatDate(contract?.end_date)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Signatures électroniques */}
|
|
<div className="bg-white rounded-xl border shadow-sm p-6">
|
|
<h2 className="font-semibold text-slate-900 mb-4 flex items-center gap-2">
|
|
<Send className="h-5 w-5" />
|
|
Signatures électroniques
|
|
</h2>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
|
|
<div>
|
|
<div className="text-sm font-medium text-slate-900">Statut global</div>
|
|
<div className="text-xs text-slate-600 mt-0.5">État actuel du processus de signature</div>
|
|
</div>
|
|
<div>
|
|
{getSignatureStatusBadge(avenant.signature_status)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="p-4 border rounded-lg">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="text-sm font-medium text-slate-900">Employeur</div>
|
|
{avenant.signature_status === 'pending_employee' || avenant.signature_status === 'signed' ? (
|
|
<Check className="w-5 h-5 text-green-600" strokeWidth={3} />
|
|
) : avenant.signature_status === 'pending_employer' ? (
|
|
<X className="w-5 h-5 text-red-600" strokeWidth={3} />
|
|
) : (
|
|
<Clock className="w-5 h-5 text-slate-400" />
|
|
)}
|
|
</div>
|
|
{avenant.last_employer_notification_at && (
|
|
<div className="text-xs text-slate-600">
|
|
Notifié le {new Date(avenant.last_employer_notification_at).toLocaleDateString('fr-FR')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="p-4 border rounded-lg">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="text-sm font-medium text-slate-900">Salarié</div>
|
|
{avenant.signature_status === 'signed' ? (
|
|
<Check className="w-5 h-5 text-green-600" strokeWidth={3} />
|
|
) : avenant.signature_status === 'pending_employee' || avenant.signature_status === 'pending_employer' ? (
|
|
<X className="w-5 h-5 text-red-600" strokeWidth={3} />
|
|
) : (
|
|
<Clock className="w-5 h-5 text-slate-400" />
|
|
)}
|
|
</div>
|
|
{avenant.last_employee_notification_at && (
|
|
<div className="text-xs text-slate-600">
|
|
Notifié le {new Date(avenant.last_employee_notification_at).toLocaleDateString('fr-FR')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Éléments modifiés */}
|
|
<div className="bg-white rounded-xl border shadow-sm p-6">
|
|
<h2 className="font-semibold text-slate-900 mb-4">Éléments modifiés</h2>
|
|
<div className="space-y-4">
|
|
{avenant.elements_avenantes?.map((element: string) => (
|
|
<div key={element} className="border-l-4 border-indigo-500 pl-4 py-2">
|
|
<h3 className="font-medium text-slate-900 mb-2">{getElementLabel(element)}</h3>
|
|
|
|
{element === "objet" && avenant.objet_data && (
|
|
<div className="text-sm text-slate-600 space-y-1">
|
|
{avenant.objet_data.profession_label && (
|
|
<div>Profession : {avenant.objet_data.profession_label}</div>
|
|
)}
|
|
{avenant.objet_data.production_name && (
|
|
<div>Production : {avenant.objet_data.production_name}</div>
|
|
)}
|
|
{avenant.objet_data.production_numero_objet && (
|
|
<div>N° objet : {avenant.objet_data.production_numero_objet}</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{element === "duree" && avenant.duree_data && (
|
|
<div className="text-sm text-slate-600 space-y-1">
|
|
{avenant.duree_data.date_debut && (
|
|
<div>Nouvelle date de début : {formatDate(avenant.duree_data.date_debut)}</div>
|
|
)}
|
|
{avenant.duree_data.date_fin && (
|
|
<div>Nouvelle date de fin : {formatDate(avenant.duree_data.date_fin)}</div>
|
|
)}
|
|
|
|
{/* Pour les artistes */}
|
|
{avenant.duree_data.nb_representations !== undefined && avenant.duree_data.nb_representations > 0 && (
|
|
<div>Représentations : {avenant.duree_data.nb_representations}</div>
|
|
)}
|
|
{avenant.duree_data.nb_repetitions !== undefined && avenant.duree_data.nb_repetitions > 0 && (
|
|
<div>Répétitions : {avenant.duree_data.nb_repetitions}</div>
|
|
)}
|
|
|
|
{/* Pour les techniciens */}
|
|
{avenant.duree_data.nb_heures !== undefined && avenant.duree_data.nb_heures > 0 && (
|
|
<div>Nombre d'heures : {avenant.duree_data.nb_heures}h</div>
|
|
)}
|
|
|
|
{/* Dates de travail */}
|
|
{Array.isArray(avenant.duree_data.dates_representations) && avenant.duree_data.dates_representations.length > 0 && (
|
|
<div>
|
|
<div className="font-medium mt-2">Dates de représentations :</div>
|
|
<div className="flex flex-wrap gap-2 mt-1">
|
|
{avenant.duree_data.dates_representations.map((d: any, idx: number) => (
|
|
<div key={idx} className="inline-flex items-center gap-2 px-2 py-1 bg-blue-100 rounded text-xs">
|
|
<span className="font-medium">{formatDate(d.date)}</span>
|
|
{d.quantity && d.quantity > 0 && (
|
|
<span className="text-blue-700">({d.quantity})</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{Array.isArray(avenant.duree_data.dates_repetitions) && avenant.duree_data.dates_repetitions.length > 0 && (
|
|
<div>
|
|
<div className="font-medium mt-2">Dates de répétitions :</div>
|
|
<div className="flex flex-wrap gap-2 mt-1">
|
|
{avenant.duree_data.dates_repetitions.map((d: any, idx: number) => (
|
|
<div key={idx} className="inline-flex items-center gap-2 px-2 py-1 bg-purple-100 rounded text-xs">
|
|
<span className="font-medium">{formatDate(d.date)}</span>
|
|
{d.quantity && d.quantity > 0 && (
|
|
<span className="text-purple-700">({d.quantity})</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Jours de travail (techniciens) */}
|
|
{avenant.duree_data.jours_travail && (
|
|
<div>
|
|
<div className="font-medium mt-2">Jours de travail :</div>
|
|
{Array.isArray(avenant.duree_data.jours_travail) ? (
|
|
<div className="flex flex-wrap gap-2 mt-1">
|
|
{avenant.duree_data.jours_travail.map((d: any, idx: number) => (
|
|
<div key={idx} className="inline-flex items-center gap-2 px-2 py-1 bg-slate-100 rounded text-xs">
|
|
<span className="font-medium">{formatDate(d.date)}</span>
|
|
{d.quantity && d.quantity > 0 && (
|
|
<span className="text-slate-600">({d.quantity}h)</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-xs">{avenant.duree_data.jours_travail}</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{element === "lieu_horaire" && avenant.lieu_horaire_data && (
|
|
<div className="text-sm text-slate-600 space-y-1">
|
|
{avenant.lieu_horaire_data.lieu && (
|
|
<div>Lieu : {avenant.lieu_horaire_data.lieu}</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{element === "remuneration" && avenant.remuneration_data && (
|
|
<div className="text-sm text-slate-600 space-y-1">
|
|
{avenant.remuneration_data.gross_pay && (
|
|
<div>Salaire brut : {avenant.remuneration_data.gross_pay} €</div>
|
|
)}
|
|
{avenant.remuneration_data.type_salaire && (
|
|
<div>Type : {avenant.remuneration_data.type_salaire}</div>
|
|
)}
|
|
{avenant.remuneration_data.precisions_salaire && (
|
|
<div>Précisions : {avenant.remuneration_data.precisions_salaire}</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* PDF */}
|
|
<div className="bg-white rounded-xl border shadow-sm p-6">
|
|
<h2 className="font-semibold text-slate-900 mb-4">Document signé</h2>
|
|
<div className="flex items-start gap-4 p-4 bg-slate-50 rounded-lg border">
|
|
<div className="flex-shrink-0 w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center">
|
|
<FileText className="h-6 w-6 text-indigo-600" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-medium text-slate-900">
|
|
{avenant.pdf_s3_key ? `Avenant ${avenant.numero_avenant}.pdf` : "PDF non généré"}
|
|
</div>
|
|
<div className="text-sm text-slate-500 mt-1">
|
|
{avenant.pdf_s3_key ? "Document stocké sur AWS S3" : "Aucun document disponible"}
|
|
</div>
|
|
{avenant.pdf_s3_key && (
|
|
<div className="text-xs text-slate-400 mt-1 font-mono truncate">
|
|
{avenant.pdf_s3_key}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex-shrink-0 flex gap-2">
|
|
{loadingPdf ? (
|
|
<div className="px-4 py-2 bg-slate-100 text-slate-500 rounded-lg">
|
|
Chargement...
|
|
</div>
|
|
) : pdfUrl ? (
|
|
<>
|
|
<a
|
|
href={pdfUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
Télécharger
|
|
</a>
|
|
<button
|
|
onClick={handleRegeneratePdf}
|
|
disabled={isRegeneratingPdf}
|
|
className="inline-flex items-center gap-2 px-4 py-2 border border-indigo-300 text-indigo-700 rounded-lg hover:bg-indigo-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<RefreshCw className={`h-4 w-4 ${isRegeneratingPdf ? "animate-spin" : ""}`} />
|
|
{isRegeneratingPdf ? "Génération..." : "Regénérer"}
|
|
</button>
|
|
</>
|
|
) : (
|
|
<button
|
|
onClick={handleRegeneratePdf}
|
|
disabled={isRegeneratingPdf}
|
|
className="inline-flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<RefreshCw className={`h-4 w-4 ${isRegeneratingPdf ? "animate-spin" : ""}`} />
|
|
{isRegeneratingPdf ? "Génération..." : "Générer le PDF"}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="bg-white rounded-xl border shadow-sm p-6">
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={() => router.push("/staff/avenants")}
|
|
className="px-6 py-3 text-slate-700 hover:bg-slate-100 rounded-lg transition-colors font-medium"
|
|
>
|
|
Retour à la liste
|
|
</button>
|
|
{avenant.statut === "draft" && (
|
|
<>
|
|
<button
|
|
onClick={() => {
|
|
if (!avenant.pdf_s3_key) {
|
|
alert("Vous devez d'abord générer le PDF avant de l'envoyer en signature.");
|
|
return;
|
|
}
|
|
setShowSendSignatureModal(true);
|
|
}}
|
|
disabled={isSendingSignature}
|
|
className="inline-flex items-center gap-2 px-6 py-3 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<Send className="h-4 w-4" />
|
|
Envoyer pour signature
|
|
</button>
|
|
<button className="px-6 py-3 border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 transition-colors font-medium">
|
|
Modifier
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modale de suppression */}
|
|
<DeleteAvenantModal
|
|
isOpen={showDeleteModal}
|
|
onClose={() => setShowDeleteModal(false)}
|
|
onConfirm={handleDelete}
|
|
numeroAvenant={avenant.numero_avenant}
|
|
isDeleting={isDeleting}
|
|
/>
|
|
|
|
{/* Modale d'envoi en signature */}
|
|
<SendSignatureModal
|
|
isOpen={showSendSignatureModal}
|
|
onClose={() => {
|
|
setShowSendSignatureModal(false);
|
|
setSendSignatureSuccess(false);
|
|
}}
|
|
onConfirm={handleSendForSignature}
|
|
isSending={isSendingSignature}
|
|
success={sendSignatureSuccess}
|
|
avenantData={{
|
|
numero_avenant: avenant.numero_avenant,
|
|
contractReference: contract?.contract_number || "N/A",
|
|
employeeName: contract?.salaries ? `${contract.salaries.prenom} ${contract.salaries.nom}` : "N/A",
|
|
employerEmail: contract?.organizations?.organization_details?.email_signature || "N/A",
|
|
employeeEmail: contract?.salaries?.adresse_mail || "N/A",
|
|
}}
|
|
/>
|
|
|
|
{/* Modale de changement de statut */}
|
|
<ChangeStatusModal
|
|
isOpen={showChangeStatusModal}
|
|
onClose={() => setShowChangeStatusModal(false)}
|
|
onConfirm={handleChangeStatus}
|
|
currentStatus={avenant.statut}
|
|
isChanging={isChangingStatus}
|
|
numeroAvenant={avenant.numero_avenant}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|