feat: Ajouter support des avenants d'annulation avec envoi à PDFMonkey

- Modifier NouvelAvenantPageClient pour gérer type_avenant annulation
- Désactiver la sélection d'éléments pour les annulations
- Ajouter message d'information pour les avenants d'annulation
- Adapter l'API generate-pdf pour envoyer annulation: Oui à PDFMonkey
- Modifier l'API create pour accepter les annulations sans éléments requis
- Ne pas mettre à jour le contrat pour les annulations
This commit is contained in:
odentas 2025-10-24 19:50:30 +02:00
parent 7e2a022bf0
commit 90d9f6b56f
8 changed files with 211 additions and 114 deletions

View file

@ -42,13 +42,22 @@ export async function POST(request: NextRequest) {
pdf_s3_key, pdf_s3_key,
} = body; } = body;
if (!contract_id || !date_effet || !elements_avenantes || elements_avenantes.length === 0) { if (!contract_id || !date_effet) {
return NextResponse.json( return NextResponse.json(
{ error: "Données manquantes" }, { error: "Données manquantes" },
{ status: 400 } { status: 400 }
); );
} }
// Pour les modifications, au moins un élément doit être sélectionné
// Pour les annulations, aucun élément n'est requis
if (type_avenant === "modification" && (!elements_avenantes || elements_avenantes.length === 0)) {
return NextResponse.json(
{ error: "Au moins un élément doit être sélectionné pour une modification" },
{ status: 400 }
);
}
// Récupérer le contrat pour validation et numérotation // Récupérer le contrat pour validation et numérotation
const { data: contract, error: contractError } = await supabase const { data: contract, error: contractError } = await supabase
.from("cddu_contracts") .from("cddu_contracts")
@ -82,7 +91,7 @@ export async function POST(request: NextRequest) {
date_effet, date_effet,
type_avenant, type_avenant,
motif_avenant: motif_avenant || null, motif_avenant: motif_avenant || null,
elements_avenantes: elements_avenantes, elements_avenantes: elements_avenantes || [],
objet_data: objet_data || null, objet_data: objet_data || null,
duree_data: duree_data || null, duree_data: duree_data || null,
lieu_horaire_data: lieu_horaire_data || null, lieu_horaire_data: lieu_horaire_data || null,
@ -104,64 +113,67 @@ export async function POST(request: NextRequest) {
} }
// Mettre à jour le contrat avec les nouvelles données // Mettre à jour le contrat avec les nouvelles données
// Pour les annulations, on ne met pas à jour le contrat
const updateData: any = {}; const updateData: any = {};
if (elements_avenantes.includes("objet") && objet_data) { if (type_avenant === "modification") {
if (objet_data.profession_code && objet_data.profession_label) { if (elements_avenantes.includes("objet") && objet_data) {
updateData.profession = `${objet_data.profession_code} - ${objet_data.profession_label}`; if (objet_data.profession_code && objet_data.profession_label) {
updateData.profession = `${objet_data.profession_code} - ${objet_data.profession_label}`;
}
if (objet_data.production_name) {
updateData.production_name = objet_data.production_name;
}
if (objet_data.production_numero_objet) {
updateData.numero_objet = objet_data.production_numero_objet;
}
} }
if (objet_data.production_name) {
updateData.production_name = objet_data.production_name;
}
if (objet_data.production_numero_objet) {
updateData.numero_objet = objet_data.production_numero_objet;
}
}
if (elements_avenantes.includes("duree") && duree_data) { if (elements_avenantes.includes("duree") && duree_data) {
if (duree_data.date_debut) { if (duree_data.date_debut) {
updateData.start_date = duree_data.date_debut; updateData.start_date = duree_data.date_debut;
}
if (duree_data.date_fin) {
updateData.end_date = duree_data.date_fin;
}
if (duree_data.nb_representations !== undefined) {
updateData.cachets_representations = duree_data.nb_representations;
}
if (duree_data.nb_repetitions !== undefined) {
updateData.services_repetitions = duree_data.nb_repetitions;
}
if (duree_data.nb_heures !== undefined) {
updateData.nombre_d_heures = duree_data.nb_heures;
}
if (duree_data.dates_representations) {
updateData.jours_representations = duree_data.dates_representations;
}
if (duree_data.dates_repetitions) {
updateData.jours_repetitions = duree_data.dates_repetitions;
}
if (duree_data.jours_travail) {
updateData.jours_travail = duree_data.jours_travail;
}
} }
if (duree_data.date_fin) {
updateData.end_date = duree_data.date_fin;
}
if (duree_data.nb_representations !== undefined) {
updateData.cachets_representations = duree_data.nb_representations;
}
if (duree_data.nb_repetitions !== undefined) {
updateData.services_repetitions = duree_data.nb_repetitions;
}
if (duree_data.nb_heures !== undefined) {
updateData.nombre_d_heures = duree_data.nb_heures;
}
if (duree_data.dates_representations) {
updateData.jours_representations = duree_data.dates_representations;
}
if (duree_data.dates_repetitions) {
updateData.jours_repetitions = duree_data.dates_repetitions;
}
if (duree_data.jours_travail) {
updateData.jours_travail = duree_data.jours_travail;
}
}
if (elements_avenantes.includes("lieu_horaire") && lieu_horaire_data) { if (elements_avenantes.includes("lieu_horaire") && lieu_horaire_data) {
if (lieu_horaire_data.lieu) { if (lieu_horaire_data.lieu) {
updateData.lieu_travail = lieu_horaire_data.lieu; updateData.lieu_travail = lieu_horaire_data.lieu;
}
} }
}
if (elements_avenantes.includes("remuneration") && remuneration_data) { if (elements_avenantes.includes("remuneration") && remuneration_data) {
if (remuneration_data.gross_pay !== undefined) { if (remuneration_data.gross_pay !== undefined) {
updateData.gross_pay = remuneration_data.gross_pay; updateData.gross_pay = remuneration_data.gross_pay;
}
if (remuneration_data.precisions_salaire) {
updateData.precisions_salaire = remuneration_data.precisions_salaire;
}
if (remuneration_data.type_salaire) {
updateData.type_salaire = remuneration_data.type_salaire;
}
} }
if (remuneration_data.precisions_salaire) { } // Fin du if (type_avenant === "modification")
updateData.precisions_salaire = remuneration_data.precisions_salaire;
}
if (remuneration_data.type_salaire) {
updateData.type_salaire = remuneration_data.type_salaire;
}
}
// Mettre à jour le contrat si des données ont été modifiées // Mettre à jour le contrat si des données ont été modifiées
if (Object.keys(updateData).length > 0) { if (Object.keys(updateData).length > 0) {

View file

@ -173,12 +173,19 @@ export async function POST(request: NextRequest) {
// Déterminer les éléments avenantés // Déterminer les éléments avenantés
const elementsAvenantes = amendmentData.elements || []; const elementsAvenantes = amendmentData.elements || [];
const typeAvenant = amendmentData.type_avenant || "modification";
let elementsText = ""; let elementsText = "";
if (elementsAvenantes.includes("objet")) elementsText += "Objet,"; if (elementsAvenantes.includes("objet")) elementsText += "Objet,";
if (elementsAvenantes.includes("duree")) elementsText += "Durée de l'engagement,"; if (elementsAvenantes.includes("duree")) elementsText += "Durée de l'engagement,";
if (elementsAvenantes.includes("lieu_horaire")) elementsText += "Lieu et horaires,"; if (elementsAvenantes.includes("lieu_horaire")) elementsText += "Lieu et horaires,";
if (elementsAvenantes.includes("remuneration")) elementsText += "Rémunération,"; if (elementsAvenantes.includes("remuneration")) elementsText += "Rémunération,";
elementsText = elementsText.replace(/,$/, ""); // Retirer la virgule finale elementsText = elementsText.replace(/,$/, ""); // Retirer la virgule finale
// Si c'est une annulation, on n'affiche pas d'éléments spécifiques
if (typeAvenant === "annulation") {
elementsText = "Annulation du contrat";
}
// Préparer les données pour le PDF (valeurs du contrat ou de l'avenant) // Préparer les données pour le PDF (valeurs du contrat ou de l'avenant)
const professionData = amendmentData.objet_data || {}; const professionData = amendmentData.objet_data || {};
@ -250,7 +257,7 @@ export async function POST(request: NextRequest) {
// Construction du payload pour PDFMonkey // Construction du payload pour PDFMonkey
const dataPayload = { const dataPayload = {
annulation: "Non", annulation: typeAvenant === "annulation" ? "Oui" : "Non",
structure_name: organization?.name || orgDetails.structure || "", structure_name: organization?.name || orgDetails.structure || "",
structure_adresse: orgDetails.adresse || "", structure_adresse: orgDetails.adresse || "",
structure_cpville: orgDetails.cp || "", structure_cpville: orgDetails.cp || "",

View file

@ -55,15 +55,30 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<head> <head>
<title>Espace Paie Odentas</title> <title>Espace Paie Odentas</title>
<meta name="description" content="Plateforme de gestion de paie Odentas" /> <meta name="description" content="Plateforme de gestion de paie Odentas" />
<link rel="icon" href="/favicon.ico" sizes="any" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, viewport-fit=cover" />
<link rel="icon" href="/favicon.png" type="image/png" />
{/* Favicons */}
<link rel="icon" href="/favicon.ico" sizes="32x32" />
<link rel="icon" href="/favicon.png" type="image/png" sizes="any" />
<link rel="icon" href="/favicon-16x16.png" sizes="16x16" type="image/png" /> <link rel="icon" href="/favicon-16x16.png" sizes="16x16" type="image/png" />
<link rel="icon" href="/favicon-32x32.png" sizes="32x32" type="image/png" /> <link rel="icon" href="/favicon-32x32.png" sizes="32x32" type="image/png" />
{/* Apple Touch Icon */}
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Espace Paie" />
{/* Manifest PWA */}
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<link rel="alternate" href="/site.webmanifest" type="application/manifest+json" />
{/* Theme color */}
<meta name="theme-color" content="#2D7FF9" /> <meta name="theme-color" content="#2D7FF9" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="msapplication-TileColor" content="#2D7FF9" />
{/* PWA */}
<meta name="mobile-web-app-capable" content="yes" />
<meta name="application-name" content="Espace Paie" />
</head> </head>
<body> <body>
{/* Barre de progression pour les changements de page */} {/* Barre de progression pour les changements de page */}

View file

@ -158,9 +158,14 @@ export default function NouvelAvenantPageClient() {
// Validation // Validation
const canSubmit = useMemo(() => { const canSubmit = useMemo(() => {
// Pour une annulation, pas besoin de sélectionner des éléments
if (typeAvenant === "annulation") {
return !!(selectedContract && dateEffet);
}
// Pour une modification, il faut au moins un élément
if (!selectedContract || !dateEffet || selectedElements.length === 0) return false; if (!selectedContract || !dateEffet || selectedElements.length === 0) return false;
return true; return true;
}, [selectedContract, dateEffet, selectedElements]); }, [selectedContract, dateEffet, selectedElements, typeAvenant]);
// Génération du PDF // Génération du PDF
const handleGeneratePdf = async () => { const handleGeneratePdf = async () => {
@ -172,6 +177,7 @@ export default function NouvelAvenantPageClient() {
contract_id: selectedContract.id, contract_id: selectedContract.id,
date_effet: dateEffet, date_effet: dateEffet,
date_signature: dateSignature || undefined, date_signature: dateSignature || undefined,
type_avenant: typeAvenant,
elements: selectedElements, elements: selectedElements,
objet_data: selectedElements.includes("objet") ? objetData : undefined, objet_data: selectedElements.includes("objet") ? objetData : undefined,
duree_data: selectedElements.includes("duree") ? dureeData : undefined, duree_data: selectedElements.includes("duree") ? dureeData : undefined,
@ -443,48 +449,67 @@ export default function NouvelAvenantPageClient() {
</div> </div>
</div> </div>
{/* Éléments à avenanter */} {/* Éléments à avenanter - Seulement pour les modifications */}
<div className="bg-white rounded-xl border shadow-sm p-6"> {typeAvenant === "modification" && (
<h2 className="font-semibold text-slate-900 mb-4"> <div className="bg-white rounded-xl border shadow-sm p-6">
Éléments à avenanter <span className="text-red-500">*</span> <h2 className="font-semibold text-slate-900 mb-4">
</h2> Éléments à avenanter <span className="text-red-500">*</span>
<div className="grid grid-cols-2 gap-3"> </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: "objet" as const, label: "Objet (profession, production)" },
{ value: "lieu_horaire" as const, label: "Lieu et horaires" }, { value: "duree" as const, label: "Durée de l'engagement" },
{ value: "remuneration" as const, label: "Rémunération" }, { value: "lieu_horaire" as const, label: "Lieu et horaires" },
].map((element) => ( { value: "remuneration" as const, label: "Rémunération" },
<button ].map((element) => (
key={element.value} <button
onClick={() => toggleElement(element.value)} key={element.value}
className={`p-4 border rounded-lg text-left transition-all ${ onClick={() => toggleElement(element.value)}
selectedElements.includes(element.value) className={`p-4 border rounded-lg text-left transition-all ${
? "bg-indigo-50 border-indigo-500 text-indigo-700" selectedElements.includes(element.value)
: "bg-white border-slate-200 text-slate-700 hover:bg-slate-50" ? "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 <div className="flex items-center gap-2">
className={`w-5 h-5 rounded border flex items-center justify-center ${ <div
selectedElements.includes(element.value) className={`w-5 h-5 rounded border flex items-center justify-center ${
? "bg-indigo-600 border-indigo-600" selectedElements.includes(element.value)
: "border-slate-300" ? "bg-indigo-600 border-indigo-600"
}`} : "border-slate-300"
> }`}
{selectedElements.includes(element.value) && ( >
<CheckCircle2 className="h-3 w-3 text-white" /> {selectedElements.includes(element.value) && (
)} <CheckCircle2 className="h-3 w-3 text-white" />
)}
</div>
<span className="text-sm font-medium">{element.label}</span>
</div> </div>
<span className="text-sm font-medium">{element.label}</span> </button>
</div> ))}
</button> </div>
))}
</div> </div>
</div> )}
{/* Formulaires conditionnels */} {/* Message pour les annulations */}
{selectedElements.includes("objet") && ( {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. Aucune modification spécifique n'est requise.
</div>
</div>
</div>
</div>
)}
{/* Formulaires conditionnels - Seulement pour les modifications */}
{typeAvenant === "modification" && selectedElements.includes("objet") && (
<div className="bg-white rounded-xl border shadow-sm p-6"> <div className="bg-white rounded-xl border shadow-sm p-6">
<AmendmentObjetForm <AmendmentObjetForm
originalData={selectedContract} originalData={selectedContract}
@ -494,7 +519,7 @@ export default function NouvelAvenantPageClient() {
</div> </div>
)} )}
{selectedElements.includes("duree") && ( {typeAvenant === "modification" && selectedElements.includes("duree") && (
<div className="bg-white rounded-xl border shadow-sm p-6"> <div className="bg-white rounded-xl border shadow-sm p-6">
<AmendmentDureeForm <AmendmentDureeForm
originalData={selectedContract} originalData={selectedContract}
@ -504,7 +529,7 @@ export default function NouvelAvenantPageClient() {
</div> </div>
)} )}
{selectedElements.includes("remuneration") && ( {typeAvenant === "modification" && selectedElements.includes("remuneration") && (
<div className="bg-white rounded-xl border shadow-sm p-6"> <div className="bg-white rounded-xl border shadow-sm p-6">
<AmendmentRemunerationForm <AmendmentRemunerationForm
originalData={selectedContract} originalData={selectedContract}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View file

@ -3,30 +3,69 @@
"short_name": "Espace Paie", "short_name": "Espace Paie",
"description": "Plateforme de gestion de paie Odentas", "description": "Plateforme de gestion de paie Odentas",
"start_url": "/", "start_url": "/",
"scope": "/",
"display": "standalone", "display": "standalone",
"orientation": "portrait-primary",
"background_color": "#ffffff", "background_color": "#ffffff",
"theme_color": "#2D7FF9", "theme_color": "#2D7FF9",
"lang": "fr-FR",
"dir": "ltr",
"categories": ["business", "finance", "productivity"],
"icons": [ "icons": [
{ {
"src": "/favicon.png", "src": "/favicon-16x16.png",
"sizes": "any", "sizes": "16x16",
"type": "image/png"
},
{
"src": "/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "/android-chrome-144x144.png",
"sizes": "144x144",
"type": "image/png", "type": "image/png",
"purpose": "any maskable" "purpose": "any"
},
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}, },
{ {
"src": "/apple-touch-icon.png", "src": "/apple-touch-icon.png",
"sizes": "180x180", "sizes": "180x180",
"type": "image/png" "type": "image/png",
"purpose": "any"
},
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "any"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
} }
] ],
"screenshots": [],
"shortcuts": [],
"prefer_related_applications": false
} }

View file

@ -1 +0,0 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}