- 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
462 lines
16 KiB
TypeScript
462 lines
16 KiB
TypeScript
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 }
|
||
);
|
||
}
|
||
}
|