espace-paie-odentas/app/api/staff/facturation/bulk-create/route.ts
odentas 897af4b23a feat: Ajout fonctionnalités virements, facturation, signatures et emails
- Ajout sous-header total net à payer sur page virements-salaires
- Migration transfer_done_at pour tracking précis des virements
- Nouvelle page saisie tableau pour création factures en masse
- APIs bulk pour mise à jour dates signature et jours technicien
- API demande mandat SEPA avec email template
- Webhook DocuSeal pour signature contrats (mode TEST)
- Composants modaux détails et vérification PDF fiches de paie
- Upload/suppression/remplacement PDFs dans PayslipsGrid
- Amélioration affichage colonnes et filtres grilles contrats/paies
- Template email mandat SEPA avec sous-texte CTA
- APIs bulk facturation (création, update statut/date paiement)
- API clients sans facture pour période donnée
- Corrections calculs dates et montants avec auto-remplissage
2025-11-02 23:26:19 +01:00

151 lines
4.6 KiB
TypeScript

// app/api/staff/facturation/bulk-create/route.ts
import { NextResponse } from "next/server";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
export const dynamic = 'force-dynamic';
export const revalidate = 0;
export const runtime = 'nodejs';
async function isStaffUser(supabase: any, userId: string): Promise<boolean> {
try {
const { data: staffRow } = await supabase
.from('staff_users')
.select('is_staff')
.eq('user_id', userId)
.maybeSingle();
return !!staffRow?.is_staff;
} catch {
return false;
}
}
type InvoicePayload = {
org_id: string;
numero: string;
periode?: string | null;
date?: string | null;
due_date?: string | null;
payment_date?: string | null;
sepa_day?: string | null;
montant_ht: number;
montant_ttc: number;
statut: string;
notes?: string | null;
pdf_s3_key?: string | null;
};
// POST - Création en masse de factures
export async function POST(req: Request) {
try {
const body = await req.json();
const { invoices } = body;
// Validation des données
if (!Array.isArray(invoices) || invoices.length === 0) {
return NextResponse.json({ error: 'invalid_invoices' }, { status: 400 });
}
// Limiter le nombre de factures
if (invoices.length > 100) {
return NextResponse.json({ error: 'too_many_invoices', message: 'Maximum 100 invoices per batch' }, { status: 400 });
}
const supabase = createRouteHandlerClient({ cookies });
// Auth et vérification staff
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
if (sessionError || !session?.user?.id) {
return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
}
const isStaff = await isStaffUser(supabase, session.user.id);
if (!isStaff) {
return NextResponse.json({ error: 'forbidden' }, { status: 403 });
}
// Valider chaque facture
const validInvoices: InvoicePayload[] = [];
const validationErrors: any[] = [];
invoices.forEach((invoice: any, index: number) => {
const errors: string[] = [];
if (!invoice.org_id) errors.push('org_id required');
if (!invoice.numero) errors.push('numero required');
if (!invoice.montant_ttc || invoice.montant_ttc <= 0) errors.push('montant_ttc must be > 0');
if (errors.length > 0) {
validationErrors.push({ index, errors });
} else {
validInvoices.push({
org_id: invoice.org_id,
numero: invoice.numero,
periode: invoice.periode || null,
date: invoice.date || null,
due_date: invoice.due_date || null,
payment_date: invoice.payment_date || null,
sepa_day: invoice.sepa_day || null,
montant_ht: invoice.montant_ht || 0,
montant_ttc: invoice.montant_ttc,
statut: invoice.statut || 'emise',
notes: invoice.notes || null,
pdf_s3_key: invoice.pdf_s3_key || null,
});
}
});
if (validationErrors.length > 0) {
return NextResponse.json({
error: 'validation_failed',
validationErrors,
message: `${validationErrors.length} invoice(s) have validation errors`
}, { status: 400 });
}
// Mapper les données vers le schéma de la table invoices
const invoicesToInsert = validInvoices.map(inv => ({
org_id: inv.org_id,
invoice_number: inv.numero,
period_label: inv.periode,
invoice_date: inv.date,
due_date: inv.due_date,
payment_date: inv.payment_date,
sepa_day: inv.sepa_day,
amount_ht: inv.montant_ht,
amount_ttc: inv.montant_ttc,
status: inv.statut,
notes: inv.notes,
pdf_s3_key: inv.pdf_s3_key,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
}));
// Insérer toutes les factures
const { data: createdInvoices, error: insertError } = await supabase
.from('invoices')
.insert(invoicesToInsert)
.select();
if (insertError) {
console.error('Erreur lors de l\'insertion des factures:', insertError);
return NextResponse.json({
error: 'insert_failed',
details: insertError.message
}, { status: 500 });
}
return NextResponse.json({
success: true,
created: createdInvoices?.length || 0,
message: `${createdInvoices?.length || 0} facture(s) créée(s) avec succès`
});
} catch (error: any) {
console.error('Erreur dans bulk-create:', error);
return NextResponse.json(
{ error: 'internal_server_error', details: error.message },
{ status: 500 }
);
}
}