espace-paie-odentas/app/api/staff/amendments/generate-pdf/route.ts
odentas 90d9f6b56f 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
2025-10-24 19:50:30 +02:00

397 lines
14 KiB
TypeScript

// app/api/staff/amendments/generate-pdf/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createSbServer } from "@/lib/supabaseServer";
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
// Fonction de formatage d'une date en "DD/MM/YYYY"
function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return "";
const date = new Date(dateStr);
if (isNaN(date.getTime())) return dateStr;
const dd = String(date.getDate()).padStart(2, '0');
const mm = String(date.getMonth() + 1).padStart(2, '0');
const yyyy = date.getFullYear();
return `${dd}/${mm}/${yyyy}`;
}
// Fonction de polling pour vérifier le statut du document PDFMonkey
async function pollDocumentStatus(documentId: string, pdfMonkeyUrl: string, pdfMonkeyApiKey: string) {
const url = `${pdfMonkeyUrl}/${documentId}`;
let attempts = 0;
const maxAttempts = 10;
let status = "pending";
while (status !== "success" && attempts < maxAttempts) {
console.log(`Polling PDFMonkey (tentative ${attempts + 1}/${maxAttempts})...`);
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${pdfMonkeyApiKey}`
}
});
if (!response.ok) {
throw new Error(`Erreur PDFMonkey: ${response.status} ${response.statusText}`);
}
const data = await response.json();
status = data.document.status;
console.log("Statut du document:", status);
if (status === "success") {
return data.document;
}
if (status === "failure") {
throw new Error("PDFMonkey document generation failed");
}
await new Promise(resolve => setTimeout(resolve, 3000));
attempts++;
}
throw new Error("PDFMonkey polling timed out");
}
// Slugifier un texte
function slugify(text: string): string {
return text
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
export async function POST(request: NextRequest) {
try {
const sb = createSbServer();
// Vérification de l'authentification et du staff
const { data: { user }, error: authError } = await sb.auth.getUser();
if (authError || !user) {
return NextResponse.json(
{ error: "Authentification requise" },
{ status: 401 }
);
}
const { data: staffUser } = await sb
.from("staff_users")
.select("is_staff")
.eq("user_id", user.id)
.maybeSingle();
if (!staffUser?.is_staff) {
return NextResponse.json(
{ error: "Accès refusé - réservé au staff" },
{ status: 403 }
);
}
// Variables d'environnement
const pdfMonkeyUrl = process.env.PDFMONKEY_URL;
const pdfMonkeyApiKey = process.env.PDFMONKEY_API_KEY;
if (!pdfMonkeyUrl || !pdfMonkeyApiKey) {
return NextResponse.json(
{ error: "Configuration PDFMonkey manquante" },
{ status: 500 }
);
}
// Configuration S3
const s3Client = new S3Client({
region: process.env.AWS_REGION || "eu-west-3",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
// Récupération des données du body
const body = await request.json();
const { contractId, amendmentData } = body;
if (!contractId || !amendmentData) {
return NextResponse.json(
{ error: "Données manquantes" },
{ status: 400 }
);
}
// Récupération du contrat
const { data: contract, error: contractError } = await sb
.from("cddu_contracts")
.select("*")
.eq("id", contractId)
.single();
if (contractError || !contract) {
return NextResponse.json(
{ error: "Contrat non trouvé" },
{ status: 404 }
);
}
// Récupération des données de l'organisation
const { data: orgDetails, error: orgError } = await sb
.from("organization_details")
.select("*")
.eq("org_id", contract.org_id)
.single();
if (orgError || !orgDetails) {
return NextResponse.json(
{ error: "Détails de l'organisation non trouvés" },
{ status: 404 }
);
}
// Récupération du nom de la structure
const { data: organization } = await sb
.from("organizations")
.select("name")
.eq("id", contract.org_id)
.single();
// Récupération des données du salarié
const { data: salarie, error: salarieError } = await sb
.from("salaries")
.select("*")
.eq("id", contract.employee_id)
.single();
if (salarieError || !salarie) {
return NextResponse.json(
{ error: "Salarié non trouvé" },
{ status: 404 }
);
}
// 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,";
if (elementsAvenantes.includes("lieu_horaire")) elementsText += "Lieu et horaires,";
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 || {};
const remunerationData = amendmentData.remuneration_data || {};
// Déterminer la catégorie professionnelle
const profession = professionData.profession_label || contract.profession || "";
let employee_catpro = contract.categorie_pro || "Artiste";
if (profession === "Metteur en scène") {
employee_catpro = "Metteur en scène";
}
// Préparer les détails de cachets - convertir en nombres
const cachetsRepresentations = parseInt(dureeData.nb_representations || contract.cachets_representations || 0);
const cachetsRepetitions = parseInt(dureeData.nb_repetitions || contract.cachets_repetitions || 0);
const heures = parseInt(dureeData.nb_heures || contract.nb_heures || 0);
const heuresParJour = parseInt(contract.nombre_d_heures_par_jour || 0);
let detailsCachets = "";
if (cachetsRepresentations > 0) {
detailsCachets += `${cachetsRepresentations} cachet${cachetsRepresentations > 1 ? 's' : ''} de représentation`;
}
if (cachetsRepetitions > 0) {
if (detailsCachets) detailsCachets += ", ";
detailsCachets += `${cachetsRepetitions} service${cachetsRepetitions > 1 ? 's' : ''} de répétition`;
}
if (!detailsCachets && heures > 0) {
detailsCachets = `${heures} heure${heures > 1 ? 's' : ''}`;
}
// Dates travaillées
let datesTravaillees = "";
if (dureeData.dates_representations) {
datesTravaillees += dureeData.dates_representations;
}
// Pour les répétitions, utiliser dates_repetitions_heures si disponible, sinon dates_repetitions
if (dureeData.dates_repetitions_heures) {
if (datesTravaillees) datesTravaillees += " ; ";
datesTravaillees += dureeData.dates_repetitions_heures;
} else if (dureeData.dates_repetitions) {
if (datesTravaillees) datesTravaillees += " ; ";
datesTravaillees += dureeData.dates_repetitions;
}
if (dureeData.jours_travail) {
if (datesTravaillees) datesTravaillees += " ; ";
datesTravaillees += dureeData.jours_travail;
}
if (!datesTravaillees) {
// Utiliser les dates du contrat original
if (contract.jours_representations) datesTravaillees += contract.jours_representations;
if (contract.jours_repetitions) {
if (datesTravaillees) datesTravaillees += " ; ";
datesTravaillees += contract.jours_repetitions;
}
if (contract.jours_travail) {
if (datesTravaillees) datesTravaillees += " ; ";
datesTravaillees += contract.jours_travail;
}
}
// Panier repas
const panierRepas = contract.panier_repas === "Oui" ? 1 : "";
const panierRepasCCN = contract.panier_repas === "Oui" ? "Oui" : "Non";
const montantPanierRepas = contract.montant_panier_repas || "20,20";
// CCN
const ccn = contract.convention_collective || "Convention Collective Nationale du Spectacle Vivant Privé";
// Construction du payload pour PDFMonkey
const dataPayload = {
annulation: typeAvenant === "annulation" ? "Oui" : "Non",
structure_name: organization?.name || orgDetails.structure || "",
structure_adresse: orgDetails.adresse || "",
structure_cpville: orgDetails.cp || "",
structure_ville: orgDetails.ville || "",
structure_siret: orgDetails.siret || "",
structure_licence: orgDetails.licence_spectacles || "",
structure_signataire: `${orgDetails.prenom_signataire || ""} ${orgDetails.nom_signataire || ""}`.trim(),
structure_signatairequalite: orgDetails.qualite_signataire || "",
delegation: orgDetails.delegation_signature ? "Oui" : "Non",
employee_civ: salarie.civilite || "",
employee_firstname: salarie.prenom || "",
employee_lastname: salarie.nom || "",
employee_dob: formatDate(salarie.date_naissance),
employee_cob: salarie.lieu_de_naissance || "",
employee_address: salarie.adresse || "",
employee_ss: salarie.nir || "",
employee_cs: salarie.conges_spectacles || "",
employee_profession: profession,
employee_catpro: employee_catpro,
spectacle: professionData.production_name || contract.production_name || "",
numobjet: professionData.production_numero_objet || contract.objet_spectacle || "",
debut_contrat: formatDate(dureeData.date_debut || contract.start_date),
fin_contrat: formatDate(dureeData.date_fin || contract.end_date),
date_avenant: formatDate(amendmentData.date_signature),
dates_travaillees: datesTravaillees,
details_cachets: detailsCachets,
salaire_brut: remunerationData.gross_pay
? parseFloat(remunerationData.gross_pay.toString()).toLocaleString('fr-FR', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
: contract.gross_pay
? parseFloat(contract.gross_pay.toString()).toLocaleString('fr-FR', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
: "",
precisions_salaire: remunerationData.precisions_salaire || contract.precisions_salaire || "",
signature_contrat: formatDate(contract.date_signature),
CCN: ccn,
effet_avenant: formatDate(amendmentData.date_effet),
elements_avenant: elementsText,
panierrepas: panierRepas,
panierrepasccn: panierRepasCCN,
montantpanierrepas: montantPanierRepas,
cachets: {
representations: cachetsRepresentations,
repetitions: cachetsRepetitions,
heures: heures,
heuresparjour: heuresParJour
},
dates_representations_detail: dureeData.dates_representations || contract.jours_representations || "",
dates_repetitions_detail: dureeData.dates_repetitions_heures || dureeData.dates_repetitions || contract.jours_repetitions || "",
imageUrl: orgDetails.logo || ""
};
console.log("Payload PDFMonkey:", JSON.stringify(dataPayload, null, 2));
// Appel à PDFMonkey pour générer le PDF
const templateId = "BC5E26D6-4A3B-45F8-8376-25F83C17A413";
const pdfResponse = await fetch(pdfMonkeyUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${pdfMonkeyApiKey}`
},
body: JSON.stringify({
document: {
document_template_id: templateId,
payload: dataPayload,
status: "pending"
}
})
});
if (!pdfResponse.ok) {
const errorText = await pdfResponse.text();
console.error("Erreur PDFMonkey:", errorText);
return NextResponse.json(
{ error: "Erreur lors de la génération du PDF", details: errorText },
{ status: 500 }
);
}
const pdfData = await pdfResponse.json();
const documentId = pdfData.document.id;
console.log("Document PDFMonkey créé:", documentId);
// Polling pour attendre que le PDF soit prêt
const completedDocument = await pollDocumentStatus(documentId, pdfMonkeyUrl, pdfMonkeyApiKey);
// Télécharger le PDF depuis PDFMonkey
const pdfUrl = completedDocument.download_url;
const pdfBuffer = await fetch(pdfUrl).then(res => res.arrayBuffer());
// Upload vers S3
const orgSlug = slugify(organization?.name || orgDetails.structure || "organisation");
const filename = `avenant_${contract.contract_number}_${Date.now()}.pdf`;
const s3Key = `avenants/${orgSlug}/${filename}`;
await s3Client.send(
new PutObjectCommand({
Bucket: "odentas-docs",
Key: s3Key,
Body: Buffer.from(pdfBuffer),
ContentType: "application/pdf",
})
);
console.log("PDF uploadé sur S3:", s3Key);
// Générer un lien présigné pour GET (visualisation)
const presignedUrl = await getSignedUrl(
s3Client,
new GetObjectCommand({
Bucket: "odentas-docs",
Key: s3Key,
}),
{ expiresIn: 3600 } // 1 heure
);
return NextResponse.json({
success: true,
s3Key,
presignedUrl,
filename
});
} catch (error: any) {
console.error("Erreur lors de la génération du PDF:", error);
return NextResponse.json(
{ error: "Erreur serveur", details: error.message },
{ status: 500 }
);
}
}