espace-paie-odentas/components/contrats/NouveauCDDUForm.tsx

2143 lines
91 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 { useState, useMemo, useEffect, useRef, useCallback } from "react";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { usePostHog } from "posthog-js/react";
import { api } from "@/lib/fetcher";
import { Loader2, Search, Info } from "lucide-react";
import { NotesSection } from "@/components/NotesSection";
import { PROFESSIONS_ARTISTE } from "@/components/constants/ProfessionsArtiste";
import { useDemoMode } from "@/hooks/useDemoMode";
/* =========================
Types
========================= */
type SalarieOption = { matricule: string; nom: string; email?: string | null };
type SpectacleOption = { id?: string; nom: string; numero_objet?: string | null };
type ProfessionOption = { code: string; label: string };
type TechnicienOption = ProfessionOption;
type ClientInfo = { id: string; name: string; api_name?: string } | null;
// Préremplissage pour le mode édition + override d'envoi
type NouveauCDDUPrefill = {
spectacle?: string;
numero_objet?: string | null;
salarie?: { matricule: string; nom: string; email?: string | null } | null;
categorie?: "Artiste" | "Technicien" | "Autre";
profession?: { code?: string; label?: string } | null;
date_debut?: string; // YYYY-MM-DD
date_fin?: string; // YYYY-MM-DD
nb_representations?: number | "";
nb_services_repetition?: number | "";
dates_representations?: string;
dates_repetitions?: string;
heures_total?: number | "";
minutes_total?: "0" | "30";
jours_travail?: string;
type_salaire?: "Brut" | "Net avant PAS" | "Coût total employeur" | "Minimum conventionnel";
montant?: number | "";
panier_repas?: "Oui" | "Non";
notes?: string;
reference?: string;
multi_mois?: boolean;
nb_heures_annexes?: number | string;
};
/* =========================
Constantes & helpers
========================= */
const NEW_SALARIE_PATH =
process.env.NEXT_PUBLIC_NEW_SALARIE_PATH || "/salaries/nouveau?embed=1";
const EURO = new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" });
function todayYmd() {
const d = new Date();
const tzOff = d.getTimezoneOffset();
const local = new Date(d.getTime() - tzOff * 60000);
return local.toISOString().slice(0, 10); // YYYY-MM-DD
}
function formatFr(d: string) {
const [y, m, day] = d.split("-");
return `${day}/${m}/${y}`;
}
function generateReference() {
const letters = "ABCDEFGHIKLMNPQRSTUVWXYZ"; // sans O
const digits = "123456789"; // sans 0
const pool = letters + digits;
const pick = (s: string) => s[Math.floor(Math.random() * s.length)];
while (true) {
let ref = "";
for (let i = 0; i < 8; i++) ref += pick(pool);
if (ref.startsWith("RG")) continue; // ne pas commencer par RG
if (!/[A-Z]/.test(ref)) continue; // au moins une lettre
if (!/[1-9]/.test(ref)) continue; // au moins un chiffre
return ref;
}
}
// Helper: parse base64-encoded JSON safely
function parseBase64Json<T = any>(b64: string | null): T | null {
if (!b64) return null;
try {
const json = atob(b64);
return JSON.parse(json) as T;
} catch {
return null;
}
}
function norm(s: string){
return s
.toLowerCase()
.normalize('NFD')
.replace(/\p{Diacritic}/gu, '')
.replace(/[^a-z0-9\s]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
// Petit hook local de debounce (réutilisable)
function useDebouncedValue<T>(value: T, delay = 300) {
const [debounced, setDebounced] = useState<T>(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
/* =========================
Hook: blocage navigation si brouillon
========================= */
function useUnsavedChangesPrompt(enabled: boolean) {
const allowNavRef = useRef(false);
const pendingHrefRef = useRef<string | null>(null);
const [open, setOpen] = useState(false);
const [, forceRender] = useState(0);
useEffect(() => {
function onBeforeUnload(e: BeforeUnloadEvent) {
if (!enabled || allowNavRef.current) return;
e.preventDefault();
e.returnValue = ""; // Chrome a besoin d'une valeur
}
if (enabled) window.addEventListener("beforeunload", onBeforeUnload);
return () => window.removeEventListener("beforeunload", onBeforeUnload);
}, [enabled]);
useEffect(() => {
function onDocClick(ev: MouseEvent) {
if (!enabled || allowNavRef.current) return;
const target = ev.target as HTMLElement | null;
if (!target) return;
const a = target.closest?.("a[href]") as HTMLAnchorElement | null;
if (!a) return;
const url = new URL(a.href, window.location.href);
const sameOrigin = url.origin === window.location.origin;
if (!sameOrigin) return; // laisser sortir du site sans bloquer
if (url.pathname === window.location.pathname && url.hash) return; // autoriser ancres
ev.preventDefault();
pendingHrefRef.current = url.toString();
setOpen(true);
}
document.addEventListener("click", onDocClick, true);
return () => document.removeEventListener("click", onDocClick, true);
}, [enabled]);
useEffect(() => {
if (!enabled) return;
history.pushState({ _block: true }, "");
function onPopState(ev: PopStateEvent) {
if (allowNavRef.current || !enabled) return;
ev.preventDefault?.();
history.pushState({ _block: true }, "");
setOpen(true);
}
window.addEventListener("popstate", onPopState);
return () => window.removeEventListener("popstate", onPopState);
}, [enabled]);
const confirmLeave = useCallback(() => {
allowNavRef.current = true;
const href = pendingHrefRef.current;
setOpen(false);
if (href) { window.location.assign(href); } else { history.back(); }
}, []);
const cancelLeave = useCallback(() => {
pendingHrefRef.current = null;
setOpen(false);
forceRender((n) => n + 1);
}, []);
return { open, confirmLeave, cancelLeave, allowNavRef };
}
/* =========================
UI atoms
========================= */
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="rounded-2xl border bg-white">
<div className="px-4 py-3 border-b font-medium text-slate-700 bg-slate-50/60">
{title}
</div>
<div className="p-4 space-y-4">{children}</div>
</section>
);
}
function Label({ children, required = false }: { children: React.ReactNode; required?: boolean }) {
return (
<label className="text-xs text-slate-600 block">
{children} {required && <span className="text-rose-600">*</span>}
</label>
);
}
function FieldRow({ children }: { children: React.ReactNode }) {
return <div className="grid grid-cols-1 md:grid-cols-2 gap-4">{children}</div>;
}
/* =========================
API searches
========================= */
function useSearchSalaries(q: string, orgId?: string | null) {
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 || null, name: me.active_org_name || "Organisation", api_name: me.active_org_api_name } as ClientInfo;
} catch { return null; }
},
staleTime: 30_000,
});
const params = new URLSearchParams();
params.set("page", "1");
params.set("limit", "10");
if (q.trim()) params.set("q", q.trim());
// Utiliser l'org_id fourni ou celui du clientInfo
if (orgId) params.set("org_id", orgId);
return useQuery({
queryKey: ["search-salaries", q, orgId || clientInfo?.id],
queryFn: () =>
api<{ items: SalarieOption[] }>(`/salaries?${params.toString()}`, {}, clientInfo).then((r) => ({
items: r.items?.map((s) => ({ matricule: s.matricule, nom: s.nom, email: s.email ?? undefined })) ?? [],
})),
enabled: (!!orgId || !!clientInfo),
staleTime: 10_000,
});
}
function useSearchSpectacles(q: string, orgId?: string | null) {
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 || null, name: me.active_org_name || "Organisation", api_name: me.active_org_api_name } as ClientInfo;
} catch { return null; }
},
staleTime: 30_000,
});
const params = new URLSearchParams();
params.set("page", "1");
params.set("limit", "10");
if (q.trim()) params.set("q", q.trim());
// Utiliser l'org_id fourni ou celui du clientInfo
if (orgId) params.set("org_id", orgId);
return useQuery({
queryKey: ["search-spectacles", q, orgId || clientInfo?.id],
queryFn: () =>
api<{ items: SpectacleOption[] }>(`/spectacles?${params.toString()}`, {}, clientInfo).then((r) => ({
items:
r.items?.map((s) => ({
id: (s as any).id || null,
nom: (s as any).nom || (s as any).Spectacle || (s as any).titre || "",
numero_objet: (s as any).numero_objet || (s as any)["n° d'objet"] || (s as any).numero || null,
})) ?? [],
})),
enabled: !!clientInfo,
staleTime: 10_000,
});
}
/* =========================
Formulaire principal
========================= */
export function NouveauCDDUForm({
prefill,
onSubmitOverride,
contractIdForNotes,
}: {
prefill?: NouveauCDDUPrefill;
onSubmitOverride?: (payload: {
regime: "CDDU_MONO" | "CDDU_MULTI";
multi_mois: boolean;
spectacle: string;
numero_objet?: string | null;
salarie: { matricule: string; nom: string; email?: string | null };
categorie_professionnelle: "Artiste" | "Technicien" | "Autre";
profession: string;
profession_code?: string;
date_debut: string;
date_fin?: string;
nb_representations?: number | "";
nb_services_repetition?: number | "";
dates_representations?: string;
dates_repetitions?: string;
heures_travail?: number | "";
minutes_travail?: "0" | "30";
jours_travail?: string;
type_salaire: "Brut" | "Net avant PAS" | "Coût total employeur" | "Minimum conventionnel";
montant?: number | "";
panier_repas: "Oui" | "Non";
reference: string;
notes?: string;
valider_direct: boolean;
}) => Promise<void>;
contractIdForNotes?: string;
}) {
const router = useRouter();
const posthog = usePostHog();
const isDemoMode = useDemoMode();
// --- form state
const [reference, setReference] = useState<string>("");
const [spectacle, setSpectacle] = useState("");
const [objet, setObjet] = useState("");
const [spectacleQuery, setSpectacleQuery] = useState("");
const [spectaclePick, setSpectaclePick] = useState<SpectacleOption | null>(null);
const [spectacleManual, setSpectacleManual] = useState(false);
const spectacleInputRef = useRef<HTMLInputElement | null>(null);
const [salarieQuery, setSalarieQuery] = useState("");
const [salarie, setSalarie] = useState<SalarieOption | null>(null);
// L'utilisateur a explicitement cliqué sur "Changer" (ne plus forcer le salarie depuis l'URL)
const [urlSalarieDismissed, setUrlSalarieDismissed] = useState(false);
const [categoriePro, setCategoriePro] = useState<"Artiste" | "Technicien" | "Autre" | "Cadre" | "Non Cadre" | "Je ne sais pas">("Artiste");
const [profession, setProfession] = useState("");
const [professionQuery, setProfessionQuery] = useState("");
const [professionPick, setProfessionPick] = useState<ProfessionOption | null>(null);
// Index actif (navigation clavier) pour les listes de résultats
const [spectacleActive, setSpectacleActive] = useState(0);
const [salarieActive, setSalarieActive] = useState(0);
const [professionActive, setProfessionActive] = useState(0);
const [techniciens, setTechniciens] = useState<TechnicienOption[] | null>(null);
const [techLoading, setTechLoading] = useState(false);
const techLoadedRef = useRef(false);
const [isMultiMois, setIsMultiMois] = useState<"Oui" | "Non">("Non");
const [dateDebut, setDateDebut] = useState("");
const [dateFin, setDateFin] = useState("");
const [confirmPastStart, setConfirmPastStart] = useState(false);
const [heuresTotal, setHeuresTotal] = useState<number | "">("");
const [minutesTotal, setMinutesTotal] = useState<"0" | "30">("0");
const [joursTravail, setJoursTravail] = useState("");
const [nbRep, setNbRep] = useState<number | "">("");
const [nbServ, setNbServ] = useState<number | "">("");
const [datesRep, setDatesRep] = useState("");
const [datesServ, setDatesServ] = useState("");
const [typeSalaire, setTypeSalaire] = useState<"Brut" | "Net avant PAS" | "Coût total employeur" | "Minimum conventionnel">("Brut");
const [montantSalaire, setMontantSalaire] = useState<number | "">("");
const [panierRepas, setPanierRepas] = useState<"Oui" | "Non">("Non");
const [notes, setNotes] = useState("");
const [validerDirect, setValiderDirect] = useState<"Oui" | "Non">("Oui");
// Confirmation e-mail à la création
const [emailConfirm, setEmailConfirm] = useState<"Oui" | "Non">("Oui");
// États spécifiques au Régime Général
const [isRegimeRG, setIsRegimeRG] = useState(false);
const [typeContratRG, setTypeContratRG] = useState<"CDD de droit commun" | "CDI" | "Stage" | "Contrat de professionnalisation" | "Contrat d'apprentissage" | "Autre">("CDD de droit commun");
const [motifCDD, setMotifCDD] = useState<"Remplacement d'une absence" | "Accroissement temporaire d'activité">("Remplacement d'une absence");
const [nomPersonneRemplacee, setNomPersonneRemplacee] = useState("");
const [situationPersonneRemplacee, setSituationPersonneRemplacee] = useState<"Absent temporairement ou contrat suspendu (maladie, maternité, congés payés, congé parental, etc...)" | "Salarié provisoirement à temps partiel (congé parental, congé création d'entreprise, etc...)" | "Salarié ayant quitté définitivement l'entreprise, poste en attente de suppression" | "Remplacement du chef d'entreprise">("Absent temporairement ou contrat suspendu (maladie, maternité, congés payés, congé parental, etc...)");
const [motifAccroissement, setMotifAccroissement] = useState("");
// États pour la gestion des organisations (staff)
const [selectedOrg, setSelectedOrg] = useState<{ id: string; name: string } | null>(null);
const [isStaff, setIsStaff] = useState(false);
const [organizations, setOrganizations] = useState<{ id: string; name: string }[]>([]);
const [loading, setLoading] = useState(false);
const [redirecting, setRedirecting] = useState(false);
const [err, setErr] = useState<string | null>(null);
// Empêcher la re-application du préremplissage via ?dupe= après modifications utilisateur
const [dupeApplied, setDupeApplied] = useState(false);
// Afficher un modal de chargement pendant le pré-remplissage (duplication)
const [prefillLoading, setPrefillLoading] = useState(false);
// Détection de saisie en cours (dirty)
const isDirty = useMemo(() => {
const hasSpectacle = spectacle.trim() !== "" || spectaclePick !== null || spectacleManual;
const hasSalarie = salarie !== null || salarieQuery.trim() !== "";
const hasProfession = profession.trim() !== "" || professionPick !== null || professionQuery.trim() !== "";
const hasDates = !!dateDebut || !!dateFin;
const hasVolumes = (nbRep !== "" && Number(nbRep) > 0) || (nbServ !== "" && Number(nbServ) > 0) || (heuresTotal !== "") || (joursTravail.trim() !== "") || (datesRep.trim() !== "") || (datesServ.trim() !== "");
const hasSalaire = typeSalaire !== "Brut" || (montantSalaire !== "" && Number(montantSalaire) > 0) || panierRepas !== "Non";
const hasAutres = objet.trim() !== "" || notes.trim() !== "" || isMultiMois === "Oui";
const hasRG = isRegimeRG && (
typeContratRG !== "CDD de droit commun" ||
(typeContratRG === "CDD de droit commun" && motifCDD === "Remplacement d'une absence" && (nomPersonneRemplacee.trim() !== "" || situationPersonneRemplacee !== "Absent temporairement ou contrat suspendu (maladie, maternité, congés payés, congé parental, etc...)")) ||
(typeContratRG === "CDD de droit commun" && motifCDD === "Accroissement temporaire d'activité" && motifAccroissement.trim() !== "")
);
return hasSpectacle || hasSalarie || hasProfession || hasDates || hasVolumes || hasSalaire || hasAutres || hasRG;
}, [
spectacle, spectaclePick, spectacleManual,
salarie, salarieQuery,
profession, professionPick, professionQuery,
dateDebut, dateFin,
nbRep, nbServ, heuresTotal, joursTravail, datesRep, datesServ,
typeSalaire, montantSalaire, panierRepas,
objet, notes, isMultiMois,
isRegimeRG, typeContratRG, motifCDD, nomPersonneRemplacee, situationPersonneRemplacee, motifAccroissement,
]);
// Modal "Nouveau salarié"
const [newSalarieOpen, setNewSalarieOpen] = useState(false);
useEffect(() => {
if (!newSalarieOpen) return;
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = prev;
};
}, [newSalarieOpen]);
// Hydrate le formulaire si des valeurs initiales sont fournies (mode édition)
useEffect(() => {
if (!prefill) return;
setSpectacle(prefill.spectacle || "");
setObjet(String(prefill.numero_objet ?? ""));
// Afficher la production dans la zone sélectionnée (lecture seule) quand on connaît le nom/numéro
if (prefill.spectacle) {
setSpectaclePick({ nom: prefill.spectacle, numero_objet: prefill.numero_objet ?? null });
setSpectacleManual(false);
setSpectacleQuery("");
}
setSalarie(prefill.salarie ?? null);
setCategoriePro(prefill.categorie ?? "Artiste");
setProfession(prefill.profession?.label || "");
setProfessionPick(
prefill.profession?.label
? { code: prefill.profession.code || "", label: prefill.profession.label }
: null
);
setDateDebut(prefill.date_debut || "");
setDateFin(prefill.date_fin || "");
setNbRep(prefill.nb_representations ?? "");
setNbServ(prefill.nb_services_repetition ?? "");
setDatesRep(prefill.dates_representations || "");
setDatesServ(prefill.dates_repetitions || "");
setHeuresTotal(prefill.heures_total ?? "");
setMinutesTotal(prefill.minutes_total ?? "0");
setJoursTravail(prefill.jours_travail || "");
if (typeof prefill.multi_mois === "boolean") setIsMultiMois(prefill.multi_mois ? "Oui" : "Non");
setTypeSalaire(prefill.type_salaire ?? "Brut");
setMontantSalaire(prefill.montant ?? "");
setPanierRepas(prefill.panier_repas ?? "Non");
setNotes(prefill.notes || "");
// Si technicien: préremplir heures/minutes depuis nb_heures_annexes si fourni
const cat = prefill.categorie ?? "Artiste";
if (
cat === "Technicien" &&
prefill.nb_heures_annexes !== undefined &&
prefill.nb_heures_annexes !== null &&
prefill.nb_heures_annexes !== "" &&
(prefill.heures_total === undefined || prefill.heures_total === "")
) {
const raw = String(prefill.nb_heures_annexes).replace(",", ".");
const total = parseFloat(raw);
if (Number.isFinite(total)) {
const h = Math.floor(total);
const frac = total - h;
// Si décimale, on met +30 min (comportement demandé)
const mins: "0" | "30" = frac > 0 ? "30" : "0";
setHeuresTotal(h);
setMinutesTotal(mins);
}
}
if (prefill.reference) setReference(prefill.reference);
}, [prefill]);
// Détection du paramètre regime=RG dans l'URL
useEffect(() => {
let search = "";
try {
search = window.location.search || "";
} catch {
return;
}
const urlParams = new URLSearchParams(search);
const regime = urlParams.get("regime");
if (regime === "RG") {
setIsRegimeRG(true);
// Adapter les valeurs par défaut pour le RG
setCategoriePro("Non Cadre");
setIsMultiMois("Non"); // Pas de multi-mois en RG
} else {
setIsRegimeRG(false);
setCategoriePro("Artiste");
}
}, []);
// Pré-remplissage du salarié via paramètre d'URL ?salarie=MATRICULE (ou ?matricule=)
useEffect(() => {
// Si déjà défini (via prefill ou choix manuel), ou si l'utilisateur a explicitement cliqué sur "Changer", ne rien faire
if (urlSalarieDismissed || salarie || prefill?.salarie) return;
// Lecture du paramètre d'URL (SSR safe)
let search = "";
try {
search = window.location.search || "";
} catch {
return;
}
const urlParams = new URLSearchParams(search);
const matriculeParam = urlParams.get("salarie") || urlParams.get("matricule");
if (!matriculeParam) return;
let cancelled = false;
(async () => {
// Récupérer le contexte client (même logique que dans les hooks)
let clientInfo: ClientInfo = null;
try {
const res = await fetch("/api/me", {
cache: "no-store",
headers: { Accept: "application/json" },
credentials: "include",
});
if (res.ok) {
const me = await res.json();
clientInfo = {
id: me.active_org_id || null,
name: me.active_org_name || "Organisation",
api_name: me.active_org_api_name,
} as ClientInfo;
}
} catch {}
// 1) Essai par endpoint détail
try {
const detail: any = await api(`/salaries/${encodeURIComponent(matriculeParam)}`, {}, clientInfo);
if (!cancelled && detail) {
const displayName = (
[detail.prenom, detail.nom_usage || detail.nom]
.filter(Boolean)
.join(" ")
.trim()
) ||
detail.nom_usage ||
detail.nom ||
detail.pseudo ||
matriculeParam;
setSalarie({
matricule: detail.matricule ?? matriculeParam,
nom: displayName,
email: detail.email ?? undefined,
});
return;
}
} catch {
// 2) Fallback: recherche générique
try {
const p = new URLSearchParams();
p.set("page", "1");
p.set("limit", "1");
p.set("q", matriculeParam);
const result: any = await api(`/salaries?${p.toString()}`, {}, clientInfo);
const it = result?.items?.[0];
if (!cancelled && it) {
const displayName = ([it.prenom, it.nom].filter(Boolean).join(" ") || it.nom || it.pseudo || it.matricule).trim();
setSalarie({ matricule: it.matricule, nom: displayName, email: it.email ?? undefined });
}
} catch {}
}
})();
return () => {
cancelled = true;
};
}, [salarie, prefill?.salarie, urlSalarieDismissed]);
// Dans /components/contrats/NouveauCDDUForm.tsx
// Remplacez la section useEffect de duplication (lignes ~195-270) par ce code amélioré :
// Duplication via URL par identifiant : ?dupe_id=UUID
useEffect(() => {
// Ne pas appliquer si on est déjà en mode édition avec prefill ou si déjà appliqué
if (prefill || dupeApplied) return;
// Lecture sécurisée du paramètre
let search = "";
try { search = window.location.search || ""; } catch { return; }
const params = new URLSearchParams(search);
const dupeId = params.get("dupe_id");
if (!dupeId) return;
setPrefillLoading(true);
let cancelled = false;
(async () => {
try {
// 1) Contexte client
let clientInfo: ClientInfo = null;
try {
const res = await fetch("/api/me", { cache: "no-store", headers: { Accept: "application/json" }, credentials: "include" });
if (res.ok) {
const me = await res.json();
clientInfo = { id: me.active_org_id || null, name: me.active_org_name || "Organisation", api_name: me.active_org_api_name } as ClientInfo;
}
} catch {}
// 2) Récupération du contrat complet
const contrat: any = await api(`/contrats/${encodeURIComponent(dupeId)}`, {}, clientInfo);
if (cancelled || !contrat) { setPrefillLoading(false); return; }
// 3) Mapping vers le format de pré-remplissage du formulaire
const payload = {
spectacle: contrat.production || "",
numero_objet: contrat.objet ?? "",
salarie: (contrat.salarie_matricule || contrat.salarie) ? {
matricule: contrat.salarie_matricule || "",
nom: (contrat.salarie?.nom || contrat.salarie_nom || "").toString(),
email: contrat.salarie?.email || undefined,
} : null,
categorie: (() => {
const c = (contrat.categorie_prof || "").toString().toLowerCase();
return c.includes("tech") ? "Technicien" : "Artiste";
})(),
profession: { label: contrat.profession || "", code: contrat.profession_code || "" },
date_debut: contrat.date_debut || "",
date_fin: contrat.date_fin || "",
nb_representations: Number.isFinite(Number(contrat.nb_representations)) ? Number(contrat.nb_representations) : "",
nb_services_repetition: Number.isFinite(Number(contrat.nb_services_repetitions)) ? Number(contrat.nb_services_repetitions) : "",
dates_representations: contrat.dates_representations || "",
dates_repetitions: contrat.dates_repetitions || "",
heures_total: (() => {
const aem = Number(contrat.nb_heures_aem);
if (Number.isFinite(aem) && aem >= 0) return aem;
const rep = Number(contrat.nb_heures_repetitions || 0);
const ann = Number(contrat.nb_heures_annexes || 0);
const total = rep + ann;
return Number.isFinite(total) && total > 0 ? total : "";
})(),
minutes_total: (contrat.minutes_total === "30" ? "30" : "0") as "0" | "30",
jours_travail: typeof contrat.jours_travailles === "number" ? String(contrat.jours_travailles) : (contrat.jours_travail || ""),
type_salaire: (() => {
const t = (contrat.type_salaire || "").toString().toLowerCase();
if (t.includes("net") && t.includes("pas")) return "Net avant PAS" as const;
if (t.includes("coût") || t.includes("cout") || t.includes("total")) return "Coût total employeur" as const;
if (t.includes("minimum") || t.includes("conventionnel")) return "Minimum conventionnel" as const;
return "Brut" as const;
})(),
montant: (() => {
const raw = (contrat.salaire_demande || contrat.montant || "").toString();
const cleaned = raw.replace(/[^0-9,.-]/g, '').replace(/,(?=\d{1,2}$)/, '.').replace(/,/g, '.');
const num = parseFloat(cleaned);
return Number.isFinite(num) && num > 0 ? num : "";
})(),
panier_repas: (() => {
const v = (contrat.panier_repas || "").toString().toLowerCase();
if (["oui","true","1"].includes(v)) return "Oui" as const;
if (["non","false","0"].includes(v)) return "Non" as const;
return "Non" as const;
})(),
notes: contrat.notes || "",
multi_mois: !!(contrat.is_multi_mois || (contrat.regime && String(contrat.regime).toUpperCase()==="CDDU_MULTI")),
};
// 4) Appliquer au formulaire (mêmes setters que pour ?dupe=)
if (payload.spectacle) {
setSpectacle(String(payload.spectacle));
setSpectaclePick({ nom: String(payload.spectacle), numero_objet: (payload.numero_objet as any) ?? null });
setSpectacleManual(false);
setSpectacleQuery("");
}
if (payload.numero_objet !== undefined) setObjet(String(payload.numero_objet ?? ""));
if (payload.salarie) setSalarie({ matricule: payload.salarie.matricule, nom: payload.salarie.nom, email: payload.salarie.email });
if (payload.categorie) setCategoriePro(payload.categorie as any);
if (payload.profession?.label) {
setProfession(String(payload.profession.label));
setProfessionPick({ code: String(payload.profession.code || ""), label: String(payload.profession.label) });
}
if (payload.date_debut) setDateDebut(String(payload.date_debut));
if (payload.date_fin) setDateFin(String(payload.date_fin));
if (payload.nb_representations !== "") setNbRep(payload.nb_representations as any);
if (payload.nb_services_repetition !== "") setNbServ(payload.nb_services_repetition as any);
if (payload.dates_representations) setDatesRep(String(payload.dates_representations));
if (payload.dates_repetitions) setDatesServ(String(payload.dates_repetitions));
if (payload.heures_total !== "") setHeuresTotal(payload.heures_total as any);
if (payload.minutes_total) setMinutesTotal(payload.minutes_total);
if (payload.jours_travail) setJoursTravail(String(payload.jours_travail));
if (payload.type_salaire) setTypeSalaire(payload.type_salaire);
if (payload.montant !== "") setMontantSalaire(payload.montant as any);
if (payload.panier_repas) setPanierRepas(payload.panier_repas);
if (payload.notes) setNotes(String(payload.notes));
setIsMultiMois(payload.multi_mois ? "Oui" : "Non");
// 5) Nouvelle référence systématique
setReference(generateReference());
// 6) Marquer appliqué
setDupeApplied(true);
setPrefillLoading(false);
} catch (e) {
console.warn("⚠️ Duplication par id échouée:", e);
setPrefillLoading(false);
}
})();
return () => { cancelled = true; setPrefillLoading(false); };
}, [prefill, dupeApplied]);
// Duplication via URL: ?dupe=base64(json) ou ?duplicate=base64(json)
useEffect(() => {
// Ne pas appliquer si on est déjà en mode édition avec prefill ou si déjà appliqué
if (prefill || dupeApplied) return;
// SSR safe-guard
let search = "";
try {
search = window.location.search || "";
} catch {
return;
}
const params = new URLSearchParams(search);
const dupe = params.get("dupe") || params.get("duplicate");
if (!dupe) return;
const data = parseBase64Json<Partial<NouveauCDDUPrefill>>(dupe);
if (!data) {
console.warn("❌ Impossible de décoder les données de duplication");
return;
}
setPrefillLoading(true);
console.log("🔄 Données de duplication reçues:", data); // Debug
// Application des données avec vérifications
// ========================================
// Production/Spectacle
if (data.spectacle) {
const spectacleName = String(data.spectacle).trim();
if (spectacleName) {
setSpectacle(spectacleName);
setSpectaclePick({ nom: spectacleName, numero_objet: (data.numero_objet as any) ?? null });
setSpectacleManual(false);
setSpectacleQuery("");
console.log("✅ Production pré-remplie:", spectacleName);
}
}
// Numéro d'objet
if (!objet && data.numero_objet !== undefined && data.numero_objet !== null) {
const objetValue = String(data.numero_objet).trim();
setObjet(objetValue);
if (objetValue) {
console.log("✅ Numéro d'objet pré-rempli:", objetValue);
}
}
// Salarié
if (!salarie && data.salarie) {
const salarieData = {
matricule: String(data.salarie.matricule || "").trim(),
nom: String(data.salarie.nom || "").trim(),
email: data.salarie.email ? String(data.salarie.email).trim() : undefined,
};
if (salarieData.matricule && salarieData.nom) {
setSalarie(salarieData);
console.log("✅ Salarié pré-rempli:", salarieData);
// Si l'email n'est pas fourni dans la duplication, tenter un enrichissement
if (!salarieData.email || salarieData.email === "") {
(async () => {
try {
let clientInfo: ClientInfo = null;
const res = await fetch("/api/me", {
cache: "no-store",
headers: { Accept: "application/json" },
credentials: "include",
});
if (res.ok) {
const me = await res.json();
clientInfo = {
id: me.active_org_id || null,
name: me.active_org_name || "Organisation",
api_name: me.active_org_api_name,
} as ClientInfo;
}
const detail: any = await api(`/salaries/${encodeURIComponent(salarieData.matricule)}`, {}, clientInfo);
if (detail?.email) {
setSalarie((prev) => (prev ? { ...prev, email: detail.email } : prev));
console.log("✅ Email salarié enrichi:", detail.email);
}
} catch (e) {
console.warn("⚠️ Impossible d'enrichir l'email du salarié:", e);
}
})();
}
}
}
// Catégorie professionnelle avec vérification améliorée
if (data.categorie) {
const categorieStr = String(data.categorie).trim().toLowerCase();
let newCategorie: "Artiste" | "Technicien" | "Autre" = "Artiste";
if (categorieStr.includes("technicien") || categorieStr === "technicien") {
newCategorie = "Technicien";
} else if (categorieStr.includes("artiste") || categorieStr === "artiste") {
newCategorie = "Artiste";
} else if (categorieStr.includes("autre") || categorieStr === "autre") {
newCategorie = "Autre";
}
setCategoriePro(newCategorie);
console.log("✅ Catégorie professionnelle pré-remplie:", newCategorie);
}
// Profession
if (!profession && (data.profession?.label || data.profession)) {
const professionData = data.profession?.label ? data.profession : { label: String(data.profession), code: "" };
const professionLabel = String(professionData.label).trim();
if (professionLabel) {
setProfession(professionLabel);
setProfessionPick({
code: String(professionData.code || "").trim(),
label: professionLabel,
});
console.log("✅ Profession pré-remplie:", professionLabel);
}
}
// Dates
if (!dateDebut && data.date_debut) {
const debutValue = String(data.date_debut).trim();
if (debutValue.match(/^\d{4}-\d{2}-\d{2}$/)) {
setDateDebut(debutValue);
console.log("✅ Date début pré-remplie:", debutValue);
}
}
if (!dateFin && data.date_fin) {
const finValue = String(data.date_fin).trim();
if (finValue.match(/^\d{4}-\d{2}-\d{2}$/)) {
setDateFin(finValue);
console.log("✅ Date fin pré-remplie:", finValue);
}
}
// Nombres avec vérifications
if (nbRep === "" && data.nb_representations !== undefined && data.nb_representations !== null) {
const repValue = Number(data.nb_representations);
if (Number.isInteger(repValue) && repValue >= 0) {
setNbRep(repValue);
console.log("✅ Nb représentations pré-rempli:", repValue);
}
}
if (nbServ === "" && data.nb_services_repetition !== undefined && data.nb_services_repetition !== null) {
const servValue = Number(data.nb_services_repetition);
if (Number.isInteger(servValue) && servValue >= 0) {
setNbServ(servValue);
console.log("✅ Nb services répétition pré-rempli:", servValue);
}
}
// Dates textuelles
if (!datesRep && data.dates_representations) {
const datesRepValue = String(data.dates_representations).trim();
if (datesRepValue) {
setDatesRep(datesRepValue);
console.log("✅ Dates représentations pré-remplies:", datesRepValue);
}
}
if (!datesServ && data.dates_repetitions) {
const datesServValue = String(data.dates_repetitions).trim();
if (datesServValue) {
setDatesServ(datesServValue);
console.log("✅ Dates répétitions pré-remplies:", datesServValue);
}
}
// Heures et minutes
if (heuresTotal === "" && data.heures_total !== undefined && data.heures_total !== null) {
const heuresValue = Number(data.heures_total);
if (Number.isFinite(heuresValue) && heuresValue >= 0) {
setHeuresTotal(Math.floor(heuresValue)); // Arrondir à l'entier inférieur
console.log("✅ Heures total pré-remplies:", Math.floor(heuresValue));
}
}
if (data.minutes_total && (data.minutes_total === "0" || data.minutes_total === "30")) {
setMinutesTotal(data.minutes_total);
console.log("✅ Minutes total pré-remplies:", data.minutes_total);
}
if (!joursTravail && data.jours_travail) {
const joursTravailValue = String(data.jours_travail).trim();
if (joursTravailValue) {
setJoursTravail(joursTravailValue);
console.log("✅ Jours travail pré-remplis:", joursTravailValue);
}
}
// Type de salaire avec vérification
if (data.type_salaire) {
const typeValue = String(data.type_salaire).trim();
const validTypes: Array<"Brut" | "Net avant PAS" | "Coût total employeur" | "Minimum conventionnel"> =
["Brut", "Net avant PAS", "Coût total employeur", "Minimum conventionnel"];
if (validTypes.includes(typeValue as any)) {
setTypeSalaire(typeValue as any);
console.log("✅ Type salaire pré-rempli:", typeValue);
} else {
console.warn("⚠️ Type de salaire invalide:", typeValue);
}
}
// Montant avec validation
if (montantSalaire === "" && data.montant !== undefined && data.montant !== null) {
const montantValue = Number(data.montant);
if (Number.isFinite(montantValue) && montantValue > 0) {
setMontantSalaire(montantValue);
console.log("✅ Montant pré-rempli:", montantValue);
}
}
// Panier repas
if (data.panier_repas && (data.panier_repas === "Oui" || data.panier_repas === "Non")) {
setPanierRepas(data.panier_repas);
console.log("✅ Panier repas pré-rempli:", data.panier_repas);
}
// Multi-mois
if (typeof data.multi_mois === "boolean") {
setIsMultiMois(data.multi_mois ? "Oui" : "Non");
console.log("✅ Multi-mois pré-rempli:", data.multi_mois ? "Oui" : "Non");
}
// Toujours régénérer une nouvelle référence pour la duplication
const newRef = generateReference();
setReference(newRef);
console.log("✅ Nouvelle référence générée:", newRef);
// Marquer comme appliqué pour ne pas réécraser les changements utilisateur
setDupeApplied(true);
setPrefillLoading(false);
}, [prefill, dupeApplied]);
const { open: showLeaveConfirm, confirmLeave, cancelLeave, allowNavRef } = useUnsavedChangesPrompt(isDirty && !redirecting && !loading);
useEffect(() => {
// En création seulement : pas de prefill.reference -> on génère
if (!prefill?.reference) {
setReference(generateReference());
}
}, [prefill?.reference]);
const filteredProfessions = useMemo(() => {
const q = professionQuery.trim().toLowerCase();
if (!q) return PROFESSIONS_ARTISTE;
return PROFESSIONS_ARTISTE.filter(p =>
p.code.toLowerCase().includes(q) || p.label.toLowerCase().includes(q)
).slice(0, 20);
}, [professionQuery]);
const filteredTechniciens = useMemo(() => {
if (!techniciens) return [] as TechnicienOption[];
const q = norm(professionQuery);
if (!q) return techniciens.slice(0, 30);
return techniciens
.filter(p => norm(p.code).includes(q) || norm(p.label).includes(q))
.slice(0, 30);
}, [techniciens, professionQuery]);
const useHeuresMode = useMemo(() => {
return (
categoriePro === "Technicien" ||
(categoriePro === "Artiste" && professionPick?.code === "MET040")
);
}, [categoriePro, professionPick]);
const debouncedSalarieQuery = useDebouncedValue(salarieQuery, 300);
const { data: salarieSearch, isFetching: fetchingSalaries } = useSearchSalaries(debouncedSalarieQuery, selectedOrg?.id);
const { data: spectacleSearch, isFetching: fetchingSpectacles } = useSearchSpectacles(spectacleQuery, selectedOrg?.id);
// 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();
}, []);
// Réinitialiser l'index actif quand la query ou le nombre de résultats change
useEffect(() => {
setSpectacleActive(0);
}, [spectacleQuery, spectacleSearch?.items?.length]);
useEffect(() => {
setSalarieActive(0);
}, [salarieQuery, salarieSearch?.items?.length]);
const currentProfList = (categoriePro === 'Artiste' ? filteredProfessions : filteredTechniciens);
useEffect(() => {
setProfessionActive(0);
}, [professionQuery, currentProfList.length, categoriePro]);
const isPastStart = useMemo(() => {
if (!dateDebut) return false;
return dateDebut < todayYmd();
}, [dateDebut]);
async function ensureTechniciensLoaded(){
if (techLoadedRef.current || techniciens) return;
try {
setTechLoading(true);
const res = await fetch('/data/professions-techniciens.json', { cache: 'force-cache' });
if (!res.ok) throw new Error('HTTP '+res.status);
const list: TechnicienOption[] = await res.json();
setTechniciens(list);
techLoadedRef.current = true;
} finally {
setTechLoading(false);
}
}
// Upload de fichiers supprimé
async function onSubmit() {
setErr(null);
// validations minimales
if (!isRegimeRG && !spectacle.trim()) return setErr("Le champ Spectacle est obligatoire.");
if (!salarie) return setErr("Veuillez sélectionner un·e salarié·e.");
if (!profession.trim()) return setErr("La profession est obligatoire.");
if (!dateDebut) return setErr("La date de début du contrat est obligatoire.");
// Validations spécifiques au RG
if (isRegimeRG) {
if (typeContratRG === "CDD de droit commun") {
if (motifCDD === "Remplacement d'une absence") {
if (!nomPersonneRemplacee.trim()) return setErr("Veuillez indiquer le nom de la personne remplacée.");
}
if (motifCDD === "Accroissement temporaire d'activité") {
if (!motifAccroissement.trim()) return setErr("Veuillez décrire le motif de l'accroissement d'activité.");
}
}
// Pour CDI, pas de date de fin obligatoire
if (typeContratRG !== "CDI" && !dateFin) {
return setErr("La date de fin est obligatoire pour ce type de contrat.");
}
}
if (!isRegimeRG && useHeuresMode) {
if (heuresTotal === "" || Number(heuresTotal) < 0)
return setErr("Veuillez indiquer le nombre d'heures de travail.");
}
if (dateFin && dateDebut && dateFin < dateDebut)
return setErr("La date de fin doit être postérieure ou égale à la date de début.");
if (isPastStart && !confirmPastStart)
return setErr("Veuillez cocher la case de confirmation liée au démarrage dans le passé.");
if (typeSalaire !== "Minimum conventionnel") {
if (montantSalaire === "" || Number(montantSalaire) <= 0)
return setErr("Veuillez saisir un montant de rémunération valide.");
}
// Valeurs structurées communes (PATCH ou webhook)
const payload = {
regime: isRegimeRG ? "RG" : (isMultiMois === "Oui" ? "CDDU_MULTI" : "CDDU_MONO"),
multi_mois: !isRegimeRG && isMultiMois === "Oui",
spectacle: isRegimeRG ? undefined : spectacle,
numero_objet: isRegimeRG ? undefined : (objet || undefined),
// Champs spécifiques au RG
type_contrat_rg: isRegimeRG ? typeContratRG : undefined,
motif_cdd: isRegimeRG && typeContratRG === "CDD de droit commun" ? motifCDD : undefined,
nom_personne_remplacee: isRegimeRG && typeContratRG === "CDD de droit commun" && motifCDD === "Remplacement d'une absence" ? nomPersonneRemplacee : undefined,
situation_personne_remplacee: isRegimeRG && typeContratRG === "CDD de droit commun" && motifCDD === "Remplacement d'une absence" ? situationPersonneRemplacee : undefined,
motif_accroissement: isRegimeRG && typeContratRG === "CDD de droit commun" && motifCDD === "Accroissement temporaire d'activité" ? motifAccroissement : undefined,
// Champs communs
salarie: salarie!, // validé plus haut
categorie_professionnelle: categoriePro,
profession,
profession_code: !isRegimeRG ? (professionPick?.code || undefined) : undefined,
date_debut: dateDebut,
date_fin: (isRegimeRG && typeContratRG === "CDI") ? undefined : (dateFin || undefined),
nb_representations: !isRegimeRG ? nbRep : undefined,
nb_services_repetition: !isRegimeRG ? nbServ : undefined,
dates_representations: !isRegimeRG ? (datesRep || undefined) : undefined,
dates_repetitions: !isRegimeRG ? (datesServ || undefined) : undefined,
heures_travail: !isRegimeRG && useHeuresMode ? heuresTotal : undefined,
minutes_travail: !isRegimeRG && useHeuresMode ? minutesTotal : undefined,
jours_travail: !isRegimeRG && useHeuresMode ? (joursTravail || undefined) : undefined,
type_salaire: typeSalaire,
montant: typeSalaire !== "Minimum conventionnel" ? (montantSalaire === "" ? undefined : montantSalaire) : undefined,
panier_repas: panierRepas,
reference,
notes: notes || undefined,
valider_direct: validerDirect === "Oui",
} as const;
setLoading(true);
try {
if (onSubmitOverride) {
// MODE ÉDITION → PATCH via l'API Next (proxy Lambda)
await onSubmitOverride(payload as any);
} else {
// MODE CRÉATION → utiliser l'API appropriée selon le régime
const contractData = {
// Champs communs
salarie_matricule: payload.salarie.matricule,
salarie_nom: payload.salarie.nom,
salarie_email: payload.salarie.email,
// Préférence d'envoi d'e-mail de confirmation
send_email_confirmation: emailConfirm === "Oui",
categorie: payload.categorie_professionnelle,
profession: payload.profession,
date_debut: payload.date_debut,
date_fin: payload.date_fin,
type_salaire: payload.type_salaire,
montant: payload.montant,
panier_repas: payload.panier_repas,
reference: payload.reference,
notes: payload.notes,
// Ajouter l'organisation sélectionnée si staff
org_id: selectedOrg?.id || null,
// Champs spécifiques selon le régime
...(isRegimeRG ? {
// Champs RG
regime: "RG",
type_contrat: payload.type_contrat_rg,
motif_cdd: payload.motif_cdd,
nom_personne_remplacee: payload.nom_personne_remplacee,
situation_personne_remplacee: payload.situation_personne_remplacee,
motif_accroissement: payload.motif_accroissement,
} : {
// Champs CDDU
regime: payload.regime,
production_id: spectaclePick?.id || null,
spectacle: payload.spectacle,
numero_objet: payload.numero_objet,
profession_code: payload.profession_code,
profession_label: payload.profession,
nb_representations: payload.nb_representations,
nb_services_repetition: payload.nb_services_repetition,
dates_representations: payload.dates_representations,
dates_repetitions: payload.dates_repetitions,
heures_total: payload.heures_travail,
minutes_total: payload.minutes_travail,
jours_travail: payload.jours_travail,
multi_mois: payload.multi_mois,
})
};
// Utiliser l'endpoint approprié
const apiEndpoint = isRegimeRG ? "/api/rg-contracts" : "/api/cddu-contracts";
const res = await fetch(apiEndpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(contractData)
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData.error || `Erreur HTTP ${res.status}`);
}
// 🎯 Tracker l'événement de création de contrat avec PostHog
const result = await res.json().catch(() => ({}));
posthog?.capture('contract_created', {
contract_type: isRegimeRG ? 'RG' : 'CDDU',
regime: isRegimeRG ? 'Régime Général' : payload.regime,
multi_mois: payload.multi_mois,
categorie_professionnelle: payload.categorie_professionnelle,
contract_id: result?.contract?.id || result?.contract?.contract_number,
has_notes: !!payload.notes,
validation_immediate: payload.valider_direct,
});
console.log('📊 PostHog: Événement contract_created envoyé');
}
// Autoriser la navigation après soumission + feedback
allowNavRef.current = true;
setRedirecting(true);
setTimeout(() => {
router.push("/contrats");
}, 3000);
} catch (e: any) {
setErr(e?.message || "Impossible denvoyer le formulaire.");
} finally {
setLoading(false);
}
}
return (
<>
<div className="space-y-5">
{!prefill && (
<div className="rounded-2xl border bg-white p-4">
<div className="text-lg font-semibold">{isRegimeRG ? "Nouveau contrat Régime Général" : "Nouveau CDDU"}</div>
<p className="text-sm text-slate-600 mt-1">
Ce formulaire est dédié aux CDD dusage (intermittents du spectacle).
</p>
</div>
)}
{/* Référence */}
<Section title="Référence Contrat">
<div className="grid grid-cols-1 md:grid-cols-[240px_1fr] items-center gap-4">
<Label>Référence</Label>
<div className="flex items-center gap-2 text-sm">
<input
value={reference || "(génération en cours…)"}
onFocus={() => {
if (!reference && !prefill?.reference) {
setReference(generateReference());
}
}}
onChange={isStaff ? (e) => setReference(e.target.value) : undefined}
readOnly={!isStaff}
className={`w-full px-3 py-2 rounded-lg border text-sm ${
isStaff
? "bg-white"
: "bg-slate-50"
}`}
/>
<span className="inline-flex items-center gap-1 text-xs text-slate-500">
<Info className="w-3.5 h-3.5" />
{isStaff ? "Générée automatiquement - modifiable par Staff" : "Générée automatiquement par l'Espace Paie"}
</span>
</div>
</div>
</Section>
{/* Sélection d'organisation (pour les utilisateurs staff) */}
{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>
</div>
</div>
</Section>
)}
{/* Production - Masquée en mode Régime Général */}
{!isRegimeRG && (
<Section title="Production">
<FieldRow>
<div>
<Label required>Production</Label>
{spectaclePick ? (
<div className="flex items-center gap-2">
<input
readOnly
value={`${spectaclePick.nom}${spectaclePick.numero_objet ? `${spectaclePick.numero_objet}` : ""}`}
className="w-full px-3 py-2 rounded-lg border bg-slate-50 text-sm"
/>
<button
type="button"
onClick={() => { setSpectaclePick(null); setSpectacleQuery(""); }}
className="text-xs px-2 py-2 rounded-lg border"
>
Changer
</button>
</div>
) : (
<div>
{spectacleManual ? (
<>
<input
ref={spectacleInputRef}
value={spectacle}
onChange={(e) => setSpectacle(e.target.value)}
placeholder="Saisir le nom de la nouvelle production…"
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<div className="mt-2 flex items-center gap-2">
<button
type="button"
onClick={() => { setSpectacleManual(false); setSpectacleQuery(""); setSpectaclePick(null); }}
className="inline-flex items-center gap-2 text-xs px-2 py-1 rounded-lg bg-gradient-to-r from-blue-500 to-indigo-600 text-white shadow hover:opacity-90 transition"
>
Rechercher une production existante
</button>
</div>
</>
) : (
<>
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
value={spectacleQuery}
onChange={(e) => setSpectacleQuery(e.target.value)}
onKeyDown={(e) => {
const items = spectacleSearch?.items || [];
if (items.length === 0) return;
if (e.key === 'ArrowDown') { e.preventDefault(); setSpectacleActive(i => Math.min(i + 1, items.length - 1)); }
if (e.key === 'ArrowUp') { e.preventDefault(); setSpectacleActive(i => Math.max(i - 1, 0)); }
if (e.key === 'Enter') {
e.preventDefault();
const s = items[spectacleActive];
if (s) {
setSpectaclePick(s);
setSpectacle(s.nom);
if (s.numero_objet) setObjet(String(s.numero_objet));
}
}
if (e.key === 'Escape') {
e.preventDefault();
(e.currentTarget as HTMLInputElement).blur();
}
}}
placeholder="Rechercher une production…"
className="w-full pl-9 px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
<div className="mt-2">
<button
type="button"
onClick={() => {
setSpectacleManual(true);
setSpectaclePick(null);
setSpectacleQuery("");
setSpectacle("");
setTimeout(() => spectacleInputRef.current?.focus(), 0);
}}
className="inline-flex items-center gap-2 text-xs px-2 py-1 rounded-lg bg-gradient-to-r from-blue-500 to-indigo-600 text-white shadow hover:opacity-90 transition"
>
Nouvelle production
</button>
</div>
{fetchingSpectacles && (
<div className="text-xs text-slate-500 mt-2">
<Loader2 className="w-3.5 h-3.5 inline animate-spin mr-1" />
Recherche
</div>
)}
{spectacleSearch?.items && (
<div className="mt-2 max-h-56 overflow-auto rounded-lg border">
{spectacleSearch.items.length === 0 ? (
<div className="p-3 text-sm text-slate-500">Aucun résultat.</div>
) : (
spectacleSearch.items.map((s, idx) => (
<button
type="button"
key={`${s.nom}-${s.numero_objet ?? idx}`}
onClick={() => {
setSpectaclePick(s);
setSpectacle(s.nom);
if (s.numero_objet) setObjet(String(s.numero_objet));
}}
className={`w-full text-left px-3 py-2 text-sm ${idx === spectacleActive ? 'bg-slate-50' : 'hover:bg-slate-50'}`}
onMouseEnter={() => setSpectacleActive(idx)}
>
<div className="font-medium">{s.nom}</div>
<div className="text-xs text-slate-500">{s.numero_objet ? `n° dobjet : ${s.numero_objet}` : "Sans n° dobjet"}</div>
</button>
))
)}
</div>
)}
</>
)}
</div>
)}
</div>
<div>
<Label>Avez-vous déjà un n° dobjet ?</Label>
<input
value={objet}
onChange={(e) => setObjet(e.target.value)}
placeholder="ex : 2B728735689"
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<p className="text-[11px] text-slate-500 mt-1">
Si vous choisissez une production existante, le numéro se remplit automatiquement sil est connu. Si vous n'avez pas encore de n° d'objet pour une nouvelle production, nous nous occuperons gratuitement de la démarche.
</p>
</div>
</FieldRow>
</Section>
)}
{/* Salarié·e */}
<Section title="Salarié">
<FieldRow>
<div>
<Label required>Salarié</Label>
{salarie ? (
<div className="flex items-center gap-2">
<input
readOnly
value={`${salarie.nom}${salarie.matricule}`}
className="w-full px-3 py-2 rounded-lg border bg-slate-50 text-sm"
/>
<button
type="button"
onClick={() => { setUrlSalarieDismissed(true); setSalarie(null); setSalarieQuery(""); }}
className="text-xs px-2 py-2 rounded-lg border"
>
Changer
</button>
</div>
) : (
<div>
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
value={salarieQuery}
onChange={(e) => setSalarieQuery(e.target.value)}
onKeyDown={(e) => {
const items = salarieSearch?.items || [];
if (items.length === 0) return;
if (e.key === 'ArrowDown') { e.preventDefault(); setSalarieActive(i => Math.min(i + 1, items.length - 1)); }
if (e.key === 'ArrowUp') { e.preventDefault(); setSalarieActive(i => Math.max(i - 1, 0)); }
if (e.key === 'Enter') {
e.preventDefault();
const s = items[salarieActive];
if (s) setSalarie(s);
}
if (e.key === 'Escape') {
e.preventDefault();
(e.currentTarget as HTMLInputElement).blur();
}
}}
placeholder="Rechercher un salarié…"
className="w-full pl-9 px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
<div className="mt-2">
<button
type="button"
onClick={() => setNewSalarieOpen(true)}
className="inline-flex items-center gap-2 text-xs px-2 py-1 rounded-lg bg-gradient-to-r from-blue-500 to-indigo-600 text-white shadow hover:opacity-90 transition"
>
Nouveau salarié
</button>
</div>
{fetchingSalaries && (
<div className="text-xs text-slate-500 mt-2">
<Loader2 className="w-3.5 h-3.5 inline animate-spin mr-1" />
Recherche
</div>
)}
{salarieSearch?.items && (
<div className="mt-2 max-h-56 overflow-auto rounded-lg border">
{salarieSearch.items.length === 0 ? (
<div className="p-3 text-sm text-slate-500">Aucun résultat.</div>
) : (
salarieSearch.items.map((s, idx) => (
<button
type="button"
key={s.matricule}
onClick={() => setSalarie(s)}
className={`w-full text-left px-3 py-2 text-sm ${idx === salarieActive ? 'bg-slate-50' : 'hover:bg-slate-50'}`}
onMouseEnter={() => setSalarieActive(idx)}
>
<div className="font-medium">{s.nom}</div>
<div className="text-xs text-slate-500">{s.matricule} {s.email ? `${s.email}` : ""}</div>
</button>
))
)}
</div>
)}
</div>
)}
</div>
<div>
<Label>Son adresse email</Label>
<input
value={salarie?.email || ""}
readOnly
placeholder="—"
className="w-full px-3 py-2 rounded-lg border bg-slate-50 text-sm"
/>
<p className="text-[11px] text-slate-500 mt-1">
Nous utilisons l'adresse e-mail de votre salarié pour lui envoyer une demande de justificatifs et d'état-civil, puis les notifications afférentes à la gestion de son contrat de travail. Cette zone peut être laissée vide si le salarié est déjà connu de nos services.
</p>
</div>
</FieldRow>
<FieldRow>
<div>
<Label required>Catégorie professionnelle</Label>
<select
value={categoriePro}
onChange={(e) => setCategoriePro(e.target.value as any)}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
>
{isRegimeRG ? (
<>
<option>Cadre</option>
<option>Non Cadre</option>
<option>Je ne sais pas</option>
</>
) : (
<>
<option>Artiste</option>
<option>Technicien</option>
</>
)}
</select>
</div>
<div>
<Label required>Profession pour ce contrat {categoriePro}</Label>
{isRegimeRG ? (
// En mode RG, simple champ de saisie libre
<div>
<input
value={profession}
onChange={(e) => setProfession(e.target.value)}
placeholder="Saisissez la profession..."
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<p className="text-[11px] text-slate-500 mt-1">
Saisissez librement la profession du salarié pour ce contrat.
</p>
</div>
) : (
// Mode CDDU avec listes de professions
(categoriePro === "Artiste" || categoriePro === "Technicien") ? (
professionPick ? (
<>
<div className="flex items-center gap-2">
<input
readOnly
value={`${professionPick.label}${professionPick.code}`}
className="w-full px-3 py-2 rounded-lg border bg-slate-50 text-sm"
/>
<button
type="button"
onClick={() => { setProfessionPick(null); setProfessionQuery(""); setProfession(""); }}
className="text-xs px-2 py-2 rounded-lg border"
>
Changer
</button>
</div>
<p className="text-[11px] text-slate-500 mt-1">
Cette liste contient l'ensemble des professions de l'Annexe 8 (Techniciens) ou 10 (Artistes) et leur Code Emploi. Si une profession n'apparaît pas, il n'est pas possible de réaliser un CDDU pour ce contrat.
</p>
</>
) : (
<div>
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
value={professionQuery}
onFocus={() => { if (categoriePro === 'Technicien') ensureTechniciensLoaded(); }}
onChange={(e) => { setProfessionQuery(e.target.value); if (categoriePro === 'Technicien') ensureTechniciensLoaded(); }}
onKeyDown={(e) => {
const items = (categoriePro === 'Artiste' ? filteredProfessions : filteredTechniciens);
if (items.length === 0) return;
if (e.key === 'ArrowDown') { e.preventDefault(); setProfessionActive(i => Math.min(i + 1, items.length - 1)); }
if (e.key === 'ArrowUp') { e.preventDefault(); setProfessionActive(i => Math.max(i - 1, 0)); }
if (e.key === 'Enter') {
e.preventDefault();
const p = items[professionActive];
if (p) { setProfessionPick(p as any); setProfession((p as any).label); }
}
if (e.key === 'Escape') { e.preventDefault(); (e.currentTarget as HTMLInputElement).blur(); }
}}
placeholder={`Rechercher une profession ${categoriePro.toLowerCase()} (min. 1 lettre)…`}
className="w-full pl-9 px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
{techLoading && categoriePro === 'Technicien' && (
<div className="text-xs text-slate-500 mt-2">
<Loader2 className="w-3.5 h-3.5 inline animate-spin mr-1" />
Chargement de la liste des professions
</div>
)}
<div className="mt-2 max-h-56 overflow-auto rounded-lg border">
{(
(categoriePro === 'Artiste' ? filteredProfessions : filteredTechniciens)
).length === 0 ? (
<div className="p-3 text-sm text-slate-500">Aucun résultat.</div>
) : (
(categoriePro === 'Artiste' ? filteredProfessions : filteredTechniciens).map((p, idx) => (
<button
type="button"
key={p.code}
onClick={() => { setProfessionPick(p); setProfession(p.label); }}
className={`w-full text-left px-3 py-2 text-sm ${idx === professionActive ? 'bg-slate-50' : 'hover:bg-slate-50'}`}
onMouseEnter={() => setProfessionActive(idx)}
>
<div className="font-medium">{p.label}</div>
<div className="text-xs text-slate-500">{p.code}</div>
</button>
))
)}
</div>
<p className="text-[11px] text-slate-500 mt-1">
Cette liste contient l'ensemble des professions de l'Annexe 8 (Techniciens) ou 10 (Artistes) et leur Code Emploi. Si une profession n'apparaît pas, il n'est pas possible de réaliser un CDDU pour ce contrat.
</p>
</div>
)
) : (
<>
<input
value={profession}
onChange={(e) => setProfession(e.target.value)}
placeholder="ex : Régisseur.se, …"
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<p className="text-[11px] text-slate-500 mt-1">
Si une profession napparaît pas dans lannexe, indique-la précisément (ex: « Régisseur.se plateau »).
</p>
</>
)
)}
</div>
</FieldRow>
</Section>
{/* Période / volumes en mode CDDU, Contrat en mode RG */}
<Section title={isRegimeRG ? "Contrat" : "Période et volumes"}>
{isRegimeRG ? (
// Contenu pour Régime Général
<>
<FieldRow>
<div>
<Label required>Type de contrat</Label>
<select
value={typeContratRG}
onChange={(e) => setTypeContratRG(e.target.value as any)}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
>
<option>CDD de droit commun</option>
<option>CDI</option>
<option>Stage</option>
<option>Contrat de professionnalisation</option>
<option>Contrat d'apprentissage</option>
<option>Autre</option>
</select>
</div>
</FieldRow>
{/* Motif CDD si CDD sélectionné */}
{typeContratRG === "CDD de droit commun" && (
<FieldRow>
<div>
<Label required>Motif du CDD</Label>
<select
value={motifCDD}
onChange={(e) => setMotifCDD(e.target.value as any)}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
>
<option>Remplacement d'une absence</option>
<option>Accroissement temporaire d'activité</option>
</select>
</div>
</FieldRow>
)}
{/* Champs conditionnels selon le motif CDD */}
{typeContratRG === "CDD de droit commun" && motifCDD === "Remplacement d'une absence" && (
<FieldRow>
<div>
<Label required>Nom de la personne remplacée</Label>
<input
value={nomPersonneRemplacee}
onChange={(e) => setNomPersonneRemplacee(e.target.value)}
placeholder="Nom et prénom de la personne remplacée"
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
<div>
<Label required>Situation de la personne remplacée</Label>
<select
value={situationPersonneRemplacee}
onChange={(e) => setSituationPersonneRemplacee(e.target.value as any)}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
>
<option>Absent temporairement ou contrat suspendu (maladie, maternité, congés payés, congé parental, etc...)</option>
<option>Salarié provisoirement à temps partiel (congé parental, congé création d'entreprise, etc...)</option>
<option>Salarié ayant quitté définitivement l'entreprise, poste en attente de suppression</option>
<option>Remplacement du chef d'entreprise</option>
</select>
</div>
</FieldRow>
)}
{typeContratRG === "CDD de droit commun" && motifCDD === "Accroissement temporaire d'activité" && (
<FieldRow>
<div>
<Label required>Décrivez le motif de l'accroissement d'activité</Label>
<textarea
value={motifAccroissement}
onChange={(e) => setMotifAccroissement(e.target.value)}
placeholder="Détaillez les raisons de l'accroissement temporaire d'activité..."
rows={3}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
</FieldRow>
)}
</>
) : (
// Contenu pour CDDU (existant)
<>
<FieldRow>
<div>
<Label required>Le contrat est-il multi-mois ?</Label>
<div className="flex items-center gap-6 mt-2">
<label className="inline-flex items-center gap-2 text-sm">
<input type="radio" checked={isMultiMois === "Oui"} onChange={() => setIsMultiMois("Oui")} />
Oui
</label>
<label className="inline-flex items-center gap-2 text-sm">
<input type="radio" checked={isMultiMois === "Non"} onChange={() => setIsMultiMois("Non")} />
Non
</label>
</div>
</div>
</FieldRow>
</>
)}
<FieldRow>
<div>
<Label required>Date de début du contrat</Label>
<input
type="date"
value={dateDebut}
onChange={(e) => {
const v = e.target.value;
setDateDebut(v);
setConfirmPastStart(false);
if (dateFin && v && dateFin < v) setDateFin(v);
}}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
{/* Masquer la date de fin si CDI en mode RG */}
{!(isRegimeRG && typeContratRG === "CDI") && (
<div>
<Label>Date de fin du contrat</Label>
<input
type="date"
value={dateFin}
min={dateDebut || undefined}
onChange={(e) => setDateFin(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">Pour un contrat d'une journée, sélectionnez la même date que début.</p>
</div>
)}
</FieldRow>
{/* Sections spécifiques aux CDDU - masquées en mode RG */}
{!isRegimeRG && (
!useHeuresMode ? (
<>
<FieldRow>
<div>
<Label>Combien de représentations ?</Label>
<input
type="number"
min={0}
value={nbRep}
onChange={(e) => setNbRep(e.target.value === "" ? "" : Number(e.target.value))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
<div>
<Label>Combien de services de répétition ?</Label>
<input
type="number"
min={0}
value={nbServ}
onChange={(e) => setNbServ(e.target.value === "" ? "" : Number(e.target.value))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
</FieldRow>
<FieldRow>
<div>
<Label>Indiquez les dates de représentations</Label>
<input
value={datesRep}
onChange={(e) => setDatesRep(e.target.value)}
placeholder="ex : 12/10, 13/10, 24/10"
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
<div>
<Label>Indiquez les dates de répétitions</Label>
<input
value={datesServ}
onChange={(e) => setDatesServ(e.target.value)}
placeholder="ex : 10/10, 11/10"
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
</FieldRow>
</>
) : (
<>
<FieldRow>
<div>
<Label required>Combien d'heures de travail au total ?</Label>
<div className="flex items-center gap-2">
<input
type="number"
min={0}
value={heuresTotal}
onChange={(e) => setHeuresTotal(e.target.value === "" ? "" : Number(e.target.value))}
placeholder="ex : 3"
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<select
value={minutesTotal}
onChange={(e) => setMinutesTotal(e.target.value as "0" | "30")}
className="px-3 py-2 rounded-lg border bg-white text-sm"
aria-label="Minutes"
>
<option value="0">+ 00 min</option>
<option value="30">+ 30 min</option>
</select>
</div>
<p className="text-[11px] text-slate-500 mt-1">
Utilisez le menu pour ajouter 30 minutes (ex. 3 h 30).
</p>
</div>
<div>
<Label>Indiquez les jours de travail</Label>
<input
value={joursTravail}
onChange={(e) => setJoursTravail(e.target.value)}
placeholder="ex : 12/10, 13/10, 24/10"
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<p className="text-[11px] text-slate-500 mt-1">Listez les jours travaillés séparés par des virgules.</p>
</div>
</FieldRow>
</>
)
)}
</Section>
{isPastStart && (
<div className="rounded-2xl border border-amber-300 bg-amber-50 text-amber-950 p-4">
<div className="text-lg font-semibold flex items-center gap-2">
<span className="inline-block text-amber-600">⚠️</span>
Attention
</div>
<div className="mt-3 space-y-3 text-sm leading-relaxed">
<p>Vous avez saisi une date de démarrage du contrat dans le passé.</p>
<p>
Si vous validez la demande avec cette date, nous effectuerons la DPAE dans les 24 heures ouvrées, mais veuillez noter que
nous recevrons une alerte de l'URSSAF.
</p>
<p>
En effet, conformément aux articles L1221-10 à L1221-12-1 du Code du travail, la <strong>Déclaration Préalable à l'Embauche</strong> est
obligatoire et doit être effectuée dans les <strong>7 jours qui précèdent</strong> le commencement du contrat. Tout manquement peut être
sanctionné administrativement d'une pénalité de 300 fois le taux horaire du minimum garanti, soit en 2025, de <strong>1266 par DPAE
manquante</strong>.
</p>
<p>Nous vous recommandons donc vivement d'anticiper vos demandes de contrats.</p>
</div>
<label className="mt-4 flex items-start gap-2 text-sm">
<input
type="checkbox"
checked={confirmPastStart}
onChange={(e) => setConfirmPastStart(e.target.checked)}
className="mt-1"
/>
<span>
J'ai lu et compris l'alerte ci-dessus et valide la date de début de contrat au <strong>{formatFr(dateDebut)}</strong>.
</span>
</label>
</div>
)}
{/* Rémunération */}
<Section title="Rémunération">
<FieldRow>
<div>
<Label required>Type de salaire</Label>
<select
value={typeSalaire}
onChange={(e) => {
const v = e.target.value as typeof typeSalaire;
setTypeSalaire(v);
if (v === "Minimum conventionnel") setMontantSalaire("");
}}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
>
<option value="Brut">Brut</option>
<option value="Net avant PAS">Net avant PAS</option>
<option value="Coût total employeur">Coût total employeur</option>
<option value="Minimum conventionnel">Minimum conventionnel</option>
</select>
{typeSalaire !== "Minimum conventionnel" && (
<div className="mt-2">
<Label required>Montant</Label>
<div className="flex items-center gap-2">
<input
type="number"
min={0}
step="0.01"
value={montantSalaire}
onChange={(e) => setMontantSalaire(e.target.value === "" ? "" : Number(e.target.value))}
placeholder="ex : 250,00"
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<span className="text-sm text-slate-600"></span>
</div>
<p className="text-[11px] text-slate-500 mt-1">
Saisissez le montant en euros correspondant au type sélectionné.
</p>
</div>
)}
</div>
<div>
<Label required>Panier(s) repas</Label>
<div className="flex items-center gap-6 mt-2">
<label className="inline-flex items-center gap-2 text-sm">
<input type="radio" checked={panierRepas === "Oui"} onChange={() => setPanierRepas("Oui")} />
Oui
</label>
<label className="inline-flex items-center gap-2 text-sm">
<input type="radio" checked={panierRepas === "Non"} onChange={() => setPanierRepas("Non")} />
Non
</label>
</div>
</div>
</FieldRow>
</Section>
{/* Autres infos */}
<Section title="Autres informations">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Colonne gauche: champs du formulaire */}
<div className="space-y-4">
<div>
<Label>Notes</Label>
<textarea
rows={4}
value={notes}
onChange={(e) => setNotes(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">Utilisez ce champ pour toute info complémentaire.</p>
</div>
<div>
{/* Envoi de fichiers retiré */}
</div>
<div>
<Label required>Voulez-vous directement valider cette demande ?</Label>
<div className="flex items-center gap-6 mt-2">
<label className="inline-flex items-center gap-2 text-sm">
<input type="radio" checked={validerDirect === "Oui"} onChange={() => setValiderDirect("Oui")} />
Oui
</label>
<label className="inline-flex items-center gap-2 text-sm">
<input type="radio" checked={validerDirect === "Non"} onChange={() => setValiderDirect("Non")} />
Non
</label>
</div>
<p className="text-[11px] text-slate-500 mt-1">
Si vous choisissez Non, la demande sera enregistrée en brouillon. Vous devrez la valider ultérieurement pour que nous puissions la traiter.
</p>
</div>
<div>
<Label required>Voulez-vous recevoir une confirmation par e-mail ?</Label>
<div className="flex items-center gap-6 mt-2">
<label className="inline-flex items-center gap-2 text-sm">
<input type="radio" checked={emailConfirm === "Oui"} onChange={() => setEmailConfirm("Oui")} />
Oui
</label>
<label className="inline-flex items-center gap-2 text-sm">
<input type="radio" checked={emailConfirm === "Non"} onChange={() => setEmailConfirm("Non")} />
Non
</label>
</div>
<p className="text-[11px] text-slate-500 mt-1">
Si vous choisissez Non, aucune notification e-mail ne sera envoyée à la création du contrat.
</p>
</div>
</div>
{/* Colonne droite: Notes du contrat (mode édition uniquement) */}
<div>
{contractIdForNotes ? (
<NotesSection contractId={contractIdForNotes} title="Notes du contrat" showAddButton={false} compact />
) : null}
</div>
</div>
</Section>
{/* Actions */}
<div className="flex items-center justify-end gap-3">
{err && <div className="text-sm text-rose-600 mr-auto">{err}</div>}
<button
type="button"
onClick={() => router.push("/contrats")}
className="px-4 py-2 rounded-lg border"
disabled={loading}
>
Revenir sans envoyer
</button>
<button
type="button"
onClick={onSubmit}
disabled={loading || isDemoMode}
className="px-4 py-2 rounded-lg bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed"
title={isDemoMode ? "Désactivé en mode démo" : ""}
>
{loading ? (<><Loader2 className="w-4 h-4 inline animate-spin mr-2" /> Envoi</>) : "Envoyer"}
</button>
</div>
</div>
{/* Overlays */}
{prefillLoading && (
<div className="fixed inset-0 z-[55] 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">Pré-remplissage en cours</div>
<p className="text-sm text-slate-600 mt-1">Merci de patienter pendant que nous chargeons les informations du contrat.</p>
</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>
)}
{showLeaveConfirm && (
<div className="fixed inset-0 z-[60] 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-2">
<button onClick={cancelLeave} className="px-3 py-2 rounded-lg border">Rester</button>
<button onClick={confirmLeave} className="px-3 py-2 rounded-lg bg-rose-600 text-white hover:bg-rose-700">Quitter sans enregistrer</button>
</div>
</div>
</div>
)}
{newSalarieOpen && (
<div className="fixed inset-0 z-[70] flex items-center justify-center">
<div className="absolute inset-0 bg-black/40" />
<div className="relative w-[96vw] max-w-3xl h-[90vh] rounded-2xl overflow-hidden shadow-2xl border border-slate-200 bg-white flex flex-col">
<div className="px-5 py-3 bg-gradient-to-r from-emerald-600 to-teal-600 text-white flex items-center justify-between">
<div className="text-lg font-semibold truncate">Créer un nouveau salarié</div>
<button
type="button"
onClick={() => setNewSalarieOpen(false)}
className="px-3 py-1.5 rounded-lg bg-white/10 hover:bg-white/20 text-white text-sm"
>
Fermer
</button>
</div>
<div className="flex-1 overflow-y-auto">
<iframe
src={NEW_SALARIE_PATH}
className="w-full h-full min-h-[600px] border-0 bg-white"
title="Nouveau salarié"
/>
</div>
</div>
</div>
)}
</>
);
}