424 lines
18 KiB
TypeScript
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 });
|
|
}
|
|
}
|