- 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
151 lines
4.6 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|