508 lines
19 KiB
TypeScript
508 lines
19 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 {
|
|
// 🎭 Mode démo : retourner des données fictives
|
|
const isDemoMode = process.env.DEMO_MODE === 'true';
|
|
|
|
if (isDemoMode) {
|
|
console.log('🎭 [API /salaries] Mode démo - retour de données fictives');
|
|
|
|
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") || "10", 10) || 10));
|
|
const q = (searchParams.get("search") || searchParams.get("q") || "").trim();
|
|
|
|
// Données fictives de salariés
|
|
const DEMO_SALARIES: SalarieRow[] = [
|
|
{
|
|
matricule: "demo-sal-001",
|
|
nom: "MARTIN Alice",
|
|
email: "alice.martin@demo.fr",
|
|
transat_connecte: true,
|
|
dernier_emploi: "Comédien",
|
|
code_salarie: "demo-sal-001",
|
|
prenom: "Alice",
|
|
civilite: "Mme",
|
|
tel: "06 12 34 56 78",
|
|
org_id: "demo-org",
|
|
org_name: "Organisation Démo",
|
|
},
|
|
{
|
|
matricule: "demo-sal-002",
|
|
nom: "DUBOIS Pierre",
|
|
email: "pierre.dubois@demo.fr",
|
|
transat_connecte: false,
|
|
dernier_emploi: "Metteur en scène",
|
|
code_salarie: "demo-sal-002",
|
|
prenom: "Pierre",
|
|
civilite: "M.",
|
|
tel: "06 23 45 67 89",
|
|
org_id: "demo-org",
|
|
org_name: "Organisation Démo",
|
|
},
|
|
{
|
|
matricule: "demo-sal-003",
|
|
nom: "LEROY Sophie",
|
|
email: "sophie.leroy@demo.fr",
|
|
transat_connecte: true,
|
|
dernier_emploi: "Danseur",
|
|
code_salarie: "demo-sal-003",
|
|
prenom: "Sophie",
|
|
civilite: "Mme",
|
|
tel: "06 34 56 78 90",
|
|
org_id: "demo-org",
|
|
org_name: "Organisation Démo",
|
|
},
|
|
{
|
|
matricule: "demo-sal-004",
|
|
nom: "BERNARD Marc",
|
|
email: "marc.bernard@demo.fr",
|
|
transat_connecte: false,
|
|
dernier_emploi: "Technicien son",
|
|
code_salarie: "demo-sal-004",
|
|
prenom: "Marc",
|
|
civilite: "M.",
|
|
tel: "06 45 67 89 01",
|
|
org_id: "demo-org",
|
|
org_name: "Organisation Démo",
|
|
},
|
|
{
|
|
matricule: "demo-sal-005",
|
|
nom: "GARCIA Elena",
|
|
email: "elena.garcia@demo.fr",
|
|
transat_connecte: true,
|
|
dernier_emploi: "Costumière",
|
|
code_salarie: "demo-sal-005",
|
|
prenom: "Elena",
|
|
civilite: "Mme",
|
|
tel: "06 56 78 90 12",
|
|
org_id: "demo-org",
|
|
org_name: "Organisation Démo",
|
|
},
|
|
];
|
|
|
|
// Filtrer par recherche si nécessaire
|
|
let filteredSalaries = DEMO_SALARIES;
|
|
if (q) {
|
|
const searchTerm = q.toLowerCase();
|
|
filteredSalaries = DEMO_SALARIES.filter(salarie =>
|
|
salarie.nom.toLowerCase().includes(searchTerm) ||
|
|
salarie.prenom?.toLowerCase().includes(searchTerm) ||
|
|
salarie.matricule.toLowerCase().includes(searchTerm) ||
|
|
salarie.email?.toLowerCase().includes(searchTerm) ||
|
|
salarie.dernier_emploi?.toLowerCase().includes(searchTerm)
|
|
);
|
|
}
|
|
|
|
// Pagination
|
|
const offset = (page - 1) * limit;
|
|
const paginatedSalaries = filteredSalaries.slice(offset, offset + limit);
|
|
const total = filteredSalaries.length;
|
|
const hasMore = offset + paginatedSalaries.length < total;
|
|
const totalPages = Math.ceil(total / limit);
|
|
|
|
const payload: SalariesResponse = {
|
|
items: paginatedSalaries,
|
|
page,
|
|
limit,
|
|
total,
|
|
totalPages,
|
|
hasMore,
|
|
};
|
|
|
|
return NextResponse.json(payload);
|
|
}
|
|
|
|
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, email_notifs, email_notifs_cc').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,
|
|
email_notifs: ('data' in orgDetailsResult && orgDetailsResult.data) ? orgDetailsResult.data.email_notifs || null : null,
|
|
email_notifs_cc: ('data' in orgDetailsResult && orgDetailsResult.data) ? orgDetailsResult.data.email_notifs_cc || null : null
|
|
} : null
|
|
};
|
|
|
|
console.log('🔍 [EMAIL] orgDetails final:', orgDetails);
|
|
|
|
// 1. Email de notification à l'employeur (corrigé: utilise email_notifs au lieu de user.email)
|
|
if (orgDetails?.data?.email_notifs) {
|
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
|
|
|
// Nettoyer et valider l'email principal
|
|
const cleanEmail = (email: string | null): string | null => {
|
|
if (!email) return null;
|
|
return email.trim().toLowerCase();
|
|
};
|
|
|
|
const emailNotifs = cleanEmail(orgDetails.data.email_notifs);
|
|
const emailNotifsCC = cleanEmail(orgDetails.data.email_notifs_cc);
|
|
|
|
console.log('📧 [EMAIL] Envoi à email_notifs:', emailNotifs);
|
|
console.log('📧 [EMAIL] CC à email_notifs_cc:', emailNotifsCC || 'Aucun');
|
|
|
|
if (emailNotifs) {
|
|
await sendUniversalEmailV2({
|
|
type: 'employee-created',
|
|
toEmail: emailNotifs,
|
|
ccEmail: emailNotifsCC || undefined,
|
|
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}`,
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
console.warn('⚠️ [EMAIL] email_notifs non défini pour l\'organisation', orgId);
|
|
}
|
|
|
|
// 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 });
|
|
}
|
|
}
|
|
|