espace-paie-odentas/app/api/virements-salaires/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

350 lines
No EOL
14 KiB
TypeScript

// app/api/virements-salaires/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import { detectDemoModeFromHeaders } from "@/lib/demo-detector";
export async function GET(req: NextRequest) {
// 🎭 En mode démo, retourner des données vides (les données fictives sont gérées côté client)
if (detectDemoModeFromHeaders(req.headers)) {
console.log('🎭 [VIREMENTS API] Mode démo détecté - retour géré côté client');
return NextResponse.json({
items: [],
enabled: true,
org: {
structure_api: 'demo-org',
virements_salaires: 'odentas',
iban: null,
bic: null,
},
client: undefined,
}, { status: 200 });
}
try {
const { searchParams } = new URL(req.url);
const year = searchParams.get("year");
const period = searchParams.get("period");
const from = searchParams.get("from");
const to = searchParams.get("to");
const orgIdParam = searchParams.get("org_id"); // Pour le staff uniquement
console.log("[virements-salaires][incoming] url=", req.url);
console.log("[virements-salaires][incoming] searchParams=", {
year,
period,
from,
to,
orgIdParam,
});
// ✅ AJOUT : Récupération de l'identification du client côté serveur
const sb = createRouteHandlerClient({ cookies });
const { data: { user }, error: userError } = await sb.auth.getUser();
if (!user) {
console.error("[virements-salaires][auth] No authenticated user");
return new NextResponse("Unauthorized", { status: 401 });
}
// Vérification staff
let isStaff = false;
try {
const { data: staffData } = await sb
.from("staff_users")
.select("is_staff")
.eq("user_id", user.id)
.maybeSingle();
isStaff = !!staffData?.is_staff;
} catch {}
// Récupération organisation active
let activeOrgId: string | null = null;
let orgName: string | null = null;
let orgApiName: string | null = null;
if (isStaff) {
// Staff: utilise le paramètre org_id si fourni, sinon les cookies
if (orgIdParam) {
activeOrgId = orgIdParam;
try {
const { data } = await sb
.from("organizations")
.select("name, structure_api")
.eq("id", activeOrgId)
.maybeSingle();
orgName = data?.name ?? null;
orgApiName = data?.structure_api ?? null;
} catch {}
} else {
const cookiesStore = cookies();
activeOrgId = cookiesStore.get("active_org_id")?.value || null;
if (activeOrgId) {
try {
const { data } = await sb
.from("organizations")
.select("name, structure_api")
.eq("id", activeOrgId)
.maybeSingle();
orgName = data?.name ?? null;
orgApiName = data?.structure_api ?? null;
} catch {}
}
}
} else {
// Client: récupérer l'organisation via requête directe
try {
const { data: memberData } = await sb
.from("organization_members")
.select(`
organization:organizations(id, name, structure_api)
`)
.eq("user_id", user.id)
.eq("revoked", false)
.maybeSingle();
if (memberData?.organization) {
const org = memberData.organization as any;
activeOrgId = org.id ?? null;
orgName = org.name ?? null;
orgApiName = org.structure_api ?? null;
}
} catch {}
}
// If no active org found: allow staff users to continue (global access).
// For non-staff users, deny as before.
if (!activeOrgId || !orgName) {
if (!isStaff) {
console.error("[virements-salaires][auth] No active organization found (non-staff)");
return new NextResponse("No active organization", { status: 403 });
}
console.log("[virements-salaires][auth] No active org found but user is staff — continuing with null-org semantics");
}
console.log("[virements-salaires][auth] User authenticated:", {
userId: user.id,
orgId: activeOrgId,
orgName: orgName,
isStaff: isStaff
});
// Récupération des informations de l'organisation pour déterminer le mode de gestion
let orgSummary: any = null;
let isOdentasMode = false;
if (activeOrgId) {
try {
// Récupération des données de base de l'organisation
const { data: orgData } = await sb
.from("organizations")
.select("structure_api, iban, bic")
.eq("id", activeOrgId)
.maybeSingle();
// Récupération des détails incluant virements_salaires
const { data: orgDetailsData, error: orgDetailsError } = await sb
.from("organization_details")
.select("virements_salaires")
.eq("org_id", activeOrgId)
.maybeSingle();
console.log("[virements-salaires][debug] orgDetailsData query result:", {
data: orgDetailsData,
error: orgDetailsError,
activeOrgId
});
orgSummary = {
...orgData,
virements_salaires: orgDetailsData?.virements_salaires || null
};
isOdentasMode = orgDetailsData?.virements_salaires === 'Odentas';
console.log("[virements-salaires][mode] Organization details:", {
orgId: activeOrgId,
virements_salaires: orgDetailsData?.virements_salaires,
isOdentasMode
});
} catch (e) {
console.error("[virements-salaires][org] Error fetching org data:", e);
}
}
// Utilisation exclusive de Supabase - interrogation de salary_transfers
console.log("[virements-salaires][supabase] Using Supabase for all organizations");
if (activeOrgId) {
try {
let query = sb
.from("salary_transfers")
.select("*")
.eq("org_id", activeOrgId)
.order("period_month", { ascending: false });
// Filtrage par année si spécifié
if (year) {
const yearNum = Number(year);
const startOfYear = `${yearNum}-01-01`;
const endOfYear = `${yearNum}-12-31`;
query = query.gte("period_month", startOfYear).lte("period_month", endOfYear);
}
const { data: salaryTransfers, error } = await query;
if (error) {
console.error("[virements-salaires][supabase] Error:", error);
return new NextResponse("Database error", { status: 500 });
}
// Transformation des données pour correspondre au format attendu par le frontend
const transformedItems = (salaryTransfers || []).map((transfer: any) => {
// Logique pour le statut "virement reçu"
// Pour juillet 2025 et avant : automatiquement marqué comme reçu
// À partir d'août 2025 : basé sur client_wire_received_at
const periodDate = new Date(transfer.period_month);
const cutoffDate = new Date('2025-08-01'); // 1er août 2025
let virementRecu: boolean;
let virementRecuDate: string | null = null;
if (periodDate < cutoffDate) {
// Juillet 2025 et avant : automatiquement reçu
virementRecu = true;
} else {
// Août 2025 et après : basé sur client_wire_received_at
virementRecu = transfer.client_wire_received_at ? true : false;
virementRecuDate = transfer.client_wire_received_at;
}
return {
id: transfer.id,
periode_label: transfer.period_label,
periode: transfer.period_month,
callsheet: transfer.callsheet_url,
num_appel: transfer.num_appel,
date_mois: transfer.period_month,
date: transfer.deadline,
total_salaries_eur: parseFloat(transfer.total_net || '0'),
total: parseFloat(transfer.total_net || '0'),
virement_recu: virementRecu,
virement_recu_date: virementRecuDate, // Date de réception pour affichage
salaires_payes: transfer.notification_ok === true,
pdf_url: transfer.callsheet_url || null // Retourne directement l'URL S3 signée
};
});
console.log("[virements-salaires][supabase] Retrieved", transformedItems.length, "salary transfers");
// Si le mode n'est pas Odentas, récupérer les payslips non virés (transfer_done = false)
let clientData = undefined;
if (!isOdentasMode && activeOrgId) {
try {
console.log("[virements-salaires][payslips] Mode 'Votre structure' - récupération des payslips transfer_done = false");
// Base query payslips de l'organisation
let payslipsQuery = sb
.from("payslips")
.select("id, contract_id, organization_id, transfer_done, transfer_done_at, net_after_withholding, period_start, period_end, pay_date, updated_at, processed")
.eq("organization_id", activeOrgId);
// Filtrage par année (période de paie)
if (year) {
const yearNum = Number(year);
const startOfYear = `${yearNum}-01-01`;
const endOfYear = `${yearNum}-12-31`;
payslipsQuery = payslipsQuery.gte("period_start", startOfYear).lte("period_start", endOfYear);
}
const { data: allPayslips, error: payslipsError } = await payslipsQuery.order("period_start", { ascending: false });
if (payslipsError) {
console.error("[virements-salaires][payslips] Error:", payslipsError);
} else {
// Masquer les payslips dont processed est FALSE
const unpaidPayslips = (allPayslips || []).filter(p => !p.transfer_done && p.processed !== false);
// Filtrer les paies récentes (virées dans les 30 derniers jours)
// On utilise transfer_done_at (date exacte du marquage) au lieu de updated_at
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const recentPayslips = (allPayslips || []).filter(p => {
if (p.transfer_done && p.processed !== false && p.transfer_done_at) {
const ts = new Date(p.transfer_done_at);
return ts >= thirtyDaysAgo;
}
return false;
});
// Récupérer les contrats liés pour enrichir l'affichage
const contractIds = Array.from(new Set((unpaidPayslips.concat(recentPayslips)).map(p => p.contract_id).filter(Boolean)));
let contractsById: Record<string, any> = {};
if (contractIds.length > 0) {
const { data: contracts, error: contractsErr } = await sb
.from("cddu_contracts")
.select("id, contract_number, employee_name, employee_matricule, contract_kind, profession, start_date, end_date")
.in("id", contractIds as any);
if (contractsErr) {
console.error("[virements-salaires][contracts] Error fetching contracts:", contractsErr);
} else {
for (const c of contracts || []) contractsById[c.id] = c;
}
}
const mapPayslip = (p: any) => {
const c = contractsById[p.contract_id] || {};
return {
id: p.id,
kind: (c.contract_kind || 'CDDU_MONO') as 'CDDU_MONO' | 'CDDU_MULTI' | 'RG',
source: 'payslip' as const,
contract_id: p.contract_id,
salarie: c.employee_name || null,
salarie_matricule: c.employee_matricule || null,
reference: c.contract_number || null,
profession: c.profession || null,
date_debut: c.start_date || null,
date_fin: c.end_date || null,
periode: p.period_start || null,
net_a_payer: p.net_after_withholding != null ? parseFloat(p.net_after_withholding as any) : null,
virement_effectue: (p.transfer_done ? 'oui' : 'non') as 'oui' | 'non' | 'na'
};
};
const unpaid = unpaidPayslips.map(mapPayslip);
const recent = recentPayslips.map(mapPayslip);
console.log("[virements-salaires][payslips] unpaid:", unpaid.length, "recent:", recent.length);
clientData = { unpaid, recent };
}
} catch (e) {
console.error("[virements-salaires][payslips] Exception:", e);
}
}
const responseData = {
items: transformedItems,
enabled: isOdentasMode, // true seulement si virements_salaires = "Odentas"
org: orgSummary,
client: clientData
};
return NextResponse.json(responseData);
} catch (e: any) {
console.error("[virements-salaires][supabase] Exception:", e?.message);
return new NextResponse("Database error", { status: 500 });
}
} else {
// Pas d'organisation active
return NextResponse.json({
items: [],
enabled: false,
org: orgSummary,
client: undefined
});
}
} catch (e: any) {
console.error("[virements-salaires][auth] Error:", e?.message);
return new NextResponse("Authentication error", { status: 500 });
}
}