- 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
397 lines
14 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|