- 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
350 lines
No EOL
14 KiB
TypeScript
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 });
|
|
}
|
|
} |