2487 lines
89 KiB
HTML
2487 lines
89 KiB
HTML
<!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 l’Annexe 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 d’abattement (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 l’abattement (factor)
|
||
// - Technicien : factor = 1 → inchangé
|
||
const remuJourAssiette = remuJour * factor;
|
||
|
||
// Seuils (toujours calculés sur l’assiette 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 l’assiette 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 d’assiette (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, l’assiette 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 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)) {
|
||
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 l’ordre sPAIEctacle —
|
||
// 1) Chômage d’abord
|
||
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 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)) {
|
||
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 l’ajoute 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 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)) {
|
||
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()
|
||
}
|
||
}, '*');
|
||
|
||
console.log('📊 PostHog: Événement simulateur_calculation enrichi envoyé');
|
||
} 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 d’abattement
|
||
let applique = false, taux = 0, code = null, label = "Aucun";
|
||
|
||
// Techniciens : jamais d’abattement
|
||
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 d’ouverture offerts jusqu’au 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>
|