- 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
144 lines
6.6 KiB
TypeScript
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 });
|
|
}
|
|
}
|