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:
parent
7e2a022bf0
commit
90d9f6b56f
8 changed files with 211 additions and 114 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 || "",
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
BIN
public/android-chrome-144x144.png
Normal file
BIN
public/android-chrome-144x144.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
public/android-chrome-384x384.png
Normal file
BIN
public/android-chrome-384x384.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"}
|
||||
Loading…
Reference in a new issue