espace-paie-odentas/public/simulateur-embed.html
2025-10-22 11:00:44 +02:00

2485 lines
89 KiB
HTML
Raw Permalink 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.

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simulateur Paie</title>
<!-- Bootstrap 5 Bundle avec Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Compact mode styles applied when body has .compact-mode (triggered by ?compact=1) */
body.compact-mode {
padding: 8px !important;
font-size: 12px;
}
body.compact-mode .simulateur {
--c-gap: 8px;
--c-gap-small: 6px;
--c-radius: 10px;
--c-border: #e2e8f0;
--c-text: #0f172a;
--c-muted: #64748b;
gap: var(--c-gap);
}
body.compact-mode .form-section {
margin-bottom: 6px;
}
/* 2-colonnes en compact, avec wrappers de paires label+champ */
body.compact-mode .form-section {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
column-gap: 8px;
row-gap: 4px;
align-items: start;
grid-auto-rows: min-content;
background: #f8fafc;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 10px;
}
body.compact-mode .form-section .form-row { display: flex; flex-direction: column; min-width: 0; width: 100%; gap: 2px; }
/* Éléments qui doivent occuper toute la largeur */
body.compact-mode .options,
body.compact-mode #calcBtn,
body.compact-mode #openCalculatorBtn,
body.compact-mode #result,
body.compact-mode .detail-table,
body.compact-mode #abattementSectionWrapper { grid-column: 1 / -1; }
/* Réduire l'espacement entre les éléments d'une même section */
body.compact-mode .form-section > * { margin-bottom: 0 !important; }
body.compact-mode label {
font-size: 12px;
font-weight: 600;
margin-bottom: 2px;
}
body.compact-mode input[type="number"],
body.compact-mode input[type="text"],
body.compact-mode select {
font-size: 12px;
padding: 4px 8px;
height: auto; /* let Bootstrap control the height */
min-height: 31px;
line-height: 1.3;
color: #111827; /* slate-900 for contrast */
width: 100%;
min-width: 0;
box-sizing: border-box;
}
/* Améliorer la lisibilité du texte sélectionné dans les selects */
body.compact-mode select {
padding-right: 28px; /* room for chevron */
font-size: 12px;
line-height: 1.3;
height: auto;
min-height: 31px;
background-color: #ffffff;
color: #111827;
appearance: auto;
-webkit-appearance: menulist; /* Safari fix: ensure native dropdown rendering */
text-overflow: clip; /* avoid hidden text issues */
overflow: visible;
}
body.compact-mode select option { font-size: 12px; }
body.compact-mode .options label {
font-size: 12px;
}
body.compact-mode #openCalculatorBtn {
font-size: 10px !important;
padding: 1px 5px !important;
margin-top: 2px !important;
}
body.compact-mode #calcBtn {
font-size: 12px;
padding: 5px 8px;
}
body.compact-mode .result h4 {
font-size: 13px;
margin: 6px 0;
}
body.compact-mode #result {
font-size: 12px;
}
/* Hide detail table in compact */
body.compact-mode #detailTable {
display: none !important;
}
/* Reduce visual clutter: hide popover info icons in compact */
body.compact-mode [data-bs-toggle="popover"] {
display: none;
}
/* Diminuer le spacing des groupes Bootstrap dans la section abattement */
body.compact-mode .d-flex.mb-3 {
margin-bottom: 4px !important;
gap: 6px !important;
}
/* Hide helper paragraphs and reduce section paddings */
body.compact-mode p#daysCount { display: none; }
body.compact-mode .simulateur h1, body.compact-mode h2, body.compact-mode h3, body.compact-mode h4 { margin: 6px 0; }
/* Layout tweaks: simple, tidy options row */
body.compact-mode .options { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
body.compact-mode .options label { margin: 0; display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 8px; border: 1px solid var(--c-border); background: #fff; }
body.compact-mode .options input[type="radio"] { accent-color: #6366f1; }
body.compact-mode .options label:has(input[type="radio"]:checked) { background: #eef2ff; border-color: #6366f1; box-shadow: 0 0 0 2px rgba(99,102,241,0.08); }
/* Compact calculator modal inside embed if used */
body.compact-mode #calculator { transform: scale(0.9); transform-origin: top right; }
/* Responsive fallback: single column if space is too narrow */
@media (max-width: 480px) {
body.compact-mode .form-section { grid-template-columns: 1fr; }
body.compact-mode .options { grid-template-columns: 1fr; }
}
</style>
</head>
<body style="margin:0;padding:20px;background:#f9fafb;font-family:system-ui,-apple-system,sans-serif;">
<div class="simulateur">
<!-- Section 1: CCN, Catégorie, Statut -->
<div class="form-section">
<label for="conventionSelect">
Convention Collective
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
data-bs-content="Choisissez votre Convention Collective pour intégrer dans le calcul vos éventuelles cotisations conventionnelles obligatoires.">
<i class="fa fa-info-circle"></i>
</span>
</label>
<select id="conventionSelect">
<option value="1285">1285 Entreprises Artistiques & Culturelles (CCNEAC)</option>
<option value="3090">3090 Spectacle Vivant Privé (CCNSVP)</option>
<option value="1518">1518 ÉCLAT (ex-Animation)</option>
<option value="1922">1922 Radiodiffusion</option>
<option value="2121">2121 Édition phonographique</option>
<option value="2412">2412 Production de films d'animation</option>
<option value="2642">2642 Production audiovisuelle</option>
<option value="3097">3097 Production cinématographique</option>
<option value="3241">3241 Télédiffusion</option>
<option value="3252">3252 Entreprises au service de la création et de l'événement</option>
</select>
<label for="categorieSelect">Catégorie</label>
<select id="categorieSelect">
<option value="artiste" selected>Artiste (Annexe 10)</option>
<option value="technicien">Technicien (Annexe 8)</option>
</select>
<label for="statutSelect">Statut
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
data-bs-content="Le statut 'Artiste cadre' concerne des professions comme 'Metteur en scène', 'Chorégraphe', 'Réalisateur', 'Chef des choeurs', 'Chef d'orchestre'.">
<i class="fa fa-info-circle"></i>
</span>
</label>
<select id="statutSelect">
<option value="non-cadre" selected>Artiste non-cadre</option>
<option value="cadre">Artiste cadre</option>
</select>
</div>
<!-- Section 2: Abattement (uniquement Artiste) -->
<div class="form-section" style="display: none;" id="abattementSectionWrapper">
<div id="abattementSection">
<label>Votre salarié a-t-il choisi de bénéficier de l'abattement pour frais professionnels ?
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
data-bs-content="Abattement pour frais professionnels des artistes (dispositif en extinction progressive jusqu'au 01/01/2032).">
<i class="fa fa-info-circle"></i>
</span>
</label>
<div class="d-flex mb-3" style="gap:10px;">
<div>
<input type="radio" name="abattement" id="abattementOui" value="oui">
<label for="abattementOui">Oui</label>
</div>
<div>
<input type="radio" name="abattement" id="abattementNon" value="non" checked>
<label for="abattementNon">Non</label>
</div>
</div>
<div id="professionBlock" style="display:none;">
<label for="professionSelect">Profession du salarié</label>
<select id="professionSelect">
<option value="">-- Sélectionnez une profession --</option>
<option value="drama">Artiste dramatique / lyrique / cinématographique / chorégraphique / de cirque (21%)</option>
<option value="musique">Artiste musicien / choriste / chef d'orchestre (18%)</option>
</select>
</div>
</div>
</div>
<!-- Section 3: Cachets, Heures, Dates -->
<div class="form-section">
<div id="cachetsGroup">
<label for="cachetsInput">Nombre de cachets
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
data-bs-content="Saisissez le nombre de cachets de représentation et/ou de répétitions.">
<i class="fa fa-info-circle"></i>
</span>
</label>
<input type="number" id="cachetsInput" step="1" placeholder="Ex : 10">
</div>
<label for="heuresInput">Nombre d'heures
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
data-bs-content="Saisissez le nombre d'heures de travail (répétitions rémunérées à l'heure).">
<i class="fa fa-info-circle"></i>
</span>
</label>
<input type="number" id="heuresInput" step="0.1" placeholder="Ex : 35">
<label for="datesInput">Dates de travail
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
data-bs-content="Sélectionnez les dates précises (contrats multi-mois non supportés).">
<i class="fa fa-info-circle"></i>
</span>
</label>
<input type="text" id="datesInput" placeholder="Cliquez pour sélectionner des dates" readonly>
<p id="daysCount">Veuillez sélectionner les jours de travail.</p>
</div>
<!-- Section 4: Rémunération et Type -->
<div class="form-section">
<label for="montantInput">Montant total de la rémunération (€)
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
data-bs-content="Saisissez le point de départ puis choisissez le type (Brut / Net avant PAS / Coût employeur).">
<i class="fa fa-info-circle"></i>
</span>
</label>
<input type="number" id="montantInput" step="0.01" placeholder="Ex: 2000">
<button id="openCalculatorBtn" type="button" style="margin-top: 6px; padding: 3px 8px; font-size: 0.6875rem; background: #f1f5f9; color: #64748b; border: 1px solid #e2e8f0; border-radius: 4px; cursor: pointer; display: inline-flex; align-items: center; gap: 3px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); transition: all 0.2s; width: auto; font-weight: 400;">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink: 0;">
<rect width="16" height="20" x="4" y="2" rx="2"/>
<line x1="8" x2="16" y1="6" y2="6"/>
<line x1="16" x2="16" y1="14" y2="14"/>
<path d="M16 10h.01"/>
<path d="M12 10h.01"/>
<path d="M8 10h.01"/>
<path d="M12 14h.01"/>
<path d="M8 14h.01"/>
<path d="M12 18h.01"/>
<path d="M8 18h.01"/>
</svg>
Calculatrice
</button>
<div class="options">
<label><input type="radio" name="type" value="brut" checked> Salaire Brut</label>
<label><input type="radio" name="type" value="net"> Salaire Net avant PAS
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
data-bs-content="Le net à payer dépend du taux de PAS communiqué par l'administration fiscale.">
<i class="fa fa-info-circle"></i>
</span>
</label>
<label><input type="radio" name="type" value="cost"> Coût Total Employeur</label>
</div>
</div>
<button id="calcBtn">Calculer</button>
</div>
<!-- Alerts -->
<div id="alerts"></div>
<!-- Résultats -->
<div class="result">
<h4>Résultat de la simulation</h4>
<div id="result"></div>
</div>
<!-- Détail des cotisations -->
<div class="detail-table" id="detailTable"></div>
<!-- Calculatrice draggable -->
<div id="calculator" class="calculator-modal" style="display: none;">
<div class="calculator-header">
<span class="calculator-title">🧮 Calculatrice</span>
<button id="closeCalculator" class="calculator-close">×</button>
</div>
<div class="calculator-body">
<div class="calculator-display" id="calcDisplay">0</div>
<div class="calculator-buttons">
<button class="calc-btn calc-clear" data-value="C">C</button>
<button class="calc-btn calc-operator" data-value="/">/</button>
<button class="calc-btn calc-operator" data-value="*">×</button>
<button class="calc-btn calc-operator" data-value="-"></button>
<button class="calc-btn calc-number" data-value="7">7</button>
<button class="calc-btn calc-number" data-value="8">8</button>
<button class="calc-btn calc-number" data-value="9">9</button>
<button class="calc-btn calc-operator calc-plus" data-value="+">+</button>
<button class="calc-btn calc-number" data-value="4">4</button>
<button class="calc-btn calc-number" data-value="5">5</button>
<button class="calc-btn calc-number" data-value="6">6</button>
<button class="calc-btn calc-number" data-value="1">1</button>
<button class="calc-btn calc-number" data-value="2">2</button>
<button class="calc-btn calc-number" data-value="3">3</button>
<button class="calc-btn calc-equals" data-value="=">=</button>
<button class="calc-btn calc-number calc-zero" data-value="0">0</button>
<button class="calc-btn calc-number" data-value=".">.</button>
<button class="calc-btn calc-use" id="useCalcResult">Utiliser</button>
</div>
</div>
</div>
<!-- Flatpickr & SweetAlert -->
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
<script>
/* === UI init === */
document.addEventListener('DOMContentLoaded', function () {
// Enable compact mode if requested via query string
try {
const params = new URLSearchParams(window.location.search);
if (params.get('compact') === '1') {
document.body.classList.add('compact-mode');
}
} catch (e) { /* ignore */ }
// En mode compact, regrouper chaque paire Label + (Select/Input) dans un wrapper .form-row
try {
if (document.body.classList.contains('compact-mode')) {
const sections = Array.from(document.querySelectorAll('.simulateur .form-section'));
sections.forEach(section => {
if (section.id === 'abattementSectionWrapper') return; // cette section gère son propre layout
const children = Array.from(section.children);
for (let i = 0; i < children.length; i++) {
const el = children[i];
if (!el || el.classList?.contains('form-row')) continue;
if (el.matches('.options, #calcBtn, #openCalculatorBtn, #result, .detail-table')) continue;
if (el.id === 'cachetsGroup') { el.classList.add('form-row'); continue; }
if (el.tagName && el.tagName.toLowerCase() === 'label') {
const next = children[i + 1];
const wrapper = document.createElement('div');
wrapper.className = 'form-row';
section.insertBefore(wrapper, el);
wrapper.appendChild(el);
if (next && /^(select|input|button)$/i.test(next.tagName)) {
wrapper.appendChild(next);
if (next.id === 'datesInput' || next.id === 'conventionSelect' || next.id === 'professionSelect') {
wrapper.style.gridColumn = '1 / -1';
}
i++;
}
}
}
});
}
} catch (_) { /* ignore compact wrapping errors */ }
// Popovers
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
popoverTriggerList.map(function (el) { return new bootstrap.Popover(el, { html: true }); });
// Harmonise les champs avec Bootstrap pour un rendu pro en compact
try {
if (document.body.classList.contains('compact-mode')) {
document.querySelectorAll('input[type="text"], input[type="number"]').forEach(el => {
el.classList.add('form-control', 'form-control-sm');
});
document.querySelectorAll('select').forEach(el => {
el.classList.add('form-select', 'form-select-sm');
});
document.querySelectorAll('label').forEach(el => {
el.classList.add('form-label');
});
}
} catch (_) { /* ignore */ }
// Flatpickr (multiple dates)
flatpickr("#datesInput", {
mode: "multiple",
dateFormat: "Y-m-d",
onChange: function(selectedDates) {
let message = "";
const count = selectedDates.length;
if(count === 0) {
message = "Aucun jour sélectionné";
} else {
const sorted = selectedDates.sort((a, b) => a - b);
const diffDays = Math.round((sorted[sorted.length - 1] - sorted[0]) / (1000 * 60 * 60 * 24)) + 1;
if(count <= 5) {
message = (diffDays <= 5)
? `Vous avez sélectionné ${count} jour(s) sur une plage de 5 jours ou moins.`
: `Vous avez sélectionné ${count} jour(s) sur une plage de plus de 5 jours.`;
} else {
message = `Vous avez sélectionné ${count} jour(s).`;
}
}
document.getElementById("daysCount").textContent = message;
}
});
// Abattement (affiche la liste des professions si Oui)
document.querySelectorAll('input[name="abattement"]').forEach(radio => {
radio.addEventListener('change', function() {
document.getElementById("professionBlock").style.display = (this.value === "oui") ? "block" : "none";
});
});
// Catégorie toggle (masque cachets & abattement/statut pour technicien)
document.getElementById('categorieSelect').addEventListener('change', toggleUIOnCategorieChange);
toggleUIOnCategorieChange();
});
// === Bridge: receive submissions from compact embed and perform calculation ===
window.addEventListener('message', (ev) => {
const msg = ev.data;
if (!msg || msg.type !== 'simulateur_compact_submit' || !msg.data) return;
try {
// Map payload to full form controls
const d = msg.data;
const setVal = (id, val) => { const el = document.getElementById(id); if (el) el.value = val ?? ''; };
setVal('conventionSelect', d.ccn);
setVal('categorieSelect', d.categorie);
setVal('statutSelect', d.statut);
const abYes = document.getElementById('abattementOui');
const abNo = document.getElementById('abattementNon');
if (d.abattement === 'oui' && abYes) abYes.checked = true; else if (abNo) abNo.checked = true;
const profSel = document.getElementById('professionSelect'); if (profSel) profSel.value = d.profession || '';
setVal('cachetsInput', d.cachets);
setVal('heuresInput', d.heures);
setVal('datesInput', d.dates);
setVal('montantInput', d.montant);
const type = d.type || 'brut';
const typeRadio = document.querySelector(`input[name="type"][value="${type}"]`); if (typeRadio) typeRadio.checked = true;
// Apply dependent UI toggles
toggleUIOnCategorieChange();
const abEvent = new Event('change');
document.querySelectorAll('input[name="abattement"]').forEach(r => r.dispatchEvent(abEvent));
// Trigger calculation
document.getElementById('calcBtn')?.click();
} catch (e) {
console.error('Compact bridge error', e);
}
});
/* ====== Constantes générales ====== */
const SMIC_HORAIRE = 11.88; // 2025
const PHSS = 29; // €
const PMSS = 3925; // €
const PLAFOND_JOUR_SS = 216; // URSSAF 2025
const PHSS_TO_DAILY = 4.366;
/* ===== Helpers format ===== */
function fmtEuro(n){ return (n||0).toLocaleString('fr-FR',{minimumFractionDigits:2, maximumFractionDigits:2}); }
function fmtPct(n){ return (n||0).toLocaleString('fr-FR',{minimumFractionDigits:2, maximumFractionDigits:2}) + " %"; }
/* ===== Catégorie ===== */
function isTechnicien(){ return document.getElementById('categorieSelect')?.value === 'technicien'; }
function toggleUIOnCategorieChange(){
const cachetsGroup = document.getElementById('cachetsGroup');
const abattementSection = document.getElementById('abattementSectionWrapper');
const statutLabel = document.querySelector('label[for="statutSelect"]');
const statutSelect = document.getElementById('statutSelect');
const info = document.getElementById('info');
const h1 = document.querySelector('h1');
if (isTechnicien()){
if (cachetsGroup) cachetsGroup.style.display = 'none';
if (abattementSection) abattementSection.style.display = 'none';
if (statutLabel) statutLabel.style.display = 'none';
if (statutSelect) statutSelect.style.display = 'none';
if (h1) h1.innerHTML = 'Simulateur paie Technicien <br>CDDU Intermittent du Spectacle (Annexe 8)';
if (info) info.textContent = 'Ce simulateur est paramétré pour les professions de lAnnexe 8 (techniciens).';
} else {
if (cachetsGroup) cachetsGroup.style.display = '';
if (abattementSection) abattementSection.style.display = ''; // contrôlé ensuite par Oui/Non
if (statutLabel) statutLabel.style.display = '';
if (statutSelect) statutSelect.style.display = '';
if (h1) h1.innerHTML = 'Simulateur paie Artiste <br>CDDU Intermittent du Spectacle (Annexe 10)';
if (info) info.innerHTML = 'Ce simulateur n\'est pas adapté aux contrats des professions de l\'Annexe 8 (techniciens du spectacle).<br> Reportez-vous au simulateur Technicien si besoin.';
}
}
/* ===== Dates helpers ===== */
function getSelectedDatesArray() {
const datesStr = document.getElementById("datesInput").value;
if (!datesStr) return [];
const arr = datesStr.split(",").map(s => new Date(s.trim())).filter(d => !isNaN(d));
return arr.sort((a, b) => a - b);
}
function getPrevoyanceBase(brutTotal) {
const dates = getSelectedDatesArray();
const nbDates = dates.length || 0;
if (nbDates === 0) return 0;
// Nombre de jours « retenus » pour la prévoyance :
// - Si des cachets sont saisis (artistes), on retient le nombre de jours avec cachet,
// donc min(cachets, nbDates).
// - Sinon (heures), on retient toutes les dates sélectionnées.
const cachetsRaw = parseInt(document.getElementById('cachetsInput')?.value, 10);
const cachets = (isNaN(cachetsRaw) || cachetsRaw < 0 || isTechnicien()) ? 0 : cachetsRaw;
const daysCounted = (cachets > 0) ? Math.min(cachets, nbDates) : nbDates;
const plafond = PLAFOND_JOUR_SS * daysCounted;
return Math.min(brutTotal, plafond);
}
/* ===== Abattement (Artiste uniquement) ===== */
function getAbattementFactor() {
if (isTechnicien()) return 1; // pas d'abattement pour techniciens
let factor = 1;
const ab = document.querySelector('input[name="abattement"]:checked');
if(ab && ab.value === "oui") {
const prof = document.getElementById("professionSelect").value;
if(prof === "drama") factor = 0.79; // -21%
else if(prof === "musique") factor = 0.82; // -18%
}
return factor;
}
/* ===== Plafond URSSAF (assiette TA) ===== */
function getPlafondUrssaf() {
const datesStr = document.getElementById("datesInput").value;
if (!datesStr) return 0;
const datesArray = datesStr
.split(",")
.map(s => new Date(s.trim()))
.filter(d => !isNaN(d))
.sort((a, b) => a - b);
if (datesArray.length === 0) return 0;
const first = datesArray[0];
const last = datesArray[datesArray.length - 1];
const year = first.getFullYear();
const month = first.getMonth();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const diffDays = Math.round((last - first) / (1000 * 60 * 60 * 24)) + 1;
const cat = document.getElementById('categorieSelect')?.value || 'artiste';
// < 5 jours : règles différentes artiste vs technicien
if (diffDays < 5) {
if (cat === 'technicien') {
// Technicien : PHSS × 4,366 × nbJours
return PHSS * PHSS_TO_DAILY * diffDays;
} else {
// Artiste : PHSS × nbJours × 12
return PHSS * diffDays * 12;
}
}
// ≥ 5 jours : PMSS prorata durée
return PMSS * (diffDays / daysInMonth);
}
// ===== IRC Tranche 1 Max (T1 cap, AGIRC-ARRCO) =====
function getIrcT1Max(){
// Mode intermittent ajusté : 12 × PMSS × (span / (daysInMonth × 0.95)), plafonné à 12 × PMSS
const dates = getSelectedDatesArray();
if (!dates.length) return 0;
const first = dates[0], last = dates[dates.length-1];
const diffDays = Math.round((last - first) / (1000*60*60*24)) + 1;
const y = first.getFullYear();
const m = first.getMonth();
const daysInMonth = new Date(y, m + 1, 0).getDate();
const adjusted = PMSS * 12 * (diffDays / (daysInMonth * 0.95));
return Math.min(adjusted, PMSS * 12);
}
// ===== Helper: split IRC base between T1 and T2 (avant abattement) =====
function getIrcBasesBeforeAbattement(brut){
const t1Max = getIrcT1Max();
const baseT1 = Math.min(brut, Math.max(0, t1Max));
const baseT2 = Math.max(0, brut - Math.max(0, t1Max));
return { baseT1, baseT2 };
}
/* ===== Compléments maladie / AF jour par jour ===== */
const HOURS_PER_CACHET_FOR_COMPLEMENT = 8; // pour les seuils
const HOURS_PER_DAY_FOR_COMPLEMENT_IF_CACHET = 6.7; // forfait si cachet ce jour
const COMPLEMENT_MALADIE_BASE_MODE = 'FULL_IF_ANY';
const COMPLEMENT_AF_BASE_MODE = 'FULL_IF_ANY';
function distributeCachetsAcrossDays(dates, cachets) {
const n = dates.length;
const dist = Array(n).fill(0);
for (let i = 0; i < cachets; i++) dist[i % n] += 1;
return dist;
}
function getAssietteChomageMaxPourMoisCourant() {
const datesStr = document.getElementById("datesInput").value;
if (!datesStr) return Infinity;
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;
const nbJours = dates.length;
const y = dates[0].getFullYear();
const m = dates[0].getMonth();
const daysInMonth = new Date(y, m + 1, 0).getDate();
// Ajout : nombre de cachets et nbJoursChomage (max des deux)
const cachetsRaw = parseInt(document.getElementById('cachetsInput')?.value, 10);
const cachets = (isNaN(cachetsRaw) || cachetsRaw < 0 || isTechnicien()) ? 0 : cachetsRaw;
const nbJoursChomage = Math.max(nbJours, cachets);
// Règle attendue :
// - < 5 jours : plafond chômage = 4 × plafond URSSAF de la période
// - ≥ 5 jours : plafond chômage = 4 × PMSS × (span en jours / daysInMonth)
if (diffDays < 5) {
const plafUrssaf = getPlafondUrssaf();
return 4 * plafUrssaf;
}
return 4 * PMSS * (diffDays / daysInMonth);
}
// === Debug panel helper for chômage assiette max
function getChomageDebugInfo() {
const dates = getSelectedDatesArray();
if (!dates.length) return null;
const first = dates[0], last = dates[dates.length-1];
const diffDays = Math.round((last - first) / (1000*60*60*24)) + 1;
const nbJours = dates.length;
const y = dates[0].getFullYear();
const m = dates[0].getMonth();
const daysInMonth = new Date(y, m + 1, 0).getDate();
const cachetsRaw = parseInt(document.getElementById('cachetsInput')?.value, 10);
const cachets = (isNaN(cachetsRaw) || cachetsRaw < 0 || isTechnicien()) ? 0 : cachetsRaw;
const nbJoursChomage = Math.max(nbJours, cachets);
const plafondShort = 4 * getPlafondUrssaf();
// Use diffDays for plafondLong as per new rule
const plafondLong = 4 * PMSS * (diffDays / daysInMonth);
const assiette = (diffDays < 5) ? plafondShort : plafondLong;
return { diffDays, nbJours, cachets, nbJoursChomage, daysInMonth, assiette, branch: (diffDays < 5) ? '<5 jours' : '>=5 jours', usedRatioDays: (diffDays < 5) ? null : diffDays };
}
// Taux complémentaires selon catégorie
function getComplementsRates(cat){
if (cat === "technicien") return { maladie: 6.0, af: 1.8 };
return { maladie: 4.2, af: 1.26 };
}
function computeDailyComplementsFromBrut(brutTotal, cachets, heures) {
const dates = getSelectedDatesArray();
if (!dates.length || (cachets <= 0 && (!heures || heures <= 0))) {
return { compMaladieAmount: 0, compAFAmount: 0, details: [], baseMaladieTotal: 0, baseAFTotal: 0 };
}
const HOURS_EQ_FOR_DISTRIBUTION = 7; // pour ventiler le brut
const cachetsPerDay = distributeCachetsAcrossDays(dates, cachets);
const hoursPerDay = (heures && heures > 0) ? (heures / dates.length) : 0;
const totalEqHours =
(cachetsPerDay.reduce((a, b) => a + b, 0) * HOURS_EQ_FOR_DISTRIBUTION) +
(hoursPerDay * dates.length);
if (totalEqHours === 0) {
return { compMaladieAmount: 0, compAFAmount: 0, details: [], baseMaladieTotal: 0, baseAFTotal: 0 };
}
const brutPerEqHour = brutTotal / totalEqHours;
let compMaladieAmount = 0;
let compAFAmount = 0;
const details = [];
const cat = document.getElementById('categorieSelect')?.value || 'artiste';
const { maladie: TAUX_COMPLEMENT_MALADIE, af: TAUX_COMPLEMENT_AF } = getComplementsRates(cat);
// 👉 facteur dabattement (1 pour techniciens)
const factor = getAbattementFactor();
const isCachetThreshold = (cJour, hJour) => {
if (cJour > 0) return Math.max(HOURS_PER_DAY_FOR_COMPLEMENT_IF_CACHET, hJour);
return hJour;
};
for (let i = 0; i < dates.length; i++) {
const cJour = cachetsPerDay[i];
const hJour = hoursPerDay;
// Remu « contractuelle » ventilée sur la journée
const remuJour = brutPerEqHour * ((cJour * HOURS_EQ_FOR_DISTRIBUTION) + hJour);
// 👉 Assiette « cotisable » pour les compléments :
// - Artiste : appliquer labattement (factor)
// - Technicien : factor = 1 → inchangé
const remuJourAssiette = remuJour * factor;
// Seuils (toujours calculés sur lassiette cotisable du jour)
const eqHoursForThresholds = isCachetThreshold(cJour, hJour);
const seuilMaladie = SMIC_HORAIRE * eqHoursForThresholds * 2.5;
const seuilAF = SMIC_HORAIRE * eqHoursForThresholds * 3.5;
let baseMaladie = 0, baseAF = 0;
// Déclenchement uniquement si lassiette abattue dépasse les seuils
if (remuJourAssiette > seuilMaladie) baseMaladie = remuJourAssiette;
if (remuJourAssiette > seuilAF) baseAF = remuJourAssiette;
const montantMaladie = baseMaladie * (TAUX_COMPLEMENT_MALADIE / 100);
const montantAF = baseAF * (TAUX_COMPLEMENT_AF / 100);
compMaladieAmount += montantMaladie;
compAFAmount += montantAF;
details.push({
date: dates[i].toISOString().slice(0,10),
cachets: cJour,
heures: hJour,
remuJour, // info : brut ventilé
remuJourAssiette, // assiette après abattement (clé)
seuilMaladie,
seuilAF,
baseMaladie,
baseAF,
montantMaladie,
montantAF
});
}
// Totaux dassiette (pour affichage)
let baseMaladieTotal = details.reduce((s,d)=> s + d.baseMaladie, 0);
let baseAFTotal = details.reduce((s,d)=> s + d.baseAF, 0);
const hasDayMaladie = baseMaladieTotal > 0;
const hasDayAF = baseAFTotal > 0;
// 👉 Si tu gardes le mode FULL_IF_ANY, lassiette du complément
// doit aussi être « abattue » pour les artistes.
if (COMPLEMENT_MALADIE_BASE_MODE === 'FULL_IF_ANY' && hasDayMaladie) {
baseMaladieTotal = brutTotal * factor;
compMaladieAmount = baseMaladieTotal * (TAUX_COMPLEMENT_MALADIE / 100);
}
if (COMPLEMENT_AF_BASE_MODE === 'FULL_IF_ANY' && hasDayAF) {
baseAFTotal = brutTotal * factor;
compAFAmount = baseAFTotal * (TAUX_COMPLEMENT_AF / 100);
}
return { compMaladieAmount, compAFAmount, details, baseMaladieTotal, baseAFTotal };
}
/* ===== Cotisations par code (profils Artiste / Technicien) ===== */
function baseLibelle(code, cat){
const isTech = (cat === 'technicien');
const L = {
"contrib_solidarite": isTech ? "Contribution Solidarité" : "Contribution Solidarité artiste",
"maladie": isTech ? "Assurance maladie" : "Assurance maladie artiste",
"vieillesse": isTech ? "Assurance vieillesse" : "Assurance vieillesse artiste",
"alloc_fam": isTech ? "Allocations familiales" : "Allocations familiales artiste",
"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 mois",
"chomage": "Assurance chômage intermittent",
"ags": "AGS intermittent",
"retraite_t1": isTech ? "Retraite non-cadre Int. T1" : "Retraite artiste Tranche 1",
"ceg_t1": isTech ? "CEG non-cadre Int. T1" : "CEG artiste intermittent Tranche 1",
"retraite_t2": isTech ? "Retraite non-cadre Int. T2" : "Retraite artiste Tranche 2",
"ceg_t2": isTech ? "CEG non-cadre Int. T2" : "CEG artiste intermittent Tranche 2",
"cet": isTech ? "CET non-cadre Int." : "CET artiste intermittent",
"prevoyance_ta": isTech ? "Prévoyance non-cadre Int. TA" : "Prévoyance non-cadre Artiste Int. TA",
"conges_spectacles": "Congés spectacles",
"medecine_t1": isTech ? "Médecine du travail int. T1" : "Médecine du travail int.",
"cfpc_conv": "Congé formation part conventionnelle",
"cfp_ta": "CFP-TA",
"paritarisme": "Financement du paritarisme",
"csg_deductible": "CSG déductible",
"csg_imposable": "CSG imposable",
"rds": "RDS imposable",
"casc_svp": "CASC-SVP",
"fnas": "FNAS artiste intermittent",
"fcap": "FCAP"
};
return L[code] || code;
}
function getProfileRates(cat){
if (cat === 'technicien'){
return {
sal: {
contrib_solidarite: 0, maladie: 0, vieillesse: 0.4, alloc_fam: 0, at: 0,
vieillesse_ta: 6.9, fnal_plaf: 0, maj_chomage: 0, chomage: 2.4, ags: 0,
retraite_t1: 3.93, ceg_t1: 0.86, prevoyance_ta: 0.12, conges_spectacles: 0,
medecine_t1: 0, cfpc_conv: 0, cfp_ta: 0, paritarisme: 0,
csg_deductible: 6.8, csg_imposable: 2.4, rds: 0.5
},
emp: {
contrib_solidarite: 0.3, maladie: 7.0, vieillesse: 2.02, alloc_fam: 3.45, at: 1.69,
vieillesse_ta: 8.55, fnal_plaf: 0.1, maj_chomage: 0.5, chomage: 9.0, ags: 0.25,
retraite_t1: 3.94, ceg_t1: 1.29, prevoyance_ta: 1.04, conges_spectacles: 15.5,
medecine_t1: 0.32, cfpc_conv: 0.1, cfp_ta: 2.0, paritarisme: 0.016,
csg_deductible: 0, csg_imposable: 0, rds: 0,
casc_svp: 0.4, fnas: 1.45, fcap: 0.25
}
};
}
// Artiste
return {
sal: {
contrib_solidarite: 0, maladie: 0, vieillesse: 0.28, alloc_fam: 0, at: 0,
vieillesse_ta: 4.83, fnal_plaf: 0, maj_chomage: 0, chomage: 2.4, ags: 0,
ceg_t1: 0.86, retraite_t1: 4.44, retraite_t2: 10.79, ceg_t2: 1.08,
cet: 0.14,
prevoyance_ta: 0.12, conges_spectacles: 0,
casc_svp: 0, fnas: 0, fcap: 0, medecine_t1: 0, cfpc_conv: 0, cfp_ta: 0,
paritarisme: 0, csg_deductible: 6.8, csg_imposable: 2.4, rds: 0.5
},
emp: {
contrib_solidarite: 0.3, maladie: 4.90, vieillesse: 1.41, alloc_fam: 2.42, at: 1.18,
vieillesse_ta: 5.99, fnal_plaf: 0.07, maj_chomage: 0.5, chomage: 9.0, ags: 0.25,
ceg_t1: 1.29, retraite_t1: 4.45, retraite_t2: 10.80, ceg_t2: 1.62,
cet: 0.21,
prevoyance_ta: 1.04, conges_spectacles: 15.5,
casc_svp: 0.4, fnas: 1.45, fcap: 0.25, medecine_t1: 0.32, cfpc_conv: 0.1,
cfp_ta: 2.0, paritarisme: 0.016, csg_deductible: 0, csg_imposable: 0, rds: 0
}
};
}
function getCotisations(){
const cat = document.getElementById('categorieSelect')?.value || 'artiste';
const cc = document.getElementById('conventionSelect')?.value;
const rates = getProfileRates(cat);
let codes = [
"contrib_solidarite","maladie","vieillesse","alloc_fam","at",
"vieillesse_ta","fnal_plaf","maj_chomage","chomage","ags",
"retraite_t1","ceg_t1","prevoyance_ta","conges_spectacles",
"medecine_t1","cfpc_conv","cfp_ta","paritarisme",
"csg_deductible","csg_imposable","rds"
];
// Pour ARTISTE, insérer T2 après ceg_t1, puis CET
if (cat === 'artiste') {
const idx = codes.indexOf("ceg_t1");
if (idx !== -1) {
codes.splice(idx + 1, 0, "retraite_t2","ceg_t2","cet");
}
}
if (cc === "3090") codes.push("casc_svp"); // CCNSVP
if (cc === "1285") { codes.push("fnas","fcap"); } // CCNEAC
const sal = [], emp = [];
for (const code of codes){
sal.push({ code, libelle: baseLibelle(code, cat), taux: (rates.sal[code] ?? 0) });
emp.push({ code, libelle: baseLibelle(code, cat), taux: (rates.emp[code] ?? 0) });
}
// Statut cadre pour ARTISTE
const statut = document.getElementById("statutSelect")?.value;
if (cat === 'artiste' && statut === 'cadre'){
const rSal = sal.find(x=>x.code==="retraite_t1");
const rEmp = emp.find(x=>x.code==="retraite_t1");
if (rSal){ rSal.libelle = "Retraite cadre Tranche 1"; rSal.taux = 3.93; }
if (rEmp){ rEmp.libelle = "Retraite cadre Tranche 1"; rEmp.taux = 3.94; }
const pSal = sal.find(x=>x.code==="prevoyance_ta");
const pEmp = emp.find(x=>x.code==="prevoyance_ta");
if (pSal){ pSal.libelle = "Prévoyance cadre Int TA"; pSal.taux = 0.12; }
if (pEmp){ pEmp.libelle = "Prévoyance cadre Int TA"; pEmp.taux = 1.62; }
sal.push({ code:"apec", libelle:"APEC", taux:0.024 });
emp.push({ code:"apec", libelle:"APEC", taux:0.036 });
}
return { sal, emp, cat };
}
/* ===== Somme des contributions employeur incluses dans l'assiette CSG/CRDS ===== */
function employerContribsForCSGBase(brut){
const { emp } = getCotisations();
const dates = getSelectedDatesArray();
if (!dates.length) return 0;
const prevBase = getPrevoyanceBase(brut);
let total = 0;
for (const e of emp){
let amount = 0;
switch (e.code){
// ✅ Inclure : prévoyance TA (sur base plafonnée jour)
case 'prevoyance_ta':
amount = prevBase * (e.taux / 100);
break;
// ✅ Inclure : Paritarisme et APEC (si cadre)
case 'paritarisme':
case 'apec':
amount = brut * (e.taux / 100);
break;
// ❌ Exclure : chômage / maj_chomage / ags, IRC (T1/T2/CEG), FNAL, maladie, vieillesse, AF, AT, CFPC conv, etc.
default:
break;
}
total += amount;
}
return total;
}
/* ===== Prévoyance employeur ===== */
function prevoyanceEmployerAmount(brut) {
const { emp } = getCotisations();
const cot = emp.find(e => e.code === "prevoyance_ta");
const basePrev = getPrevoyanceBase(brut);
return basePrev * (cot ? (cot.taux / 100) : 0);
}
/* ===== Net (part salariale) ===== */
function calculateNet(brut, plafondUrssaf) {
const { sal } = getCotisations();
let totalDeduction = 0;
const prevEmp = prevoyanceEmployerAmount(brut);
const factor = getAbattementFactor();
const nonAbattementCodes = [
"maj_chomage","chomage","ags","conges_spectacles",
"csg_deductible","csg_imposable","rds",
"prevoyance_ta",
"cet"
];
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
sal.forEach(c => {
let base;
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 * 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"))) {
base = getPrevoyanceBase(brut);
} else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) {
const incEmp = employerContribsForCSGBase(brut);
base = (brut + incEmp) * 0.9825;
} else if (c.code === "retraite_t1" || c.code === "ceg_t1") {
const { baseT1 } = getIrcBasesBeforeAbattement(brut);
base = baseT1; // abattement éventuel appliqué plus bas si autorisé
} else if (c.code === "retraite_t2" || c.code === "ceg_t2") {
const { baseT2 } = getIrcBasesBeforeAbattement(brut);
base = baseT2; // abattement éventuel appliqué plus bas si autorisé
} else if (c.code === "cet") {
base = brut;
appliedAbattement = true;
} else {
base = brut;
}
if (!appliedAbattement && !nonAbattementCodes.includes(c.code)) {
base = base * factor;
}
totalDeduction += base * (c.taux / 100);
});
return brut - totalDeduction;
}
/* ===== Charges employeur (avec abattement éventuel + Fillon ventilée) ===== */
function computeEmployerCharges(brut, cachets, heures, plafondUrssaf) {
const { sal, emp } = getCotisations();
let totalCharges = 0;
const prevEmp = prevoyanceEmployerAmount(brut);
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
const factor = getAbattementFactor();
const nonAbattementCodes = [
"maj_chomage","chomage","ags","conges_spectacles",
"csg_deductible","csg_imposable","rds",
"prevoyance_ta",
"cet"
];
// Fusionne codes salariaux/patronaux pour calculer la part patronale
const merged = sal.map(cs => {
const ce = emp.find(e => e.code === cs.code);
return {
code: cs.code,
libelle: cs.libelle,
tauxSalarial: cs.taux,
tauxPatronal: ce ? ce.taux : 0
};
});
// Somme des charges patronales "brutes"
merged.forEach(c => {
let base;
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 * factor, plafondUrssaf) * 1.115; // abattement AVANT plafonnement
appliedAbattement = true;
} else if (["fnas","fcap"].includes(c.code)) {
base = brut; // jamais dabattement
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)) {
const incEmp = employerContribsForCSGBase(brut);
base = (brut + incEmp) * 0.9825;
} else if (c.code === "retraite_t1" || c.code === "ceg_t1") {
const { baseT1 } = getIrcBasesBeforeAbattement(brut);
base = baseT1; // abattement éventuel appliqué plus bas si autorisé
} else if (c.code === "retraite_t2" || c.code === "ceg_t2") {
const { baseT2 } = getIrcBasesBeforeAbattement(brut);
base = baseT2; // abattement éventuel appliqué plus bas si autorisé
} else if (c.code === "cet") {
base = brut;
appliedAbattement = true;
} else {
base = brut;
}
if (!appliedAbattement && !nonAbattementCodes.includes(c.code)) {
base = base * factor;
}
totalCharges += base * (c.tauxPatronal / 100);
});
// Ajout des compléments (patronal pur)
const dates = getSelectedDatesArray();
if (dates.length) {
const { compMaladieAmount, compAFAmount } = computeDailyComplementsFromBrut(brut, cachets, heures);
totalCharges += compMaladieAmount + compAFAmount;
}
// —— Réduction Fillon (TECH uniquement, <50 ETP) ——
if (isTechnicien() && brut > 0 && heures > 0) {
// Objet de travail
let lf = { brut, heures, total: 0, urssaf: 0, chomage: 0, irc: 0 };
if (typeof computeFillonVentilated === "function") {
const res = computeFillonVentilated(brut, heures);
lf = { brut, heures, total: res.total, urssaf: res.urssaf, chomage: res.chomage, irc: res.irc };
} else if (typeof computeFillonAmount === "function") {
// Fallback global si la ventilation n'est pas dispo
const t = computeFillonAmount(brut, heures, /*isGe50=*/false);
lf = { brut, heures, total: t, urssaf: t, chomage: 0, irc: 0 };
}
if (lf.total > 0) {
totalCharges = Math.max(0, totalCharges - lf.total);
}
window.lastFillon = lf;
} else {
window.lastFillon = { brut, heures, total: 0, urssaf: 0, chomage: 0, irc: 0 };
}
return totalCharges;
}
/* ===== Réduction Fillon constantes & fonctions de base (< 50 ETP) ===== */
const FILLON_T_LT50 = 0.3193; // T pour entreprises < 50 ETP (FNAL 0,10%)
const FILLON_NEUTRALISATION = 16/15; // facteur de neutralisation pratique
/**
* Coefficient Fillon (C) techniciens uniquement
* @param {number} brut - rémunération brute
* @param {number} heures - nombre d'heures
* @param {boolean} isGe50 - true si ≥50 ETP (ici inutile, on mettra false)
* @returns {number} C >= 0
*/
function computeFillonCoefficient(brut, heures, isGe50){
if (!isTechnicien() || !(brut > 0) || !(heures > 0)) return 0;
const smicRef = heures * SMIC_HORAIRE;
// Tous tes clients sont <50 → on fige T = FILLON_T_LT50
const T = FILLON_T_LT50;
let C = (T / 0.6) * (1.6 * (smicRef / brut) - 1);
if (C < 0) C = 0;
// Borne théorique haute quand brut ≈ SMIC
const Cmax = (T / 0.6) * 0.6;
if (C > Cmax) C = Cmax;
return C;
}
/**
* Montant global de réduction Fillon (fallback global sans ventilation)
* @param {number} brut
* @param {number} heures
* @param {boolean} isGe50 - ignoré ici (on reste <50)
* @returns {number} montant ≥ 0
*/
function computeFillonAmount(brut, heures, isGe50){
const C = computeFillonCoefficient(brut, heures, false);
if (C <= 0) return 0;
return Math.max(0, brut * C * FILLON_NEUTRALISATION);
}
/**
* Ventilation URSSAF / CHOMAGE / IRC de la réduction Fillon (<50 ETP)
* - URSSAF = maladie + vieillesse (plaf. & déplaf.) + alloc_fam + FNAL + AT + CSA
* - CHOMAGE = assurance chômage UNIQUEMENT (exclut AGS et majorations CDD)
* - IRC = retraite complémentaire (TU1 + CEG sur T1) avec borne 6,01% du brut
*/
function round2(x){ return Math.round((x + Number.EPSILON) * 100) / 100; }
function computeFillonVentilated(brut, heures){
if (!isTechnicien() || brut <= 0 || heures <= 0) return { total: 0, urssaf: 0, chomage: 0, irc: 0 };
// Coefficient + total (clients <50 ETP)
const C = computeFillonCoefficient(brut, heures, false);
if (C <= 0) return { total: 0, urssaf: 0, chomage: 0, irc: 0 };
let reste = round2(brut * C * FILLON_NEUTRALISATION);
// Assiettes/bases “réelles”
const { emp } = getCotisations();
const plafondUrssaf = getPlafondUrssaf();
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
// — Montants “à payer” par bac (employeur), avant réduction —
// URSSAF : maladie + vieillesse (plaf. & déplaf.) + AF + FNAL + AT/MP ✅ inclut AT
const urssafCodes = ["maladie","vieillesse","vieillesse_ta","alloc_fam","fnal_plaf","at"];
let urssafDue = 0;
for (const e of emp){
if (!urssafCodes.includes(e.code)) continue;
let base = brut;
if (e.code === "vieillesse_ta") base = Math.min(brut, plafondUrssaf);
if (e.code === "fnal_plaf") base = Math.min(brut, plafondUrssaf) * 1.115;
urssafDue += base * (e.taux / 100);
}
urssafDue = round2(urssafDue);
// IRC (AGIRC-ARRCO) : retraite T1 + CEG T1, plafonné à 6,01 % du brut
const ircCodes = ["retraite_t1","ceg_t1"];
let ircDue = 0;
for (const e of emp){
if (!ircCodes.includes(e.code)) continue;
ircDue += brut * (e.taux / 100);
}
const ircCap = round2(brut * 0.0601);
ircDue = Math.min(round2(ircDue), ircCap); // applique la borne 6,01 %
// Chômage : uniquement la cotisation “chomage” (exclut AGS et maj. CDD)
let chomageDue = 0;
for (const e of emp){
if (e.code !== "chomage") continue;
const base = Math.min(brut, assietteChomageMax);
chomageDue += base * (e.taux / 100);
}
chomageDue = round2(chomageDue);
// — Imputation et arrondis, dans lordre sPAIEctacle —
// 1) Chômage dabord
const choRed = Math.min(reste, chomageDue);
reste = round2(reste - choRed);
// 2) Puis URSSAF (incluant AT)
const urssafRed = Math.min(reste, urssafDue);
reste = round2(reste - urssafRed);
// 3) Enfin IRC (borne déjà appliquée)
const ircRed = Math.min(reste, ircDue);
reste = round2(reste - ircRed);
return { total: round2(brut * C * FILLON_NEUTRALISATION), urssaf: urssafRed, chomage: choRed, irc: ircRed };
}
function calculateCostEmployer(brut, cachets, heures, plafondUrssaf) {
return brut + computeEmployerCharges(brut, cachets, heures, plafondUrssaf);
}
/* ===== Combos & inversions ===== */
function calculateSalaries(brut, cachets, heures, plafondUrssaf) {
const net = calculateNet(brut, plafondUrssaf);
const costEmployer = calculateCostEmployer(brut, cachets, heures, plafondUrssaf);
return { net, costEmployer };
}
function findBrutFromCostEmployer(targetCostEmployer, cachets, heures, plafondUrssaf) {
const tolerance = 0.0001;
let low = targetCostEmployer / 3;
let high = targetCostEmployer * 2;
while (calculateCostEmployer(high, cachets, heures, plafondUrssaf) < targetCostEmployer) {
high *= 2;
}
for (let i = 0; i < 80 && (high - low) > tolerance; i++) {
const mid = (low + high) / 2;
const ce = calculateCostEmployer(mid, cachets, heures, plafondUrssaf);
if (ce < targetCostEmployer) low = mid; else high = mid;
}
return (low + high) / 2;
}
function findBrutFromNet(targetNet, cachets, heures, plafondUrssaf) {
const tolerance = 0.001;
let low = targetNet;
let high = targetNet * 2;
while (calculateSalaries(high, cachets, heures, plafondUrssaf).net < targetNet) high *= 2;
while (high - low > tolerance) {
const mid = (low + high) / 2;
const { net } = calculateSalaries(mid, cachets, heures, plafondUrssaf);
if (net < targetNet) low = mid; else high = mid;
}
return (low + high) / 2;
}
/* ===== Table de détail ===== */
function generateDetailTable(brut, cachets, heures, plafondUrssaf) {
const prevEmp = prevoyanceEmployerAmount(brut);
const factor = getAbattementFactor();
const nonAbattementCodes = [
"maj_chomage","chomage","ags","conges_spectacles",
"csg_deductible","csg_imposable","rds",
"prevoyance_ta",
"cet"
];
const { sal, emp, cat } = getCotisations();
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
let merged = sal.map(cs => {
const ce = emp.find(e => e.code === cs.code);
return {
code: cs.code,
libelle: cs.libelle,
tauxSalarial: cs.taux,
tauxPatronal: ce ? ce.taux : 0
};
});
let html = `<h4>Détail des cotisations</h4>
<div class="cotisations-scroll-container">
<table class="cotisations-table">
<tr>
<th>Cotisation</th>
<th>Taux Salarial (%)</th>
<th>Taux Patronal (%)</th>
<th>Base (€)</th>
<th>Montant Salarial (€)</th>
<th>Montant Patronal (€)</th>
</tr>`;
merged.forEach(c => {
let base;
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 * factor, plafondUrssaf) * 1.115; // abattement AVANT plafonnement
appliedAbattement = true;
} else if (["fnas","fcap"].includes(c.code)) {
base = brut; // jamais dabattement
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)) {
const incEmp = employerContribsForCSGBase(brut);
base = (brut + incEmp) * 0.9825;
} else if (c.code === "retraite_t1" || c.code === "ceg_t1") {
const { baseT1 } = getIrcBasesBeforeAbattement(brut);
base = baseT1; // abattement éventuel appliqué plus bas si autorisé
} else if (c.code === "retraite_t2" || c.code === "ceg_t2") {
const { baseT2 } = getIrcBasesBeforeAbattement(brut);
base = baseT2; // abattement éventuel appliqué plus bas si autorisé
} else if (c.code === "cet") {
base = brut;
appliedAbattement = true;
} else {
base = brut;
}
if (!appliedAbattement && !nonAbattementCodes.includes(c.code)) {
base = base * factor;
}
const montantSalarial = base * (c.tauxSalarial / 100);
const montantPatronal = base * (c.tauxPatronal / 100);
html += `<tr>
<td>${c.libelle}</td>
<td>${fmtPct(c.tauxSalarial)}</td>
<td>${fmtPct(c.tauxPatronal)}</td>
<td>${fmtEuro(base)}</td>
<td>${fmtEuro(montantSalarial)}</td>
<td>${fmtEuro(montantPatronal)}</td>
</tr>`;
});
// 👉 ICI : ajout des lignes Fillon si technicien
if (isTechnicien() && window.lastFillon && window.lastFillon.total > 0) {
const { total } = window.lastFillon;
html += `
<tr>
<td>Réduction Fillon totale</td>
<td>—</td><td>—</td><td>${fmtEuro(brut)}</td>
<td>0,00</td><td>-${fmtEuro(total)}</td>
</tr>
`;
}
// Compléments en bas
const dates = getSelectedDatesArray();
if (dates.length) {
const { baseMaladieTotal, baseAFTotal, compMaladieAmount, compAFAmount } =
computeDailyComplementsFromBrut(brut, cachets, heures);
const modeLabelMaladie = (COMPLEMENT_MALADIE_BASE_MODE === 'FULL_IF_ANY') ? 'assiette contrat' : 'récap jours';
const modeLabelAF = (COMPLEMENT_AF_BASE_MODE === 'FULL_IF_ANY') ? 'assiette contrat' : 'récap jours';
const compLabelMaladie = (cat === 'technicien') ? "Complément maladie" : "Complément maladie artiste";
const compLabelAF = (cat === 'technicien') ? "Complément AF" : "Complément AF artiste";
const { maladie: TAUX_COMPLEMENT_MALADIE, af: TAUX_COMPLEMENT_AF } = getComplementsRates(cat);
html += `
<tr>
<td>${compLabelMaladie} (${modeLabelMaladie})</td>
<td>0,00 %</td>
<td>${fmtPct(TAUX_COMPLEMENT_MALADIE)}</td>
<td>${fmtEuro(baseMaladieTotal)}</td>
<td>0,00</td>
<td>${fmtEuro(compMaladieAmount)}</td>
</tr>
<tr>
<td>${compLabelAF} (${modeLabelAF})</td>
<td>0,00 %</td>
<td>${fmtPct(TAUX_COMPLEMENT_AF)}</td>
<td>${fmtEuro(baseAFTotal)}</td>
<td>0,00</td>
<td>${fmtEuro(compAFAmount)}</td>
</tr>`;
}
html += `</table></div>`;
return html;
}
/* ===== Structure pour export PDF (contributions + totaux) ===== */
function buildContributionsStructured(brut, cachets, heures, plafondUrssaf){
const prevEmp = prevoyanceEmployerAmount(brut);
const factor = getAbattementFactor();
const nonAbattementCodes = [
"maj_chomage","chomage","ags","conges_spectacles",
"csg_deductible","csg_imposable","rds",
"prevoyance_ta",
"cet"
];
const { sal, emp, cat } = getCotisations();
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
let merged = sal.map(cs => {
const ce = emp.find(e => e.code === cs.code);
return {
code: cs.code,
libelle: cs.libelle,
tauxSalarial: cs.taux,
tauxPatronal: ce ? ce.taux : 0
};
});
// Ajout des compléments comme lignes dédiées (patronal)
const dates = getSelectedDatesArray();
let compMaladie = 0, compAF = 0, baseMaladieTot = 0, baseAFTot = 0;
if (dates.length){
const comp = computeDailyComplementsFromBrut(brut, cachets, heures);
compMaladie = comp.compMaladieAmount;
compAF = comp.compAFAmount;
baseMaladieTot = comp.baseMaladieTotal;
baseAFTot = comp.baseAFTotal;
const compLabelMaladie = (cat === 'technicien') ? "Complément maladie" : "Complément maladie artiste";
const compLabelAF = (cat === 'technicien') ? "Complément AF" : "Complément AF artiste";
const { maladie: TAUX_COMPLEMENT_MALADIE, af: TAUX_COMPLEMENT_AF } = getComplementsRates(cat);
merged.push({ code:"comp_maladie", libelle: compLabelMaladie, tauxSalarial: 0, tauxPatronal: TAUX_COMPLEMENT_MALADIE, _isComplement: "maladie", _base: baseMaladieTot, _montantPat: compMaladie });
merged.push({ code:"comp_af", libelle: compLabelAF, tauxSalarial: 0, tauxPatronal: TAUX_COMPLEMENT_AF, _isComplement: "af", _base: baseAFTot, _montantPat: compAF });
}
// ——— Réduction Fillon totale (une seule ligne) ———
let fillonTotal = 0;
if (isTechnicien()) {
// priorise la valeur calculée pendant computeEmployerCharges
if (window.lastFillon && window.lastFillon.total > 0) {
fillonTotal = window.lastFillon.total;
} else if (typeof computeFillonVentilated === "function") {
const { total } = computeFillonVentilated(brut, heures);
fillonTotal = total || 0;
} else if (typeof computeFillonAmount === "function") {
fillonTotal = computeFillonAmount(brut, heures, /*isGe50=*/false) || 0;
}
}
// Si on a une réduction, on lajoute comme ligne dédiée (montant patronal négatif)
if (fillonTotal > 0) {
merged.push({
code: "fillon_total",
libelle: "Réduction Fillon totale",
tauxSalarial: 0,
tauxPatronal: 0,
_isFillon: true,
_base: brut, // pour affichage de la base dans le PDF
_montantPat: -fillonTotal // négatif côté employeur
});
}
let lignes = [];
let totalSal = 0;
let totalPat = 0;
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 dabattement
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)) {
const incEmp = employerContribsForCSGBase(brut);
base = (brut + incEmp) * 0.9825;
} else if (c.code === "retraite_t1" || c.code === "ceg_t1") {
const { baseT1 } = getIrcBasesBeforeAbattement(brut);
base = baseT1; // abattement éventuel appliqué plus bas si autorisé
} else if (c.code === "retraite_t2" || c.code === "ceg_t2") {
const { baseT2 } = getIrcBasesBeforeAbattement(brut);
base = baseT2; // abattement éventuel appliqué plus bas si autorisé
} else if (c.code === "cet") {
base = brut;
appliedAbattement = true;
} else {
base = brut;
}
if (!appliedAbattement && !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 {
contributions,
totals: {
salariale: fmtEuro(totalSal),
employeur: fmtEuro(totalPat)
}
};
}
/* ===== Bouton Calculer ===== */
document.getElementById('calcBtn').addEventListener('click', function() {
// Vérif multi-mois
const datesStr = document.getElementById("datesInput").value;
if (datesStr) {
const datesArray = datesStr.split(",").map(s => new Date(s.trim()));
const firstMonth = datesArray[0].getMonth();
const firstYear = datesArray[0].getFullYear();
const multiMonth = datesArray.some(d => d.getMonth() !== firstMonth || d.getFullYear() !== firstYear);
if (multiMonth) {
swal("Simulation impossible", "Le simulateur n'est pas encore prévu pour les contrats multi-mois. Contactez-nous pour une simulation personnalisée.", "warning");
return;
}
}
const montant = parseFloat(document.getElementById('montantInput').value);
const heures = parseFloat(document.getElementById('heuresInput').value) || 0;
const cachetsRaw = parseInt(document.getElementById('cachetsInput')?.value, 10);
const cachets = isTechnicien() ? 0 : (isNaN(cachetsRaw) ? 0 : Math.max(0, cachetsRaw));
if (isNaN(montant) || montant <= 0) {
alert("Veuillez entrer un montant valide.");
return;
}
const plafondUrssaf = getPlafondUrssaf();
const type = document.querySelector('input[name="type"]:checked').value;
let brut, net, costEmployer;
if (type === 'brut') {
brut = montant;
({ net, costEmployer } = calculateSalaries(brut, cachets, heures, plafondUrssaf));
} else if (type === 'net') {
const targetNet = montant;
brut = findBrutFromNet(targetNet, cachets, heures, plafondUrssaf);
({ net, costEmployer } = calculateSalaries(brut, cachets, heures, plafondUrssaf));
} else if (type === 'cost') {
const targetCE = montant;
brut = findBrutFromCostEmployer(targetCE, cachets, heures, plafondUrssaf);
({ net, costEmployer } = calculateSalaries(brut, cachets, heures, plafondUrssaf));
}
// 📊 Envoyer un événement PostHog enrichi via postMessage au parent
try {
// Récupération de toutes les données du formulaire
const ccnElement = document.getElementById('conventionSelect');
const ccnValue = ccnElement?.value || '';
const ccnText = ccnElement?.options[ccnElement.selectedIndex]?.text || '';
const categorieValue = isTechnicien() ? 'technicien' : 'artiste';
const statutValue = document.getElementById('statutSelect')?.value || 'non-cadre';
// Abattement
const abattementChecked = document.querySelector('input[name="abattement"]:checked')?.value || 'non';
const professionValue = document.getElementById('professionSelect')?.value || '';
// Dates
const datesInput = document.getElementById('datesInput')?.value || '';
const datesArray = datesInput ? datesInput.split(',').map(d => d.trim()).filter(Boolean) : [];
window.parent.postMessage({
type: 'simulateur_calculation',
data: {
// Paramètres du formulaire
ccn_id: ccnValue,
ccn_nom: ccnText,
categorie: categorieValue,
statut: statutValue,
abattement_active: abattementChecked === 'oui',
abattement_profession: professionValue,
// Cachets, heures, dates
cachets: cachets,
heures: heures,
dates_travail: datesArray,
nombre_jours: datesArray.length,
// Montant saisi et type
montant_saisi: montant,
type_calcul: type, // 'brut', 'net', ou 'cost'
// Résultats calculés
resultat_brut: parseFloat(brut.toFixed(2)),
resultat_net: parseFloat(net.toFixed(2)),
resultat_cost: parseFloat(costEmployer.toFixed(2)),
// Métadonnées
plafond_urssaf: plafondUrssaf,
timestamp: new Date().toISOString()
}
}, '*');
} catch (e) {
console.error('Erreur lors de l\'envoi de l\'événement PostHog:', e);
}
// —— Alerts & hard stop on extreme simulations ——
const alertsEl = document.getElementById('alerts');
if (alertsEl) alertsEl.innerHTML = '';
// Hard stop > 100 000 €
if (brut > 100000) {
try {
swal({
title: "Montant très élevé",
text: "Cette simulation dépasse le périmètre de l'outil. Merci de nous contacter pour confirmer ou valider la simulation.",
icon: "warning",
buttons: {
cancel: "Fermer",
support: {
text: "Contacter le support",
value: "support",
closeModal: true
}
}
}).then(value => {
if (value === "support") window.location.href = "/support";
});
} catch(e){
// Fallback simple si SweetAlert indisponible
if (confirm("Montant très élevé. Ouvrir la page support ?")) window.location.href = "/support";
}
// Stop calculation rendering
document.getElementById('result').innerHTML = '';
document.getElementById('detailTable').innerHTML = '';
return;
}
// Alerte non bloquante > 10 000 €
if (brut > 10000) {
if (alertsEl) {
alertsEl.innerHTML = `
<div class="alert alert-warning" role="alert" style="margin:16px 0">
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
<div>
<strong>Simulation extrême :</strong> le montant saisi est très élevé.\n
Nous vous invitons à nous contacter pour confirmer ou valider la simulation.
</div>
<a href="/support" class="btn btn-sm btn-primary">Contacter le support</a>
</div>
</div>`;
}
}
// // Ajout du debug chômage (masqué)
// const dbg = getChomageDebugInfo();
// const dbgHtml = dbg ? `
// <div style="margin-top:12px;padding:8px;border:1px dashed #cbd5e1;border-radius:8px;background:#f8fafc">
// <div style="font-weight:600;margin-bottom:4px">Debug chômage</div>
// <div>Branche: <strong>${dbg.branch}</strong> — Jours sélectionnés: <strong>${dbg.nbJours}</strong>, Cachets: <strong>${dbg.cachets}</strong>, Jours retenus chômage: <strong>${dbg.nbJoursChomage}</strong></div>
// <div>Jours dans le mois: <strong>${dbg.daysInMonth}</strong>, DiffDays (span): <strong>${dbg.diffDays}</strong></div>
// <div>Assiette chômage max calculée: <strong>${fmtEuro(dbg.assiette)}</strong> €</div>
// </div>` : '';
// Table de résultats sans debug chômage ni ligne Plafond URSSAF
const resultTable = `
<table class="result-table">
<tr>
<th>Brut</th>
<th>Net avant PAS</th>
<th>Coût<br>Employeur</th>
</tr>
<tr>
<td>${fmtEuro(brut)} €</td>
<td>${fmtEuro(net)} €</td>
<td>${fmtEuro(costEmployer)} €</td>
</tr>
</table>
`;
document.getElementById('result').innerHTML = resultTable;
document.getElementById('detailTable').innerHTML = generateDetailTable(brut, cachets, heures, plafondUrssaf);
});
function getAbattementInfo(){
// Par défaut : pas dabattement
let applique = false, taux = 0, code = null, label = "Aucun";
// Techniciens : jamais dabattement
if (isTechnicien()) return { applique:false, taux:0, code:null, label:"Aucun" };
const ab = document.querySelector('input[name="abattement"]:checked');
if (ab && ab.value === "oui") {
applique = true;
const prof = document.getElementById("professionSelect").value;
if (prof === "drama") { code = "drama"; taux = 21; label = "Artiste dramatique… (21%)"; }
else if (prof === "musique") { code = "musique"; taux = 18; label = "Musicien / choriste… (18%)"; }
}
return { applique, taux, code, label };
}
/* ===== Export PDFMonkey: window.odentasSimulation ===== */
(function hookExportPayload(){
const btn = document.getElementById('calcBtn');
if (!btn) return;
btn.addEventListener('click', function onAfterCalcBuildPayload(){
setTimeout(() => {
try{
const dates = getSelectedDatesArray();
const nbJours = dates.length;
const periode = nbJours
? { debut: dates[0].toISOString().slice(0,10), fin: dates[nbJours-1].toISOString().slice(0,10) }
: { debut: null, fin: null };
const montant = parseFloat(document.getElementById('montantInput').value);
const heures = parseFloat(document.getElementById('heuresInput').value) || 0;
const cachetsRaw = parseInt(document.getElementById('cachetsInput')?.value, 10);
const cachets = isTechnicien() ? 0 : (isNaN(cachetsRaw) ? 0 : Math.max(0, cachetsRaw));
const type = document.querySelector('input[name="type"]:checked')?.value || 'brut';
const abattement = getAbattementInfo();
const plafondUrssaf = getPlafondUrssaf();
let brut, net, costEmployer;
if (type === 'brut') {
brut = montant;
({ net, costEmployer } = calculateSalaries(brut, cachets, heures, plafondUrssaf));
} else if (type === 'net') {
const targetNet = montant;
brut = findBrutFromNet(targetNet, cachets, heures, plafondUrssaf);
({ net, costEmployer } = calculateSalaries(brut, cachets, heures, plafondUrssaf));
} else if (type === 'cost') {
const targetCE = montant;
brut = findBrutFromCostEmployer(targetCE, cachets, heures, plafondUrssaf);
({ net, costEmployer } = calculateSalaries(brut, cachets, heures, plafondUrssaf));
}
const { contributions, totals } = buildContributionsStructured(brut, cachets, heures, plafondUrssaf);
const convEl = document.getElementById("conventionSelect");
const convention = convEl?.selectedOptions?.[0]?.text || convEl?.value || "";
const statut = document.getElementById("statutSelect")?.value || "";
const cat = document.getElementById('categorieSelect')?.value || 'artiste';
window.odentasSimulation = {
profile: {
role: (cat === 'technicien') ? "Technicien" : ((statut === "cadre") ? "Artiste cadre" : "Artiste"),
convention: convention || "—",
// 👇 nouveau
abattement_applique: abattement.applique,
abattement_taux: abattement.taux, // 0, 18/19, ou 21
abattement_code: abattement.code, // "drama" | "musique" | null
abattement_label: abattement.label // texte lisible pour PDF
},
inputs: {
brut_total: fmtEuro(brut) + " €",
jours: String(nbJours || 0),
cachets: String(cachets || 0),
heures: String(heures || 0),
periode,
mode_saisie: type,
montant_saisi: fmtEuro(montant) + " €",
// 👇 si tu préfères côté "inputs"
abattement: {
applique: abattement.applique,
taux: abattement.taux,
code: abattement.code,
label: abattement.label
}
},
results: {
kpis: {
brut: fmtEuro(brut) + " €",
net_avant_pas: fmtEuro(net) + " €",
cout_total_employeur: fmtEuro(costEmployer) + " €",
total_part_salariale: totals.salariale,
total_part_employeur: totals.employeur
},
totaux_parts: {
total_part_salariale: totals.salariale,
total_part_employeur: totals.employeur
},
contributions
},
cta: { titre: "Externalisez votre paie", sous_titre: "Frais douverture offerts jusquau 30/09" },
branding: { logo_url: "https://odentas.fr/static/logo.png" },
legal: { disclaimer: "Cette simulation est fournie à titre indicatif sur la base des informations saisies." },
contact: { email: "bonjour@odentas.fr", site: "odentas.fr" }
};
console.debug("odentasSimulation ready:", window.odentasSimulation);
}catch(err){ console.error("build odentasSimulation failed:", err); }
}, 0);
});
})();
/* ===== Calculatrice ===== */
(function() {
let calcDisplay = '';
let calcOperator = '';
let calcFirstValue = '';
let calcWaitingForSecond = false;
const calculator = document.getElementById('calculator');
const display = document.getElementById('calcDisplay');
const openBtn = document.getElementById('openCalculatorBtn');
const closeBtn = document.getElementById('closeCalculator');
const useBtn = document.getElementById('useCalcResult');
const calcButtons = document.querySelectorAll('.calc-btn');
// Ouvrir/Fermer la calculatrice
openBtn.addEventListener('click', () => {
// Envoyer un message à la page parent pour ouvrir la calculatrice
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: 'openCalculator' }, '*');
} else {
// Fallback si pas dans un iframe
calculator.style.display = 'block';
resetCalculator();
}
});
closeBtn.addEventListener('click', () => {
calculator.style.display = 'none';
});
// Drag & Drop de la calculatrice
let isDragging = false;
let dragOffsetX = 0;
let dragOffsetY = 0;
const header = calculator.querySelector('.calculator-header');
header.addEventListener('mousedown', (e) => {
if (e.target === closeBtn) return;
e.preventDefault();
isDragging = true;
const rect = calculator.getBoundingClientRect();
dragOffsetX = e.clientX - rect.left;
dragOffsetY = e.clientY - rect.top;
// Empêcher l'iframe de capturer les événements
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
e.preventDefault();
const newLeft = e.clientX - dragOffsetX;
const newTop = e.clientY - dragOffsetY;
// Limiter aux bords de la fenêtre
const maxX = window.innerWidth - calculator.offsetWidth;
const maxY = window.innerHeight - calculator.offsetHeight;
calculator.style.left = Math.max(0, Math.min(newLeft, maxX)) + 'px';
calculator.style.top = Math.max(0, Math.min(newTop, maxY)) + 'px';
calculator.style.transform = 'none';
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
document.body.style.userSelect = '';
}
});
// Logique de la calculatrice
function resetCalculator() {
calcDisplay = '0';
calcOperator = '';
calcFirstValue = '';
calcWaitingForSecond = false;
updateDisplay();
}
function updateDisplay() {
display.textContent = calcDisplay || '0';
}
function handleNumber(num) {
if (calcWaitingForSecond) {
calcDisplay = num;
calcWaitingForSecond = false;
} else {
calcDisplay = calcDisplay === '0' ? num : calcDisplay + num;
}
updateDisplay();
}
function handleOperator(op) {
if (calcOperator && !calcWaitingForSecond) {
calculate();
}
calcFirstValue = calcDisplay;
calcOperator = op;
calcWaitingForSecond = true;
}
function calculate() {
const first = parseFloat(calcFirstValue);
const second = parseFloat(calcDisplay);
let result = 0;
switch(calcOperator) {
case '+': result = first + second; break;
case '-': result = first - second; break;
case '*': result = first * second; break;
case '/': result = second !== 0 ? first / second : 0; break;
}
calcDisplay = result.toString();
calcOperator = '';
calcFirstValue = '';
calcWaitingForSecond = false;
updateDisplay();
}
function handleClear() {
resetCalculator();
}
function handleDecimal() {
if (calcWaitingForSecond) {
calcDisplay = '0.';
calcWaitingForSecond = false;
} else if (!calcDisplay.includes('.')) {
calcDisplay += '.';
}
updateDisplay();
}
// Événements des boutons
calcButtons.forEach(btn => {
btn.addEventListener('click', () => {
const value = btn.dataset.value;
if (!isNaN(value)) {
handleNumber(value);
} else if (value === '.') {
handleDecimal();
} else if (value === 'C') {
handleClear();
} else if (value === '=') {
if (calcOperator) calculate();
} else if (['+', '-', '*', '/'].includes(value)) {
handleOperator(value);
}
});
});
// Bouton "Utiliser" - injecte le résultat dans le champ montant
useBtn.addEventListener('click', () => {
const result = parseFloat(calcDisplay);
if (!isNaN(result) && result > 0) {
document.getElementById('montantInput').value = result.toFixed(2);
calculator.style.display = 'none';
}
});
})();
</script>
<style>
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
line-height: 1.5;
}
.simulateur {
margin: 0 auto !important;
padding: 24px !important;
background: #ffffff !important;
border-radius: 12px !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.03) !important;
color: #1e293b !important;
}
.simulateur label {
display: block !important;
margin-top: 16px !important;
margin-bottom: 6px !important;
font-size: 0.875rem !important;
font-weight: 500 !important;
color: #334155 !important;
}
.simulateur label .fa-info-circle {
color: #6366f1 !important;
margin-left: 4px !important;
font-size: 0.875rem !important;
}
.simulateur input,
.simulateur select {
width: 100% !important;
padding: 10px 12px !important;
margin-top: 0 !important;
font-size: 0.9375rem !important;
border: 1.5px solid #e2e8f0 !important;
border-radius: 8px !important;
background: #ffffff !important;
color: #1e293b !important;
transition: all 0.15s ease !important;
}
.simulateur input:focus,
.simulateur select:focus {
outline: none !important;
border-color: #6366f1 !important;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1) !important;
}
.simulateur input::placeholder {
color: #94a3b8 !important;
}
.simulateur .options {
display: flex !important;
flex-wrap: wrap !important;
gap: 12px !important;
margin-top: 12px !important;
}
.simulateur .options label {
display: inline-flex !important;
align-items: center !important;
margin: 0 !important;
padding: 8px 16px !important;
background: #f8fafc !important;
border: 1.5px solid #e2e8f0 !important;
border-radius: 8px !important;
font-size: 0.875rem !important;
font-weight: 500 !important;
color: #475569 !important;
cursor: pointer !important;
transition: all 0.15s ease !important;
}
.simulateur .options label:hover {
background: #f1f5f9 !important;
border-color: #cbd5e1 !important;
}
.simulateur .options input[type="radio"] {
width: auto !important;
margin-right: 8px !important;
margin-top: 0 !important;
accent-color: #6366f1 !important;
}
.simulateur .options input[type="radio"]:checked + label,
.simulateur .options label:has(input[type="radio"]:checked) {
background: #eef2ff !important;
border-color: #6366f1 !important;
color: #4338ca !important;
}
.simulateur button {
width: 100% !important;
padding: 12px 24px !important;
margin-top: 24px !important;
font-size: 0.9375rem !important;
font-weight: 600 !important;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
color: #ffffff !important;
border: none !important;
border-radius: 10px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
box-shadow: 0 4px 6px -1px rgba(99, 102, 241, 0.2), 0 2px 4px -1px rgba(99, 102, 241, 0.1) !important;
}
.simulateur button:hover {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%) !important;
transform: translateY(-1px) !important;
box-shadow: 0 6px 8px -1px rgba(99, 102, 241, 0.3), 0 4px 6px -1px rgba(99, 102, 241, 0.15) !important;
}
.simulateur button:active {
transform: translateY(0) !important;
}
/* Bouton calculatrice - override */
.simulateur #openCalculatorBtn {
width: auto !important;
padding: 3px 8px !important;
margin-top: 6px !important;
font-size: 0.6875rem !important;
font-weight: 400 !important;
background: #f1f5f9 !important;
color: #64748b !important;
border: 1px solid #e2e8f0 !important;
border-radius: 4px !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
display: inline-flex !important;
align-items: center !important;
gap: 3px !important;
}
.simulateur #openCalculatorBtn:hover {
background: #e2e8f0 !important;
color: #475569 !important;
border-color: #cbd5e1 !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important;
transform: none !important;
}
.simulateur #openCalculatorBtn:active {
background: #cbd5e1 !important;
transform: none !important;
}
.result, .detail-table {
margin: 24px auto !important;
background: #ffffff !important;
padding: 20px !important;
border-radius: 12px !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.03) !important;
color: #1e293b !important;
}
.result h4 {
margin: 0 0 16px 0 !important;
font-size: 1.125rem !important;
font-weight: 600 !important;
color: #1e293b !important;
}
table {
width: 100% !important;
border-collapse: separate !important;
border-spacing: 0 !important;
font-size: 0.875rem !important;
color: #1e293b !important;
border-radius: 8px !important;
overflow: hidden !important;
}
table th, table td {
border: 1px solid #e2e8f0 !important;
padding: 10px 12px !important;
text-align: left !important;
color: #1e293b !important;
}
table th {
background: #f8fafc !important;
font-weight: 600 !important;
font-size: 0.8125rem !important;
text-transform: uppercase !important;
letter-spacing: 0.025em !important;
color: #475569 !important;
}
table tr:hover {
background: #f8fafc !important;
}
/* Customisation du calendrier Flatpickr */
.flatpickr-calendar {
font-size: 14px !important;
border-radius: 12px !important;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
border: 1px solid #e2e8f0 !important;
}
.flatpickr-month {
background: #f8fafc !important;
border-bottom: 1px solid #e2e8f0 !important;
color: #1e293b !important;
}
.flatpickr-day.selected,
.flatpickr-day.startRange,
.flatpickr-day.endRange {
background: #6366f1 !important;
border-color: #6366f1 !important;
color: #ffffff !important;
}
.flatpickr-day:hover {
background: #eef2ff !important;
border-color: #c7d2fe !important;
}
.result-table {
font-size: 1rem !important;
background: linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 100%) !important;
border: 2px solid #86efac !important;
border-radius: 8px !important;
}
.result-table th {
background: linear-gradient(135deg, #dcfce7 0%, #d1fae5 100%) !important;
font-weight: 700 !important;
color: #166534 !important;
line-height: 1.3 !important;
padding: 8px 12px !important;
}
.result-table td {
font-weight: 600 !important;
color: #15803d !important;
white-space: nowrap !important;
}
.cotisations-scroll-container {
overflow-x: auto !important;
margin: 0 -20px !important;
padding: 0 20px !important;
}
.cotisations-table {
font-size: 0.875rem !important;
background: #ffffff !important;
border: 1px solid #e2e8f0 !important;
min-width: 800px !important;
}
.cotisations-table th {
background: #f8fafc !important;
white-space: nowrap !important;
}
.cotisations-table td {
font-weight: normal !important;
white-space: nowrap !important;
}
#daysCount {
font-size: 0.8125rem !important;
color: #64748b !important;
margin-top: 8px !important;
font-style: italic !important;
}
#info {
font-size: 0.8125rem !important;
color: #64748b !important;
line-height: 1.5 !important;
font-style: italic !important;
text-align: center !important;
margin-top: 16px !important;
}
/* Sections avec espacement */
.form-section {
padding: 16px !important;
background: #f8fafc !important;
border-radius: 8px !important;
margin-top: 16px !important;
border: 1px solid #e2e8f0 !important;
}
.form-section:first-of-type {
margin-top: 0 !important;
}
.form-section > label:first-child {
margin-top: 0 !important;
}
.form-section > div:first-child > label:first-child {
margin-top: 0 !important;
}
#abattementSection,
#cachetsGroup,
#professionBlock {
padding: 0 !important;
background: transparent !important;
border-radius: 0 !important;
margin-top: 12px !important;
border: none !important;
}
#professionBlock {
margin-top: 12px !important;
}
.d-flex {
display: flex !important;
gap: 16px !important;
}
.d-flex > div {
display: flex !important;
align-items: center !important;
gap: 8px !important;
}
.d-flex input[type="radio"] {
width: auto !important;
accent-color: #6366f1 !important;
}
.d-flex label {
margin: 0 !important;
font-weight: 500 !important;
cursor: pointer !important;
}
/* Calculatrice */
.calculator-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 280px;
background: white;
border-radius: 16px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
z-index: 9999;
overflow: hidden;
pointer-events: auto;
}
.calculator-header {
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
color: white;
padding: 12px 16px;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
}
.calculator-title {
font-weight: 600;
font-size: 0.9375rem;
}
.calculator-close {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 28px;
height: 28px;
border-radius: 6px;
cursor: pointer;
font-size: 1.5rem;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.calculator-close:hover {
background: rgba(255, 255, 255, 0.3);
}
.calculator-body {
padding: 16px;
}
.calculator-display {
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
border: 2px solid #cbd5e1;
border-radius: 10px;
padding: 16px;
text-align: right;
font-size: 1.5rem;
font-weight: 600;
color: #1e293b;
margin-bottom: 16px;
min-height: 50px;
word-break: break-all;
}
.calculator-buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.calc-btn {
padding: 16px 8px;
border: none;
border-radius: 8px;
font-size: 1.125rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
background: #f8fafc;
color: #1e293b;
border: 1px solid #e2e8f0;
}
.calc-btn:hover {
background: #f1f5f9;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.calc-btn:active {
transform: translateY(0);
}
.calc-number {
background: white;
color: #1e293b;
}
.calc-operator {
background: linear-gradient(135deg, #f59e0b 0%, #f97316 100%);
color: white;
border: none;
}
.calc-operator:hover {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
}
.calc-clear {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
border: none;
}
.calc-clear:hover {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
}
.calc-equals {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
grid-row: span 2;
border: none;
}
.calc-equals:hover {
background: linear-gradient(135deg, #059669 0%, #047857 100%);
}
.calc-plus {
grid-row: span 2;
}
.calc-zero {
grid-column: span 2;
}
.calc-use {
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
color: white;
font-size: 0.9375rem;
border: none;
}
.calc-use:hover {
background: linear-gradient(135deg, #4f46e5 0%, #4338ca 100%);
}
</style></body></html>