✨ 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'
611 lines
24 KiB
TypeScript
611 lines
24 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useMemo } from "react";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import { Search, X, Loader2, CheckCircle2, Calendar } from "lucide-react";
|
|
import {
|
|
Amendment,
|
|
AmendmentElementType,
|
|
ContractSearchResult,
|
|
OriginalContractData
|
|
} from "@/types/amendments";
|
|
import AmendmentObjetForm from "@/components/staff/amendments/AmendmentObjetForm";
|
|
import AmendmentDureeForm from "@/components/staff/amendments/AmendmentDureeForm";
|
|
import AmendmentRemunerationForm from "@/components/staff/amendments/AmendmentRemunerationForm";
|
|
|
|
interface NouvelAvenantModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onAmendmentCreated: (amendment: Amendment) => void;
|
|
}
|
|
|
|
export default function NouvelAvenantModal({
|
|
isOpen,
|
|
onClose,
|
|
onAmendmentCreated,
|
|
}: NouvelAvenantModalProps) {
|
|
// États
|
|
const [step, setStep] = useState<"search" | "form">("search");
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [isSearching, setIsSearching] = useState(false);
|
|
const [searchResults, setSearchResults] = useState<ContractSearchResult[]>([]);
|
|
const [selectedContract, setSelectedContract] = useState<OriginalContractData | null>(null);
|
|
|
|
// Données du formulaire
|
|
const [dateEffet, setDateEffet] = useState("");
|
|
const [dateSignature, setDateSignature] = useState("");
|
|
const [typeAvenant, setTypeAvenant] = useState<"modification" | "annulation">("modification");
|
|
const [motifAvenant, setMotifAvenant] = useState("");
|
|
const [selectedElements, setSelectedElements] = useState<AmendmentElementType[]>([]);
|
|
|
|
// Données spécifiques selon les éléments
|
|
const [objetData, setObjetData] = useState<any>({});
|
|
const [dureeData, setDureeData] = useState<any>({});
|
|
const [remunerationData, setRemunerationData] = useState<any>({});
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
// PDF generation
|
|
const [isGeneratingPdf, setIsGeneratingPdf] = useState(false);
|
|
const [pdfPresignedUrl, setPdfPresignedUrl] = useState<string | null>(null);
|
|
|
|
// État pour bloquer la fermeture du Dialog quand un sous-modal est ouvert
|
|
const [hasOpenSubModal, setHasOpenSubModal] = useState(false);
|
|
const [isSubModalOpen, setIsSubModalOpen] = useState(false);
|
|
|
|
// Synchroniser hasOpenSubModal avec isSubModalOpen
|
|
useEffect(() => {
|
|
setHasOpenSubModal(isSubModalOpen);
|
|
}, [isSubModalOpen]);
|
|
|
|
// Reset au changement d'état du modal
|
|
useEffect(() => {
|
|
if (!isOpen) {
|
|
setTimeout(() => {
|
|
setStep("search");
|
|
setSearchTerm("");
|
|
setSearchResults([]);
|
|
setSelectedContract(null);
|
|
setDateEffet("");
|
|
setDateSignature("");
|
|
setSelectedElements([]);
|
|
setObjetData({});
|
|
setDureeData({});
|
|
setRemunerationData({});
|
|
setPdfPresignedUrl(null);
|
|
}, 300);
|
|
}
|
|
}, [isOpen]);
|
|
|
|
// Recherche de contrats (debounced)
|
|
useEffect(() => {
|
|
if (searchTerm.length < 2) {
|
|
setSearchResults([]);
|
|
return;
|
|
}
|
|
|
|
const timer = setTimeout(async () => {
|
|
setIsSearching(true);
|
|
try {
|
|
const response = await fetch(
|
|
`/api/staff/contracts/search?q=${encodeURIComponent(searchTerm)}`
|
|
);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
// L'API retourne { rows: [...], count: ... }
|
|
setSearchResults(data.rows || data.contracts || []);
|
|
}
|
|
} catch (error) {
|
|
console.error("Erreur recherche contrats:", error);
|
|
} finally {
|
|
setIsSearching(false);
|
|
}
|
|
}, 400);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [searchTerm]);
|
|
|
|
// Sélection d'un contrat
|
|
const handleSelectContract = async (contractId: string) => {
|
|
try {
|
|
const response = await fetch(`/api/staff/contracts/${contractId}`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
const contract = data.contract || data;
|
|
|
|
console.log("Données du contrat reçues:", contract);
|
|
|
|
// Mapper les données du contrat vers OriginalContractData
|
|
const contractData: OriginalContractData = {
|
|
id: contract.id,
|
|
contract_number: contract.contract_number,
|
|
employee_id: contract.employee_id,
|
|
employee_name: contract.employee_name,
|
|
employee_matricule: contract.employee_matricule,
|
|
org_id: contract.org_id,
|
|
organization_name: contract.structure,
|
|
type_de_contrat: contract.type_de_contrat,
|
|
categorie_pro: contract.categorie_pro,
|
|
profession: contract.profession,
|
|
production_name: contract.production_name,
|
|
numero_objet: contract.n_objet,
|
|
start_date: contract.start_date,
|
|
end_date: contract.end_date,
|
|
nb_representations: contract.cachets_representations || contract.nb_representations,
|
|
nb_repetitions: contract.services_repetitions || contract.nb_services_repetition,
|
|
nb_heures: contract.nombre_d_heures || contract.heures_total,
|
|
dates_representations: contract.jours_representations || contract.dates_representations,
|
|
dates_repetitions: contract.jours_repetitions || contract.dates_repetitions,
|
|
jours_travail: contract.jours_travail_non_artiste || contract.jours_travail,
|
|
lieu_travail: contract.lieu_travail,
|
|
gross_pay: contract.gross_pay || contract.brut,
|
|
precisions_salaire: contract.precisions_salaire,
|
|
type_salaire: contract.type_salaire,
|
|
};
|
|
|
|
setSelectedContract(contractData);
|
|
|
|
// Pré-remplir les données avec les valeurs du contrat
|
|
setObjetData({
|
|
profession_code: contractData.profession?.split(" - ")[0] || "",
|
|
profession_label: contractData.profession?.split(" - ")[1] || contractData.profession || "",
|
|
production_name: contractData.production_name || "",
|
|
production_numero_objet: contractData.numero_objet || "",
|
|
});
|
|
|
|
setDureeData({
|
|
date_debut: contractData.start_date || "",
|
|
date_fin: contractData.end_date || "",
|
|
nb_representations: contractData.nb_representations || "",
|
|
nb_repetitions: contractData.nb_repetitions || "",
|
|
nb_heures: contractData.nb_heures || "",
|
|
dates_representations: contractData.dates_representations || "",
|
|
dates_repetitions: contractData.dates_repetitions || "",
|
|
jours_travail: contractData.jours_travail || "",
|
|
});
|
|
|
|
setRemunerationData({
|
|
gross_pay: contractData.gross_pay || "",
|
|
precisions_salaire: contractData.precisions_salaire || "",
|
|
type_salaire: contractData.type_salaire || "Brut",
|
|
});
|
|
|
|
setStep("form");
|
|
}
|
|
} catch (error) {
|
|
console.error("Erreur chargement contrat:", error);
|
|
}
|
|
};
|
|
|
|
// Toggle élément à avenanter
|
|
const toggleElement = (element: AmendmentElementType) => {
|
|
if (selectedElements.includes(element)) {
|
|
setSelectedElements(selectedElements.filter((e) => e !== element));
|
|
} else {
|
|
setSelectedElements([...selectedElements, element]);
|
|
}
|
|
};
|
|
|
|
// Validation
|
|
const canSubmit = useMemo(() => {
|
|
if (!selectedContract || !dateEffet || selectedElements.length === 0) return false;
|
|
return true;
|
|
}, [selectedContract, dateEffet, selectedElements]);
|
|
|
|
// Génération du PDF
|
|
const handleGeneratePdf = async () => {
|
|
if (!canSubmit || !selectedContract) return;
|
|
|
|
setIsGeneratingPdf(true);
|
|
try {
|
|
const amendmentData = {
|
|
contract_id: selectedContract.id,
|
|
date_effet: dateEffet,
|
|
date_signature: dateSignature || undefined,
|
|
elements: selectedElements,
|
|
objet_data: selectedElements.includes("objet") ? objetData : undefined,
|
|
duree_data: selectedElements.includes("duree") ? dureeData : undefined,
|
|
remuneration_data: selectedElements.includes("remuneration") ? remunerationData : undefined,
|
|
};
|
|
|
|
const response = await fetch("/api/staff/amendments/generate-pdf", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
contractId: selectedContract.id,
|
|
amendmentData,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || "Erreur lors de la génération du PDF");
|
|
}
|
|
|
|
const data = await response.json();
|
|
setPdfPresignedUrl(data.presignedUrl);
|
|
} catch (error: any) {
|
|
console.error("Erreur génération PDF:", error);
|
|
alert("Erreur lors de la génération du PDF: " + error.message);
|
|
} finally {
|
|
setIsGeneratingPdf(false);
|
|
}
|
|
};
|
|
|
|
// Soumission
|
|
const handleSubmit = async () => {
|
|
if (!canSubmit) return;
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
// Préparer les données de l'avenant
|
|
const amendmentData = {
|
|
contract_id: selectedContract!.id,
|
|
date_effet: dateEffet,
|
|
date_signature: dateSignature || dateEffet,
|
|
type_avenant: typeAvenant,
|
|
motif_avenant: motifAvenant,
|
|
elements: selectedElements,
|
|
objet_data: selectedElements.includes("objet") ? objetData : undefined,
|
|
duree_data: selectedElements.includes("duree") ? dureeData : undefined,
|
|
lieu_horaire_data: selectedElements.includes("lieu_horaire") ? {} : undefined,
|
|
remuneration_data: selectedElements.includes("remuneration") ? remunerationData : undefined,
|
|
};
|
|
|
|
// Appeler l'API pour créer l'avenant et mettre à jour le contrat
|
|
const response = await fetch('/api/staff/amendments/create', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(amendmentData),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || "Erreur lors de la création de l'avenant");
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Créer l'objet Amendment pour l'affichage
|
|
const amendment: Amendment = {
|
|
id: data.avenant.id,
|
|
contract_id: selectedContract!.id,
|
|
contract_number: selectedContract!.contract_number,
|
|
employee_name: selectedContract!.employee_name,
|
|
organization_name: selectedContract!.organization_name,
|
|
date_effet: dateEffet,
|
|
date_signature: dateSignature || undefined,
|
|
status: "draft",
|
|
elements: selectedElements,
|
|
objet_data: selectedElements.includes("objet") ? objetData : undefined,
|
|
duree_data: selectedElements.includes("duree") ? dureeData : undefined,
|
|
remuneration_data: selectedElements.includes("remuneration") ? remunerationData : undefined,
|
|
created_at: data.avenant.created_at,
|
|
};
|
|
|
|
onAmendmentCreated(amendment);
|
|
alert(`Avenant ${data.avenant.numero_avenant} créé avec succès !`);
|
|
onClose();
|
|
} catch (error) {
|
|
console.error("Erreur création avenant:", error);
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateStr?: string) => {
|
|
if (!dateStr) return "-";
|
|
const [y, m, d] = dateStr.split("-");
|
|
return `${d}/${m}/${y}`;
|
|
};
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose} modal={!hasOpenSubModal}>
|
|
<DialogContent
|
|
className="max-w-4xl max-h-[90vh] overflow-y-auto"
|
|
onPointerDownOutside={(e) => {
|
|
if (hasOpenSubModal) {
|
|
e.preventDefault();
|
|
}
|
|
}}
|
|
onInteractOutside={(e) => {
|
|
if (hasOpenSubModal) {
|
|
e.preventDefault();
|
|
}
|
|
}}
|
|
>
|
|
<DialogHeader>
|
|
<DialogTitle className="text-xl font-semibold">
|
|
{step === "search" ? "Rechercher un contrat" : "Nouvel avenant"}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
{/* Étape 1: Recherche du contrat */}
|
|
{step === "search" && (
|
|
<div className="space-y-4">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Rechercher par n° contrat, salarié, organisation, référence..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-3 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
{isSearching && (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="h-6 w-6 animate-spin text-indigo-600" />
|
|
</div>
|
|
)}
|
|
|
|
{!isSearching && searchResults.length > 0 && (
|
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
|
{searchResults.map((contract) => (
|
|
<button
|
|
key={contract.id}
|
|
onClick={() => handleSelectContract(contract.id)}
|
|
className="w-full p-4 border rounded-lg hover:bg-slate-50 transition-colors text-left"
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="font-medium text-slate-900">
|
|
{contract.contract_number}
|
|
</div>
|
|
<div className="text-sm text-slate-600 mt-1">
|
|
{contract.employee_name}
|
|
{contract.employee_matricule && ` (${contract.employee_matricule})`}
|
|
</div>
|
|
<div className="text-sm text-slate-600">
|
|
{contract.organization_name || contract.structure}
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-xs text-slate-500">
|
|
{formatDate(contract.start_date)} - {formatDate(contract.end_date)}
|
|
</div>
|
|
{contract.profession && (
|
|
<div className="text-xs text-slate-500 mt-1">{contract.profession}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{!isSearching && searchTerm.length >= 2 && searchResults.length === 0 && (
|
|
<div className="py-8 text-center text-slate-600">
|
|
Aucun contrat trouvé pour "{searchTerm}"
|
|
</div>
|
|
)}
|
|
|
|
{searchTerm.length < 2 && (
|
|
<div className="py-8 text-center text-slate-500 text-sm">
|
|
Tapez au moins 2 caractères pour rechercher
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Étape 2: Formulaire de l'avenant */}
|
|
{step === "form" && selectedContract && (
|
|
<div className="space-y-6">
|
|
{/* Infos du contrat */}
|
|
<div className="bg-slate-50 rounded-lg p-4 border">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<div className="font-medium text-slate-900">
|
|
Contrat {selectedContract.contract_number}
|
|
</div>
|
|
<div className="text-sm text-slate-600 mt-1">
|
|
{selectedContract.employee_name} • {selectedContract.organization_name}
|
|
</div>
|
|
<div className="text-xs text-slate-500 mt-1">
|
|
{formatDate(selectedContract.start_date)} - {formatDate(selectedContract.end_date)}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setStep("search")}
|
|
className="text-slate-400 hover:text-slate-600"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dates de l'avenant */}
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-slate-900 mb-3">Dates de l'avenant</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-2">
|
|
Date d'effet de l'avenant <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="relative">
|
|
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
|
|
<input
|
|
type="date"
|
|
value={dateEffet}
|
|
onChange={(e) => setDateEffet(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-2">
|
|
Date de signature
|
|
</label>
|
|
<div className="relative">
|
|
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
|
|
<input
|
|
type="date"
|
|
value={dateSignature}
|
|
onChange={(e) => setDateSignature(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Type et Motif de l'avenant */}
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-slate-900 mb-3">Informations complémentaires</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-2">
|
|
Type d'avenant <span className="text-red-500">*</span>
|
|
</label>
|
|
<select
|
|
value={typeAvenant}
|
|
onChange={(e) => setTypeAvenant(e.target.value as "modification" | "annulation")}
|
|
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
>
|
|
<option value="modification">Modification</option>
|
|
<option value="annulation">Annulation</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-2">
|
|
Motif de l'avenant
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={motifAvenant}
|
|
onChange={(e) => setMotifAvenant(e.target.value)}
|
|
placeholder="Ex: Changement de dates, modification du salaire..."
|
|
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Éléments à avenanter */}
|
|
<div>
|
|
<label className="block text-xs font-medium text-slate-700 mb-3">
|
|
Éléments à avenanter <span className="text-red-500">*</span>
|
|
</label>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{[
|
|
{ value: "objet" as const, label: "Objet (profession, production)" },
|
|
{ value: "duree" as const, label: "Durée de l'engagement" },
|
|
{ value: "lieu_horaire" as const, label: "Lieu et horaires" },
|
|
{ value: "remuneration" as const, label: "Rémunération" },
|
|
].map((element) => (
|
|
<button
|
|
key={element.value}
|
|
onClick={() => toggleElement(element.value)}
|
|
className={`p-3 border rounded-lg text-left transition-all ${
|
|
selectedElements.includes(element.value)
|
|
? "bg-indigo-50 border-indigo-500 text-indigo-700"
|
|
: "bg-white border-slate-200 text-slate-700 hover:bg-slate-50"
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className={`w-5 h-5 rounded border flex items-center justify-center ${
|
|
selectedElements.includes(element.value)
|
|
? "bg-indigo-600 border-indigo-600"
|
|
: "border-slate-300"
|
|
}`}
|
|
>
|
|
{selectedElements.includes(element.value) && (
|
|
<CheckCircle2 className="h-3 w-3 text-white" />
|
|
)}
|
|
</div>
|
|
<span className="text-sm font-medium">{element.label}</span>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Formulaires conditionnels */}
|
|
{selectedElements.includes("objet") && (
|
|
<AmendmentObjetForm
|
|
originalData={selectedContract}
|
|
data={objetData}
|
|
onChange={setObjetData}
|
|
/>
|
|
)}
|
|
|
|
{selectedElements.includes("duree") && (
|
|
<AmendmentDureeForm
|
|
originalData={selectedContract}
|
|
data={dureeData}
|
|
onChange={setDureeData}
|
|
/>
|
|
)}
|
|
|
|
{selectedElements.includes("remuneration") && (
|
|
<AmendmentRemunerationForm
|
|
originalData={selectedContract}
|
|
data={remunerationData}
|
|
onChange={setRemunerationData}
|
|
/>
|
|
)}
|
|
|
|
{/* TODO: AmendmentLieuHoraireForm pour l'instant pas implémenté */}
|
|
|
|
{/* Lien PDF si généré */}
|
|
{pdfPresignedUrl && (
|
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
|
|
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-green-900">PDF généré avec succès</div>
|
|
<div className="text-sm text-green-700">Le PDF de l'avenant est prêt</div>
|
|
</div>
|
|
</div>
|
|
<a
|
|
href={pdfPresignedUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm font-medium"
|
|
>
|
|
Voir le PDF
|
|
</a>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center justify-between pt-4 border-t">
|
|
<button
|
|
onClick={handleGeneratePdf}
|
|
disabled={!canSubmit || isGeneratingPdf}
|
|
className="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
{isGeneratingPdf && <Loader2 className="h-4 w-4 animate-spin" />}
|
|
Créer le PDF
|
|
</button>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={onClose}
|
|
className="px-4 py-2 text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
|
|
>
|
|
Annuler
|
|
</button>
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={!canSubmit || isSubmitting}
|
|
className="px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
{isSubmitting && <Loader2 className="h-4 w-4 animate-spin" />}
|
|
Créer l'avenant
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|