espace-paie-odentas/app/(app)/salaries/nouveau/page.tsx

863 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import Link from "next/link";
import { Loader2, ArrowLeft, X, Check } from "lucide-react";
import { usePageTitle } from "@/hooks/usePageTitle";
import { useDemoMode } from "@/hooks/useDemoMode";
type ClientInfo = {
id: string;
name: string;
api_name?: string;
} | null;
// Si ces composants existent déjà dans le projet (utilisés sur Nouveau CDDU), on les réutilise.
// Sinon, le markup Tailwind ci-dessous suffit car on produit des <div>/<label>/<input> standard.
function Label({ children, required = false }: { children: React.ReactNode; required?: boolean }) {
return (
<label className="block text-sm font-medium">
{children} {required && <span className="text-red-500">*</span>}
</label>
);
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="rounded-2xl border bg-white p-5">
<h2 className="text-base font-semibold mb-4">{title}</h2>
{children}
</section>
);
}
function FieldRow({ children }: { children: React.ReactNode }) {
return <div className="grid grid-cols-1 md:grid-cols-2 gap-5">{children}</div>;
}
// Helpers
const emailRx = /^[^\s@]+@[^\s@]+\.[^\s@]+$/i;
function upper(s: string) {
return s.toUpperCase();
}
function capitalizeFirst(s: string) {
if (!s) return s;
return s.charAt(0).toUpperCase() + s.slice(1);
}
// IBAN helpers (mod 97)
function normalizeIban(s: string) {
return s.replace(/\s+/g, "").toUpperCase();
}
function ibanToDigits(s: string) {
// A=10 ... Z=35
return s.replace(/[A-Z]/g, (ch) => String(ch.charCodeAt(0) - 55));
}
function isValidIBAN(input: string): boolean {
if (!input) return false;
const iban = normalizeIban(input);
if (iban.length < 15 || iban.length > 34) return false;
if (!/^[A-Z0-9]+$/.test(iban)) return false;
const rearranged = iban.slice(4) + iban.slice(0, 4);
const digits = ibanToDigits(rearranged);
let rem = 0;
for (let i = 0; i < digits.length; i++) {
const code = digits.charCodeAt(i) - 48; // '0' => 48
if (code < 0 || code > 9) return false;
rem = (rem * 10 + code) % 97;
}
return rem === 1;
}
export default function NouveauSalariePage() {
usePageTitle("Nouveau salarié");
const router = useRouter();
const search = useSearchParams();
const embed = (search.get("embed") || "").toLowerCase() === "1" || (search.get("embed") || "").toLowerCase() === "true";
const { isDemoMode } = useDemoMode();
// Récupération dynamique des infos client via /api/me
const { data: clientInfo } = useQuery({
queryKey: ["client-info"],
queryFn: async () => {
try {
const res = await fetch("/api/me", {
cache: "no-store",
headers: { Accept: "application/json" },
credentials: "include"
});
if (!res.ok) return null;
const me = await res.json();
return {
id: me.active_org_id || "unknown",
name: me.active_org_name || "Organisation",
api_name: me.active_org_api_name
} as ClientInfo;
} catch {
return null;
}
},
staleTime: 30_000, // Cache 30s
});
// UI states
const [loading, setLoading] = useState(false);
const [redirecting, setRedirecting] = useState(false);
const [err, setErr] = useState<string | null>(null);
// Onglets formulaire: simplifié / complet (design similaire à /contrats)
const [formMode, setFormMode] = useState<"simplifie" | "complet">("simplifie");
// États pour la gestion des organisations (staff uniquement)
const [isStaff, setIsStaff] = useState(false);
const [organizations, setOrganizations] = useState<{ id: string; name: string }[]>([]);
const [selectedOrg, setSelectedOrg] = useState<{ id: string; name: string } | null>(null);
// Form states
const [civilite, setCivilite] = useState<"Monsieur" | "Madame" | "">("");
const [nom, setNom] = useState("");
const [prenom, setPrenom] = useState("");
const [nomNaissance, setNomNaissance] = useState("");
const [pseudo, setPseudo] = useState("");
const [email, setEmail] = useState("");
const [adresse, setAdresse] = useState("");
const [complementAdresse, setComplementAdresse] = useState("");
const [telephone, setTelephone] = useState("");
// Autocomplete adresse.data.gouv.fr
const [addrQuery, setAddrQuery] = useState("");
const [addrOpen, setAddrOpen] = useState(false);
const [addrLoading, setAddrLoading] = useState(false);
const [addrResults, setAddrResults] = useState<Array<any>>([]);
const [addrTimer, setAddrTimer] = useState<any>(null);
const [addrMeta, setAddrMeta] = useState<{
postcode?: string; city?: string; insee?: string; lat?: string; lon?: string
} | null>(null);
const [dateNaissance, setDateNaissance] = useState(""); // YYYY-MM-DD
const [lieuNaissance, setLieuNaissance] = useState("");
const [nir, setNir] = useState("");
const [congesSpectacles, setCongesSpectacles] = useState("");
const [iban, setIban] = useState("");
const [ibanError, setIbanError] = useState<string | null>(null);
const [bic, setBic] = useState("");
const [notes, setNotes] = useState("");
const [files, setFiles] = useState<FileList | null>(null);
// ------- Leave-confirm (unsaved changes) -------
const [showLeaveConfirm, setShowLeaveConfirm] = useState(false);
const pendingHrefRef = useRef<string | null>(null);
const allowNavRef = useRef(false);
// Astuces de saisie
const prenomInputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
// capitalise la 1ère lettre au fur et à mesure
if (prenom) {
const fixed = capitalizeFirst(prenom);
if (fixed !== prenom) setPrenom(fixed);
}
}, [prenom]);
// Vérifier si l'utilisateur est staff et récupérer les organisations
useEffect(() => {
const checkStaffAndLoadOrgs = async () => {
try {
const res = await fetch("/api/me", {
cache: "no-store",
headers: { Accept: "application/json" },
credentials: "include"
});
if (res.ok) {
const me = await res.json();
const userIsStaff = me.is_staff || false;
setIsStaff(userIsStaff);
// Si l'utilisateur est staff, récupérer la liste des organisations
if (userIsStaff) {
const orgRes = await fetch("/api/organizations", {
headers: { Accept: "application/json" },
credentials: "include"
});
if (orgRes.ok) {
const orgData = await orgRes.json();
setOrganizations(orgData.items || []);
}
}
}
} catch (error) {
console.error("Erreur lors de la vérification staff:", error);
}
};
checkStaffAndLoadOrgs();
}, []);
// Validation simple
const canSubmit = useMemo(() => {
const coreOk = civilite !== "" && nom.trim() && prenom.trim() && emailRx.test(email.trim());
const ibanOk = !iban || isValidIBAN(iban);
// Si staff, vérifier qu'une organisation est sélectionnée
const orgOk = !isStaff || (isStaff && selectedOrg !== null);
return coreOk && ibanOk && orgOk;
}, [civilite, nom, prenom, email, iban, isStaff, selectedOrg]);
// considère le formulaire comme "modifié" si l'un des champs est rempli/modifié
const isDirty = useMemo(() => {
return (
civilite !== "" ||
nom.trim() !== "" ||
prenom.trim() !== "" ||
nomNaissance.trim() !== "" ||
pseudo.trim() !== "" ||
email.trim() !== "" ||
adresse.trim() !== "" ||
complementAdresse.trim() !== "" ||
telephone.trim() !== "" ||
dateNaissance !== "" ||
lieuNaissance.trim() !== "" ||
nir.trim() !== "" ||
congesSpectacles.trim() !== "" ||
iban.trim() !== "" ||
bic.trim() !== "" ||
notes.trim() !== "" ||
(files && files.length > 0) ||
addrQuery.trim() !== ""
);
}, [civilite, nom, prenom, nomNaissance, pseudo, email, adresse, complementAdresse, telephone, dateNaissance, lieuNaissance, nir, congesSpectacles, iban, bic, notes, files, addrQuery]);
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setErr(null);
if (!canSubmit) {
return setErr("Merci de remplir au minimum civilité, nom, prénom et une adresse e-mail valide.");
}
setLoading(true);
// autorise explicitement la navigation après envoi
allowNavRef.current = true;
try {
if (iban && !isValidIBAN(iban)) {
setIbanError("IBAN invalide (vérifiez la clé et le format).");
throw new Error("IBAN invalide");
}
const fd = new FormData();
// identité
fd.append("civilite_salarie", civilite);
fd.append("nomusage", upper(nom.trim()));
fd.append("prenom", capitalizeFirst(prenom.trim()));
if (nomNaissance.trim()) fd.append("nomnaissance_salarie", upper(nomNaissance.trim()));
if (pseudo.trim()) fd.append("pseudonyme", pseudo.trim());
// contact
fd.append("email_salarie", email.trim());
if (adresse.trim()) fd.append("adresse_salarie", adresse.trim());
if (complementAdresse.trim()) fd.append("adresse_complement", complementAdresse.trim());
if (telephone.trim()) fd.append("tel_salarie", telephone.trim());
if (addrMeta?.postcode) fd.append("adresse_postcode", addrMeta.postcode);
if (addrMeta?.city) fd.append("adresse_city", addrMeta.city);
if (addrMeta?.insee) fd.append("adresse_insee", addrMeta.insee);
if (addrMeta?.lat) fd.append("adresse_lat", addrMeta.lat);
if (addrMeta?.lon) fd.append("adresse_lon", addrMeta.lon);
// état civil
if (dateNaissance) fd.append("dob_salarie", dateNaissance);
if (lieuNaissance.trim()) fd.append("cob_salarie", lieuNaissance.trim());
if (nir.trim()) fd.append("ss_salarie", nir.trim());
if (congesSpectacles.trim()) fd.append("cs_salarie", congesSpectacles.trim());
// bancaires
if (iban.trim()) fd.append("iban_salarie", iban.trim());
if (bic.trim()) fd.append("bic_salarie", bic.trim());
// autres
if (notes.trim()) fd.append("notes", notes.trim());
// fichiers
if (files && files.length > 0) {
Array.from(files).forEach((f) => fd.append("pieces_jointes", f, f.name));
}
// contexte employeur (récupéré automatiquement via les headers de l'API)
// Plus besoin de récupérer depuis localStorage car les headers sont automatiquement ajoutés
if (clientInfo?.name) {
fd.append("structure", clientInfo.name);
}
// Send to our internal API which will insert into Supabase `salaries` table
const payload: any = {
civilite_salarie: civilite,
nom: nom.trim(),
prenom: prenom.trim(),
nomnaissance_salarie: nomNaissance.trim() || undefined,
pseudonyme: pseudo.trim() || undefined,
email_salarie: email.trim(),
adresse_salarie: adresse.trim() || undefined,
adresse_complement: complementAdresse.trim() || undefined,
adresse_postcode: addrMeta?.postcode || undefined,
adresse_city: addrMeta?.city || undefined,
adresse_insee: addrMeta?.insee || undefined,
adresse_lat: addrMeta?.lat || undefined,
adresse_lon: addrMeta?.lon || undefined,
tel_salarie: telephone.trim() || undefined,
dob_salarie: dateNaissance || undefined,
cob_salarie: lieuNaissance.trim() || undefined,
ss_salarie: nir.trim() || undefined,
cs_salarie: congesSpectacles.trim() || undefined,
iban_salarie: iban.trim() || undefined,
bic_salarie: bic.trim() || undefined,
notes: notes.trim() || undefined,
structure: selectedOrg?.name || clientInfo?.name || undefined,
// Ajouter l'organisation sélectionnée si staff
employer_id: isStaff && selectedOrg ? selectedOrg.id : undefined,
};
const res = await fetch('/api/salaries', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
const result = await res.json().catch(() => ({}));
if (!res.ok || !result?.ok) {
const msg = result?.error || result?.message || `Erreur HTTP ${res.status}`;
throw new Error(msg);
}
if (embed) {
// En mode embed: pas de redirection, pas d'overlay bloquant
setRedirecting(false);
} else {
// succès → overlay 3s puis liste des salariés
setRedirecting(true);
setTimeout(() => router.push("/salaries"), 3000);
}
} catch (e: any) {
setErr(e?.message || "Impossible denvoyer le formulaire.");
} finally {
setLoading(false);
}
}
// Intercepte les clics sur liens internes si le formulaire est modifié
useEffect(() => {
function onClick(e: MouseEvent) {
if (!isDirty || allowNavRef.current) return;
// N'intercepte pas les ouvertures dans un nouvel onglet ou les clics non-gauche
if ((e as MouseEvent).button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
const target = e.target as HTMLElement | null;
const anchor = target?.closest && (target.closest('a[href]') as HTMLAnchorElement | null);
if (!anchor) return;
// ignore nouveaux onglets/téléchargements/externes
if (anchor.target === '_blank' || anchor.download) return;
const href = anchor.getAttribute('href');
if (!href) return;
if (href.startsWith('http') && !href.startsWith(window.location.origin)) return;
// Bloque la navigation client avant que Next.js ne la déclenche
e.preventDefault();
e.stopPropagation();
pendingHrefRef.current = href;
setShowLeaveConfirm(true);
}
function onBeforeUnload(ev: BeforeUnloadEvent) {
if (!isDirty || allowNavRef.current) return;
ev.preventDefault();
ev.returnValue = '';
}
document.addEventListener('click', onClick, true);
window.addEventListener('beforeunload', onBeforeUnload);
return () => {
document.removeEventListener('click', onClick, true);
window.removeEventListener('beforeunload', onBeforeUnload);
};
}, [isDirty]);
function confirmLeave() {
const href = pendingHrefRef.current;
setShowLeaveConfirm(false);
if (href) {
allowNavRef.current = true;
// utilise le routeur pour rester en navigation client
router.push(href);
}
}
function cancelLeave() {
pendingHrefRef.current = null;
setShowLeaveConfirm(false);
}
function formatAddr(feature: any) {
const l = feature?.properties;
return l?.label || "";
}
useEffect(() => {
if (!addrQuery || addrQuery.trim().length < 3) {
setAddrResults([]);
setAddrOpen(false);
if (addrTimer) clearTimeout(addrTimer);
return;
}
if (addrTimer) clearTimeout(addrTimer);
const t = setTimeout(async () => {
try {
setAddrLoading(true);
const url = `https://api-adresse.data.gouv.fr/search/?q=${encodeURIComponent(addrQuery)}&limit=7`;
const res = await fetch(url);
const json = await res.json();
const feats = Array.isArray(json?.features) ? json.features : [];
setAddrResults(feats);
setAddrOpen(true);
} catch {
setAddrResults([]);
setAddrOpen(false);
} finally {
setAddrLoading(false);
}
}, 250);
setAddrTimer(t);
return () => clearTimeout(t);
}, [addrQuery]);
return (
<>
<div className="space-y-5">
{!embed && (
<>
{/* Fil d'Ariane / retour */}
<div className="flex items-center gap-2">
<Link href="/salaries" className="inline-flex items-center gap-1 text-sm underline">
<ArrowLeft className="w-4 h-4" /> Retour aux salariés
</Link>
</div>
</>
)}
{!embed && <h1 className="text-xl font-semibold">Nouveau salarié</h1>}
<Section title="Enregistrement d'un nouveau salarié">
<p className="text-sm text-slate-600">
Pour enregistrer votre nouveau salarié, indiquez les informations cidessous. Nous avons besoin a minima de son
<strong> nom</strong>, son <strong>prénom</strong> et son <strong>adresse email</strong>.
</p>
<p className="text-sm text-slate-600 mt-2">
Un email lui sera envoyé pour déposer ses justificatifs et, si nécessaire, compléter son étatcivil.
</p>
</Section>
{/* Onglets (design calqué sur /contrats) */}
<div className="inline-flex rounded-xl border p-1 bg-slate-50">
<button
type="button"
onClick={() => setFormMode("simplifie")}
className={`px-3 py-1.5 text-sm rounded-lg ${formMode==='simplifie' ? 'bg-white shadow border' : 'opacity-80'}`}
>
Formulaire simplifiée
</button>
<button
type="button"
onClick={() => setFormMode("complet")}
className={`px-3 py-1.5 text-sm rounded-lg ${formMode==='complet' ? 'bg-white shadow border' : 'opacity-80'}`}
>
Formulaire complet
</button>
</div>
{/* Sélection d'organisation (pour les utilisateurs staff uniquement) */}
{isStaff && (
<Section title="Organisation">
<div className="grid grid-cols-1 md:grid-cols-[240px_1fr] items-center gap-4">
<Label required>Structure</Label>
<div>
<select
value={selectedOrg?.id || ""}
onChange={(e) => {
const orgId = e.target.value;
const org = organizations.find(o => o.id === orgId);
setSelectedOrg(org || null);
}}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
required
>
<option value="">Sélectionner une structure...</option>
{organizations.map((org) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</select>
<p className="text-[11px] text-slate-500 mt-1">
Sélectionnez l'organisation pour laquelle vous créez ce salarié.
</p>
</div>
</div>
</Section>
)}
<form onSubmit={onSubmit} className="space-y-5">
{/* Formulaire simplifié: civilité, nom dusage, prénom, email */}
{formMode === "simplifie" && (
<>
<Section title="Identité">
<div className="space-y-3">
<Label required>Sa civilité</Label>
<div className="flex items-center gap-6">
<label className="inline-flex items-center gap-2 text-sm">
<input type="radio" name="civilite" checked={civilite === "Monsieur"} onChange={() => setCivilite("Monsieur")} />
Monsieur
</label>
<label className="inline-flex items-center gap-2 text-sm">
<input type="radio" name="civilite" checked={civilite === "Madame"} onChange={() => setCivilite("Madame")} />
Madame
</label>
</div>
<FieldRow>
<div>
<Label required>Son nom de famille (ou nom dusage)</Label>
<input
value={nom}
onChange={(e) => setNom(upper(e.target.value))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<p className="text-[11px] text-slate-500 mt-1">La saisie se fait automatiquement en majuscules.</p>
</div>
<div>
<Label required>Son prénom</Label>
<input
ref={prenomInputRef}
value={prenom}
onChange={(e) => setPrenom(e.target.value)}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<p className="text-[11px] text-slate-500 mt-1">Une majuscule est automatiquement ajoutée au début du prénom.</p>
</div>
</FieldRow>
</div>
</Section>
<Section title="Coordonnées">
<div>
<Label required>Son adresse email</Label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
</Section>
</>
)}
{/* Formulaire complet: contenu existant */}
{formMode === "complet" && (
<>
<Section title="Identité">
<div className="space-y-3">
<Label required>Sa civilité</Label>
<div className="flex items-center gap-6">
<label className="inline-flex items-center gap-2 text-sm">
<input type="radio" name="civilite" checked={civilite === "Monsieur"} onChange={() => setCivilite("Monsieur")} />
Monsieur
</label>
<label className="inline-flex items-center gap-2 text-sm">
<input type="radio" name="civilite" checked={civilite === "Madame"} onChange={() => setCivilite("Madame")} />
Madame
</label>
</div>
<FieldRow>
<div>
<Label required>Son nom de famille (ou nom dusage)</Label>
<input
value={nom}
onChange={(e) => setNom(upper(e.target.value))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<p className="text-[11px] text-slate-500 mt-1">La saisie se fait automatiquement en majuscules.</p>
</div>
<div>
<Label required>Son prénom</Label>
<input
ref={prenomInputRef}
value={prenom}
onChange={(e) => setPrenom(e.target.value)}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<p className="text-[11px] text-slate-500 mt-1">Une majuscule est automatiquement ajoutée au début du prénom.</p>
</div>
</FieldRow>
<FieldRow>
<div>
<Label>Son nom de naissance</Label>
<input
value={nomNaissance}
onChange={(e) => setNomNaissance(upper(e.target.value))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<p className="text-[11px] text-slate-500 mt-1">Remplir uniquement sil diffère du nom dusage (automatiquement en majuscules).</p>
</div>
<div>
<Label>Son pseudonyme</Label>
<input
value={pseudo}
onChange={(e) => setPseudo(e.target.value)}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<p className="text-[11px] text-slate-500 mt-1">Si votre salarié utilise un nom de scène ou de plume, nous le ferons apparaître sur ses contrats.</p>
</div>
</FieldRow>
</div>
</Section>
<Section title="Coordonnées">
<FieldRow>
<div>
<Label required>Son adresse email</Label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
<div className="relative">
<Label>Son adresse postale complète</Label>
<div className="relative">
<input
value={adresse}
onChange={(e) => { setAdresse(e.target.value); setAddrQuery(e.target.value); setAddrOpen(false); setAddrMeta(null); }}
onFocus={() => { if (addrResults.length > 0) setAddrOpen(true); }}
placeholder="Saisir au moins 3 caractères (ex : 10 rue de Rivoli, Paris)"
className="w-full pr-9 px-3 py-2 rounded-lg border bg-white text-sm"
/>
{adresse && (
<button
type="button"
aria-label="Effacer ladresse"
onClick={() => { setAdresse(""); setAddrQuery(""); setAddrResults([]); setAddrOpen(false); setAddrMeta(null); }}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded hover:bg-slate-100 text-slate-500"
title="Effacer"
>
<X className="w-4 h-4" />
</button>
)}
</div>
{(addrLoading || (addrOpen && addrResults.length > 0)) && (
<div className="absolute z-20 mt-1 w-full rounded-lg border bg-white shadow-lg max-h-64 overflow-auto">
{addrLoading && (
<div className="p-3 text-xs text-slate-500">Recherche dadresses…</div>
)}
{!addrLoading && addrResults.length === 0 && (
<div className="p-3 text-xs text-slate-500">Aucun résultat</div>
)}
{!addrLoading && addrResults.length > 0 && (
<ul className="divide-y divide-slate-100">
{addrResults.map((f: any, idx: number) => {
const p = f?.properties || {};
const label = formatAddr(f);
return (
<li key={idx}>
<button
type="button"
className="block w-full text-left px-3 py-2 hover:bg-slate-50 text-sm"
onClick={() => {
setAdresse(label);
setAddrOpen(false);
setAddrResults([]);
setAddrQuery("");
setAddrMeta({
postcode: p.postcode,
city: p.city || p.context?.split(", ")?.[0],
insee: p.citycode,
lat: Array.isArray(f.geometry?.coordinates) ? String(f.geometry.coordinates[1]) : undefined,
lon: Array.isArray(f.geometry?.coordinates) ? String(f.geometry.coordinates[0]) : undefined,
});
}}
>
<div className="font-medium">{label}</div>
<div className="text-[11px] text-slate-500">{p.postcode || ""} {p.city || ""}</div>
</button>
</li>
);
})}
</ul>
)}
</div>
)}
</div>
</FieldRow>
<FieldRow>
<div>
<Label>Son numéro de téléphone</Label>
<input
value={telephone}
onChange={(e) => setTelephone(e.target.value)}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<p className="text-[11px] text-slate-500 mt-1">Utilisé uniquement pour les SMS de signature (si numéros français).</p>
</div>
<div>
<Label>Complément dadresse</Label>
<input
value={complementAdresse}
onChange={(e) => setComplementAdresse(e.target.value)}
placeholder="Bâtiment, étage, appartement, digicode…"
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
</FieldRow>
</Section>
<Section title="État civil">
<FieldRow>
<div>
<Label>Sa date de naissance</Label>
<input
type="date"
value={dateNaissance}
onChange={(e) => setDateNaissance(e.target.value)}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
<div>
<Label>Son lieu de naissance</Label>
<input
value={lieuNaissance}
onChange={(e) => setLieuNaissance(e.target.value)}
placeholder="Ville, département, pays le cas échéant"
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
</FieldRow>
<FieldRow>
<div>
<Label>Son numéro de Sécurité Sociale</Label>
<input
value={nir}
onChange={(e) => setNir(e.target.value)}
placeholder="15 chiffres ou numéro provisoire"
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<p className="text-[11px] text-slate-500 mt-1">Indiquez le NIR complet ou provisoire si pas encore définitif.</p>
</div>
<div>
<Label>Son numéro Congés Spectacles</Label>
<input
value={congesSpectacles}
onChange={(e) => setCongesSpectacles(e.target.value)}
placeholder="ex : X123456"
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
</FieldRow>
</Section>
<Section title="Coordonnées bancaires">
<FieldRow>
<div>
<Label>Son IBAN</Label>
<input
value={iban}
onChange={(e) => { setIban(e.target.value); if (ibanError) setIbanError(null); }}
onBlur={() => setIbanError(iban && !isValidIBAN(iban) ? "IBAN invalide (vérifiez la clé et le format)." : null)}
placeholder="FR.. .. .. .. .. .. .. .. .. .."
className={`w-full px-3 py-2 rounded-lg border bg-white text-sm ${ibanError ? 'border-red-400' : 'border-slate-300'}`}
/>
{ibanError ? (
<p className="text-[11px] text-red-600 mt-1" aria-live="polite">{ibanError}</p>
) : iban && isValidIBAN(iban) ? (
<p className="text-[11px] text-emerald-600 mt-1 inline-flex items-center gap-1" aria-live="polite">
<Check className="w-3.5 h-3.5" /> IBAN valide
</p>
) : (
<p className="text-[11px] text-slate-500 mt-1">Doit provenir dune banque de la zone SEPA.</p>
)}
</div>
<div>
<Label>Son BIC</Label>
<input value={bic} onChange={(e) => setBic(e.target.value)} className="w-full px-3 py-2 rounded-lg border bg-white text-sm" />
</div>
</FieldRow>
</Section>
<Section title="Autres informations">
<div>
<Label>Notes</Label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={4}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
<div className="mt-4">
<Label>Envoi de fichier·s</Label>
<input
type="file"
multiple
onChange={(e) => setFiles(e.target.files)}
className="block w-full text-sm text-slate-600 file:mr-3 file:py-2 file:px-3 file:rounded-lg file:border file:bg-white file:border-slate-300"
/>
<p className="text-[11px] text-slate-500 mt-1">Vous pouvez transmettre tous fichiers que vous jugez utiles (20Mo max.).</p>
</div>
</Section>
</>
)}
{err && (
<div className="rounded-xl border border-red-300 bg-red-50 text-red-800 p-3">{err}</div>
)}
<div className="flex items-center gap-3">
<Link href="/salaries" className="px-4 py-2 rounded-lg border text-sm">Relire avant envoi</Link>
<button
type="submit"
disabled={!canSubmit || loading || isDemoMode}
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-emerald-600 text-white disabled:opacity-60 disabled:cursor-not-allowed"
title={isDemoMode ? "Désactivé en mode démo" : ""}
>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
Envoyer
</button>
</div>
</form>
</div>
{showLeaveConfirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="w-full max-w-md rounded-2xl border bg-white p-5 shadow-xl">
<div className="text-base font-semibold">Quitter cette page ?</div>
<p className="text-sm text-slate-600 mt-2">
Vous avez une saisie en cours. En quittant maintenant, vous perdrez les informations non envoyées.
</p>
<div className="mt-4 flex items-center justify-end gap-3">
<button onClick={cancelLeave} className="px-4 py-2 rounded-lg border text-sm">Rester</button>
<button onClick={confirmLeave} className="px-4 py-2 rounded-lg bg-rose-600 text-white text-sm hover:bg-rose-700">Quitter sans enregistrer</button>
</div>
</div>
</div>
)}
{redirecting && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/70">
<div className="rounded-2xl border bg-white p-6 text-center shadow-xl">
<Loader2 className="w-6 h-6 animate-spin mx-auto mb-3" />
<div className="font-medium">Envoi réussi</div>
<p className="text-sm text-slate-600 mt-1">Redirection dans quelques secondes…</p>
</div>
</div>
)}
{embed && (
<style jsx global>{`
header, .site-header, [data-app-header] { display: none !important; }
body { background: transparent !important; }
`}</style>
)}
</>
);
}