espace-paie-odentas/app/api/staff/payslips/search/route.ts

424 lines
18 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { createSbServer } from "@/lib/supabaseServer";
async function assertStaff(sb: ReturnType<typeof createSbServer>, userId: string) {
const { data: me } = await sb
.from("staff_users")
.select("is_staff")
.eq("user_id", userId)
.maybeSingle();
return !!me?.is_staff;
}
export async function GET(req: NextRequest) {
const supabase = createSbServer();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: "Non authentifié." }, { status: 401 });
}
const isStaff = await assertStaff(supabase, user.id);
if (!isStaff) {
return NextResponse.json({ error: "Accès réservé au staff." }, { status: 403 });
}
const { searchParams } = new URL(req.url);
// Paramètres de recherche et filtres
const q = searchParams.get('q') || '';
const structure = searchParams.get('structure') || '';
const typeDeContrat = searchParams.get('type_de_contrat') || '';
const processed = searchParams.get('processed') || '';
const transferDone = searchParams.get('transfer_done') || '';
const aemStatus = searchParams.get('aem_status') || '';
const periodFrom = searchParams.get('period_from') || '';
const periodTo = searchParams.get('period_to') || '';
const sort = searchParams.get('sort') || 'period_start';
const order = searchParams.get('order') || 'desc';
const limit = parseInt(searchParams.get('limit') || '50');
const offset = parseInt(searchParams.get('offset') || '0');
console.log('[staff/payslips/search] Filters:', { q, structure, typeDeContrat, processed, transferDone, aemStatus, periodFrom, periodTo, sort, order, limit, offset });
console.log('[staff/payslips/search] typeDeContrat truthy check:', !!typeDeContrat, 'length:', typeDeContrat?.length);
try {
// Construction de la requête avec jointure
let query = supabase
.from("payslips")
.select(
`id, contract_id, period_start, period_end, period_month, pay_number, pay_date,
gross_amount, net_amount, net_after_withholding, employer_cost,
processed, aem_status, transfer_done, organization_id, storage_path, created_at,
cddu_contracts!contract_id(
id, contract_number, employee_name, employee_id, structure, type_de_contrat, org_id, analytique,
salaries!employee_id(salarie, nom, prenom),
organizations!org_id(organization_details(code_employeur))
)`,
{ count: "exact" }
);
// Filtre de recherche textuelle (n° contrat, nom salarié)
if (q) {
// Nous devons filtrer côté serveur sur les données de la jointure
// Supabase ne permet pas de filtrer directement sur les jointures dans le SELECT
// On va d'abord récupérer tous les contrats qui matchent, puis filter les payslips
const { data: matchingContracts } = await supabase
.from("cddu_contracts")
.select("id")
.or(`contract_number.ilike.%${q}%,employee_name.ilike.%${q}%`);
if (matchingContracts && matchingContracts.length > 0) {
const contractIds = matchingContracts.map(c => c.id);
query = query.in('contract_id', contractIds);
} else {
// Aucun contrat ne correspond, retourner un résultat vide
return NextResponse.json({ rows: [], count: 0 });
}
}
// Filtre par structure (via contrat)
if (structure) {
const { data: matchingContracts, error: structureError } = await supabase
.from("cddu_contracts")
.select("id")
.eq("structure", structure);
if (structureError) {
console.error('[staff/payslips/search] Structure filter error:', structureError);
return NextResponse.json({ error: "Erreur lors du filtre par structure" }, { status: 500 });
}
console.log('[staff/payslips/search] Matching contracts for structure:', matchingContracts?.length || 0);
if (matchingContracts && matchingContracts.length > 0) {
const contractIds = matchingContracts.map(c => c.id);
// Si trop de contrats (>400), on va récupérer toutes les payslips et filtrer côté serveur
if (contractIds.length > 400) {
console.log('[staff/payslips/search] Too many contracts, switching to server-side filtering');
// Récupérer toutes les payslips pour la période
let allQuery = supabase
.from("payslips")
.select(
`id, contract_id, period_start, period_end, period_month, pay_number, pay_date,
gross_amount, net_amount, net_after_withholding, employer_cost,
processed, aem_status, transfer_done, organization_id, storage_path, created_at,
cddu_contracts!contract_id(
id, contract_number, employee_name, employee_id, structure, type_de_contrat, org_id, analytique,
salaries!employee_id(salarie, nom, prenom),
organizations!org_id(organization_details(code_employeur))
)`,
{ count: "exact" }
);
// Filtres de période
if (periodFrom) {
allQuery = allQuery.gte('period_start', periodFrom);
}
if (periodTo) {
allQuery = allQuery.lte('period_start', periodTo);
}
// Filtres additionnels
if (processed !== '') {
allQuery = allQuery.eq('processed', processed === 'true');
}
if (transferDone !== '') {
allQuery = allQuery.eq('transfer_done', transferDone === 'true');
}
if (aemStatus) {
allQuery = allQuery.eq('aem_status', aemStatus);
}
// Limiter à 10000 pour ne pas surcharger
allQuery = allQuery.limit(10000);
const { data: allPayslips, error: allError } = await allQuery;
if (allError) {
console.error('[staff/payslips/search] Error fetching payslips:', allError);
return NextResponse.json({ error: allError.message }, { status: 500 });
}
// Filtrer côté serveur par contract_id
const contractIdsSet = new Set(contractIds);
let filteredPayslips = (allPayslips || []).filter(p => contractIdsSet.has(p.contract_id));
console.log('[staff/payslips/search] Filtered to', filteredPayslips.length, 'payslips');
// Tri côté serveur
const ascending = order === 'asc';
filteredPayslips.sort((a: any, b: any) => {
let valueA: any, valueB: any;
if (sort === 'employee_name') {
valueA = a.cddu_contracts?.salaries?.nom || a.cddu_contracts?.employee_name || '';
valueB = b.cddu_contracts?.salaries?.nom || b.cddu_contracts?.employee_name || '';
} else if (sort === 'contract_number') {
valueA = a.cddu_contracts?.contract_number || '';
valueB = b.cddu_contracts?.contract_number || '';
} else if (sort === 'structure') {
valueA = a.cddu_contracts?.structure || '';
valueB = b.cddu_contracts?.structure || '';
} else if (sort === 'analytique') {
valueA = a.cddu_contracts?.analytique || '';
valueB = b.cddu_contracts?.analytique || '';
} else {
valueA = a[sort];
valueB = b[sort];
}
if (typeof valueA === 'string') valueA = valueA.toLowerCase();
if (typeof valueB === 'string') valueB = valueB.toLowerCase();
if (valueA < valueB) return ascending ? -1 : 1;
if (valueA > valueB) return ascending ? 1 : -1;
return 0;
});
// Pagination
const paginatedPayslips = filteredPayslips.slice(offset, offset + limit);
return NextResponse.json({
rows: paginatedPayslips,
count: filteredPayslips.length
});
} else {
// Assez peu de contrats, on peut utiliser .in()
query = query.in('contract_id', contractIds);
}
} else {
console.log('[staff/payslips/search] No contracts found for structure, returning empty');
return NextResponse.json({ rows: [], count: 0 });
}
}
// Filtre par type de contrat (CDDU vs RG)
// Au lieu de récupérer les IDs de contrats (limité à 1000), on récupère toutes les payslips
// et on filtre côté serveur par le type_de_contrat de la relation
if (typeDeContrat) {
console.log('[staff/payslips/search] Filtering by type:', typeDeContrat);
// Récupérer toutes les payslips avec leurs contrats pour la période
let allQuery = supabase
.from("payslips")
.select(
`id, contract_id, period_start, period_end, period_month, pay_number, pay_date,
gross_amount, net_amount, net_after_withholding, employer_cost,
processed, aem_status, transfer_done, organization_id, created_at,
cddu_contracts!contract_id(
id, contract_number, employee_name, employee_id, structure, type_de_contrat, org_id, analytique,
salaries!employee_id(salarie, nom, prenom)
)`,
{ count: "exact" }
);
// Appliquer les filtres de période en amont
if (periodFrom) {
allQuery = allQuery.gte('period_start', periodFrom);
}
if (periodTo) {
allQuery = allQuery.lte('period_start', periodTo);
}
// Limiter à 10000 pour ne pas surcharger
allQuery = allQuery.limit(10000);
const { data: allPayslips, error: allError } = await allQuery;
if (allError) {
console.error('[staff/payslips/search] Error fetching all payslips:', allError);
return NextResponse.json({ error: allError.message }, { status: 500 });
}
console.log('[staff/payslips/search] Retrieved', allPayslips?.length, 'total payslips from DB');
// Filtrer côté serveur par type de contrat
let filteredPayslips = (allPayslips || []).filter(p => {
const contract = Array.isArray(p.cddu_contracts) ? p.cddu_contracts[0] : p.cddu_contracts;
if (!contract) return false;
if (typeDeContrat === "RG") {
return contract.type_de_contrat === "CDD de droit commun" || contract.type_de_contrat === "CDI";
} else {
return contract.type_de_contrat === typeDeContrat;
}
});
console.log('[staff/payslips/search] Filtered to', filteredPayslips.length, 'payslips after contract type filter');
// Appliquer les autres filtres manuellement
let finalPayslips = filteredPayslips;
if (processed !== '') {
finalPayslips = finalPayslips.filter(p => p.processed === (processed === 'true'));
}
if (transferDone !== '') {
finalPayslips = finalPayslips.filter(p => p.transfer_done === (transferDone === 'true'));
// Si on filtre sur "virement en attente", ne garder que les clients Odentas
if (transferDone === 'false') {
finalPayslips = finalPayslips.filter((p: any) => {
const contract = Array.isArray(p.cddu_contracts) ? p.cddu_contracts[0] : p.cddu_contracts;
const org = contract?.organizations || (Array.isArray(p.cddu_contracts?.organizations) ? p.cddu_contracts.organizations[0] : p.cddu_contracts?.organizations);
const orgDetails = org?.organization_details || (Array.isArray(org?.organization_details) ? org.organization_details[0] : org?.organization_details);
return orgDetails?.virements_salaires?.toLowerCase() === 'odentas';
});
}
}
if (aemStatus) {
finalPayslips = finalPayslips.filter(p => p.aem_status === aemStatus);
}
// Tri côté serveur
const ascending = order === 'asc';
finalPayslips.sort((a, b) => {
let valueA: any, valueB: any;
// cddu_contracts est un objet, pas un tableau dans ce contexte
const contractA = Array.isArray(a.cddu_contracts) ? a.cddu_contracts[0] : a.cddu_contracts;
const contractB = Array.isArray(b.cddu_contracts) ? b.cddu_contracts[0] : b.cddu_contracts;
if (sort === 'employee_name') {
valueA = contractA?.employee_name || '';
valueB = contractB?.employee_name || '';
} else if (sort === 'contract_number') {
valueA = contractA?.contract_number || '';
valueB = contractB?.contract_number || '';
} else if (sort === 'structure') {
valueA = contractA?.structure || '';
valueB = contractB?.structure || '';
} else if (sort === 'analytique') {
valueA = contractA?.analytique || '';
valueB = contractB?.analytique || '';
} else {
valueA = (a as any)[sort];
valueB = (b as any)[sort];
}
if (typeof valueA === 'string') valueA = valueA.toLowerCase();
if (typeof valueB === 'string') valueB = valueB.toLowerCase();
if (valueA < valueB) return ascending ? -1 : 1;
if (valueA > valueB) return ascending ? 1 : -1;
return 0;
});
// Pagination
const paginatedPayslips = finalPayslips.slice(offset, offset + limit);
return NextResponse.json({
rows: paginatedPayslips,
count: finalPayslips.length
});
}
// Filtre par état de traitement
if (processed !== '') {
query = query.eq('processed', processed === 'true');
}
// Filtre par état de virement
if (transferDone !== '') {
query = query.eq('transfer_done', transferDone === 'true');
// Si on filtre sur "virement en attente", ne garder que les clients avec virements_salaires = "Odentas"
if (transferDone === 'false') {
const { data: odentasOrgs } = await supabase
.from('organization_details')
.select('org_id')
.ilike('virements_salaires', 'odentas');
if (odentasOrgs && odentasOrgs.length > 0) {
const odentasOrgIds = odentasOrgs.map(o => o.org_id).filter(Boolean);
query = query.in('organization_id', odentasOrgIds);
} else {
// Aucune org Odentas, retourner vide
return NextResponse.json({ rows: [], count: 0 });
}
}
}
// Filtre par AEM
if (aemStatus) {
query = query.eq('aem_status', aemStatus);
}
// Filtre par période
if (periodFrom) {
query = query.gte('period_start', periodFrom);
}
if (periodTo) {
query = query.lte('period_start', periodTo);
}
// Tri
const ascending = order === 'asc';
// Pour le tri par des champs de la jointure, on devra trier côté client
// Pour l'instant, on trie uniquement sur les champs de payslips
const sortableFields = ['period_start', 'period_end', 'pay_number', 'pay_date', 'gross_amount', 'net_amount', 'created_at'];
if (sortableFields.includes(sort)) {
query = query.order(sort, { ascending });
} else {
// Fallback sur period_start
query = query.order('period_start', { ascending });
}
// Pagination
query = query.range(offset, offset + limit - 1);
console.log('[staff/payslips/search] Executing query...');
const { data: payslips, error, count } = await query;
if (error) {
console.error('[staff/payslips/search] Supabase query error:', error);
console.error('[staff/payslips/search] Error details:', { message: error.message, details: error.details, hint: error.hint, code: error.code });
return NextResponse.json({ error: error.message }, { status: 500 });
}
// Si le tri est sur des champs de la jointure, on trie côté serveur
let sortedPayslips = payslips || [];
if (sort === 'contract_number' || sort === 'employee_name' || sort === 'structure' || sort === 'analytique') {
sortedPayslips = [...sortedPayslips].sort((a: any, b: any) => {
let valueA: any;
let valueB: any;
if (sort === 'contract_number') {
valueA = a.cddu_contracts?.contract_number || '';
valueB = b.cddu_contracts?.contract_number || '';
} else if (sort === 'employee_name') {
valueA = a.cddu_contracts?.salaries?.nom || a.cddu_contracts?.employee_name || '';
valueB = b.cddu_contracts?.salaries?.nom || b.cddu_contracts?.employee_name || '';
} else if (sort === 'structure') {
valueA = a.cddu_contracts?.structure || '';
valueB = b.cddu_contracts?.structure || '';
} else if (sort === 'analytique') {
valueA = a.cddu_contracts?.analytique || '';
valueB = b.cddu_contracts?.analytique || '';
}
if (typeof valueA === 'string') valueA = valueA.toLowerCase();
if (typeof valueB === 'string') valueB = valueB.toLowerCase();
if (valueA < valueB) return ascending ? -1 : 1;
if (valueA > valueB) return ascending ? 1 : -1;
return 0;
});
}
console.log(`[staff/payslips/search] Found ${sortedPayslips.length} payslips (total: ${count})`);
return NextResponse.json({
rows: sortedPayslips,
count: count ?? sortedPayslips.length
});
} catch (error: any) {
console.error('[staff/payslips/search] Unexpected error:', error);
console.error('[staff/payslips/search] Error details:', { message: error?.message, stack: error?.stack });
return NextResponse.json({ error: error?.message || "Erreur lors de la recherche" }, { status: 500 });
}
}