feat: Ajouter page de modification d'avenants
- Nouvelle route /staff/avenants/[id]/modifier - Composant ModifierAvenantPageClient qui pré-remplit les données - API PATCH /api/staff/amendments/[id] pour mise à jour - Restriction: seuls les avenants 'draft' sont modifiables - Bouton 'Modifier' fonctionnel sur page détail avenant - Pré-remplissage des formulaires avec données existantes - Régénération PDF possible après modification
This commit is contained in:
parent
1d9145a0b2
commit
49284d9a59
4 changed files with 613 additions and 1 deletions
88
app/(app)/staff/avenants/[id]/modifier/page.tsx
Normal file
88
app/(app)/staff/avenants/[id]/modifier/page.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { createSbServer } from "@/lib/supabaseServer";
|
||||
import { redirect, notFound } from "next/navigation";
|
||||
import NextDynamic from "next/dynamic";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const ModifierAvenantPageClient = NextDynamic<any>(
|
||||
() => import("@/components/staff/ModifierAvenantPageClient"),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default async function ModifierAvenantPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const sb = createSbServer();
|
||||
const { data: { user } } = await sb.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const { data: me } = await sb
|
||||
.from("staff_users")
|
||||
.select("is_staff")
|
||||
.eq("user_id", user.id)
|
||||
.maybeSingle();
|
||||
|
||||
const isStaff = !!me?.is_staff;
|
||||
if (!isStaff) {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
// Récupérer l'avenant avec les informations du contrat
|
||||
const { data: avenant, error } = await sb
|
||||
.from("avenants")
|
||||
.select(`
|
||||
*,
|
||||
cddu_contracts (
|
||||
*,
|
||||
organizations (
|
||||
id,
|
||||
name
|
||||
),
|
||||
salaries (
|
||||
id,
|
||||
prenom,
|
||||
nom,
|
||||
adresse_mail
|
||||
)
|
||||
)
|
||||
`)
|
||||
.eq("id", id)
|
||||
.single();
|
||||
|
||||
if (error || !avenant) {
|
||||
console.error("Erreur ou avenant non trouvé:", error);
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Vérifier que l'avenant est modifiable (seulement les brouillons)
|
||||
if (avenant.statut !== "draft") {
|
||||
return (
|
||||
<main className="p-6">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h1 className="text-lg font-semibold text-red-600">Modification impossible</h1>
|
||||
<p className="text-sm text-slate-600 mt-2">
|
||||
Seuls les avenants en brouillon peuvent être modifiés.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => redirect(`/staff/avenants/${id}`)}
|
||||
className="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
|
||||
>
|
||||
Retour à l'avenant
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="p-6">
|
||||
<ModifierAvenantPageClient avenant={avenant} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,6 +5,106 @@ import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
|||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const supabase = createRouteHandlerClient({ cookies });
|
||||
|
||||
// Vérifier l'authentification
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
if (authError || !user) {
|
||||
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur est staff
|
||||
const { data: staffUser } = await supabase
|
||||
.from("staff_users")
|
||||
.select("is_staff")
|
||||
.eq("user_id", user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (!staffUser?.is_staff) {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Récupérer l'avenant
|
||||
const { data: avenant, error: fetchError } = await supabase
|
||||
.from("avenants")
|
||||
.select("id, statut")
|
||||
.eq("id", id)
|
||||
.single();
|
||||
|
||||
if (fetchError || !avenant) {
|
||||
return NextResponse.json({ error: "Avenant non trouvé" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Vérifier que l'avenant est en brouillon
|
||||
if (avenant.statut !== "draft") {
|
||||
return NextResponse.json(
|
||||
{ error: "Seuls les avenants en brouillon peuvent être modifiés" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Récupérer les données de la requête
|
||||
const body = await request.json();
|
||||
const {
|
||||
date_effet,
|
||||
date_avenant,
|
||||
type_avenant,
|
||||
motif_avenant,
|
||||
elements_avenantes,
|
||||
objet_data,
|
||||
duree_data,
|
||||
lieu_horaire_data,
|
||||
remuneration_data,
|
||||
pdf_s3_key,
|
||||
} = body;
|
||||
|
||||
// Mettre à jour l'avenant
|
||||
const { data: updatedAvenant, error: updateError } = await supabase
|
||||
.from("avenants")
|
||||
.update({
|
||||
date_effet,
|
||||
date_avenant,
|
||||
type_avenant,
|
||||
motif_avenant,
|
||||
elements_avenantes,
|
||||
objet_data,
|
||||
duree_data,
|
||||
lieu_horaire_data,
|
||||
remuneration_data,
|
||||
pdf_s3_key,
|
||||
})
|
||||
.eq("id", id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (updateError) {
|
||||
console.error("Erreur mise à jour avenant:", updateError);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la mise à jour de l'avenant" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
avenant: updatedAvenant,
|
||||
message: "Avenant modifié avec succès",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Erreur API patch amendment:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur serveur" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
|
|
|
|||
421
components/staff/ModifierAvenantPageClient.tsx
Normal file
421
components/staff/ModifierAvenantPageClient.tsx
Normal file
|
|
@ -0,0 +1,421 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Loader2, CheckCircle2, Calendar, ArrowLeft, FileText } from "lucide-react";
|
||||
import {
|
||||
AmendmentElementType,
|
||||
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 ModifierAvenantPageClientProps {
|
||||
avenant: any;
|
||||
}
|
||||
|
||||
export default function ModifierAvenantPageClient({ avenant }: ModifierAvenantPageClientProps) {
|
||||
const router = useRouter();
|
||||
const contract = avenant.cddu_contracts;
|
||||
|
||||
// Mapper les données du contrat vers OriginalContractData
|
||||
const selectedContract: 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,
|
||||
};
|
||||
|
||||
// Données du formulaire - Pré-remplir avec les données de l'avenant
|
||||
const [dateEffet, setDateEffet] = useState(avenant.date_effet || "");
|
||||
const [dateSignature, setDateSignature] = useState(avenant.date_avenant || "");
|
||||
const [typeAvenant, setTypeAvenant] = useState<"modification" | "annulation">(avenant.type_avenant || "modification");
|
||||
const [motifAvenant, setMotifAvenant] = useState(avenant.motif_avenant || "");
|
||||
const [selectedElements, setSelectedElements] = useState<AmendmentElementType[]>(avenant.elements_avenantes || []);
|
||||
|
||||
// Données spécifiques selon les éléments - Pré-remplir
|
||||
const [objetData, setObjetData] = useState<any>(avenant.objet_data || {});
|
||||
const [dureeData, setDureeData] = useState<any>(avenant.duree_data || {});
|
||||
const [remunerationData, setRemunerationData] = useState<any>(avenant.remuneration_data || {});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isGeneratingPdf, setIsGeneratingPdf] = useState(false);
|
||||
const [pdfPresignedUrl, setPdfPresignedUrl] = useState<string | null>(null);
|
||||
const [pdfS3Key, setPdfS3Key] = useState<string | null>(null);
|
||||
|
||||
// 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 (typeAvenant === "annulation") {
|
||||
return !!dateEffet;
|
||||
}
|
||||
if (!dateEffet || selectedElements.length === 0) return false;
|
||||
return true;
|
||||
}, [dateEffet, selectedElements, typeAvenant]);
|
||||
|
||||
// Génération du PDF
|
||||
const handleGeneratePdf = async () => {
|
||||
if (!canSubmit) return;
|
||||
|
||||
setIsGeneratingPdf(true);
|
||||
try {
|
||||
const amendmentData = {
|
||||
contract_id: selectedContract.id,
|
||||
date_effet: dateEffet,
|
||||
date_signature: dateSignature || undefined,
|
||||
type_avenant: typeAvenant,
|
||||
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);
|
||||
setPdfS3Key(data.s3Key);
|
||||
} 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 - Mise à jour de l'avenant
|
||||
const handleSubmit = async () => {
|
||||
if (!canSubmit) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const updateData = {
|
||||
date_effet: dateEffet,
|
||||
date_avenant: dateSignature || undefined,
|
||||
type_avenant: typeAvenant,
|
||||
motif_avenant: motifAvenant || undefined,
|
||||
elements_avenantes: 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,
|
||||
pdf_s3_key: pdfS3Key || avenant.pdf_s3_key || undefined,
|
||||
};
|
||||
|
||||
// Appeler l'API pour mettre à jour l'avenant
|
||||
const response = await fetch(`/api/staff/amendments/${avenant.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updateData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || "Erreur lors de la mise à jour de l'avenant");
|
||||
}
|
||||
|
||||
alert("Avenant modifié avec succès !");
|
||||
router.push(`/staff/avenants/${avenant.id}`);
|
||||
} catch (error: any) {
|
||||
console.error("Erreur modification avenant:", error);
|
||||
alert("Erreur lors de la modification de l'avenant: " + error.message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return "-";
|
||||
const [y, m, d] = dateStr.split("-");
|
||||
return `${d}/${m}/${y}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => router.push(`/staff/avenants/${avenant.id}`)}
|
||||
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">
|
||||
Modifier l'avenant {avenant.numero_avenant}
|
||||
</h1>
|
||||
<p className="text-sm text-slate-600 mt-1">
|
||||
Contrat {selectedContract.contract_number}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Infos du contrat */}
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6">
|
||||
<div className="font-medium text-slate-900 text-lg">
|
||||
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>
|
||||
|
||||
{/* Dates */}
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6">
|
||||
<h2 className="font-semibold text-slate-900 mb-4">Dates de l'avenant</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm 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-sm 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 */}
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6">
|
||||
<h2 className="font-semibold text-slate-900 mb-4">Informations complémentaires</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm 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-sm 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 */}
|
||||
{typeAvenant === "modification" && (
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6">
|
||||
<h2 className="font-semibold text-slate-900 mb-4">
|
||||
Éléments à avenanter <span className="text-red-500">*</span>
|
||||
</h2>
|
||||
<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-4 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>
|
||||
)}
|
||||
|
||||
{/* Message pour les annulations */}
|
||||
{typeAvenant === "annulation" && (
|
||||
<div className="bg-orange-50 border border-orange-200 rounded-xl p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<FileText className="h-5 w-5 text-orange-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-orange-900">Avenant d'annulation</div>
|
||||
<div className="text-sm text-orange-700 mt-1">
|
||||
Cet avenant annulera le contrat à partir de la date d'effet.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Formulaires conditionnels */}
|
||||
{typeAvenant === "modification" && selectedElements.includes("objet") && (
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6">
|
||||
<AmendmentObjetForm
|
||||
originalData={selectedContract}
|
||||
data={objetData}
|
||||
onChange={setObjetData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{typeAvenant === "modification" && selectedElements.includes("duree") && (
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6">
|
||||
<AmendmentDureeForm
|
||||
originalData={selectedContract}
|
||||
data={dureeData}
|
||||
onChange={setDureeData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{typeAvenant === "modification" && selectedElements.includes("remuneration") && (
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6">
|
||||
<AmendmentRemunerationForm
|
||||
originalData={selectedContract}
|
||||
data={remunerationData}
|
||||
onChange={setRemunerationData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lien PDF si généré */}
|
||||
{pdfPresignedUrl && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-6">
|
||||
<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 régénéré avec succès</div>
|
||||
<div className="text-sm text-green-700">Le nouveau PDF 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="bg-white rounded-xl border shadow-sm p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={handleGeneratePdf}
|
||||
disabled={!canSubmit || isGeneratingPdf}
|
||||
className="px-6 py-3 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 font-medium"
|
||||
>
|
||||
{isGeneratingPdf && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
Regénérer le PDF
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => router.push(`/staff/avenants/${avenant.id}`)}
|
||||
className="px-6 py-3 text-slate-700 hover:bg-slate-100 rounded-lg transition-colors font-medium"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!canSubmit || isSubmitting}
|
||||
className="px-6 py-3 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 font-medium"
|
||||
>
|
||||
{isSubmitting && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
Enregistrer les modifications
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -720,7 +720,10 @@ export default function AvenantDetailPageClient({ avenant }: AvenantDetailPageCl
|
|||
<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">
|
||||
<button
|
||||
onClick={() => router.push(`/staff/avenants/${avenant.id}/modifier`)}
|
||||
className="px-6 py-3 border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 transition-colors font-medium"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
</>
|
||||
|
|
|
|||
Loading…
Reference in a new issue