-
+
{r.nom}
@@ -486,8 +491,8 @@ export default function SalariesPage() {
{r.email || — }
{r.dernier_emploi || — }
- {r.dernier_contrat && contratHref ? (
- {r.dernier_contrat.reference}
+ {r.dernier_contrat && contratHrefFinal ? (
+ {r.dernier_contrat.reference}
) : (
—
)}
diff --git a/app/(app)/signatures-electroniques/page.tsx b/app/(app)/signatures-electroniques/page.tsx
index 794dd17..a9dda84 100644
--- a/app/(app)/signatures-electroniques/page.tsx
+++ b/app/(app)/signatures-electroniques/page.tsx
@@ -5,6 +5,8 @@ import { FileSignature, BellRing, XCircle, Sparkles, CheckCircle2, AlertCircle,
import Script from 'next/script';
import { usePageTitle } from '@/hooks/usePageTitle';
import { useQuery } from '@tanstack/react-query';
+import { useDemoMode } from '@/hooks/useDemoMode';
+import { createPortal } from 'react-dom';
type AirtableRecord = {
id: string;
@@ -28,6 +30,64 @@ function classNames(...arr: Array) {
return arr.filter(Boolean).join(' ');
}
+// Composant pour un bouton désactivé avec tooltip
+function DisabledButton({ label, className }: { label: string; className?: string }) {
+ const btnRef = useRef(null);
+ const [tipOpen, setTipOpen] = useState(false);
+ const [tipPos, setTipPos] = useState<{ top: number; left: number } | null>(null);
+
+ function computePos() {
+ const el = btnRef.current;
+ if (!el) return;
+ const r = el.getBoundingClientRect();
+ setTipPos({
+ top: r.top + window.scrollY - 10,
+ left: r.left + window.scrollX + r.width / 2
+ });
+ }
+
+ useEffect(() => {
+ if (!tipOpen) return;
+ const onScroll = () => computePos();
+ const onResize = () => computePos();
+ window.addEventListener('scroll', onScroll, true);
+ window.addEventListener('resize', onResize);
+ return () => {
+ window.removeEventListener('scroll', onScroll, true);
+ window.removeEventListener('resize', onResize);
+ };
+ }, [tipOpen]);
+
+ return (
+ <>
+ { computePos(); setTipOpen(true); }}
+ onMouseLeave={() => setTipOpen(false)}
+ onFocus={() => { computePos(); setTipOpen(true); }}
+ onBlur={() => setTipOpen(false)}
+ disabled
+ className={`px-4 py-1.5 rounded-lg bg-slate-200 text-slate-400 text-sm font-medium cursor-not-allowed ${className || ''}`}
+ >
+ {label}
+
+ {tipOpen && tipPos && createPortal(
+
+
+ Désactivé en mode démo
+
+
+
,
+ document.body
+ )}
+ >
+ );
+}
+
// Hook pour récupérer les infos utilisateur
function useUserInfo() {
return useQuery({
@@ -83,6 +143,9 @@ function useOrganizations() {
export default function SignaturesElectroniques() {
usePageTitle("Signatures électroniques");
+ // 🎭 Détection du mode démo
+ const { isDemoMode } = useDemoMode();
+
const [contrats, setContrats] = useState([]);
const [loading, setLoading] = useState(true);
const [reloadingAfterSignatureChange, setReloadingAfterSignatureChange] = useState(false);
@@ -725,6 +788,18 @@ export default function SignaturesElectroniques() {
)}
+ {/* 🎭 Message informatif en mode démo */}
+ {isDemoMode && (
+
+
+
+
+
+
Mode démonstration
+
+
+ )}
+
Contrats dans la liste
@@ -770,12 +845,16 @@ export default function SignaturesElectroniques() {
Signature connue
-
setShowSignatureModal(true)}
- className="px-4 py-1.5 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors w-full sm:w-auto"
- >
- Voir / modifier la signature
-
+ {isDemoMode ? (
+
+ ) : (
+
setShowSignatureModal(true)}
+ className="px-4 py-1.5 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors w-full sm:w-auto"
+ >
+ Voir / modifier la signature
+
+ )}
>
) : (
<>
@@ -783,12 +862,16 @@ export default function SignaturesElectroniques() {
Signature non connue
- setShowSignatureModal(true)}
- className="px-4 py-1.5 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors w-full sm:w-auto"
- >
- Ajouter une signature
-
+ {isDemoMode ? (
+
+ ) : (
+ setShowSignatureModal(true)}
+ className="px-4 py-1.5 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors w-full sm:w-auto"
+ >
+ Ajouter une signature
+
+ )}
>
)}
@@ -949,7 +1032,7 @@ export default function SignaturesElectroniques() {
{/* Modal signature with docuseal-form */}
-
+
{modalTitle}
{/* Modal embed page */}
-
+
{pageEmbedTitle || 'Aperçu'}
+
- {/* En-tête */}
+ {/* En-tête compact */}
-
-
-
Simulateur de paie intermittent
+
+
+
+
+
Simulateur de paie intermittent
-
- Calculez le coût de recrutement d'un intermittent du spectacle (CDDU)
+
+ Calculez le coût de recrutement d'un intermittent du spectacle (CDDU)
{/* Layout 2 colonnes : simulateur à gauche, cards info à droite */}
-
+
{/* Colonne principale : Simulateur en iframe */}
-
+
- {/* Colonne droite : Cards d'information modernes */}
-
+ {/* Colonne droite : Cards d'information compactes */}
+
{/* Card : Mode d'emploi */}
-
-
-
-
-
Mode d'emploi
-
- Calculez le coût de recrutement d'un intermittent du spectacle en CDDU.
-
+
+
-
-
- 1
- Choisissez la Convention Collective et le statut
+
+
+ 1
+ Choisissez la Convention Collective et le statut
-
- 2
- Indiquez les cachets/heures et dates travaillées
+
+ 2
+ Indiquez les cachets/heures et dates travaillées
-
- 3
- Saisissez le montant (Brut, Net ou Coût employeur)
+
+ 3
+ Saisissez le montant (Brut, Net ou Coût employeur)
-
- 4
- Consultez les résultats avec le détail des cotisations
+
+ 4
+ Consultez les résultats avec le détail des cotisations
- Taux 2025 • Contrats multi-mois : nous contacter
+ Taux à jour 2025
- {/* Card : Disclaimer */}
-
-
-
-
-
-
+ {/* Card : Disclaimer compact */}
+
+
+
-
Limitations & mentions
-
- Le simulateur ne prévoit pas les cas particuliers : mineurs de moins de 16 ans,
- cumul annuel, taxe sur les salaires, taxe d'apprentissage, non-résidents fiscaux,
- contrats multi-mois.
+
Limitations
+
+ Le simulateur ne prévoit pas certains cas particuliers
-
- Résultats donnés à titre indicatif, sans valeur contractuelle.
+
+
+ •
+ Mineurs de moins de 16 ans
+
+
+ •
+ Cumul annuel
+
+
+ •
+ Taxe sur les salaires
+
+
+ •
+ Taxe d'apprentissage
+
+
+ •
+ Non-résidents fiscaux
+
+
+ •
+ Contrats multi-mois
+
+
+ •
+ Plus de 2 cachets par jour
+
+
+
+
+
+
+
+
+
+
+
Mentions légales
+
+ Sauf erreurs ou omissions. Les résultats fournis par le simulateur peuvent être imprécis ou erronés.
+ Les informations communiquées par le simulateur n'engagent pas la responsabilité de Odentas Media SAS quant
+ à leur utilisation et/ou leur interprétation. Elles ne sont considérées par l'utilisateur que sous sa seule responsabilité.
diff --git a/app/(app)/virements-salaires/page.tsx b/app/(app)/virements-salaires/page.tsx
index 96cbc2b..96fe551 100644
--- a/app/(app)/virements-salaires/page.tsx
+++ b/app/(app)/virements-salaires/page.tsx
@@ -5,6 +5,7 @@ import { useSearchParams, usePathname, useRouter } from "next/navigation";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Loader2, Search, Download, ExternalLink, Info, Copy, Check } from "lucide-react";
import { usePageTitle } from "@/hooks/usePageTitle";
+import { useDemoMode } from "@/hooks/useDemoMode";
// --- Types ---
type PeriodKey =
@@ -106,6 +107,80 @@ function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
+// 🎭 Données fictives pour le mode démo (virements gérés par Odentas)
+const DEMO_VIREMENTS: VirementItem[] = [
+ {
+ id: 'demo-vir-001',
+ periode_label: 'Septembre 2025',
+ periode: '2025-09',
+ callsheet: 'FDR-2025-09',
+ num_appel: 'FDR-2025-09',
+ date_mois: '2025-09-01',
+ date: '2025-09-01',
+ total_salaries_eur: 12450.50,
+ total: 12450.50,
+ virement_recu: true,
+ virement_recu_date: '2025-09-15',
+ salaires_payes: true,
+ },
+ {
+ id: 'demo-vir-002',
+ periode_label: 'Août 2025',
+ periode: '2025-08',
+ callsheet: 'FDR-2025-08',
+ num_appel: 'FDR-2025-08',
+ date_mois: '2025-08-01',
+ date: '2025-08-01',
+ total_salaries_eur: 15230.80,
+ total: 15230.80,
+ virement_recu: true,
+ virement_recu_date: '2025-08-12',
+ salaires_payes: true,
+ },
+ {
+ id: 'demo-vir-003',
+ periode_label: 'Juillet 2025',
+ periode: '2025-07',
+ callsheet: 'FDR-2025-07',
+ num_appel: 'FDR-2025-07',
+ date_mois: '2025-07-01',
+ date: '2025-07-01',
+ total_salaries_eur: 18765.25,
+ total: 18765.25,
+ virement_recu: true,
+ virement_recu_date: '2025-07-18',
+ salaires_payes: true,
+ },
+ {
+ id: 'demo-vir-004',
+ periode_label: 'Juin 2025',
+ periode: '2025-06',
+ callsheet: 'FDR-2025-06',
+ num_appel: 'FDR-2025-06',
+ date_mois: '2025-06-01',
+ date: '2025-06-01',
+ total_salaries_eur: 14890.00,
+ total: 14890.00,
+ virement_recu: true,
+ virement_recu_date: '2025-06-14',
+ salaires_payes: true,
+ },
+ {
+ id: 'demo-vir-005',
+ periode_label: 'Mai 2025',
+ periode: '2025-05',
+ callsheet: 'FDR-2025-05',
+ num_appel: 'FDR-2025-05',
+ date_mois: '2025-05-01',
+ date: '2025-05-01',
+ total_salaries_eur: 11250.75,
+ total: 11250.75,
+ virement_recu: true,
+ virement_recu_date: '2025-05-16',
+ salaires_payes: true,
+ },
+];
+
// Affiche une période au format "Mois YYYY" (ex: "Septembre 2025")
function formatPeriode(per?: string | null) {
if (!per) return '—';
@@ -214,7 +289,7 @@ function useOrganizations() {
}
// --- Hook pour récupérer les virements ---
-function useVirements(filters: Filters, selectedOrgId?: string) {
+function useVirements(filters: Filters, selectedOrgId?: string, isDemoMode: boolean = false) {
// Récupération dynamique des infos utilisateur via /api/me
const { data: userInfo } = useUserInfo();
@@ -230,8 +305,24 @@ function useVirements(filters: Filters, selectedOrgId?: string) {
}
return useQuery<{ items: VirementItem[]; enabled?: boolean; org?: OrgSummary; client?: { unpaid: ClientVirementItem[]; recent: ClientVirementItem[] } }>({
- queryKey: ["virements-salaires", filters.year, userInfo?.orgId, selectedOrgId],
+ queryKey: ["virements-salaires", filters.year, userInfo?.orgId, selectedOrgId, isDemoMode],
queryFn: async () => {
+ // 🎭 En mode démo, retourner les données fictives
+ if (isDemoMode) {
+ console.log('🎭 [VIREMENTS] Mode démo - retour de données fictives');
+ return {
+ items: DEMO_VIREMENTS,
+ enabled: true, // Mode Odentas activé en démo
+ org: {
+ structure_api: 'demo-org',
+ virements_salaires: 'odentas',
+ iban: 'FR76 1234 5678 9012 3456 7890 123',
+ bic: 'ABCDEFGH',
+ } as OrgSummary,
+ client: undefined,
+ };
+ }
+
// On récupère toutes les données de l'année, le filtrage par période se fera côté client
const simpleQs = new URLSearchParams();
simpleQs.set("year", String(filters.year));
@@ -266,6 +357,9 @@ function useVirements(filters: Filters, selectedOrgId?: string) {
export default function VirementsPage() {
usePageTitle("Virements salaires");
+ // 🎭 Détection du mode démo
+ const { isDemoMode } = useDemoMode();
+
const now = new Date();
const [filters, setFilters] = useState
({
year: now.getFullYear(),
@@ -376,15 +470,15 @@ export default function VirementsPage() {
router.replace(`${pathname}?${qs.toString()}`, { scroll: false });
}, [filters, pathname, router]);
- const { data, isLoading, isError, error, isFetching } = useVirements(filters, selectedOrgId);
+ const { data, isLoading, isError, error, isFetching } = useVirements(filters, selectedOrgId, isDemoMode);
const loadingOrg = isLoading || isFetching || data === undefined;
const org: OrgSummary = (data?.org ?? null) as OrgSummary;
- // Amélioration de la détection du mode Odentas
- const isOdentas = !loadingOrg && (
+ // Amélioration de la détection du mode Odentas (toujours actif en mode démo)
+ const isOdentas = isDemoMode || (!loadingOrg && (
Boolean(data?.enabled) ||
(typeof org?.virements_salaires === 'string' && org!.virements_salaires!.toLowerCase().includes('odentas'))
- );
+ ));
const gestionLabel = loadingOrg ? "Chargement…" : (isOdentas ? "Odentas" : "Votre structure");
const items: VirementItem[] = (data?.items ?? []) as VirementItem[];
@@ -503,6 +597,18 @@ export default function VirementsPage() {
)}
+ {/* 🎭 Message informatif en mode démo */}
+ {isDemoMode && (
+
+
+
+
+
+
Mode démonstration
+
+
+ )}
+
{/* Filtres de période */}
@@ -856,6 +962,17 @@ export default function VirementsPage() {
Coordonnées bancaires (salaires)
+ {isDemoMode ? (
+
+
+
+
Mode démonstration
+
Les coordonnées bancaires sont masquées
+
+
+
Ce compte bancaire est réservé à la réception des virements de salaires.
+
+ ) : (
@@ -905,6 +1022,7 @@ export default function VirementsPage() {
Ce compte bancaire est réservé à la réception des virements de salaires.
+ )}
diff --git a/app/(app)/virements-salaires/page.tsx.backup b/app/(app)/virements-salaires/page.tsx.backup
new file mode 100644
index 0000000..89b15cc
--- /dev/null
+++ b/app/(app)/virements-salaires/page.tsx.backup
@@ -0,0 +1,1068 @@
+// app/(app)/virements-salaires/page.tsx
+"use client";
+import React, { useMemo, useState, useEffect } from "react";
+import { useSearchParams, usePathname, useRouter } from "next/navigation";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { Loader2, Search, Download, ExternalLink, Info, Copy, Check } from "lucide-react";
+import { usePageTitle } from "@/hooks/usePageTitle";
+import { useDemoMode } from "@/hooks/useDemoMode";
+
+// --- Types ---
+type PeriodKey =
+ | "toute_annee"
+ | "premier_semestre"
+ | "second_semestre"
+ | "trimestre_1"
+ | "trimestre_2"
+ | "trimestre_3"
+ | "trimestre_4"
+ | `mois_${1|2|3|4|5|6|7|8|9|10|11|12}`;
+
+type Filters = {
+ year: number;
+ period: PeriodKey;
+ from?: string; // YYYY-MM-DD
+ to?: string; // YYYY-MM-DD
+};
+
+type VirementItem = {
+ id: string;
+ periode_label?: string;
+ periode?: string;
+ callsheet?: string;
+ num_appel?: string;
+ date_mois?: string;
+ date?: string;
+ total_salaries_eur?: number;
+ total?: number;
+ virement_recu?: boolean | string;
+ virement_recu_date?: string | null; // Date de réception du virement
+ salaires_payes?: boolean | string;
+ pdf_url?: string;
+};
+
+type OrgSummary = {
+ structure_api: string | null;
+ virements_salaires: string | null;
+ iban: string | null;
+ bic: string | null;
+ gocardless?: string | null;
+} | null;
+
+// Vue client (virements gérés par le client)
+type ClientVirementItem = {
+ id: string;
+ kind: 'CDDU_MONO' | 'CDDU_MULTI' | 'RG';
+ source: 'contrat' | 'paie_multi' | 'paie_rg' | 'payslip';
+ contract_id?: string;
+ salarie?: string | null;
+ salarie_matricule?: string | null;
+ reference?: string | null;
+ profession?: string | null;
+ date_debut?: string | null;
+ date_fin?: string | null;
+ periode?: string | null; // ex: "2025-08-01" ou "08/2025"
+ net_a_payer?: number | null;
+ virement_effectue?: 'oui' | 'non' | 'na';
+};
+
+type ClientInfo = {
+ id: string;
+ name: string;
+ api_name?: string;
+} | null;
+
+// --- Helpers ---
+function pad2(n: number) { return String(n).padStart(2, "0"); }
+function monthStartISO(y: number, m: number) { return `${y}-${pad2(m)}-01`; }
+function monthEndISO(y: number, m: number) {
+ const d = new Date(y, m, 0); // last day of month
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
+}
+function quarterRange(y: number, q: 1 | 2 | 3 | 4) {
+ const startMonth = (q - 1) * 3 + 1;
+ const endMonth = startMonth + 2;
+ return { from: monthStartISO(y, startMonth), to: monthEndISO(y, endMonth) };
+}
+
+function formatFR(iso: string) {
+ if (!iso) return "—";
+ const d = new Date(iso);
+ if (isNaN(d.getTime())) return "—";
+ const dd = String(d.getDate()).padStart(2, '0');
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
+ const yyyy = d.getFullYear();
+ return `${dd}/${mm}/${yyyy}`;
+}
+
+function maskEndDate(iso?: string | null) {
+ if (!iso) return '—';
+ // Masquer la date 2099-01-01 utilisée comme fin indéterminée
+ if (iso.startsWith('2099-01-01')) return '—';
+ return formatFR(iso);
+}
+
+function capitalize(str: string) {
+ if (!str) return str;
+ return str.charAt(0).toUpperCase() + str.slice(1);
+}
+
+// 🎭 Données fictives pour le mode démo (virements gérés par Odentas)
+const DEMO_VIREMENTS: VirementItem[] = [
+ {
+ id: 'demo-vir-001',
+ periode_label: 'Septembre 2025',
+ periode: '2025-09',
+ callsheet: 'FDR-2025-09',
+ num_appel: 'FDR-2025-09',
+ date_mois: '2025-09-01',
+ date: '2025-09-01',
+ total_salaries_eur: 12450.50,
+ total: 12450.50,
+ virement_recu: true,
+ virement_recu_date: '2025-09-15',
+ salaires_payes: true,
+ },
+ {
+ id: 'demo-vir-002',
+ periode_label: 'Août 2025',
+ periode: '2025-08',
+ callsheet: 'FDR-2025-08',
+ num_appel: 'FDR-2025-08',
+ date_mois: '2025-08-01',
+ date: '2025-08-01',
+ total_salaries_eur: 15230.80,
+ total: 15230.80,
+ virement_recu: true,
+ virement_recu_date: '2025-08-12',
+ salaires_payes: true,
+ },
+ {
+ id: 'demo-vir-003',
+ periode_label: 'Juillet 2025',
+ periode: '2025-07',
+ callsheet: 'FDR-2025-07',
+ num_appel: 'FDR-2025-07',
+ date_mois: '2025-07-01',
+ date: '2025-07-01',
+ total_salaries_eur: 18765.25,
+ total: 18765.25,
+ virement_recu: true,
+ virement_recu_date: '2025-07-18',
+ salaires_payes: true,
+ },
+ {
+ id: 'demo-vir-004',
+ periode_label: 'Juin 2025',
+ periode: '2025-06',
+ callsheet: 'FDR-2025-06',
+ num_appel: 'FDR-2025-06',
+ date_mois: '2025-06-01',
+ date: '2025-06-01',
+ total_salaries_eur: 14890.00,
+ total: 14890.00,
+ virement_recu: true,
+ virement_recu_date: '2025-06-14',
+ salaires_payes: true,
+ },
+ {
+ id: 'demo-vir-005',
+ periode_label: 'Mai 2025',
+ periode: '2025-05',
+ callsheet: 'FDR-2025-05',
+ num_appel: 'FDR-2025-05',
+ date_mois: '2025-05-01',
+ date: '2025-05-01',
+ total_salaries_eur: 11250.75,
+ total: 11250.75,
+ virement_recu: true,
+ virement_recu_date: '2025-05-16',
+ salaires_payes: true,
+ },
+];
+
+// Affiche une période au format "Mois YYYY" (ex: "Septembre 2025")
+function formatPeriode(per?: string | null) {
+ if (!per) return '—';
+ const s = String(per).trim();
+ // ISO: YYYY-MM or YYYY-MM-DD
+ const mIso = s.match(/^(\d{4})-(\d{2})(?:-(\d{2}))?$/);
+ if (mIso) {
+ const y = parseInt(mIso[1], 10);
+ const m = parseInt(mIso[2], 10);
+ const d = new Date(y, (isNaN(m) ? 1 : m) - 1, 1);
+ return capitalize(d.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }));
+ }
+ // MM/YYYY
+ const mMY = s.match(/^(\d{1,2})\/(\d{4})$/);
+ if (mMY) {
+ const m = parseInt(mMY[1], 10);
+ const y = parseInt(mMY[2], 10);
+ const d = new Date(y, (isNaN(m) ? 1 : m) - 1, 1);
+ return capitalize(d.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }));
+ }
+ // Fallback: essayer Date()
+ const d = new Date(s);
+ if (!isNaN(d.getTime())) {
+ return capitalize(d.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }));
+ }
+ return s;
+}
+
+function formatCurrency(amount?: number) {
+ if (amount == null) return "—";
+ return new Intl.NumberFormat('fr-FR', {
+ style: 'currency',
+ currency: 'EUR'
+ }).format(amount);
+}
+
+function toBoolish(v: any): boolean | undefined {
+ if (v === true) return true;
+ if (v === false) return false;
+ if (typeof v === 'string') {
+ const s = v.trim().toLowerCase();
+ if (s === 'oui' || s === 'yes' || s === 'true') return true;
+ if (s === 'non' || s === 'no' || s === 'false') return false;
+ }
+ return undefined;
+}
+
+// --- Hook pour récupérer les infos utilisateur et les organisations (staff uniquement) ---
+function useUserInfo() {
+ return useQuery({
+ queryKey: ["user-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 {
+ isStaff: Boolean(me.is_staff || me.isStaff),
+ orgId: me.orgId || me.active_org_id || null,
+ orgName: me.orgName || me.active_org_name || "Organisation",
+ api_name: me.active_org_api_name
+ };
+ } catch {
+ return null;
+ }
+ },
+ staleTime: 30_000,
+ });
+}
+
+function useOrganizations() {
+ const { data: userInfo, isSuccess: userInfoLoaded } = useUserInfo();
+
+ const query = useQuery({
+ queryKey: ["organizations"],
+ queryFn: async () => {
+ try {
+ const res = await fetch("/api/staff/organizations", {
+ cache: "no-store",
+ headers: { Accept: "application/json" },
+ credentials: "include"
+ });
+ if (!res.ok) return [];
+ const data = await res.json();
+ return data.organizations || [];
+ } catch {
+ return [];
+ }
+ },
+ enabled: false,
+ staleTime: 60_000,
+ });
+
+ React.useEffect(() => {
+ if (userInfoLoaded && userInfo?.isStaff && !query.data && !query.isFetching) {
+ query.refetch();
+ }
+ }, [userInfo, userInfoLoaded, query]);
+
+ return query;
+}
+
+// --- Hook pour récupérer les virements ---
+function useVirements(filters: Filters, selectedOrgId?: string, isDemoMode: boolean = false) {
+ // Récupération dynamique des infos utilisateur via /api/me
+ const { data: userInfo } = useUserInfo();
+
+ const qs = new URLSearchParams();
+ qs.set("year", String(filters.year));
+ qs.set("period", filters.period);
+ if (filters.from) qs.set("from", filters.from);
+ if (filters.to) qs.set("to", filters.to);
+
+ // Si c'est un staff qui a sélectionné une organisation spécifique
+ if (selectedOrgId && userInfo?.isStaff) {
+ qs.set("org_id", selectedOrgId);
+ }
+
+ return useQuery<{ items: VirementItem[]; enabled?: boolean; org?: OrgSummary; client?: { unpaid: ClientVirementItem[]; recent: ClientVirementItem[] } }>({
+ queryKey: ["virements-salaires", filters.year, userInfo?.orgId, selectedOrgId, isDemoMode],
+ queryFn: async () => {
+ // 🎭 En mode démo, retourner les données fictives
+ if (isDemoMode) {
+ console.log('🎭 [VIREMENTS] Mode démo - retour de données fictives');
+ return {
+ items: DEMO_VIREMENTS,
+ enabled: true, // Mode Odentas activé en démo
+ org: {
+ structure_api: 'demo-org',
+ virements_salaires: 'odentas',
+ iban: 'FR76 1234 5678 9012 3456 7890 123',
+ bic: 'ABCDEFGH',
+ } as OrgSummary,
+ client: undefined,
+ };
+ }
+
+ // On récupère toutes les données de l'année, le filtrage par période se fera côté client
+ const simpleQs = new URLSearchParams();
+ simpleQs.set("year", String(filters.year));
+ simpleQs.set("period", "toute_annee");
+
+ // Si c'est un staff qui a sélectionné une organisation spécifique
+ if (selectedOrgId && userInfo?.isStaff) {
+ simpleQs.set("org_id", selectedOrgId);
+ }
+
+ const res = await fetch(`/api/virements-salaires?${simpleQs.toString()}`, {
+ credentials: "include",
+ headers: { Accept: "application/json" }
+ });
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(text || `Erreur ${res.status}`);
+ }
+ const data = await res.json();
+ return {
+ items: (data?.items || []) as VirementItem[],
+ enabled: Boolean(data?.enabled),
+ org: (data?.org ?? null) as OrgSummary,
+ client: data?.client as { unpaid: ClientVirementItem[]; recent: ClientVirementItem[] } | undefined,
+ };
+ },
+ staleTime: 15_000,
+ enabled: !!userInfo, // Ne pas exécuter si pas d'infos utilisateur
+ });
+}
+
+export default function VirementsPage() {
+ usePageTitle("Virements salaires");
+
+ // 🎭 Détection du mode démo
+ const { isDemoMode } = useDemoMode();
+
+ const now = new Date();
+ const [filters, setFilters] = useState
({
+ year: now.getFullYear(),
+ period: "toute_annee"
+ });
+ const [searchQuery, setSearchQuery] = useState("");
+ const [aboutOpen, setAboutOpen] = useState(false);
+ const [copiedField, setCopiedField] = useState(null);
+ const [selectedOrgId, setSelectedOrgId] = useState("");
+
+ const { data: userInfo, isLoading: isLoadingUser } = useUserInfo();
+ const { data: organizations, isLoading: isLoadingOrgs, error: orgsError } = useOrganizations();
+ const queryClient = useQueryClient();
+
+ const years = useMemo(() => {
+ const base = now.getFullYear();
+ return Array.from({ length: 7 }, (_, i) => base - i);
+ }, [now]);
+
+ // URL sync
+ const searchParams = useSearchParams();
+ const pathname = usePathname();
+ const router = useRouter();
+
+ useEffect(() => {
+ const sp = searchParams;
+ if (!sp) return;
+ const yParam = sp.get("year");
+ const pParam = sp.get("period");
+ const from = sp.get("from") || undefined;
+ const to = sp.get("to") || undefined;
+ const next: Filters = { ...filters };
+
+ if (yParam) {
+ const y = parseInt(yParam, 10);
+ if (!isNaN(y)) next.year = y;
+ }
+
+ if (pParam) {
+ const allowed = new Set([
+ "toute_annee", "premier_semestre", "second_semestre",
+ "trimestre_1", "trimestre_2", "trimestre_3", "trimestre_4",
+ "mois_1", "mois_2", "mois_3", "mois_4", "mois_5", "mois_6",
+ "mois_7", "mois_8", "mois_9", "mois_10", "mois_11", "mois_12",
+ ] as PeriodKey[]);
+
+ if (allowed.has(pParam as PeriodKey)) {
+ next.period = pParam as PeriodKey;
+ if (!from && !to) {
+ const y = next.year;
+ if (pParam === "premier_semestre") {
+ next.from = monthStartISO(y, 1);
+ next.to = monthEndISO(y, 6);
+ }
+ else if (pParam === "second_semestre") {
+ next.from = monthStartISO(y, 7);
+ next.to = monthEndISO(y, 12);
+ }
+ else if (pParam === "trimestre_1") {
+ const r = quarterRange(y, 1);
+ next.from = r.from;
+ next.to = r.to;
+ }
+ else if (pParam === "trimestre_2") {
+ const r = quarterRange(y, 2);
+ next.from = r.from;
+ next.to = r.to;
+ }
+ else if (pParam === "trimestre_3") {
+ const r = quarterRange(y, 3);
+ next.from = r.from;
+ next.to = r.to;
+ }
+ else if (pParam === "trimestre_4") {
+ const r = quarterRange(y, 4);
+ next.from = r.from;
+ next.to = r.to;
+ }
+ else if (pParam.startsWith("mois_")) {
+ const m = parseInt(pParam.split("_")[1]!, 10);
+ if (!isNaN(m)) {
+ next.from = monthStartISO(y, m);
+ next.to = monthEndISO(y, m);
+ }
+ }
+ else if (pParam === "toute_annee") {
+ next.from = undefined;
+ next.to = undefined;
+ }
+ }
+ }
+ }
+
+ next.from = (from as any) ?? next.from;
+ next.to = (to as any) ?? next.to;
+ const changed = next.year !== filters.year || next.period !== filters.period || next.from !== filters.from || next.to !== filters.to;
+ if (changed) setFilters(next);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [searchParams?.toString()]);
+
+ useEffect(() => {
+ if (!pathname) return;
+ const qs = new URLSearchParams();
+ qs.set("year", String(filters.year));
+ qs.set("period", filters.period);
+ if (filters.from) qs.set("from", filters.from);
+ if (filters.to) qs.set("to", filters.to);
+ router.replace(`${pathname}?${qs.toString()}`, { scroll: false });
+ }, [filters, pathname, router]);
+
+ const { data, isLoading, isError, error, isFetching } = useVirements(filters, selectedOrgId, isDemoMode);
+ const loadingOrg = isLoading || isFetching || data === undefined;
+ const org: OrgSummary = (data?.org ?? null) as OrgSummary;
+
+ // Amélioration de la détection du mode Odentas (toujours actif en mode démo)
+ const isOdentas = isDemoMode || (!loadingOrg && (
+ Boolean(data?.enabled) ||
+ (typeof org?.virements_salaires === 'string' && org!.virements_salaires!.toLowerCase().includes('odentas'))
+ ));
+
+ const gestionLabel = loadingOrg ? "Chargement…" : (isOdentas ? "Odentas" : "Votre structure");
+ const items: VirementItem[] = (data?.items ?? []) as VirementItem[];
+ const clientUnpaidAll: ClientVirementItem[] = (data?.client?.unpaid ?? []) as ClientVirementItem[];
+ const clientRecentAll: ClientVirementItem[] = (data?.client?.recent ?? []) as ClientVirementItem[];
+
+ const clientFilter = (arr: ClientVirementItem[]) => {
+ if (!searchQuery.trim()) return arr;
+ const q = searchQuery.toLowerCase();
+ return arr.filter(it =>
+ (it.salarie || '').toLowerCase().includes(q) ||
+ (it.reference || '').toLowerCase().includes(q) ||
+ (it.profession || '').toLowerCase().includes(q) ||
+ (it.periode || '').toLowerCase().includes(q)
+ );
+ };
+ const clientUnpaid = clientFilter(clientUnpaidAll);
+ const clientRecent = clientFilter(clientRecentAll);
+
+ // Mutation: marquer un payslip comme viré
+ async function markPayslipDone(payslipId: string) {
+ try {
+ // Optimistic UI: masquer l'élément avant refetch
+ // (on ne modifie pas le cache TanStack ici, on refetch directement après)
+ await fetch(`/api/payslips/${payslipId}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({ transfer_done: true })
+ });
+ // Invalider les requêtes liées pour recharger la liste
+ queryClient.invalidateQueries({ queryKey: ["virements-salaires"] });
+ } catch (e) {
+ console.error('Erreur marquage payslip:', e);
+ alert('Erreur lors du marquage du virement.');
+ }
+ }
+
+ // Filtrage local pour la recherche ET la période
+ const filteredItems = useMemo((): VirementItem[] => {
+ let result: VirementItem[] = items;
+
+ // 1. Filtrage par année (toujours actif)
+ result = result.filter((item: VirementItem) => {
+ if (!item.date_mois) return false;
+ const [year] = item.date_mois.split('-').map(Number);
+ return year === filters.year;
+ });
+
+ // 2. Filtrage par période spécifique (si pas "toute_annee")
+ if (filters.period !== "toute_annee") {
+ result = result.filter((item: VirementItem) => {
+ if (!item.date_mois) return false;
+
+ const [, month] = item.date_mois.split('-').map(Number);
+
+ switch (filters.period) {
+ case "premier_semestre":
+ return month >= 1 && month <= 6;
+ case "second_semestre":
+ return month >= 7 && month <= 12;
+ case "trimestre_1":
+ return month >= 1 && month <= 3;
+ case "trimestre_2":
+ return month >= 4 && month <= 6;
+ case "trimestre_3":
+ return month >= 7 && month <= 9;
+ case "trimestre_4":
+ return month >= 10 && month <= 12;
+ default:
+ // Pour les mois individuels (mois_1, mois_2, etc.)
+ if (filters.period.startsWith("mois_")) {
+ const targetMonth = parseInt(filters.period.split("_")[1]!, 10);
+ return month === targetMonth;
+ }
+ return true;
+ }
+ });
+ }
+
+ // 3. Filtrage par recherche textuelle
+ if (searchQuery.trim()) {
+ const query = searchQuery.toLowerCase();
+ result = result.filter((item: VirementItem) =>
+ (item.callsheet || item.num_appel || "").toLowerCase().includes(query) ||
+ (item.periode_label || item.periode || "").toLowerCase().includes(query)
+ );
+ }
+
+ return result;
+ }, [items, searchQuery, filters.period, filters.year]);
+
+ return (
+
+ {/* En-tête + Recherche */}
+
+
+
+
Virements de salaires
+
+ Suivez vos appels à virement, l'état des virements reçus et les salaires payés.
+
+
+ {isOdentas && (
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="N° appel, période…"
+ className="bg-transparent outline-none text-sm flex-1"
+ />
+
+
+ )}
+
+
+ {/* 🎭 Message informatif en mode démo */}
+ {isDemoMode && (
+
+
+
+
+
+
Mode démonstration
+
+
+ )}
+
+ {/* Filtres de période */}
+
+
+ Année :
+ setFilters(f => ({ ...f, year: parseInt(e.target.value, 10) }))}
+ >
+ {years.map(y => (
+ {y}
+ ))}
+
+
+
+ {/* Sélecteur d'organisation (visible uniquement par le staff) */}
+ {userInfo?.isStaff && (
+
+ Organisation :
+ setSelectedOrgId(e.target.value)}
+ disabled={!organizations || organizations.length === 0}
+ >
+
+ {!organizations || organizations.length === 0
+ ? "Chargement..."
+ : "Toutes les organisations"}
+
+ {organizations && organizations.map((org: any) => (
+ {org.name}
+ ))}
+
+ {organizations && organizations.length > 0 && (
+ ({organizations.length})
+ )}
+
+ )}
+
+ {isOdentas && (
+
+ Période :
+ setFilters(f => ({ ...f, period: e.target.value as PeriodKey }))}
+ >
+ Toute l'année
+
+ Premier semestre
+ Second semestre
+
+
+ Trimestre 1
+ Trimestre 2
+ Trimestre 3
+ Trimestre 4
+
+
+ {[...Array(12)].map((_, i) => (
+
+ {new Date(2023, i).toLocaleDateString('fr-FR', { month: 'long' })}
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
Gestion des virements
+
Les virements de salaires sont effectués par
+
+
+
+
+ Modifier
+
+
+ Bientôt disponible, veuillez nous contacter.
+
+
+
+
+
+
+
+
+ {gestionLabel}
+
+
!loadingOrg && setAboutOpen(true)}
+ disabled={loadingOrg}
+ className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded-md border transition-colors ${loadingOrg ? 'opacity-50 cursor-not-allowed' : 'hover:bg-slate-50'}`}
+ aria-label="En savoir plus sur les virements"
+ >
+
+ En savoir plus
+
+
+
+
+
+
+
+ {/* Tableau principal */}
+ {isOdentas ? (
+
+
+
+
+
+ Période
+ N° d'appel
+ Date d'appel
+ Salaires nets
+ Virement reçu
+ Salaires payés
+ Document
+
+
+
+ {isLoading ? (
+
+
+
+ Chargement…
+
+
+ ) : isError ? (
+
+
+ Erreur : {(error as any)?.message || 'imprévue'}
+
+
+ ) : filteredItems.length === 0 ? (
+
+
+ {searchQuery ? 'Aucun virement trouvé pour cette recherche.' : 'Aucun virement de salaires trouvé.'}
+
+
+ ) : (
+ filteredItems.map((row: VirementItem) => (
+
+ {row.periode_label || formatPeriode(row.periode) || "—"}
+
+
+ {row.num_appel || row.callsheet || "—"}
+
+
+ {formatFR(row.date_mois || row.date || "")}
+
+ {formatCurrency(row.total_salaries_eur ?? row.total)}
+
+
+
+
+
+
+
+
+ {row.pdf_url ? (
+
+
+ PDF
+
+
+ ) : (
+ —
+ )}
+
+
+ ))
+ )}
+
+
+
+
+ {/* Footer avec indicateur de chargement */}
+ {filteredItems.length > 0 && (
+
+
+ {isFetching ? (
+
+
+ Mise à jour…
+
+ ) : (
+ `${filteredItems.length} appel${filteredItems.length > 1 ? 's' : ''} à virement${filteredItems.length > 1 ? 's' : ''} affiché${filteredItems.length > 1 ? 's' : ''}`
+ )}
+
+
+ )}
+
+ ) : (
+
+
+
+
+
+ Salarié·e
+ Contrat
+ Profession
+ Début contrat
+ Fin contrat
+ Période concernée
+ Net à payer
+ Marquer comme payé
+
+
+
+ {/* Unpaid first */}
+ {isLoading ? (
+ Chargement…
+ ) : isError ? (
+ Erreur : {(error as any)?.message || 'imprévue'}
+ ) : (clientUnpaid.length === 0 && clientRecent.length === 0) ? (
+ Aucun virement de salaires trouvé.
+ ) : (
+ <>
+ {clientUnpaid.map((it) => (
+
+
+ {it.salarie_matricule ? (
+
+ {it.salarie || it.salarie_matricule}
+
+ ) : (
+ it.salarie || '—'
+ )}
+
+
+ {it.contract_id && it.reference ? (
+
+ {it.reference}
+
+ ) : (
+ {it.reference || '—'}
+ )}
+
+ {it.profession || '—'}
+ {formatFR(it.date_debut || '')}
+ {maskEndDate(it.date_fin || '')}
+ {formatPeriode(it.periode)}
+ {it.net_a_payer != null ? formatCurrency(it.net_a_payer) : '—'}
+
+ {it.source === 'payslip' ? (
+ markPayslipDone(it.id)}
+ className="px-2 py-1 text-xs rounded-md border hover:bg-emerald-50 hover:border-emerald-300 text-emerald-700"
+ title="Marquer le virement comme effectué"
+ >
+ Marquer
+
+ ) : (
+ Marquer
+ )}
+
+
+ ))}
+ {clientRecent.length > 0 && (
+
+ Récemment virés (≤ 30 jours)
+
+ )}
+ {clientRecent.map((it) => (
+
+
+ {it.salarie_matricule ? (
+
+ {it.salarie || it.salarie_matricule}
+
+ ) : (
+ it.salarie || '—'
+ )}
+
+
+ {it.contract_id && it.reference ? (
+
+ {it.reference}
+
+ ) : (
+ {it.reference || '—'}
+ )}
+
+ {it.profession || '—'}
+ {formatFR(it.date_debut || '')}
+ {maskEndDate(it.date_fin || '')}
+ {formatPeriode(it.periode)}
+ {it.net_a_payer != null ? formatCurrency(it.net_a_payer) : '—'}
+
+ Oui
+
+
+ ))}
+ >
+ )}
+
+
+
+
+ )}
+ {aboutOpen && (
+
+
setAboutOpen(false)}
+ />
+
+
+
+
+
Gestion des virements de salaires
+
+
setAboutOpen(false)} className="text-slate-500 hover:text-slate-900">✕
+
+
+
+ En fin de mois, nous vous envoyons un appel à virement avec le total des salaires nets de la période concernée.
+
+
+ Dès réception de votre virement, nous redistribuons les salaires à vos salariés . Vous pouvez suivre l’état dans le tableau ci-dessous (colonnes « Virement reçu » et « Salaires payés »).
+
+
+
Coordonnées bancaires (salaires)
+
+
+
+
Bénéficiaire
+
ODENTAS MEDIA SAS
+
+
{ navigator.clipboard?.writeText('ODENTAS MEDIA SAS').then(()=>{ setCopiedField('benef'); setTimeout(()=>setCopiedField(null), 1400); }); }}
+ className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-md border hover:bg-slate-100"
+ aria-label="Copier le bénéficiaire"
+ >
+ {copiedField === 'benef' ? : } Copier
+
+
+
+
+
IBAN
+
FR76 1695 8000 0141 0850 9729 813
+
+ {org?.iban && (
+
{ navigator.clipboard?.writeText("FR76 1695 8000 0141 0850 9729 813" as string).then(()=>{ setCopiedField('iban'); setTimeout(()=>setCopiedField(null), 1400); }); }}
+ className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-md border hover:bg-slate-100"
+ aria-label="Copier l’IBAN"
+ >
+ {copiedField === 'iban' ? : } Copier
+
+ )}
+
+
+
+ {org?.bic && (
+
{ navigator.clipboard?.writeText("QNTOFRP1XXX" as string).then(()=>{ setCopiedField('bic'); setTimeout(()=>setCopiedField(null), 1400); }); }}
+ className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-md border hover:bg-slate-100"
+ aria-label="Copier le BIC"
+ >
+ {copiedField === 'bic' ? : } Copier
+
+ )}
+
+
Ce compte bancaire est réservé à la réception des virements de salaires.
+
+
+
+
+ setAboutOpen(false)} className="px-3 py-2 rounded-md border hover:bg-slate-50 text-sm">Fermer
+
+
+
+
+ )}
+
+ );
+}
+
+// --- Composants utilitaires ---
+function Th({ children, className = "" }: { children: React.ReactNode; className?: string }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function Td({ children, className = "" }: { children: React.ReactNode; className?: string }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function StatusBadge({ status, date }: { status?: boolean | string; date?: string | null }) {
+ const val = toBoolish(status);
+ if (val === true) {
+ return (
+
+
+ ✓ Oui
+
+ {date && (
+
+ {formatFR(date)}
+
+ )}
+
+ );
+ } else if (val === false) {
+ return (
+
+ ✗ Non
+
+ );
+ } else {
+ return
— ;
+ }
+}
diff --git a/app/api/organization/signature/route.ts b/app/api/organization/signature/route.ts
index 59d5c49..fb632f3 100644
--- a/app/api/organization/signature/route.ts
+++ b/app/api/organization/signature/route.ts
@@ -2,9 +2,16 @@ import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
+import { detectDemoModeFromHeaders } from '@/lib/demo-detector';
// GET: Récupérer la signature d'une organisation
export async function GET(req: NextRequest) {
+ // 🎭 Bloquer l'accès aux signatures en mode démo
+ if (detectDemoModeFromHeaders(req.headers)) {
+ console.log('🎭 [GET SIGNATURE API] Mode démo détecté - accès bloqué');
+ return NextResponse.json({ signature_b64: null }, { status: 200 });
+ }
+
try {
// Vérifier l'authentification
const supabaseAuth = createRouteHandlerClient({ cookies });
@@ -64,6 +71,12 @@ export async function GET(req: NextRequest) {
// POST: Sauvegarder ou mettre à jour la signature d'une organisation
export async function POST(req: NextRequest) {
+ // 🎭 Bloquer la modification de signatures en mode démo
+ if (detectDemoModeFromHeaders(req.headers)) {
+ console.log('🎭 [POST SIGNATURE API] Mode démo détecté - modification bloquée');
+ return NextResponse.json({ error: 'Action non disponible en mode démo' }, { status: 403 });
+ }
+
try {
// Vérifier l'authentification
const supabaseAuth = createRouteHandlerClient({ cookies });
@@ -144,6 +157,12 @@ export async function POST(req: NextRequest) {
// DELETE: Supprimer la signature d'une organisation
export async function DELETE(req: NextRequest) {
+ // 🎭 Bloquer la suppression de signatures en mode démo
+ if (detectDemoModeFromHeaders(req.headers)) {
+ console.log('🎭 [DELETE SIGNATURE API] Mode démo détecté - suppression bloquée');
+ return NextResponse.json({ error: 'Action non disponible en mode démo' }, { status: 403 });
+ }
+
try {
// Vérifier l'authentification
const supabaseAuth = createRouteHandlerClient({ cookies });
diff --git a/app/api/salaries/route.ts b/app/api/salaries/route.ts
index a6f9f41..2b7d6c4 100644
--- a/app/api/salaries/route.ts
+++ b/app/api/salaries/route.ts
@@ -35,6 +35,118 @@ async function resolveActiveOrgId(sb: any, _isStaff: boolean) {
export async function GET(req: NextRequest) {
try {
+ // 🎭 Mode démo : retourner des données fictives
+ const isDemoMode = process.env.DEMO_MODE === 'true';
+
+ if (isDemoMode) {
+ console.log('🎭 [API /salaries] Mode démo - retour de données fictives');
+
+ const { searchParams } = new URL(req.url);
+ const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10) || 1);
+ const limit = Math.min(100, Math.max(1, parseInt(searchParams.get("limit") || "10", 10) || 10));
+ const q = (searchParams.get("search") || searchParams.get("q") || "").trim();
+
+ // Données fictives de salariés
+ const DEMO_SALARIES: SalarieRow[] = [
+ {
+ matricule: "demo-sal-001",
+ nom: "MARTIN Alice",
+ email: "alice.martin@demo.fr",
+ transat_connecte: true,
+ dernier_emploi: "Comédien",
+ code_salarie: "demo-sal-001",
+ prenom: "Alice",
+ civilite: "Mme",
+ tel: "06 12 34 56 78",
+ org_id: "demo-org",
+ org_name: "Organisation Démo",
+ },
+ {
+ matricule: "demo-sal-002",
+ nom: "DUBOIS Pierre",
+ email: "pierre.dubois@demo.fr",
+ transat_connecte: false,
+ dernier_emploi: "Metteur en scène",
+ code_salarie: "demo-sal-002",
+ prenom: "Pierre",
+ civilite: "M.",
+ tel: "06 23 45 67 89",
+ org_id: "demo-org",
+ org_name: "Organisation Démo",
+ },
+ {
+ matricule: "demo-sal-003",
+ nom: "LEROY Sophie",
+ email: "sophie.leroy@demo.fr",
+ transat_connecte: true,
+ dernier_emploi: "Danseur",
+ code_salarie: "demo-sal-003",
+ prenom: "Sophie",
+ civilite: "Mme",
+ tel: "06 34 56 78 90",
+ org_id: "demo-org",
+ org_name: "Organisation Démo",
+ },
+ {
+ matricule: "demo-sal-004",
+ nom: "BERNARD Marc",
+ email: "marc.bernard@demo.fr",
+ transat_connecte: false,
+ dernier_emploi: "Technicien son",
+ code_salarie: "demo-sal-004",
+ prenom: "Marc",
+ civilite: "M.",
+ tel: "06 45 67 89 01",
+ org_id: "demo-org",
+ org_name: "Organisation Démo",
+ },
+ {
+ matricule: "demo-sal-005",
+ nom: "GARCIA Elena",
+ email: "elena.garcia@demo.fr",
+ transat_connecte: true,
+ dernier_emploi: "Costumière",
+ code_salarie: "demo-sal-005",
+ prenom: "Elena",
+ civilite: "Mme",
+ tel: "06 56 78 90 12",
+ org_id: "demo-org",
+ org_name: "Organisation Démo",
+ },
+ ];
+
+ // Filtrer par recherche si nécessaire
+ let filteredSalaries = DEMO_SALARIES;
+ if (q) {
+ const searchTerm = q.toLowerCase();
+ filteredSalaries = DEMO_SALARIES.filter(salarie =>
+ salarie.nom.toLowerCase().includes(searchTerm) ||
+ salarie.prenom?.toLowerCase().includes(searchTerm) ||
+ salarie.matricule.toLowerCase().includes(searchTerm) ||
+ salarie.email?.toLowerCase().includes(searchTerm) ||
+ salarie.dernier_emploi?.toLowerCase().includes(searchTerm)
+ );
+ }
+
+ // Pagination
+ const offset = (page - 1) * limit;
+ const paginatedSalaries = filteredSalaries.slice(offset, offset + limit);
+ const total = filteredSalaries.length;
+ const hasMore = offset + paginatedSalaries.length < total;
+ const totalPages = Math.ceil(total / limit);
+
+ const payload: SalariesResponse = {
+ items: paginatedSalaries,
+ page,
+ limit,
+ total,
+ totalPages,
+ hasMore,
+ };
+
+ return NextResponse.json(payload);
+ }
+
const sb = createSbServer();
const {
diff --git a/app/api/signatures-electroniques/contrats/route.ts b/app/api/signatures-electroniques/contrats/route.ts
index 2555650..cbb0e32 100644
--- a/app/api/signatures-electroniques/contrats/route.ts
+++ b/app/api/signatures-electroniques/contrats/route.ts
@@ -1,10 +1,17 @@
import { NextResponse, NextRequest } from 'next/server';
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
+import { detectDemoModeFromHeaders } from '@/lib/demo-detector';
// GET /api/signatures-electroniques/contrats
// Retourne les contrats à signer par l'employeur ou salarié depuis Supabase
export async function GET(req: NextRequest) {
+ // 🎭 Vérifier le mode démo - retourner des données vides pour sécuriser
+ if (detectDemoModeFromHeaders(req.headers)) {
+ console.log('🎭 [SIGNATURES API] Mode démo détecté - retour de données vides');
+ return NextResponse.json({ records: [] }, { status: 200 });
+ }
+
const reqUrl = new URL(req.url);
const scope = (reqUrl.searchParams.get('scope') || 'employeur').toLowerCase();
const orgIdParam = reqUrl.searchParams.get('org_id'); // Paramètre org_id pour les staff
diff --git a/app/api/signatures-electroniques/relance/route.ts b/app/api/signatures-electroniques/relance/route.ts
index fe701f2..feba729 100644
--- a/app/api/signatures-electroniques/relance/route.ts
+++ b/app/api/signatures-electroniques/relance/route.ts
@@ -3,11 +3,18 @@ import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { sendUniversalEmailV2, EmailDataV2 } from '@/lib/emailTemplateService';
import { ENV } from '@/lib/cleanEnv';
+import { detectDemoModeFromHeaders } from '@/lib/demo-detector';
// Envoi via le système universel (pas de configuration SES directe ici)
// POST /api/signatures-electroniques/relance
// Envoie un email de relance pour la signature d'un contrat salarié
export async function POST(req: NextRequest) {
+ // 🎭 Bloquer l'envoi de relances en mode démo
+ if (detectDemoModeFromHeaders(req.headers)) {
+ console.log('🎭 [RELANCE API] Mode démo détecté - envoi de relance bloqué');
+ return NextResponse.json({ error: 'Action non disponible en mode démo' }, { status: 403 });
+ }
+
try {
const { contractId } = await req.json();
diff --git a/app/api/virements-salaires/route.ts b/app/api/virements-salaires/route.ts
index 9ef4418..920f818 100644
--- a/app/api/virements-salaires/route.ts
+++ b/app/api/virements-salaires/route.ts
@@ -2,8 +2,25 @@
import { NextRequest, NextResponse } from "next/server";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
+import { detectDemoModeFromHeaders } from "@/lib/demo-detector";
export async function GET(req: NextRequest) {
+ // 🎭 En mode démo, retourner des données vides (les données fictives sont gérées côté client)
+ if (detectDemoModeFromHeaders(req.headers)) {
+ console.log('🎭 [VIREMENTS API] Mode démo détecté - retour géré côté client');
+ return NextResponse.json({
+ items: [],
+ enabled: true,
+ org: {
+ structure_api: 'demo-org',
+ virements_salaires: 'odentas',
+ iban: null,
+ bic: null,
+ },
+ client: undefined,
+ }, { status: 200 });
+ }
+
try {
const { searchParams } = new URL(req.url);
const year = searchParams.get("year");
diff --git a/components/DemoBanner.tsx b/components/DemoBanner.tsx
index 50b74eb..13ed03b 100644
--- a/components/DemoBanner.tsx
+++ b/components/DemoBanner.tsx
@@ -16,35 +16,21 @@ export function DemoBanner({ isDemoMode = false, isPublicDemo = false }: DemoBan
- 🎭
Mode Démonstration - Découvrez l'Espace Paie Odentas
-
-
- Données fictives utilisées • Navigation complète disponible • Testez toutes les fonctionnalités
-
);
@@ -53,7 +39,6 @@ export function DemoBanner({ isDemoMode = false, isPublicDemo = false }: DemoBan
return (
- ⚠️
Mode Développement - Démonstration avec données fictives
diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx
index edeecda..aba8ca6 100644
--- a/components/Sidebar.tsx
+++ b/components/Sidebar.tsx
@@ -2,10 +2,11 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState, useEffect, useRef } from "react";
-import { Shield, KeyRound, Home, FileSignature, Banknote, Users, Percent, FolderOpen, Receipt, Building2, UserCog, Edit3, LifeBuoy, CreditCard, Calculator, Euro, Mail, Database, Clapperboard } from "lucide-react";
+import { Shield, KeyRound, Home, FileSignature, Banknote, Users, Percent, FolderOpen, Receipt, Building2, UserCog, Edit3, LifeBuoy, CreditCard, Calculator, Euro, Mail, Database, Clapperboard, LogOut } from "lucide-react";
// import { api } from "@/lib/fetcher";
import { createPortal } from "react-dom";
import LogoutButton from "@/components/LogoutButton";
+import { useDemoMode } from "@/hooks/useDemoMode";
function AccessLink({ disabled, fullWidth = false }: { disabled: boolean; fullWidth?: boolean }) {
const btnRef = useRef(null);
@@ -82,7 +83,7 @@ function AccessLink({ disabled, fullWidth = false }: { disabled: boolean; fullWi
}
function DisabledMenuItem({ icon: Icon, label, tooltipMessage }: { icon: any; label: string; tooltipMessage: string }) {
- const btnRef = useRef(null);
+ const btnRef = useRef(null);
const [tipOpen, setTipOpen] = useState(false);
const [tipPos, setTipPos] = useState<{ top: number; left: number } | null>(null);
@@ -117,7 +118,7 @@ function DisabledMenuItem({ icon: Icon, label, tooltipMessage }: { icon: any; la
onMouseLeave={() => setTipOpen(false)}
onFocus={() => { computePos(); setTipOpen(true); }}
onBlur={() => setTipOpen(false)}
- className="flex items-center justify-between px-3 py-2 rounded-xl text-sm transition truncate cursor-not-allowed opacity-50"
+ className="block px-3 py-2 rounded-xl text-sm cursor-not-allowed opacity-50"
role="link"
aria-disabled="true"
tabIndex={0}
@@ -249,6 +250,10 @@ export default function Sidebar({ clientInfo, isStaff = false, mobile = false, o
const pathname = usePathname();
const [canManageAccess, setCanManageAccess] = useState(false);
const [userRole, setUserRole] = useState(null);
+
+ // 🎭 Détection du mode démo
+ const { isDemoMode } = useDemoMode();
+
// Signature count in sidebar disabled to reduce Airtable load
useEffect(() => {
let cancelled = false;
@@ -291,41 +296,87 @@ export default function Sidebar({ clientInfo, isStaff = false, mobile = false, o
Votre structure
{isStaff ? 'Odentas' : orgName}
Votre niveau d'accès
- {isStaff ? 'STAFF' : (userRole || '—')}
+
+ {isDemoMode ? 'DEMO' : (isStaff ? 'STAFF' : (userRole || '—'))}
+
{!isStaff && (
<>
Votre gestionnaire
- Renaud BREVIERE-ABRAHAM
+
+ {isDemoMode ? '—' : 'Renaud BREVIERE-ABRAHAM'}
+
>
)}
{/* Sous-card liens sécurité & accès (compacte) */}
-
-
- Sécurité
-
- {/* Vos accès: toujours visible, mais si pas habilité → curseur interdit + tooltip + blocage du clic */}
-
+ {/* Sécurité */}
+ {isDemoMode ? (
+
+
+ Sécurité
+
+ ) : (
+
+
+ Sécurité
+
+ )}
+
+ {/* Vos accès */}
+ {isDemoMode ? (
+
+
+ Vos accès
+
+ ) : (
+
+ )}
{/* Support */}
-
-
- Support
-
+ {isDemoMode ? (
+
+
+ Support
+
+ ) : (
+
+
+ Support
+
+ )}
- {/* Déconnexion compacte avec modale de confirmation */}
-
+ {/* Déconnexion */}
+ {isDemoMode ? (
+
+
+ Déconnexion
+
+ ) : (
+
+ )}
@@ -356,30 +407,54 @@ export default function Sidebar({ clientInfo, isStaff = false, mobile = false, o
})}
-
onNavigate && onNavigate()} className={`block px-3 py-2 rounded-xl text-sm transition truncate ${
- isActivePath(pathname, "/facturation") ? "bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-200 text-slate-700 font-semibold" : "hover:bg-slate-50"
- }`} title="Facturation">
-
-
- Facturation
-
-
-
onNavigate && onNavigate()} className={`block px-3 py-2 rounded-xl text-sm transition truncate ${
- isActivePath(pathname, "/informations") ? "bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-200 text-slate-700 font-semibold" : "hover:bg-slate-50"
- }`} title="Vos informations">
-
-
- Vos informations
-
-
-
onNavigate && onNavigate()} className={`block px-3 py-2 rounded-xl text-sm transition truncate ${
- isActivePath(pathname, "/vos-documents") ? "bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-200 text-slate-700 font-semibold" : "hover:bg-slate-50"
- }`} title="Vos documents">
-
-
- Vos documents
-
-
+ {isDemoMode ? (
+
+ ) : (
+
onNavigate && onNavigate()} className={`block px-3 py-2 rounded-xl text-sm transition truncate ${
+ isActivePath(pathname, "/facturation") ? "bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-200 text-slate-700 font-semibold" : "hover:bg-slate-50"
+ }`} title="Facturation">
+
+
+ Facturation
+
+
+ )}
+ {isDemoMode ? (
+
+ ) : (
+
onNavigate && onNavigate()} className={`block px-3 py-2 rounded-xl text-sm transition truncate ${
+ isActivePath(pathname, "/informations") ? "bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-200 text-slate-700 font-semibold" : "hover:bg-slate-50"
+ }`} title="Vos informations">
+
+
+ Vos informations
+
+
+ )}
+ {isDemoMode ? (
+
+ ) : (
+
onNavigate && onNavigate()} className={`block px-3 py-2 rounded-xl text-sm transition truncate ${
+ isActivePath(pathname, "/vos-documents") ? "bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-200 text-slate-700 font-semibold" : "hover:bg-slate-50"
+ }`} title="Vos documents">
+
+
+ Vos documents
+
+
+ )}
("");
@@ -2068,8 +2070,9 @@ useEffect(() => {
{loading ? (<> Envoi…>) : "Envoyer"}
diff --git a/lib/demo-data.ts b/lib/demo-data.ts
index 2a5cd17..b830e77 100644
--- a/lib/demo-data.ts
+++ b/lib/demo-data.ts
@@ -131,7 +131,7 @@ export const DEMO_EMPLOYEES: DemoEmployee[] = [
export const DEMO_CONTRACTS: DemoContract[] = [
{
- id: "cont-001",
+ id: "demo-cont-001",
reference: "CDDU-2024-001",
salarie_nom: "MARTIN Alice",
salarie_id: "emp-001",
@@ -146,7 +146,7 @@ export const DEMO_CONTRACTS: DemoContract[] = [
heures_semaine: 39
},
{
- id: "cont-002",
+ id: "demo-cont-002",
reference: "CDDU-2024-002",
salarie_nom: "DUBOIS Pierre",
salarie_id: "emp-002",
@@ -160,7 +160,7 @@ export const DEMO_CONTRACTS: DemoContract[] = [
heures_semaine: 45
},
{
- id: "cont-003",
+ id: "demo-cont-003",
reference: "CDDU-2024-003",
salarie_nom: "LEROY Sophie",
salarie_id: "emp-003",
@@ -175,7 +175,7 @@ export const DEMO_CONTRACTS: DemoContract[] = [
heures_semaine: 35
},
{
- id: "cont-004",
+ id: "demo-cont-004",
reference: "CDDU-2024-004",
salarie_nom: "BERNARD Julien",
salarie_id: "emp-004",
@@ -189,7 +189,7 @@ export const DEMO_CONTRACTS: DemoContract[] = [
heures_semaine: 20
},
{
- id: "cont-005",
+ id: "demo-cont-005",
reference: "CDDU-2024-005",
salarie_nom: "PETIT Marie",
salarie_id: "emp-005",
@@ -204,7 +204,7 @@ export const DEMO_CONTRACTS: DemoContract[] = [
heures_semaine: 32
},
{
- id: "cont-006",
+ id: "demo-cont-006",
reference: "CDDU-2024-006",
salarie_nom: "MARTIN Alice",
salarie_id: "emp-001",
@@ -218,7 +218,7 @@ export const DEMO_CONTRACTS: DemoContract[] = [
heures_semaine: 25
},
{
- id: "cont-007",
+ id: "demo-cont-007",
reference: "CDDU-2024-007",
salarie_nom: "DUBOIS Pierre",
salarie_id: "emp-002",
@@ -237,7 +237,7 @@ export const DEMO_PAYSLIPS: DemoPayslip[] = [
// MARTIN Alice - Les Misérables
{
id: "pay-001",
- contrat_id: "cont-001",
+ contrat_id: "demo-cont-001",
salarie_nom: "MARTIN Alice",
periode: "2024-01",
salaire_brut: 3200,
@@ -248,7 +248,7 @@ export const DEMO_PAYSLIPS: DemoPayslip[] = [
},
{
id: "pay-002",
- contrat_id: "cont-001",
+ contrat_id: "demo-cont-001",
salarie_nom: "MARTIN Alice",
periode: "2024-02",
salaire_brut: 3200,
@@ -259,7 +259,7 @@ export const DEMO_PAYSLIPS: DemoPayslip[] = [
},
{
id: "pay-003",
- contrat_id: "cont-001",
+ contrat_id: "demo-cont-001",
salarie_nom: "MARTIN Alice",
periode: "2024-03",
salaire_brut: 3200,
@@ -271,7 +271,7 @@ export const DEMO_PAYSLIPS: DemoPayslip[] = [
// DUBOIS Pierre - Le Cid
{
id: "pay-004",
- contrat_id: "cont-002",
+ contrat_id: "demo-cont-002",
salarie_nom: "DUBOIS Pierre",
periode: "2024-07",
salaire_brut: 4500,
@@ -283,7 +283,7 @@ export const DEMO_PAYSLIPS: DemoPayslip[] = [
// LEROY Sophie - Ballet
{
id: "pay-005",
- contrat_id: "cont-003",
+ contrat_id: "demo-cont-003",
salarie_nom: "LEROY Sophie",
periode: "2024-09",
salaire_brut: 2800,
@@ -294,7 +294,7 @@ export const DEMO_PAYSLIPS: DemoPayslip[] = [
},
{
id: "pay-006",
- contrat_id: "cont-003",
+ contrat_id: "demo-cont-003",
salarie_nom: "LEROY Sophie",
periode: "2024-10",
salaire_brut: 2800,
@@ -306,7 +306,7 @@ export const DEMO_PAYSLIPS: DemoPayslip[] = [
// BERNARD Julien - Jazz Festival
{
id: "pay-007",
- contrat_id: "cont-004",
+ contrat_id: "demo-cont-004",
salarie_nom: "BERNARD Julien",
periode: "2024-06",
salaire_brut: 1200,
@@ -318,7 +318,7 @@ export const DEMO_PAYSLIPS: DemoPayslip[] = [
// PETIT Marie - Opéra Carmen
{
id: "pay-008",
- contrat_id: "cont-005",
+ contrat_id: "demo-cont-005",
salarie_nom: "PETIT Marie",
periode: "2024-03",
salaire_brut: 2400,
@@ -329,7 +329,7 @@ export const DEMO_PAYSLIPS: DemoPayslip[] = [
},
{
id: "pay-009",
- contrat_id: "cont-005",
+ contrat_id: "demo-cont-005",
salarie_nom: "PETIT Marie",
periode: "2024-04",
salaire_brut: 2400,
@@ -340,7 +340,7 @@ export const DEMO_PAYSLIPS: DemoPayslip[] = [
},
{
id: "pay-010",
- contrat_id: "cont-005",
+ contrat_id: "demo-cont-005",
salarie_nom: "PETIT Marie",
periode: "2024-05",
salaire_brut: 2400,
diff --git a/public/simulateur-embed.html b/public/simulateur-embed.html
index a25279a..720e7e8 100644
--- a/public/simulateur-embed.html
+++ b/public/simulateur-embed.html
@@ -13,48 +13,49 @@
-
-
- Convention Collective
-
-
-
-
-
- 1285 – Entreprises Artistiques & Culturelles (CCNEAC)
- 3090 – Spectacle Vivant Privé (CCNSVP)
- 1518 – ÉCLAT (ex-Animation)
- 1922 – Radiodiffusion
- 2121 – Édition phonographique
- 2412 – Production de films d’animation
- 2642 – Production audiovisuelle
- 3097 – Production cinématographique
- 3241 – Télédiffusion
- 3252 – Entreprises au service de la création et de l’événement
-
+
+
+
+
+ Convention Collective
+
+
+
+
+
+ 1285 – Entreprises Artistiques & Culturelles (CCNEAC)
+ 3090 – Spectacle Vivant Privé (CCNSVP)
+ 1518 – ÉCLAT (ex-Animation)
+ 1922 – Radiodiffusion
+ 2121 – Édition phonographique
+ 2412 – Production de films d'animation
+ 2642 – Production audiovisuelle
+ 3097 – Production cinématographique
+ 3241 – Télédiffusion
+ 3252 – Entreprises au service de la création et de l'événement
+
-
- Catégorie
-
- Artiste (Annexe 10)
- Technicien (Annexe 8)
-
+ Catégorie
+
+ Artiste (Annexe 10)
+ Technicien (Annexe 8)
+
-
- Statut
-
-
-
-
-
- Artiste non-cadre
- Artiste cadre
-
+ Statut
+
+
+
+
+
+ Artiste non-cadre
+ Artiste cadre
+
+
-
-
+
+
Votre salarié a-t-il choisi de bénéficier de l'abattement pour frais professionnels ?
@@ -83,54 +84,56 @@
-
-
-
Nombre de cachets
+
+
-
- Nombre d'heures
-
-
-
-
-
-
-
- Dates de travail
-
-
-
-
-
- Veuillez sélectionner les jours de travail.
-
-
- Montant total de la rémunération (€)
-
-
-
-
-
-
-
-
Salaire Brut
-
Salaire Net avant PAS
+
+
Calculer
@@ -312,14 +315,28 @@ function distributeCachetsAcrossDays(dates, cachets) {
function getAssietteChomageMaxPourMoisCourant() {
const datesStr = document.getElementById("datesInput").value;
if (!datesStr) return Infinity;
- const dates = datesStr.split(",").map(s => new Date(s.trim())).sort((a,b)=>a-b);
- const first = dates[0], last = dates[dates.length-1];
- const y = first.getFullYear(), m = first.getMonth();
- const daysInMonth = new Date(y, m + 1, 0).getDate();
+ const dates = datesStr
+ .split(",")
+ .map(s => new Date(s.trim()))
+ .filter(d => !isNaN(d))
+ .sort((a,b)=>a-b);
+
+ if (!dates.length) return Infinity;
+
+ const first = dates[0], last = dates[dates.length-1];
const diffDays = Math.round((last - first) / (1000*60*60*24)) + 1;
- return 4 * (PMSS * diffDays / daysInMonth);
+ // Règle attendue :
+ // - < 5 jours : plafond chômage = 4 × plafond URSSAF de la période
+ // (ex. Artiste: 4 × (12×PHSS×nbJours) => 4×348=1392 pour 1 jour)
+ // - ≥ 5 jours : plafond chômage = 4 × PMSS (mensuel, sans prorata à l'intervalle)
+ if (diffDays < 5) {
+ const plafUrssaf = getPlafondUrssaf();
+ return 4 * plafUrssaf;
+ }
+
+ return 4 * PMSS;
}
// Taux complémentaires selon catégorie
@@ -438,7 +455,7 @@ function baseLibelle(code, cat){
"at": "Accident du travail",
"vieillesse_ta": isTech ? "Assurance vieillesse tranche A" : "Assurance vieillesse tranche A artiste",
"fnal_plaf": isTech ? "FNAL plafonné" : "FNAL artiste plafonné",
- "maj_chomage": isTech ? "Maj. chômage int. < 3 mois" : "Majoration chômage int. moins 3 jours",
+ "maj_chomage": isTech ? "Maj. chômage int. < 3 mois" : "Majoration chômage int. moins 3 mois",
"chomage": "Assurance chômage intermittent",
"ags": "AGS intermittent",
"retraite_t1": isTech ? "Retraite non-cadre Int. T1" : "Retraite artiste Tranche 1",
@@ -563,10 +580,13 @@ const nonAbattementCodes = [
sal.forEach(c => {
let base;
+ let appliedAbattement = false;
if (c.code === "vieillesse_ta") {
- base = Math.min(brut, plafondUrssaf);
+ base = Math.min(brut * factor, plafondUrssaf); // abattement AVANT plafonnement
+ appliedAbattement = true;
} else if (c.code === "fnal_plaf") {
- base = Math.min(brut, plafondUrssaf) * 1.115;
+ base = Math.min(brut * factor, plafondUrssaf) * 1.115; // abattement AVANT plafonnement
+ appliedAbattement = true;
} else if (["chomage","maj_chomage","ags"].includes(c.code)) {
base = Math.min(brut, assietteChomageMax);
} else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
@@ -576,7 +596,9 @@ const nonAbattementCodes = [
} else {
base = brut;
}
- if (!nonAbattementCodes.includes(c.code)) base = base * factor;
+ if (!appliedAbattement && !nonAbattementCodes.includes(c.code)) {
+ base = base * factor;
+ }
totalDeduction += base * (c.taux / 100);
});
return brut - totalDeduction;
@@ -609,12 +631,18 @@ const nonAbattementCodes = [
// Somme des charges patronales "brutes"
merged.forEach(c => {
let base;
- if (["chomage","maj_chomage","ags"].includes(c.code)) {
- base = Math.min(brut, assietteChomageMax);
- } else if (c.code === "vieillesse_ta") {
- base = Math.min(brut, plafondUrssaf);
+ let appliedAbattement = false;
+ if (c.code === "vieillesse_ta") {
+ base = Math.min(brut * factor, plafondUrssaf); // abattement AVANT plafonnement
+ appliedAbattement = true;
} else if (c.code === "fnal_plaf") {
- base = Math.min(brut, plafondUrssaf) * 1.115;
+ base = Math.min(brut * factor, plafondUrssaf) * 1.115; // abattement AVANT plafonnement
+ appliedAbattement = true;
+ } else if (["fnas","fcap"].includes(c.code)) {
+ base = brut; // jamais d’abattement
+ appliedAbattement = true; // bloque toute ré-appl. du factor
+ } else if (["chomage","maj_chomage","ags"].includes(c.code)) {
+ base = Math.min(brut, assietteChomageMax);
} else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
base = getPrevoyanceBase(brut);
} else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) {
@@ -622,7 +650,9 @@ const nonAbattementCodes = [
} else {
base = brut;
}
- if (!nonAbattementCodes.includes(c.code)) base = base * factor;
+ if (!appliedAbattement && !nonAbattementCodes.includes(c.code)) {
+ base = base * factor;
+ }
totalCharges += base * (c.tauxPatronal / 100);
});
@@ -844,10 +874,16 @@ const nonAbattementCodes = [
merged.forEach(c => {
let base;
+ let appliedAbattement = false;
if (c.code === "vieillesse_ta") {
- base = Math.min(brut, plafondUrssaf);
+ base = Math.min(brut * factor, plafondUrssaf); // abattement AVANT plafonnement
+ appliedAbattement = true;
} else if (c.code === "fnal_plaf") {
- base = Math.min(brut, plafondUrssaf) * 1.115;
+ base = Math.min(brut * factor, plafondUrssaf) * 1.115; // abattement AVANT plafonnement
+ appliedAbattement = true;
+ } else if (["fnas","fcap"].includes(c.code)) {
+ base = brut; // jamais d’abattement
+ appliedAbattement = true; // bloque toute ré-appl. du factor
} else if (["chomage","maj_chomage","ags"].includes(c.code)) {
base = Math.min(brut, assietteChomageMax);
} else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
@@ -857,7 +893,9 @@ const nonAbattementCodes = [
} else {
base = brut;
}
- if (!nonAbattementCodes.includes(c.code)) base = base * factor;
+ if (!appliedAbattement && !nonAbattementCodes.includes(c.code)) {
+ base = base * factor;
+ }
const montantSalarial = base * (c.tauxSalarial / 100);
const montantPatronal = base * (c.tauxPatronal / 100);
@@ -989,34 +1027,75 @@ if (fillonTotal > 0) {
let totalSal = 0;
let totalPat = 0;
-for (const c of merged){
- let base;
+ for (const c of merged){
+ let base;
+ let appliedAbattement = false;
+
+ // ✅ Cas spécial : ligne "Réduction Fillon totale"
+ if (c._isFillon) {
+ const montantSalarial = 0;
+ const montantPatronal = c._montantPat; // négatif
+ totalSal += montantSalarial;
+ totalPat += montantPatronal;
+
+ lignes.push({
+ libelle: c.libelle,
+ assiette: fmtEuro(c._base),
+ taux_salarial: "—",
+ taux_patronal: "—",
+ montant_salarial: fmtEuro(montantSalarial),
+ montant_patronal: fmtEuro(montantPatronal)
+ });
+ continue;
+ }
+
+ // ✅ Cas spécial : compléments (déjà calculés)
+ if (c._isComplement){
+ base = c._base;
+ const montantSalarial = 0;
+ const montantPatronal = c._montantPat;
+ totalSal += montantSalarial;
+ totalPat += montantPatronal;
+ lignes.push({
+ libelle: c.libelle,
+ assiette: fmtEuro(base),
+ taux_salarial: fmtPct(c.tauxSalarial),
+ taux_patronal: fmtPct(c.tauxPatronal),
+ montant_salarial: fmtEuro(montantSalarial),
+ montant_patronal: fmtEuro(montantPatronal)
+ });
+ continue;
+ }
+
+ // — Cas général —
+ if (["chomage","maj_chomage","ags"].includes(c.code)) {
+ base = Math.min(brut, assietteChomageMax);
+ } else if (c.code === "vieillesse_ta") {
+ base = Math.min(brut * factor, plafondUrssaf); // abattement AVANT plafonnement
+ appliedAbattement = true;
+ } else if (c.code === "fnal_plaf") {
+ base = Math.min(brut * factor, plafondUrssaf) * 1.115; // abattement AVANT plafonnement
+ appliedAbattement = true;
+ } else if (["fnas","fcap"].includes(c.code)) {
+ base = brut; // jamais d’abattement
+ appliedAbattement = true; // bloque toute ré-appl. du factor
+ } else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
+ base = getPrevoyanceBase(brut);
+ } else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) {
+ base = (brut + prevoyanceEmployerAmount(brut)) * 0.9825;
+ } else {
+ base = brut;
+ }
+ if (!appliedAbattement && !nonAbattementCodes.includes(c.code)) {
+ base = base * factor;
+ }
+
+ const montantSalarial = base * (c.tauxSalarial / 100);
+ const montantPatronal = base * (c.tauxPatronal / 100);
- // ✅ Cas spécial : ligne "Réduction Fillon totale"
- if (c._isFillon) {
- const montantSalarial = 0;
- const montantPatronal = c._montantPat; // négatif
totalSal += montantSalarial;
totalPat += montantPatronal;
- lignes.push({
- libelle: c.libelle,
- assiette: fmtEuro(c._base),
- taux_salarial: "—",
- taux_patronal: "—",
- montant_salarial: fmtEuro(montantSalarial),
- montant_patronal: fmtEuro(montantPatronal)
- });
- continue;
- }
-
- // ✅ Cas spécial : compléments (déjà calculés)
- if (c._isComplement){
- base = c._base;
- const montantSalarial = 0;
- const montantPatronal = c._montantPat;
- totalSal += montantSalarial;
- totalPat += montantPatronal;
lignes.push({
libelle: c.libelle,
assiette: fmtEuro(base),
@@ -1025,41 +1104,8 @@ for (const c of merged){
montant_salarial: fmtEuro(montantSalarial),
montant_patronal: fmtEuro(montantPatronal)
});
- continue;
}
- // — Cas général —
- if (["chomage","maj_chomage","ags"].includes(c.code)) {
- base = Math.min(brut, assietteChomageMax);
- } else if (c.code === "vieillesse_ta") {
- base = Math.min(brut, plafondUrssaf);
- } else if (c.code === "fnal_plaf") {
- base = Math.min(brut, plafondUrssaf) * 1.115;
- } else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
- base = getPrevoyanceBase(brut);
- } else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) {
- base = (brut + prevoyanceEmployerAmount(brut)) * 0.9825;
- } else {
- base = brut;
- }
- if (!nonAbattementCodes.includes(c.code)) base = base * factor;
-
- const montantSalarial = base * (c.tauxSalarial / 100);
- const montantPatronal = base * (c.tauxPatronal / 100);
-
- totalSal += montantSalarial;
- totalPat += montantPatronal;
-
- lignes.push({
- libelle: c.libelle,
- assiette: fmtEuro(base),
- taux_salarial: fmtPct(c.tauxSalarial),
- taux_patronal: fmtPct(c.tauxPatronal),
- montant_salarial: fmtEuro(montantSalarial),
- montant_patronal: fmtEuro(montantPatronal)
- });
-}
-
const contributions = [{ groupe: "Cotisations et contributions", lignes }];
return {
@@ -1247,113 +1293,228 @@ window.odentasSimulation = {