espace-paie-odentas/app/api/staff/contracts/search/route.ts
odentas d7bdb1ef08 feat: Add notification tracking system with smart reminders
- Add database columns for last_employer_notification_at and last_employee_notification_at in cddu_contracts
- Update all email sending endpoints to record timestamps (remind-employer, relance-salarie, docuseal-signature, signature-salarie)
- Create smart reminder system with 24h cooldown to prevent spam
- Add progress tracking modal with real-time status (pending/sending/success/error)
- Display actual employer/employee email addresses in reminder modal
- Show notification timestamps in contracts grid with color coding (green/orange/red based on contract start date)
- Change employer email button URL from DocuSeal direct link to /signatures-electroniques
- Create /api/staff/organizations/emails endpoint for bulk email fetching
- Add retroactive migration script for historical email_logs data
- Update Contract TypeScript type and API responses to include new fields
2025-10-22 21:49:35 +02:00

144 lines
6.6 KiB
TypeScript

export const dynamic = 'force-dynamic';
export const revalidate = 0;
export const runtime = 'nodejs';
import { NextResponse } from "next/server";
import { createSbServer } from "@/lib/supabaseServer";
export async function GET(req: Request) {
try {
const sb = createSbServer();
const { data: { user } } = await sb.auth.getUser();
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { data: me } = await sb.from("staff_users").select("is_staff").eq("user_id", user.id).maybeSingle();
if (!me?.is_staff) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
const url = new URL(req.url);
const q = url.searchParams.get("q");
const structure = url.searchParams.get("structure");
const type_de_contrat = url.searchParams.get("type_de_contrat");
const etat_de_la_demande = url.searchParams.get("etat_de_la_demande");
const etat_de_la_paie = url.searchParams.get("etat_de_la_paie");
const dpae = url.searchParams.get("dpae");
const signature_state = url.searchParams.get("signature_state");
const employee_matricule = url.searchParams.get("employee_matricule");
const start_from = url.searchParams.get("start_from");
const start_to = url.searchParams.get("start_to");
const end_from = url.searchParams.get("end_from");
const end_to = url.searchParams.get("end_to");
const sort = url.searchParams.get("sort") || "created_at";
const order = (url.searchParams.get("order") || "desc").toLowerCase() === "asc" ? "asc" : "desc";
const limit = Math.min(500, parseInt(url.searchParams.get("limit") || "100", 10));
const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0", 10));
// Build base query with salaries join
let query = sb.from("cddu_contracts").select(`
id, contract_number, employee_name, employee_matricule, employee_id, structure, type_de_contrat,
start_date, end_date, created_at, etat_de_la_demande, etat_de_la_paie, dpae, gross_pay,
contrat_signe_par_employeur, contrat_signe, org_id,
last_employer_notification_at, last_employee_notification_at,
salaries!employee_id(salarie, nom, prenom, adresse_mail)
`, { count: "exact" });
if (q) {
// simple ilike search on a few columns
query = query.or(`contract_number.ilike.%${q}%,employee_name.ilike.%${q}%,employee_matricule.ilike.%${q}%`);
}
if (employee_matricule) query = query.eq("employee_matricule", employee_matricule);
if (structure) query = query.eq("structure", structure);
// Handle special "RG" filter for common law contracts (CDD de droit commun + CDI)
if (type_de_contrat === "RG") {
query = query.in("type_de_contrat", ["CDD de droit commun", "CDI"]);
} else if (type_de_contrat) {
query = query.eq("type_de_contrat", type_de_contrat);
}
// Handle multiple etat_de_la_demande values (comma-separated)
if (etat_de_la_demande) {
const etats = etat_de_la_demande.split(',').map(e => e.trim()).filter(Boolean);
if (etats.length === 1) {
query = query.eq("etat_de_la_demande", etats[0]);
} else if (etats.length > 1) {
query = query.in("etat_de_la_demande", etats);
}
}
if (etat_de_la_paie) query = query.eq("etat_de_la_paie", etat_de_la_paie);
if (dpae) query = query.eq("dpae", dpae);
// Handle signature state filter
if (signature_state === "non_signe") {
query = query.or("contrat_signe_par_employeur.eq.Non,contrat_signe.eq.Non");
} else if (signature_state === "employeur_seulement") {
query = query.eq("contrat_signe_par_employeur", "Oui").eq("contrat_signe", "Non");
} else if (signature_state === "signe_complet") {
query = query.eq("contrat_signe_par_employeur", "Oui").eq("contrat_signe", "Oui");
}
if (start_from) query = query.gte("start_date", start_from);
if (start_to) query = query.lte("start_date", start_to);
if (end_from) query = query.gte("end_date", end_from);
if (end_to) query = query.lte("end_date", end_to);
// allow sort by start_date or end_date or created_at or employee_name
const allowedSorts = new Set(["start_date", "end_date", "created_at", "contract_number", "employee_name"]);
const sortCol = allowedSorts.has(sort) ? sort : "created_at";
// Pour le tri par nom, on doit traiter différemment
if (sortCol === "employee_name") {
// D'abord récupérer les données sans tri
query = query.range(offset, offset + limit - 1);
const { data: contractsData, error: contractsError, count } = await query;
if (contractsError) return NextResponse.json({ error: contractsError.message }, { status: 500 });
if (!contractsData || contractsData.length === 0) {
return NextResponse.json({ rows: [], count: count ?? 0 });
}
// Récupérer les informations des salariés pour le tri
const employeeIds = contractsData.map(c => c.employee_id).filter(Boolean);
const { data: salariesData, error: salariesError } = await sb
.from("salaries")
.select("id, nom, prenom")
.in("id", employeeIds);
if (salariesError) return NextResponse.json({ error: salariesError.message }, { status: 500 });
// Créer une map pour le tri
const salariesMap = new Map();
salariesData?.forEach(s => {
salariesMap.set(s.id, s.nom);
});
console.log("DEBUG TRI - Mapping salaries:", Array.from(salariesMap.entries()));
// Trier les contrats par nom de famille
const sortedContracts = contractsData.sort((a, b) => {
const nomA = salariesMap.get(a.employee_id) || '';
const nomB = salariesMap.get(b.employee_id) || '';
console.log(`DEBUG TRI - Comparaison: ${nomA} vs ${nomB} (employee_ids: ${a.employee_id} vs ${b.employee_id})`);
if (order === "asc") {
return nomA.localeCompare(nomB);
} else {
return nomB.localeCompare(nomA);
}
});
return NextResponse.json({ rows: sortedContracts, count: count ?? sortedContracts.length });
} else {
// Tri normal pour les autres colonnes
query = query.order(sortCol, { ascending: order === "asc" });
query = query.range(offset, offset + limit - 1);
const { data, error, count } = await query;
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ rows: data ?? [], count: count ?? (data ? data.length : 0) });
}
} catch (err: any) {
console.error(err);
return NextResponse.json({ error: "Internal" }, { status: 500 });
}
}