espace-paie-odentas/app/api/salaries/route.ts

376 lines
14 KiB
TypeScript

// (POST handler is defined later in this file)
export const dynamic = "force-dynamic";
import { NextResponse, NextRequest } from "next/server";
import { createSbServer, createSbServiceRole } from "@/lib/supabaseServer";
import { resolveActiveOrg } from "@/lib/resolveActiveOrg";
import { sendUniversalEmailV2 } from "@/lib/emailTemplateService";
import { generateAutoDeclarationToken } from "@/lib/autoDeclarationTokenService";
type SalarieRow = {
matricule: string;
nom: string;
email?: string | null;
transat_connecte?: boolean;
dernier_emploi?: string | null;
code_salarie?: string | null;
prenom?: string | null;
civilite?: string | null;
tel?: string | null;
org_id?: string | null;
org_name?: string | null;
};
type SalariesResponse = {
items: SalarieRow[];
page: number;
limit: number;
total?: number | null;
totalPages?: number;
hasMore: boolean;
};
async function resolveActiveOrgId(sb: any, _isStaff: boolean) {
return await resolveActiveOrg(sb);
}
export async function GET(req: NextRequest) {
try {
const sb = createSbServer();
const {
data: { user },
error: authError,
} = await sb.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
// Determine if current user is staff
let isStaff = false;
try {
const { data } = await sb
.from("staff_users")
.select("is_staff")
.eq("user_id", user.id)
.maybeSingle();
isStaff = !!data?.is_staff;
} catch {}
const orgId = await resolveActiveOrgId(sb, isStaff);
const { searchParams } = new URL(req.url);
const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10) || 1);
const limit = Math.min(100, Math.max(1, parseInt(searchParams.get("limit") || "25", 10) || 25));
// Accept either `search` or `q` (some clients send `q`)
const q = (searchParams.get("search") || searchParams.get("q") || "").trim();
console.log("👥 API salaries search:", { q, page, limit, user_id: user.id });
const offset = (page - 1) * limit;
const to = offset + limit - 1;
// Build query
let query = sb
.from("salaries")
.select(
`id, code_salarie, num_salarie, salarie, nom, prenom, civilite, adresse_mail, compte_transat, derniere_profession, employer_id, infos_caisses_organismes, tel`,
{ count: "exact" }
);
// If an organization id was resolved, scope results to that employer.
if (orgId) {
query = query.eq("employer_id", orgId);
}
if (q) {
const like = `%${q}%`;
// Use OR with ilike across relevant columns
query = query.or(
[
`nom.ilike.${like}`,
`prenom.ilike.${like}`,
`salarie.ilike.${like}`,
`adresse_mail.ilike.${like}`,
`code_salarie.ilike.${like}`,
`num_salarie.ilike.${like}`,
].join(",")
);
}
// Order by last name then first name for stability
query = query.order("nom", { ascending: true }).order("prenom", { ascending: true, nullsFirst: true }).range(offset, to);
const { data, error, count } = await query;
console.log("👥 API salaries result:", {
data: data?.length,
error: error?.message,
count
});
if (error) {
console.error("💥 [API /salaries] Supabase error:", error.message);
return NextResponse.json({ error: "supabase_error", detail: error.message }, { status: 500 });
}
const items: SalarieRow[] = (data || []).map((r: any) => {
const fullName = r.salarie || [r.nom, r.prenom].filter(Boolean).join(" ").trim() || r.nom || "";
const comp = (r.compte_transat || "").toString().toLowerCase();
const transat = comp.includes("connect") && !comp.includes("non");
return {
matricule: r.code_salarie || (r.num_salarie ? String(r.num_salarie) : r.id),
nom: fullName,
email: r.adresse_mail ?? null,
transat_connecte: transat,
dernier_emploi: r.derniere_profession ?? null,
code_salarie: r.code_salarie ?? null,
prenom: r.prenom ?? null,
civilite: r.civilite ?? null,
tel: r.tel ?? null,
org_id: r.employer_id || null,
org_name: r.infos_caisses_organismes || null,
} as SalarieRow;
});
const total = count ?? null;
const hasMore = typeof total === "number" ? offset + items.length < total : items.length === limit;
const totalPages = typeof total === "number" ? Math.ceil(total / limit) : 0;
const payload: SalariesResponse = {
items,
page,
limit,
total,
totalPages,
hasMore,
};
return NextResponse.json(payload);
} catch (e: any) {
console.error("💥 [API /salaries] Unexpected error:", e?.message);
return NextResponse.json({ error: "server_error", message: e?.message || "unknown" }, { status: 500 });
}
}
// POST: create a new salary record (used by the nouveau salarié form)
export async function POST(req: NextRequest) {
try {
let body: any = {};
try {
body = await req.json();
} catch {
return NextResponse.json({ ok: false, error: 'invalid_json' }, { status: 400 });
}
if (!body || !body.nom || !body.prenom || !body.email_salarie) {
return NextResponse.json({ ok: false, error: 'missing_required_fields' }, { status: 400 });
}
const supabase = createSbServiceRole();
// Determine employer/organization id: prefer explicit body.employer_id, else try to resolve from session
let orgId: string | null = body.employer_id || null;
if (!orgId) {
try {
const sbAuth = createSbServer();
const { data: { user }, error: authErr } = await sbAuth.auth.getUser();
if (!authErr && user) {
// resolveActiveOrg expects a Supabase client
const maybeOrg = await resolveActiveOrg(sbAuth);
if (maybeOrg) orgId = maybeOrg;
}
} catch (e) {
// ignore and continue (orgId may remain null)
}
}
// Compute next matricule/code_salarie for this org if possible
let computedCode: string | null = null;
let computedNum: number | null = null;
if (orgId) {
try {
// Récupérer le code employeur depuis organization_details
const { data: orgDetailsData, error: orgDetailsError } = await supabase
.from('organization_details')
.select('code_employeur')
.eq('org_id', orgId)
.single();
console.log('🔍 [MATRICULE] Requête organization_details:', {
orgId,
orgDetailsData,
orgDetailsError,
hasCodeEmployeur: !!orgDetailsData?.code_employeur
});
const codeEmployeur = orgDetailsData?.code_employeur || '';
console.log('🔍 [MATRICULE] code_employeur final:', codeEmployeur);
const { data: rows, error: qerr } = await supabase
.from('salaries')
.select('code_salarie')
.eq('employer_id', orgId)
.not('code_salarie', 'is', null)
.limit(1000);
if (!qerr && Array.isArray(rows)) {
console.log('🔍 [MATRICULE] Nombre de salariés existants:', rows.length);
let maxNum = -Infinity;
let detectedPrefix = '';
let maxPad = 0;
for (const r of rows) {
const s = (r?.code_salarie || '').toString();
const m = s.match(/(\d+)$/);
if (m) {
const numStr = m[1];
const num = parseInt(numStr, 10);
if (!Number.isNaN(num) && num > maxNum) {
maxNum = num;
detectedPrefix = s.slice(0, s.length - numStr.length);
maxPad = numStr.length;
}
}
}
if (maxNum !== -Infinity) {
// On a trouvé des matricules existants
const nextNum = maxNum + 1;
const padded = String(nextNum).padStart(Math.max(maxPad, 3), '0');
// Utiliser le code employeur comme préfixe, même si les anciens n'en avaient pas
const prefix = codeEmployeur || detectedPrefix;
computedCode = `${prefix}${padded}`;
computedNum = nextNum;
console.log('✅ [MATRICULE] Incrémentation:', computedCode, '(prefix utilisé:', prefix, 'prefix détecté:', detectedPrefix, ')');
} else {
// No existing numeric matricules found: use code_employeur as prefix
computedNum = 1;
computedCode = codeEmployeur ? `${codeEmployeur}001` : `001`;
console.log('✅ [MATRICULE] Premier salarié, code généré:', computedCode, '(codeEmployeur:', codeEmployeur, ')');
}
}
} catch (e) {
console.error('❌ [MATRICULE] Erreur génération:', e);
// ignore computation errors and continue without code
}
}
const fullName = [body.nom, body.prenom].filter(Boolean).join(" ").trim();
// Only include columns that exist in the `salaries` table to avoid PGRST204
const payload: any = {
salarie: fullName || null,
nom: body.nom || null,
prenom: body.prenom || null,
nom_de_naissance: body.nomnaissance_salarie || body.nom_naissance || null,
// pseudonyme fallback to 'n/a' when empty
pseudonyme: (body.pseudonyme || body.pseudo || "").toString().trim() || "n/a",
civilite: body.civilite_salarie || body.civilite || null,
// map address text into the `adresse` column (table has `adresse`)
adresse: body.adresse_salarie || body.adresse || null,
// keep primary contact fields that exist in the table
adresse_mail: body.email_salarie || body.email || null,
tel: body.tel_salarie || body.telephone || null,
date_naissance: body.dob_salarie || body.dateNaissance || null,
lieu_de_naissance: body.cob_salarie || body.lieuNaissance || null,
nir: body.ss_salarie || body.nir || null,
conges_spectacles: body.cs_salarie || body.conges_spectacles || null,
iban: body.iban_salarie || body.iban || null,
bic: body.bic_salarie || body.bic || null,
// enforce requested defaults
compte_transat: "En cours",
topaze: "NC",
justificatifs_personnels: body.justificatifs_personnels || "En attente de réception",
rf_au_sens_fiscal: true,
intermittent_mineur_16: false,
abattement_2024: body.abattement_2024 || "Sans objet",
notif_nouveau_salarie: "Fait",
notif_employeur: "Fait",
infos_caisses_organismes: body.structure || null,
employer_id: orgId || body.employer_id || null,
// computed matricule fields
code_salarie: computedCode,
num_salarie: computedNum,
};
const { data, error } = await supabase.from('salaries').insert([payload]).select().single();
if (error) {
console.error('❌ [API /salaries POST] supabase error', error);
return NextResponse.json({ ok: false, error: 'db_error', detail: error }, { status: 500 });
}
// Envoyer les e-mails de notification après la création réussie
try {
const sbAuth = createSbServer();
const { data: { user } } = await sbAuth.auth.getUser();
// Récupérer les infos de l'organisation depuis les deux tables
const orgData = orgId ? await supabase.from('organizations').select('name').eq('id', orgId).single() : { data: null };
const orgDetailsResult = orgId ? await supabase.from('organization_details').select('code_employeur, prenom_contact').eq('org_id', orgId).single() : { data: null, error: null };
console.log('🔍 [EMAIL] Données récupérées:', {
orgId,
orgData: orgData?.data,
orgDetailsData: 'data' in orgDetailsResult ? orgDetailsResult.data : null,
orgDetailsError: 'error' in orgDetailsResult ? orgDetailsResult.error : null
});
const orgDetails = {
data: orgData?.data ? {
name: orgData.data.name,
code_employeur: ('data' in orgDetailsResult && orgDetailsResult.data) ? orgDetailsResult.data.code_employeur || null : null,
prenom_contact: ('data' in orgDetailsResult && orgDetailsResult.data) ? orgDetailsResult.data.prenom_contact || null : null
} : null
};
console.log('🔍 [EMAIL] orgDetails final:', orgDetails);
// 1. Email de notification à l'équipe (existant)
if (user && orgDetails?.data) {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
await sendUniversalEmailV2({
type: 'employee-created',
toEmail: user.email || 'paie@odentas.fr', // Fallback
data: {
userName: orgDetails.data.prenom_contact || user.user_metadata?.first_name || user.user_metadata?.display_name?.split(' ')[0] || 'Cher client',
companyName: orgDetails.data.name,
employerCode: orgDetails.data.code_employeur || 'N/A',
handlerName: 'Renaud BREVIERE-ABRAHAM',
employeeName: data.salarie,
email: data.adresse_mail,
matricule: data.code_salarie,
ctaUrl: `${baseUrl}/salaries/${data.id}`,
}
});
}
// 2. Générer token et envoyer invitation au salarié (nouveau)
if (data.adresse_mail) {
try {
const result = await generateAutoDeclarationToken({
salarie_id: data.id,
send_email: true
});
if (result.success) {
console.log('✅ [API /salaries POST] Token généré et invitation envoyée au salarié');
} else {
console.error('❌ [API /salaries POST] Erreur génération token:', result.error);
}
} catch (tokenError) {
console.error('❌ [API /salaries POST] Erreur lors de la génération du token:', tokenError);
}
} else {
console.warn('⚠️ [API /salaries POST] Pas d\'email pour le salarié, invitation non envoyée');
}
} catch (emailError) {
console.error('📧 [API /salaries POST] Failed to send email notifications:', emailError);
// Ne pas bloquer la réponse en cas d'échec des e-mails
}
return NextResponse.json({ ok: true, data }, { status: 201 });
} catch (e: any) {
console.error('💥 [API /salaries POST] unexpected', e?.message || e);
return NextResponse.json({ ok: false, error: 'server_exception', detail: e?.message || String(e) }, { status: 500 });
}
}