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,64 +113,67 @@ 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 (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}`;
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}`;
}
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 (duree_data.date_debut) {
updateData.start_date = duree_data.date_debut;
if (elements_avenantes.includes("duree") && duree_data) {
if (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 (lieu_horaire_data.lieu) {
updateData.lieu_travail = lieu_horaire_data.lieu;
if (elements_avenantes.includes("lieu_horaire") && lieu_horaire_data) {
if (lieu_horaire_data.lieu) {
updateData.lieu_travail = lieu_horaire_data.lieu;
}
}
}
if (elements_avenantes.includes("remuneration") && remuneration_data) {
if (remuneration_data.gross_pay !== undefined) {
updateData.gross_pay = remuneration_data.gross_pay;
if (elements_avenantes.includes("remuneration") && remuneration_data) {
if (remuneration_data.gross_pay !== undefined) {
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) {
updateData.precisions_salaire = remuneration_data.precisions_salaire;
}
if (remuneration_data.type_salaire) {
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,48 +449,67 @@ export default function NouvelAvenantPageClient() {
</div>
</div>
{/* Éléments à avenanter */}
<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" />
)}
{/* É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>
</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>
<span className="text-sm font-medium">{element.label}</span>
</div>
</button>
))}
</button>
))}
</div>
</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"}