espace-paie-odentas/app/api/staff/naa/[id]/regenerate/route.ts
odentas 6485db4a75 feat(naa): Amélioration UX modal EditNAA - replier/déplier
- Tous les clients repliés par défaut à l'ouverture du modal
- Boutons 'Tout replier' / 'Tout déplier' pour gérer tous les clients
- Section factures repliable avec bouton Afficher/Masquer
- Affichage résumé facture sélectionnée quand section repliée
- Nouveau client déplié automatiquement pour faciliter la saisie
- Améliore la lisibilité pour NAA avec nombreux clients
2025-10-31 15:28:44 +01:00

462 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
export const dynamic = "force-dynamic";
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!,
},
});
// Fonction de polling pour attendre la génération du PDF
async function pollDocumentStatus(
documentUrl: string,
apiKey: string,
maxAttempts = 15,
intervalMs = 2000
): Promise<{ status: string; download_url?: string }> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
console.log(`[NAA Regenerate] Poll attempt ${attempt}/${maxAttempts}`);
const res = await fetch(documentUrl, {
headers: { Authorization: `Bearer ${apiKey}` }
});
if (!res.ok) {
throw new Error(`Failed to poll document status: ${res.statusText}`);
}
const data = await res.json();
const status = data.document?.status;
console.log(`[NAA Regenerate] Poll attempt ${attempt}/${maxAttempts}, status: ${status}`);
if (status === "success" && data.document?.download_url) {
return { status, download_url: data.document.download_url };
}
if (status === "error" || status === "failure") {
throw new Error(`PDF generation failed with status: ${status}`);
}
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
}
return { status: "timeout" };
}
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const cookieStore = cookies();
const supabase = createRouteHandlerClient({ cookies: () => cookieStore });
// Vérifier l'authentification staff
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
const { data: staffUser } = await supabase
.from("staff_users")
.select("user_id")
.eq("user_id", user.id)
.single();
if (!staffUser) {
return NextResponse.json({ error: "Accès non autorisé" }, { status: 403 });
}
console.log(`[NAA Regenerate] Starting regeneration for NAA ID: ${params.id}`);
// Lire le body (optionnel) pour récupérer les factures explicitement incluses
let includedInvoiceIds: string[] = [];
try {
const reqBody = await request.json();
includedInvoiceIds = Array.isArray(reqBody?.included_invoices) ? reqBody.included_invoices : [];
} catch (e) {
// pas de body
includedInvoiceIds = [];
}
// Si des invoice IDs sont fournis, précharger ces factures
let includedInvoicesMap: Record<string, any> = {};
if (includedInvoiceIds && includedInvoiceIds.length > 0) {
const { data: includedInvoices } = await supabase
.from('invoices')
.select('id, amount_ht, org_id, created_at, period_label')
.in('id', includedInvoiceIds as string[]);
if (includedInvoices && includedInvoices.length > 0) {
for (const inv of includedInvoices) {
includedInvoicesMap[inv.id] = inv;
}
}
console.log(`[NAA Regenerate] ${Object.keys(includedInvoicesMap).length} included invoices preloaded`);
}
// Récupérer le document NAA avec l'apporteur
const { data: naaDoc, error: naaError } = await supabase
.from("naa_documents")
.select(`
*,
referrers (*)
`)
.eq("id", params.id)
.single();
if (naaError || !naaDoc) {
return NextResponse.json({ error: "NAA non trouvée" }, { status: 404 });
}
// Récupérer les prestations actuelles
const { data: prestations } = await supabase
.from("naa_prestations")
.select("*")
.eq("naa_id", params.id)
.order("created_at");
const referrer = Array.isArray(naaDoc.referrers) ? naaDoc.referrers[0] : naaDoc.referrers;
// ===== RECALCUL COMPLET DES COMMISSIONS =====
console.log("[NAA Regenerate] Recalculating commissions from scratch...");
// Extraire le mois et l'année de la période (format : "Janvier 2025")
const periodeParts = naaDoc.periode.split(" ");
const monthNames = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"];
const periodMonth = monthNames.indexOf(periodeParts[0]) + 1;
const periodYear = parseInt(periodeParts[1]);
const month = periodMonth.toString().padStart(2, '0');
// Calcul du mois d'émission (mois suivant)
let emissionMonth = periodMonth + 1;
let emissionYear = periodYear;
if (emissionMonth > 12) {
emissionMonth = 1;
emissionYear += 1;
}
const emissionMonthStr = emissionMonth.toString().padStart(2, '0');
const emissionStartDate = `${emissionYear}-${emissionMonthStr}-01`;
const emissionEndDate = `${emissionYear}-${emissionMonthStr}-20`;
// Récupérer la liste actuelle des clients apportés (peut avoir changé)
const { data: referredClients, error: clientsError } = await supabase
.from("organization_details")
.select(`
org_id,
referrer_code,
commission_rate,
code_employeur,
organizations!organization_details_org_id_fkey (
id,
name
)
`)
.eq("is_referred", true)
.eq("referrer_code", naaDoc.referrer_code);
if (clientsError) {
console.error("[NAA Regenerate] Erreur récupération clients apportés:", clientsError);
return NextResponse.json(
{ error: "Erreur lors de la récupération des clients" },
{ status: 500 }
);
}
console.log(`[NAA Regenerate] Trouvé ${referredClients?.length || 0} clients apportés actuellement`);
// Recalculer les line items pour chaque client apporté
const newLineItems = [];
let totalCommission = 0;
for (const client of referredClients || []) {
const org = Array.isArray(client.organizations) ? client.organizations[0] : client.organizations;
const clientCode = client.code_employeur;
const clientName = org?.name || 'N/A';
console.log(`[NAA Regenerate] Recherche facture pour client ${clientName} (${clientCode})`);
// Si l'utilisateur a fourni des invoices incluses, vérifier s'il y en a pour ce client
let invoice: any | null = null;
if (includedInvoiceIds && includedInvoiceIds.length > 0) {
const matching = Object.values(includedInvoicesMap).filter((inv: any) => String(inv.org_id) === String(client.org_id));
if (matching && matching.length > 0) {
// prendre la plus récente
invoice = matching.sort((a: any, b: any) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
console.log(`[NAA Regenerate] ✅ Utilisation d'une facture explicitement incluse: ${invoice.id}`);
}
}
// Stratégie 1 : Recherche exacte par period_label (si pas de facture fournie)
if (!invoice) {
invoice = await supabase
.from("invoices")
.select("id, amount_ht, created_at, period_label")
.eq("org_id", client.org_id)
.eq("period_label", naaDoc.periode)
.order("created_at", { ascending: false })
.limit(1)
.maybeSingle()
.then(res => res.data);
}
// Stratégie 2 : Si pas trouvé, chercher par date de création
if (!invoice) {
invoice = await supabase
.from("invoices")
.select("id, amount_ht, created_at, period_label")
.eq("org_id", client.org_id)
.gte("created_at", emissionStartDate)
.lte("created_at", emissionEndDate)
.order("created_at", { ascending: false })
.limit(1)
.maybeSingle()
.then(res => res.data);
}
// Stratégie 3 : Chercher dans une plage élargie
if (!invoice) {
const { data: invoices } = await supabase
.from("invoices")
.select("id, amount_ht, created_at, period_label")
.eq("org_id", client.org_id)
.gte("created_at", `${periodYear}-${month}-01`)
.lte("created_at", `${emissionYear}-${emissionMonthStr}-31`)
.order("created_at", { ascending: false });
if (invoices && invoices.length > 0) {
invoice = invoices[0];
}
}
if (invoice && invoice.amount_ht > 0) {
const caHT = parseFloat(invoice.amount_ht);
const commissionRate = client.commission_rate || 0;
const commission = caHT * commissionRate;
totalCommission += commission;
console.log(`[NAA Regenerate] ✅ Commission: ${caHT}× ${(commissionRate * 100).toFixed(2)}% = ${commission.toFixed(2)}`);
newLineItems.push({
naa_id: params.id,
organization_id: client.org_id,
client_name: clientName,
client_code: clientCode,
invoice_id: invoice.id || null,
commission_rate: commissionRate,
ca_ht: caHT,
commission: commission
});
} else {
console.log(`[NAA Regenerate] ⚠️ Aucune facture trouvée ou montant HT = 0`);
}
}
// Supprimer les anciens line items et insérer les nouveaux
await supabase
.from("naa_line_items")
.delete()
.eq("naa_id", params.id);
if (newLineItems.length > 0) {
await supabase
.from("naa_line_items")
.insert(newLineItems);
}
// Recalculer les totaux
const nbreClients = newLineItems.length;
const nbrePrestations = (prestations || []).length;
const totalFacture = totalCommission - (naaDoc.deposit || 0) + (naaDoc.solde_compte_apporteur || 0);
console.log(`[NAA Regenerate] Totaux recalculés:`);
console.log(` - Nombre de clients: ${nbreClients}`);
console.log(` - Nombre de prestations: ${nbrePrestations}`);
console.log(` - Total commission: ${totalCommission.toFixed(2)}`);
console.log(` - Total facture: ${totalFacture.toFixed(2)}`);
// Mettre à jour le document NAA avec les nouveaux totaux
await supabase
.from("naa_documents")
.update({
total_commission: totalCommission,
total_facture: totalFacture,
nbre_clients: nbreClients,
nbre_prestations: nbrePrestations,
updated_at: new Date().toISOString()
})
.eq("id", params.id);
// Trier les line items par ordre alphabétique de code client
const sortedLineItems = (newLineItems || [])
.map((item: any) => ({
id: item.client_code || '',
client: item.client_name || '',
code: item.client_code || '',
comactuelle: item.commission_rate || 0,
caht: item.ca_ht || 0,
commission: item.commission || 0
}))
.sort((a: any, b: any) => {
const codeA = a.code.toLowerCase();
const codeB = b.code.toLowerCase();
return codeA.localeCompare(codeB);
}); // Trier les prestations par ordre alphabétique du code client
const sortedPrestations = (prestations || [])
.map(p => ({
client: p.client_name,
code: p.client_code,
type_prestation: p.type_prestation,
quantite: p.quantite,
tarif: p.tarif,
total: p.total
}))
.sort((a, b) => {
const codeA = (a.code || "").toUpperCase();
const codeB = (b.code || "").toUpperCase();
return codeA.localeCompare(codeB);
});
// Préparer le payload pour PDFMonkey avec les totaux recalculés
const pdfMonkeyPayload = {
apporteur_address: referrer?.address || "",
apporteur_cp: referrer?.postal_code || "",
apporteur_city: referrer?.city || "",
apporteur_code: referrer?.code || naaDoc.referrer_code,
apporteur_name: referrer?.name || "",
apporteur_contact: referrer?.contact_name || "",
callsheet_date: new Date(naaDoc.callsheet_date).toLocaleDateString("fr-FR"),
limit_date: naaDoc.limit_date ? new Date(naaDoc.limit_date).toLocaleDateString("fr-FR") : "",
callsheet_number: naaDoc.naa_number,
periode: naaDoc.periode,
total_commission: totalCommission,
solde_compte_apporteur: naaDoc.solde_compte_apporteur || 0,
total_facture: totalFacture,
deposit: naaDoc.deposit || 0,
nbre_clients: nbreClients,
nbre_prestations: nbrePrestations,
transfer_reference: naaDoc.transfer_reference || "",
logo_odentas: "",
lineItems: sortedLineItems,
prestations: sortedPrestations
};
console.log("[NAA Regenerate] Calling PDFMonkey API...");
console.log("[NAA Regenerate] Payload lineItems:", JSON.stringify(sortedLineItems, null, 2));
const pdfMonkeyApiKey = process.env.PDFMONKEY_API_KEY;
const pdfMonkeyUrl = "https://api.pdfmonkey.io/api/v1/documents";
const templateId = process.env.PDFMONKEY_NAA_TEMPLATE_ID || "422DA8A6-69E1-4798-B4A3-DF75D892BF2D";
const pdfMonkeyRes = await fetch(pdfMonkeyUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${pdfMonkeyApiKey}`,
},
body: JSON.stringify({
document: {
document_template_id: templateId,
status: "pending",
payload: pdfMonkeyPayload,
},
}),
});
if (!pdfMonkeyRes.ok) {
const errorText = await pdfMonkeyRes.text();
console.error("[NAA Regenerate] PDFMonkey API error:", errorText);
return NextResponse.json(
{ error: "Erreur lors de l'appel à PDFMonkey" },
{ status: 500 }
);
}
const pdfMonkeyData = await pdfMonkeyRes.json();
console.log("[NAA Regenerate] PDFMonkey response:", pdfMonkeyData);
const documentId = pdfMonkeyData.document?.id;
if (!documentId) {
return NextResponse.json({ error: "No document ID returned from PDFMonkey" }, { status: 500 });
}
const documentUrl = `${pdfMonkeyUrl}/${documentId}`;
console.log("[NAA Regenerate] Polling document status...");
const { status, download_url } = await pollDocumentStatus(documentUrl, pdfMonkeyApiKey!, 15, 2000);
if (status !== "success" || !download_url) {
return NextResponse.json({ error: "PDF generation failed or timed out" }, { status: 500 });
}
// Télécharger le PDF
console.log("[NAA Regenerate] Downloading PDF...");
const pdfRes = await fetch(download_url);
if (!pdfRes.ok) {
return NextResponse.json({ error: "Failed to download PDF" }, { status: 500 });
}
const pdfBuffer = Buffer.from(await pdfRes.arrayBuffer());
console.log("[NAA Regenerate] PDF downloaded, size:", pdfBuffer.length, "bytes");
// Uploader sur S3
console.log("[NAA Regenerate] Uploading to S3...");
const bucketName = process.env.AWS_S3_BUCKET_NAME || "odentas-docs";
const s3Key = `naa/${naaDoc.naa_number}.pdf`;
const uploadCommand = new PutObjectCommand({
Bucket: bucketName,
Key: s3Key,
Body: pdfBuffer,
ContentType: "application/pdf",
});
await s3Client.send(uploadCommand);
console.log("[NAA Regenerate] S3 upload successful");
const s3Url = `https://${bucketName}.s3.${process.env.AWS_REGION || "eu-west-3"}.amazonaws.com/${s3Key}`;
// Générer une URL présignée
const getObjectCommand = new GetObjectCommand({
Bucket: bucketName,
Key: s3Key,
});
const presignedUrl = await getSignedUrl(s3Client, getObjectCommand, { expiresIn: 3600 });
// Mettre à jour le document NAA avec l'URL du PDF (les totaux ont déjà été mis à jour plus haut)
await supabase
.from("naa_documents")
.update({
pdf_url: s3Url,
s3_key: s3Key,
status: "sent"
})
.eq("id", params.id);
console.log("[NAA Regenerate] ✅ NAA regeneration completed successfully");
return NextResponse.json({
success: true,
pdf_url: s3Url,
presigned_url: presignedUrl
});
} catch (error: any) {
console.error("[NAA Regenerate] ❌ Error:", error);
return NextResponse.json(
{ error: error.message || "Erreur serveur" },
{ status: 500 }
);
}
}