Payslips staff + statut AEM contrats et contrats-multi
This commit is contained in:
parent
68cbe3bc6e
commit
ecc84d9652
11 changed files with 1846 additions and 6 deletions
|
|
@ -1123,10 +1123,10 @@ export default function ContratMultiPage() {
|
|||
.sort((a,b) => (b.ordre ?? 0) - (a.ordre ?? 0))
|
||||
.map((p) => {
|
||||
const label = payLabel(p) || (p.ordre ? `Paie ${p.ordre}` : 'Paie');
|
||||
// Normalize aem_statut for robust checks (API can return 'OK', 'ok', etc.)
|
||||
// Normalize aem_statut for robust checks (API can return 'OK', 'ok', 'Traité', etc.)
|
||||
const aemNorm = String((p as any).aem_statut || '').normalize('NFD').replace(/\p{Diacritic}/gu, '').trim().toLowerCase();
|
||||
const aemOk = aemNorm === 'ok' || aemNorm === 'oui' || aemNorm.includes('ok');
|
||||
const aemPending = aemNorm.includes('a_traiter') || aemNorm.includes('a traiter') || aemNorm.includes('traiter');
|
||||
const aemOk = aemNorm === 'ok' || aemNorm === 'oui' || aemNorm.includes('ok') || aemNorm === 'traite' || aemNorm === 'traitee' || aemNorm.includes('traite');
|
||||
const aemPending = aemNorm.includes('a_traiter') || aemNorm.includes('a traiter') || (aemNorm.includes('traiter') && !aemNorm.includes('traite'));
|
||||
|
||||
const CardInner = (
|
||||
<div className="h-full rounded-2xl border bg-white p-4 hover:shadow-md transition-shadow relative">
|
||||
|
|
|
|||
|
|
@ -1476,7 +1476,11 @@ return (
|
|||
<Field label="Net après PAS" value={formatEUR(slip.net_after_withholding)} />
|
||||
<Field label="Brut" value={formatEUR(slip.gross_amount)} />
|
||||
<Field label="Coût total employeur" value={formatEUR(slip.employer_cost)} />
|
||||
<Field label="AEM" value={slip.aem_status === "OK" ? <Badge tone="ok">AEM OK</Badge> : <Badge tone="warn">À traiter</Badge>} />
|
||||
<Field label="AEM" value={(() => {
|
||||
const aemNorm = String(slip.aem_status || '').normalize('NFD').replace(/\p{Diacritic}/gu, '').trim().toLowerCase();
|
||||
const aemOk = aemNorm === 'ok' || aemNorm === 'oui' || aemNorm.includes('ok') || aemNorm === 'traite' || aemNorm === 'traitee' || aemNorm.includes('traite');
|
||||
return aemOk ? <Badge tone="ok">AEM OK</Badge> : <Badge tone="warn">À traiter</Badge>;
|
||||
})()} />
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
|
|
|
|||
98
app/(app)/staff/payslips/page.tsx
Normal file
98
app/(app)/staff/payslips/page.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// app/(app)/staff/payslips/page.tsx
|
||||
import { cookies } from "next/headers";
|
||||
import NextDynamic from "next/dynamic";
|
||||
import { createSbServer } from "@/lib/supabaseServer";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const PayslipsGrid = NextDynamic<any>(() => import("../../../../components/staff/PayslipsGrid"), { ssr: false });
|
||||
|
||||
export default async function StaffPayslipsPage() {
|
||||
const sb = createSbServer();
|
||||
const { data: { user } } = await sb.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<main className="p-6">
|
||||
<h1 className="text-lg font-semibold">Accès refusé</h1>
|
||||
<p className="text-sm text-slate-600">Vous devez être connecté.</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const { data: me } = await sb.from("staff_users").select("is_staff").eq("user_id", user.id).maybeSingle();
|
||||
const isStaff = !!me?.is_staff;
|
||||
if (!isStaff) {
|
||||
return (
|
||||
<main className="p-6">
|
||||
<h1 className="text-lg font-semibold">Accès refusé</h1>
|
||||
<p className="text-sm text-slate-600">Cette page est réservée au Staff.</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// Initial fetch: server-side list of latest payslips (limited)
|
||||
// Utiliser une jointure pour récupérer les informations du contrat et du salarié
|
||||
const { data: payslips, error } = await sb
|
||||
.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,
|
||||
salaries!employee_id(salarie, nom, prenom)
|
||||
)`
|
||||
)
|
||||
.order("period_start", { ascending: false })
|
||||
.limit(200);
|
||||
|
||||
// Server-side debug logging
|
||||
try {
|
||||
console.log("[staff/payslips] supabase fetch payslips result count:", Array.isArray(payslips) ? payslips.length : typeof payslips);
|
||||
if (error) console.log("[staff/payslips] supabase error:", error);
|
||||
if (payslips && payslips.length > 0) console.log("[staff/payslips] sample:", payslips.slice(0, 3));
|
||||
} catch (e) {
|
||||
// ignore logging errors
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<main className="p-6">
|
||||
<h1 className="text-lg font-semibold">Erreur</h1>
|
||||
<p className="text-sm text-rose-600">{error.message}</p>
|
||||
<div className="mt-4 p-3 bg-slate-50 rounded text-sm text-slate-700">
|
||||
<div><strong>Debug</strong></div>
|
||||
<div>Supabase returned an error when reading <code>payslips</code>. See server logs for details.</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// active org (optional) for staff we allow global view
|
||||
const c = cookies();
|
||||
const activeOrgId = c.get("active_org_id")?.value || null;
|
||||
|
||||
return (
|
||||
<main className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-lg font-semibold">Fiches de paie (Staff)</h1>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border bg-white p-4">
|
||||
{(!payslips || payslips.length === 0) && (
|
||||
<div className="mb-4 p-3 rounded bg-yellow-50 text-sm text-slate-800 border">
|
||||
<div><strong>Debug:</strong> Aucune fiche de paie trouvée côté serveur (initialData vide).</div>
|
||||
<div className="mt-2 text-xs text-slate-600">Si tu utilises Realtime / RLS, vérifie que la table <code>payslips</code> est publiée pour Realtime et que les policies autorisent la lecture pour ton utilisateur staff.</div>
|
||||
<details className="mt-2 text-xs">
|
||||
<summary className="cursor-pointer text-xs underline">Voir le payload serveur (preview)</summary>
|
||||
<pre className="mt-2 max-h-48 overflow-auto text-xs bg-slate-50 p-2 rounded border">{JSON.stringify(payslips ?? [], null, 2)}</pre>
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
{/* Client-side interactive grid */}
|
||||
<PayslipsGrid initialData={payslips ?? []} activeOrgId={activeOrgId} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ export async function GET(
|
|||
);
|
||||
const r = await admin
|
||||
.from("cddu_contracts")
|
||||
.select("id, contract_pdf_s3_key, employee_email")
|
||||
.select("id, contract_pdf_s3_key")
|
||||
.eq("id", params.id)
|
||||
.single();
|
||||
contract = r.data;
|
||||
|
|
@ -52,7 +52,7 @@ export async function GET(
|
|||
} else {
|
||||
const r = await sb
|
||||
.from("cddu_contracts")
|
||||
.select("id, contract_pdf_s3_key, employee_email")
|
||||
.select("id, contract_pdf_s3_key")
|
||||
.eq("id", params.id)
|
||||
.single();
|
||||
contract = r.data;
|
||||
|
|
|
|||
59
app/api/staff/payslips/bulk-delete/route.ts
Normal file
59
app/api/staff/payslips/bulk-delete/route.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
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 POST(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 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { payslipIds } = body;
|
||||
|
||||
if (!Array.isArray(payslipIds) || payslipIds.length === 0) {
|
||||
return NextResponse.json({ error: "payslipIds requis" }, { status: 400 });
|
||||
}
|
||||
|
||||
console.log(`[bulk-delete] Suppression de ${payslipIds.length} payslips:`, payslipIds);
|
||||
|
||||
// Suppression groupée
|
||||
const { data, error } = await supabase
|
||||
.from("payslips")
|
||||
.delete()
|
||||
.in("id", payslipIds)
|
||||
.select("id");
|
||||
|
||||
if (error) {
|
||||
console.error('[bulk-delete] Error:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
console.log(`[bulk-delete] ${data?.length || 0} payslips supprimées avec succès`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
deleted: data?.length || 0,
|
||||
payslips: data
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[bulk-delete] Unexpected error:', error);
|
||||
return NextResponse.json({ error: "Erreur lors de la suppression" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
55
app/api/staff/payslips/bulk-update-aem/route.ts
Normal file
55
app/api/staff/payslips/bulk-update-aem/route.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
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 POST(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 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { payslipIds, aemStatus } = body;
|
||||
|
||||
if (!Array.isArray(payslipIds) || payslipIds.length === 0) {
|
||||
return NextResponse.json({ error: "payslipIds requis" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!aemStatus || typeof aemStatus !== 'string') {
|
||||
return NextResponse.json({ error: "aemStatus requis" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Mise à jour groupée
|
||||
const { data, error } = await supabase
|
||||
.from("payslips")
|
||||
.update({ aem_status: aemStatus })
|
||||
.in("id", payslipIds)
|
||||
.select("id, aem_status");
|
||||
|
||||
if (error) {
|
||||
console.error('[bulk-update-aem] Error:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ payslips: data });
|
||||
} catch (error) {
|
||||
console.error('[bulk-update-aem] Unexpected error:', error);
|
||||
return NextResponse.json({ error: "Erreur lors de la mise à jour" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
55
app/api/staff/payslips/bulk-update-processed/route.ts
Normal file
55
app/api/staff/payslips/bulk-update-processed/route.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
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 POST(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 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { payslipIds, processed } = body;
|
||||
|
||||
if (!Array.isArray(payslipIds) || payslipIds.length === 0) {
|
||||
return NextResponse.json({ error: "payslipIds requis" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (typeof processed !== 'boolean') {
|
||||
return NextResponse.json({ error: "processed doit être un booléen" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Mise à jour groupée
|
||||
const { data, error } = await supabase
|
||||
.from("payslips")
|
||||
.update({ processed })
|
||||
.in("id", payslipIds)
|
||||
.select("id, processed");
|
||||
|
||||
if (error) {
|
||||
console.error('[bulk-update-processed] Error:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ payslips: data });
|
||||
} catch (error) {
|
||||
console.error('[bulk-update-processed] Unexpected error:', error);
|
||||
return NextResponse.json({ error: "Erreur lors de la mise à jour" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
55
app/api/staff/payslips/bulk-update-transfer/route.ts
Normal file
55
app/api/staff/payslips/bulk-update-transfer/route.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
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 POST(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 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { payslipIds, transferDone } = body;
|
||||
|
||||
if (!Array.isArray(payslipIds) || payslipIds.length === 0) {
|
||||
return NextResponse.json({ error: "payslipIds requis" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (typeof transferDone !== 'boolean') {
|
||||
return NextResponse.json({ error: "transferDone doit être un booléen" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Mise à jour groupée
|
||||
const { data, error } = await supabase
|
||||
.from("payslips")
|
||||
.update({ transfer_done: transferDone })
|
||||
.in("id", payslipIds)
|
||||
.select("id, transfer_done");
|
||||
|
||||
if (error) {
|
||||
console.error('[bulk-update-transfer] Error:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ payslips: data });
|
||||
} catch (error) {
|
||||
console.error('[bulk-update-transfer] Unexpected error:', error);
|
||||
return NextResponse.json({ error: "Erreur lors de la mise à jour" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
286
app/api/staff/payslips/search/route.ts
Normal file
286
app/api/staff/payslips/search/route.ts
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
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, created_at,
|
||||
cddu_contracts!contract_id(
|
||||
id, contract_number, employee_name, employee_id, structure, type_de_contrat, org_id,
|
||||
salaries!employee_id(salarie, nom, prenom)
|
||||
)`,
|
||||
{ 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 } = await supabase
|
||||
.from("cddu_contracts")
|
||||
.select("id")
|
||||
.eq("structure", structure);
|
||||
|
||||
if (matchingContracts && matchingContracts.length > 0) {
|
||||
const contractIds = matchingContracts.map(c => c.id);
|
||||
query = query.in('contract_id', contractIds);
|
||||
} else {
|
||||
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,
|
||||
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'));
|
||||
}
|
||||
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 {
|
||||
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');
|
||||
}
|
||||
|
||||
// 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') {
|
||||
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 || '';
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -520,6 +520,14 @@ export default function Sidebar({ clientInfo, isStaff = false, mobile = false, o
|
|||
<span>Contrats</span>
|
||||
</span>
|
||||
</Link>
|
||||
<Link href="/staff/payslips" onClick={() => onNavigate && onNavigate()} className={`block px-3 py-2 rounded-xl text-sm transition truncate ${
|
||||
isActivePath(pathname, "/staff/payslips") ? "bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-200 text-slate-700 font-semibold" : "hover:bg-slate-50"
|
||||
}`} title="Gestion des fiches de paie">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<CreditCard className="w-4 h-4" aria-hidden />
|
||||
<span>Fiches de paie</span>
|
||||
</span>
|
||||
</Link>
|
||||
<Link href="/staff/salaries" onClick={() => onNavigate && onNavigate()} className={`block px-3 py-2 rounded-xl text-sm transition truncate ${
|
||||
isActivePath(pathname, "/staff/salaries") ? "bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-200 text-slate-700 font-semibold" : "hover:bg-slate-50"
|
||||
}`} title="Gestion des salariés">
|
||||
|
|
|
|||
1220
components/staff/PayslipsGrid.tsx
Normal file
1220
components/staff/PayslipsGrid.tsx
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue