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,
} = body;
if (!contract_id || !date_effet || !elements_avenantes || elements_avenantes.length === 0) {
if (!contract_id || !date_effet) {
return NextResponse.json(
{ error: "Données manquantes" },
{ 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
const { data: contract, error: contractError } = await supabase
.from("cddu_contracts")
@ -82,7 +91,7 @@ export async function POST(request: NextRequest) {
date_effet,
type_avenant,
motif_avenant: motif_avenant || null,
elements_avenantes: elements_avenantes,
elements_avenantes: elements_avenantes || [],
objet_data: objet_data || null,
duree_data: duree_data || null,
lieu_horaire_data: lieu_horaire_data || null,
@ -104,8 +113,10 @@ export async function POST(request: NextRequest) {
}
// Mettre à jour le contrat avec les nouvelles données
// Pour les annulations, on ne met pas à jour le contrat
const updateData: any = {};
if (type_avenant === "modification") {
if (elements_avenantes.includes("objet") && objet_data) {
if (objet_data.profession_code && objet_data.profession_label) {
updateData.profession = `${objet_data.profession_code} - ${objet_data.profession_label}`;
@ -162,6 +173,7 @@ export async function POST(request: NextRequest) {
updateData.type_salaire = remuneration_data.type_salaire;
}
}
} // Fin du if (type_avenant === "modification")
// Mettre à jour le contrat si des données ont été modifiées
if (Object.keys(updateData).length > 0) {

View file

@ -173,6 +173,8 @@ export async function POST(request: NextRequest) {
// Déterminer les éléments avenantés
const elementsAvenantes = amendmentData.elements || [];
const typeAvenant = amendmentData.type_avenant || "modification";
let elementsText = "";
if (elementsAvenantes.includes("objet")) elementsText += "Objet,";
if (elementsAvenantes.includes("duree")) elementsText += "Durée de l'engagement,";
@ -180,6 +182,11 @@ export async function POST(request: NextRequest) {
if (elementsAvenantes.includes("remuneration")) elementsText += "Rémunération,";
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)
const professionData = amendmentData.objet_data || {};
const dureeData = amendmentData.duree_data || {};
@ -250,7 +257,7 @@ export async function POST(request: NextRequest) {
// Construction du payload pour PDFMonkey
const dataPayload = {
annulation: "Non",
annulation: typeAvenant === "annulation" ? "Oui" : "Non",
structure_name: organization?.name || orgDetails.structure || "",
structure_adresse: orgDetails.adresse || "",
structure_cpville: orgDetails.cp || "",

View file

@ -55,15 +55,30 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<head>
<title>Espace Paie Odentas</title>
<meta name="description" content="Plateforme de gestion de paie Odentas" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/favicon.png" type="image/png" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, viewport-fit=cover" />
{/* 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-32x32.png" sizes="32x32" type="image/png" />
{/* Apple Touch Icon */}
<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="alternate" href="/site.webmanifest" type="application/manifest+json" />
{/* Theme color */}
<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>
<body>
{/* Barre de progression pour les changements de page */}

View file

@ -158,9 +158,14 @@ export default function NouvelAvenantPageClient() {
// Validation
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;
return true;
}, [selectedContract, dateEffet, selectedElements]);
}, [selectedContract, dateEffet, selectedElements, typeAvenant]);
// Génération du PDF
const handleGeneratePdf = async () => {
@ -172,6 +177,7 @@ export default function NouvelAvenantPageClient() {
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,
@ -443,7 +449,8 @@ export default function NouvelAvenantPageClient() {
</div>
</div>
{/* Éléments à avenanter */}
{/* Éléments à avenanter - Seulement pour les modifications */}
{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>
@ -482,9 +489,27 @@ export default function NouvelAvenantPageClient() {
))}
</div>
</div>
)}
{/* Formulaires conditionnels */}
{selectedElements.includes("objet") && (
{/* 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. 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">
<AmendmentObjetForm
originalData={selectedContract}
@ -494,7 +519,7 @@ export default function NouvelAvenantPageClient() {
</div>
)}
{selectedElements.includes("duree") && (
{typeAvenant === "modification" && selectedElements.includes("duree") && (
<div className="bg-white rounded-xl border shadow-sm p-6">
<AmendmentDureeForm
originalData={selectedContract}
@ -504,7 +529,7 @@ export default function NouvelAvenantPageClient() {
</div>
)}
{selectedElements.includes("remuneration") && (
{typeAvenant === "modification" && selectedElements.includes("remuneration") && (
<div className="bg-white rounded-xl border shadow-sm p-6">
<AmendmentRemunerationForm
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",
"description": "Plateforme de gestion de paie Odentas",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#ffffff",
"theme_color": "#2D7FF9",
"lang": "fr-FR",
"dir": "ltr",
"categories": ["business", "finance", "productivity"],
"icons": [
{
"src": "/favicon.png",
"sizes": "any",
"src": "/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "/android-chrome-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
"purpose": "any"
},
{
"src": "/apple-touch-icon.png",
"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"}