espace-paie-odentas/simulateur.html

1255 lines
No EOL
49 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!-- 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" />
<h1>Simulateur paie Artiste <br>CDDU Intermittent du Spectacle (Annexe 10)</h1>
<p id="info">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.</p>
<div class="simulateur">
<!-- Convention collective -->
<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 danimation</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>
<!-- Catégorie (Annexe 10 / Annexe 8) -->
<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>
<!-- Statut (uniquement Artiste) -->
<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>
<!-- Abattement (uniquement Artiste) -->
<div id="abattementSection" style="display: none;">
<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>
<!-- Cachets (masqué en Technicien) -->
<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>
<!-- Heures -->
<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">
<!-- Dates -->
<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>
<!-- Montant + type saisie -->
<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">
<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>
<button id="calcBtn">Calculer</button>
</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>
<!-- 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 () {
// Popovers
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
popoverTriggerList.map(function (el) { return new bootstrap.Popover(el, { html: true }); });
// 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();
});
/* ====== 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('abattementSection');
const statutLabel = document.querySelector('label[for="statutSelect"]');
const statutSelect = document.getElementById('statutSelect');
const info = document.getElementById('info');
const h1 = document.querySelector('h1');
if (isTechnicien()){
if (cachetsGroup) cachetsGroup.style.display = 'none';
if (abattementSection) abattementSection.style.display = 'none';
if (statutLabel) statutLabel.style.display = 'none';
if (statutSelect) statutSelect.style.display = 'none';
if (h1) h1.innerHTML = 'Simulateur paie Technicien <br>CDDU Intermittent du Spectacle (Annexe 8)';
if (info) info.textContent = 'Ce simulateur est paramétré pour les professions de lAnnexe 8 (techniciens).';
} else {
if (cachetsGroup) cachetsGroup.style.display = '';
if (abattementSection) abattementSection.style.display = ''; // contrôlé ensuite par Oui/Non
if (statutLabel) statutLabel.style.display = '';
if (statutSelect) statutSelect.style.display = '';
if (h1) h1.innerHTML = 'Simulateur paie Artiste <br>CDDU Intermittent du Spectacle (Annexe 10)';
if (info) info.innerHTML = 'Ce simulateur n\'est pas adapté aux contrats des professions de l\'Annexe 8 (techniciens du spectacle).<br> Reportez-vous au simulateur Technicien si besoin.';
}
}
/* ===== Dates helpers ===== */
function getSelectedDatesArray() {
const datesStr = document.getElementById("datesInput").value;
if (!datesStr) return [];
const arr = datesStr.split(",").map(s => new Date(s.trim())).filter(d => !isNaN(d));
return arr.sort((a, b) => a - b);
}
function getPrevoyanceBase(brutTotal) {
const nbJours = getSelectedDatesArray().length || 0;
return Math.min(brutTotal, PLAFOND_JOUR_SS * nbJours);
}
/* ===== 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);
}
/* ===== 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())).sort((a,b)=>a-b);
const first = dates[0], last = dates[dates.length-1];
const y = first.getFullYear(), m = first.getMonth();
const daysInMonth = new Date(y, m + 1, 0).getDate();
const diffDays = Math.round((last - first) / (1000*60*60*24)) + 1;
return 4 * (PMSS * diffDays / daysInMonth);
}
// Taux complémentaires selon catégorie
function getComplementsRates(cat){
if (cat === "technicien") return { maladie: 6.0, af: 1.8 };
return { maladie: 4.2, af: 1.26 };
}
function computeDailyComplementsFromBrut(brutTotal, cachets, heures) {
const dates = getSelectedDatesArray();
if (!dates.length || (cachets <= 0 && (!heures || heures <= 0))) {
return { compMaladieAmount: 0, compAFAmount: 0, details: [], baseMaladieTotal: 0, baseAFTotal: 0 };
}
const HOURS_EQ_FOR_DISTRIBUTION = 7; // pour ventiler le brut
const cachetsPerDay = distributeCachetsAcrossDays(dates, cachets);
const hoursPerDay = (heures && heures > 0) ? (heures / dates.length) : 0;
const totalEqHours =
(cachetsPerDay.reduce((a, b) => a + b, 0) * HOURS_EQ_FOR_DISTRIBUTION) +
(hoursPerDay * dates.length);
if (totalEqHours === 0) {
return { compMaladieAmount: 0, compAFAmount: 0, details: [], baseMaladieTotal: 0, baseAFTotal: 0 };
}
const brutPerEqHour = brutTotal / totalEqHours;
let compMaladieAmount = 0;
let compAFAmount = 0;
const details = [];
const cat = document.getElementById('categorieSelect')?.value || 'artiste';
const { maladie: TAUX_COMPLEMENT_MALADIE, af: TAUX_COMPLEMENT_AF } = getComplementsRates(cat);
// 👉 facteur dabattement (1 pour techniciens)
const factor = getAbattementFactor();
const isCachetThreshold = (cJour, hJour) => {
if (cJour > 0) return Math.max(HOURS_PER_DAY_FOR_COMPLEMENT_IF_CACHET, hJour);
return hJour;
};
for (let i = 0; i < dates.length; i++) {
const cJour = cachetsPerDay[i];
const hJour = hoursPerDay;
// Remu « contractuelle » ventilée sur la journée
const remuJour = brutPerEqHour * ((cJour * HOURS_EQ_FOR_DISTRIBUTION) + hJour);
// 👉 Assiette « cotisable » pour les compléments :
// - Artiste : appliquer labattement (factor)
// - Technicien : factor = 1 → inchangé
const remuJourAssiette = remuJour * factor;
// Seuils (toujours calculés sur lassiette cotisable du jour)
const eqHoursForThresholds = isCachetThreshold(cJour, hJour);
const seuilMaladie = SMIC_HORAIRE * eqHoursForThresholds * 2.5;
const seuilAF = SMIC_HORAIRE * eqHoursForThresholds * 3.5;
let baseMaladie = 0, baseAF = 0;
// Déclenchement uniquement si lassiette abattue dépasse les seuils
if (remuJourAssiette > seuilMaladie) baseMaladie = remuJourAssiette;
if (remuJourAssiette > seuilAF) baseAF = remuJourAssiette;
const montantMaladie = baseMaladie * (TAUX_COMPLEMENT_MALADIE / 100);
const montantAF = baseAF * (TAUX_COMPLEMENT_AF / 100);
compMaladieAmount += montantMaladie;
compAFAmount += montantAF;
details.push({
date: dates[i].toISOString().slice(0,10),
cachets: cJour,
heures: hJour,
remuJour, // info : brut ventilé
remuJourAssiette, // assiette après abattement (clé)
seuilMaladie,
seuilAF,
baseMaladie,
baseAF,
montantMaladie,
montantAF
});
}
// Totaux dassiette (pour affichage)
let baseMaladieTotal = details.reduce((s,d)=> s + d.baseMaladie, 0);
let baseAFTotal = details.reduce((s,d)=> s + d.baseAF, 0);
const hasDayMaladie = baseMaladieTotal > 0;
const hasDayAF = baseAFTotal > 0;
// 👉 Si tu gardes le mode FULL_IF_ANY, lassiette du complément
// doit aussi être « abattue » pour les artistes.
if (COMPLEMENT_MALADIE_BASE_MODE === 'FULL_IF_ANY' && hasDayMaladie) {
baseMaladieTotal = brutTotal * factor;
compMaladieAmount = baseMaladieTotal * (TAUX_COMPLEMENT_MALADIE / 100);
}
if (COMPLEMENT_AF_BASE_MODE === 'FULL_IF_ANY' && hasDayAF) {
baseAFTotal = brutTotal * factor;
compAFAmount = baseAFTotal * (TAUX_COMPLEMENT_AF / 100);
}
return { compMaladieAmount, compAFAmount, details, baseMaladieTotal, baseAFTotal };
}
/* ===== Cotisations par code (profils Artiste / Technicien) ===== */
function baseLibelle(code, cat){
const isTech = (cat === 'technicien');
const L = {
"contrib_solidarite": isTech ? "Contribution Solidarité" : "Contribution Solidarité artiste",
"maladie": isTech ? "Assurance maladie" : "Assurance maladie artiste",
"vieillesse": isTech ? "Assurance vieillesse" : "Assurance vieillesse artiste",
"alloc_fam": isTech ? "Allocations familiales" : "Allocations familiales artiste",
"at": "Accident du travail",
"vieillesse_ta": isTech ? "Assurance vieillesse tranche A" : "Assurance vieillesse tranche A artiste",
"fnal_plaf": isTech ? "FNAL plafonné" : "FNAL artiste plafonné",
"maj_chomage": isTech ? "Maj. chômage int. < 3 mois" : "Majoration chômage int. moins 3 jours",
"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",
"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 = base existante
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, 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, 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);
const 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"
];
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 };
}
/* ===== 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" // ✅ ne pas abattre la prévoyance
];
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
sal.forEach(c => {
let base;
if (c.code === "vieillesse_ta") {
base = Math.min(brut, plafondUrssaf);
} else if (c.code === "fnal_plaf") {
base = Math.min(brut, plafondUrssaf) * 1.115;
} else if (["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)) {
base = (brut + prevEmp) * 0.9825;
} else {
base = brut;
}
if (!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" // ✅ ne pas abattre la prévoyance
];
// 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;
if (["chomage","maj_chomage","ags"].includes(c.code)) {
base = Math.min(brut, assietteChomageMax);
} else if (c.code === "vieillesse_ta") {
base = Math.min(brut, plafondUrssaf);
} else if (c.code === "fnal_plaf") {
base = Math.min(brut, plafondUrssaf) * 1.115;
} else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
base = getPrevoyanceBase(brut);
} else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) {
base = (brut + prevEmp) * 0.9825;
} else {
base = brut;
}
if (!nonAbattementCodes.includes(c.code)) base = base * factor;
totalCharges += base * (c.tauxPatronal / 100);
});
// Ajout des compléments (patronal pur)
const dates = getSelectedDatesArray();
if (dates.length) {
const { compMaladieAmount, compAFAmount } = computeDailyComplementsFromBrut(brut, cachets, heures);
totalCharges += compMaladieAmount + compAFAmount;
}
// —— Réduction Fillon (TECH uniquement, <50 ETP) ——
if (isTechnicien() && brut > 0 && heures > 0) {
// Objet de travail
let lf = { brut, heures, total: 0, urssaf: 0, chomage: 0, irc: 0 };
if (typeof computeFillonVentilated === "function") {
const res = computeFillonVentilated(brut, heures);
lf = { brut, heures, total: res.total, urssaf: res.urssaf, chomage: res.chomage, irc: res.irc };
} else if (typeof computeFillonAmount === "function") {
// Fallback global si la ventilation n'est pas dispo
const t = computeFillonAmount(brut, heures, /*isGe50=*/false);
lf = { brut, heures, total: t, urssaf: t, chomage: 0, irc: 0 };
}
if (lf.total > 0) {
totalCharges = Math.max(0, totalCharges - lf.total);
}
window.lastFillon = lf;
} else {
window.lastFillon = { brut, heures, total: 0, urssaf: 0, chomage: 0, irc: 0 };
}
return totalCharges;
}
/* ===== Réduction Fillon constantes & fonctions de base (< 50 ETP) ===== */
const FILLON_T_LT50 = 0.3193; // T pour entreprises < 50 ETP (FNAL 0,10%)
const FILLON_NEUTRALISATION = 16/15; // facteur de neutralisation pratique
/**
* Coefficient Fillon (C) techniciens uniquement
* @param {number} brut - rémunération brute
* @param {number} heures - nombre d'heures
* @param {boolean} isGe50 - true si ≥50 ETP (ici inutile, on mettra false)
* @returns {number} C >= 0
*/
function computeFillonCoefficient(brut, heures, isGe50){
if (!isTechnicien() || !(brut > 0) || !(heures > 0)) return 0;
const smicRef = heures * SMIC_HORAIRE;
// Tous tes clients sont <50 → on fige T = FILLON_T_LT50
const T = FILLON_T_LT50;
let C = (T / 0.6) * (1.6 * (smicRef / brut) - 1);
if (C < 0) C = 0;
// Borne théorique haute quand brut ≈ SMIC
const Cmax = (T / 0.6) * 0.6;
if (C > Cmax) C = Cmax;
return C;
}
/**
* Montant global de réduction Fillon (fallback global sans ventilation)
* @param {number} brut
* @param {number} heures
* @param {boolean} isGe50 - ignoré ici (on reste <50)
* @returns {number} montant ≥ 0
*/
function computeFillonAmount(brut, heures, isGe50){
const C = computeFillonCoefficient(brut, heures, false);
if (C <= 0) return 0;
return Math.max(0, brut * C * FILLON_NEUTRALISATION);
}
/**
* Ventilation URSSAF / CHOMAGE / IRC de la réduction Fillon (<50 ETP)
* - URSSAF = maladie + vieillesse (plaf. & déplaf.) + alloc_fam + FNAL + AT + CSA
* - CHOMAGE = assurance chômage UNIQUEMENT (exclut AGS et majorations CDD)
* - IRC = retraite complémentaire (TU1 + CEG sur T1) avec borne 6,01% du brut
*/
function round2(x){ return Math.round((x + Number.EPSILON) * 100) / 100; }
function computeFillonVentilated(brut, heures){
if (!isTechnicien() || brut <= 0 || heures <= 0) return { total: 0, urssaf: 0, chomage: 0, irc: 0 };
// Coefficient + total (clients <50 ETP)
const C = computeFillonCoefficient(brut, heures, false);
if (C <= 0) return { total: 0, urssaf: 0, chomage: 0, irc: 0 };
let reste = round2(brut * C * FILLON_NEUTRALISATION);
// Assiettes/bases “réelles”
const { emp } = getCotisations();
const plafondUrssaf = getPlafondUrssaf();
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
// — Montants “à payer” par bac (employeur), avant réduction —
// URSSAF : maladie + vieillesse (plaf. & déplaf.) + AF + FNAL + AT/MP ✅ inclut AT
const urssafCodes = ["maladie","vieillesse","vieillesse_ta","alloc_fam","fnal_plaf","at"];
let urssafDue = 0;
for (const e of emp){
if (!urssafCodes.includes(e.code)) continue;
let base = brut;
if (e.code === "vieillesse_ta") base = Math.min(brut, plafondUrssaf);
if (e.code === "fnal_plaf") base = Math.min(brut, plafondUrssaf) * 1.115;
urssafDue += base * (e.taux / 100);
}
urssafDue = round2(urssafDue);
// IRC (AGIRC-ARRCO) : retraite T1 + CEG T1, plafonné à 6,01 % du brut
const ircCodes = ["retraite_t1","ceg_t1"];
let ircDue = 0;
for (const e of emp){
if (!ircCodes.includes(e.code)) continue;
ircDue += brut * (e.taux / 100);
}
const ircCap = round2(brut * 0.0601);
ircDue = Math.min(round2(ircDue), ircCap); // applique la borne 6,01 %
// Chômage : uniquement la cotisation “chomage” (exclut AGS et maj. CDD)
let chomageDue = 0;
for (const e of emp){
if (e.code !== "chomage") continue;
const base = Math.min(brut, assietteChomageMax);
chomageDue += base * (e.taux / 100);
}
chomageDue = round2(chomageDue);
// — Imputation et arrondis, dans lordre sPAIEctacle —
// 1) Chômage dabord
const choRed = Math.min(reste, chomageDue);
reste = round2(reste - choRed);
// 2) Puis URSSAF (incluant AT)
const urssafRed = Math.min(reste, urssafDue);
reste = round2(reste - urssafRed);
// 3) Enfin IRC (borne déjà appliquée)
const ircRed = Math.min(reste, ircDue);
reste = round2(reste - ircRed);
return { total: round2(brut * C * FILLON_NEUTRALISATION), urssaf: urssafRed, chomage: choRed, irc: ircRed };
}
function calculateCostEmployer(brut, cachets, heures, plafondUrssaf) {
return brut + computeEmployerCharges(brut, cachets, heures, plafondUrssaf);
}
/* ===== Combos & inversions ===== */
function calculateSalaries(brut, cachets, heures, plafondUrssaf) {
const net = calculateNet(brut, plafondUrssaf);
const costEmployer = calculateCostEmployer(brut, cachets, heures, plafondUrssaf);
return { net, costEmployer };
}
function findBrutFromCostEmployer(targetCostEmployer, cachets, heures, plafondUrssaf) {
const tolerance = 0.0001;
let low = targetCostEmployer / 3;
let high = targetCostEmployer * 2;
while (calculateCostEmployer(high, cachets, heures, plafondUrssaf) < targetCostEmployer) {
high *= 2;
}
for (let i = 0; i < 80 && (high - low) > tolerance; i++) {
const mid = (low + high) / 2;
const ce = calculateCostEmployer(mid, cachets, heures, plafondUrssaf);
if (ce < targetCostEmployer) low = mid; else high = mid;
}
return (low + high) / 2;
}
function findBrutFromNet(targetNet, cachets, heures, plafondUrssaf) {
const tolerance = 0.001;
let low = targetNet;
let high = targetNet * 2;
while (calculateSalaries(high, cachets, heures, plafondUrssaf).net < targetNet) high *= 2;
while (high - low > tolerance) {
const mid = (low + high) / 2;
const { net } = calculateSalaries(mid, cachets, heures, plafondUrssaf);
if (net < targetNet) low = mid; else high = mid;
}
return (low + high) / 2;
}
/* ===== Table de détail ===== */
function generateDetailTable(brut, cachets, heures, plafondUrssaf) {
const prevEmp = prevoyanceEmployerAmount(brut);
const factor = getAbattementFactor();
const nonAbattementCodes = [
"maj_chomage","chomage","ags","conges_spectacles",
"csg_deductible","csg_imposable","rds",
"prevoyance_ta" // ✅ ne pas abattre la prévoyance
];
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>
<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;
if (c.code === "vieillesse_ta") {
base = Math.min(brut, plafondUrssaf);
} else if (c.code === "fnal_plaf") {
base = Math.min(brut, plafondUrssaf) * 1.115;
} else if (["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)) {
base = (brut + prevEmp) * 0.9825;
} else {
base = brut;
}
if (!nonAbattementCodes.includes(c.code)) base = base * factor;
const montantSalarial = base * (c.tauxSalarial / 100);
const montantPatronal = base * (c.tauxPatronal / 100);
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>`;
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" // ✅ ne pas abattre la prévoyance
];
const { sal, emp, cat } = getCotisations();
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
let merged = sal.map(cs => {
const ce = emp.find(e => e.code === cs.code);
return {
code: cs.code,
libelle: cs.libelle,
tauxSalarial: cs.taux,
tauxPatronal: ce ? ce.taux : 0
};
});
// Ajout des compléments comme lignes dédiées (patronal)
const dates = getSelectedDatesArray();
let compMaladie = 0, compAF = 0, baseMaladieTot = 0, baseAFTot = 0;
if (dates.length){
const comp = computeDailyComplementsFromBrut(brut, cachets, heures);
compMaladie = comp.compMaladieAmount;
compAF = comp.compAFAmount;
baseMaladieTot = comp.baseMaladieTotal;
baseAFTot = comp.baseAFTotal;
const compLabelMaladie = (cat === 'technicien') ? "Complément maladie" : "Complément maladie artiste";
const compLabelAF = (cat === 'technicien') ? "Complément AF" : "Complément AF artiste";
const { maladie: TAUX_COMPLEMENT_MALADIE, af: TAUX_COMPLEMENT_AF } = getComplementsRates(cat);
merged.push({ code:"comp_maladie", libelle: compLabelMaladie, tauxSalarial: 0, tauxPatronal: TAUX_COMPLEMENT_MALADIE, _isComplement: "maladie", _base: baseMaladieTot, _montantPat: compMaladie });
merged.push({ code:"comp_af", libelle: compLabelAF, tauxSalarial: 0, tauxPatronal: TAUX_COMPLEMENT_AF, _isComplement: "af", _base: baseAFTot, _montantPat: compAF });
}
// ——— Réduction Fillon totale (une seule ligne) ———
let fillonTotal = 0;
if (isTechnicien()) {
// priorise la valeur calculée pendant computeEmployerCharges
if (window.lastFillon && window.lastFillon.total > 0) {
fillonTotal = window.lastFillon.total;
} else if (typeof computeFillonVentilated === "function") {
const { total } = computeFillonVentilated(brut, heures);
fillonTotal = total || 0;
} else if (typeof computeFillonAmount === "function") {
fillonTotal = computeFillonAmount(brut, heures, /*isGe50=*/false) || 0;
}
}
// Si on a une réduction, on lajoute comme ligne dédiée (montant patronal négatif)
if (fillonTotal > 0) {
merged.push({
code: "fillon_total",
libelle: "Réduction Fillon totale",
tauxSalarial: 0,
tauxPatronal: 0,
_isFillon: true,
_base: brut, // pour affichage de la base dans le PDF
_montantPat: -fillonTotal // négatif côté employeur
});
}
let lignes = [];
let totalSal = 0;
let totalPat = 0;
for (const c of merged){
let base;
// ✅ 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, plafondUrssaf);
} else if (c.code === "fnal_plaf") {
base = Math.min(brut, plafondUrssaf) * 1.115;
} else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
base = getPrevoyanceBase(brut);
} else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) {
base = (brut + prevoyanceEmployerAmount(brut)) * 0.9825;
} else {
base = brut;
}
if (!nonAbattementCodes.includes(c.code)) base = base * factor;
const montantSalarial = base * (c.tauxSalarial / 100);
const montantPatronal = base * (c.tauxPatronal / 100);
totalSal += montantSalarial;
totalPat += montantPatronal;
lignes.push({
libelle: c.libelle,
assiette: fmtEuro(base),
taux_salarial: fmtPct(c.tauxSalarial),
taux_patronal: fmtPct(c.tauxPatronal),
montant_salarial: fmtEuro(montantSalarial),
montant_patronal: fmtEuro(montantPatronal)
});
}
const contributions = [{ groupe: "Cotisations et contributions", lignes }];
return {
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));
}
const resultTable = `
<table class="result-table">
<tr>
<th>Brut</th>
<th>Net avant PAS</th>
<th>Coût Total Employeur</th>
</tr>
<tr>
<td>${fmtEuro(brut)} €</td>
<td>${fmtEuro(net)} €</td>
<td>${fmtEuro(costEmployer)} €</td>
</tr>
</table>
<p>Plafond URSSAF : ${fmtEuro(plafondUrssaf)} €</p>
`;
document.getElementById('result').innerHTML = resultTable;
document.getElementById('detailTable').innerHTML = generateDetailTable(brut, cachets, heures, plafondUrssaf);
});
function getAbattementInfo(){
// Par défaut : pas dabattement
let applique = false, taux = 0, code = null, label = "Aucun";
// Techniciens : jamais dabattement
if (isTechnicien()) return { applique:false, taux:0, code:null, label:"Aucun" };
const ab = document.querySelector('input[name="abattement"]:checked');
if (ab && ab.value === "oui") {
applique = true;
const prof = document.getElementById("professionSelect").value;
if (prof === "drama") { code = "drama"; taux = 21; label = "Artiste dramatique… (21%)"; }
else if (prof === "musique") { code = "musique"; taux = 18; label = "Musicien / choriste… (18%)"; }
}
return { applique, taux, code, label };
}
/* ===== Export PDFMonkey: window.odentasSimulation ===== */
(function hookExportPayload(){
const btn = document.getElementById('calcBtn');
if (!btn) return;
btn.addEventListener('click', function onAfterCalcBuildPayload(){
setTimeout(() => {
try{
const dates = getSelectedDatesArray();
const nbJours = dates.length;
const periode = nbJours
? { debut: dates[0].toISOString().slice(0,10), fin: dates[nbJours-1].toISOString().slice(0,10) }
: { debut: null, fin: null };
const montant = parseFloat(document.getElementById('montantInput').value);
const heures = parseFloat(document.getElementById('heuresInput').value) || 0;
const cachetsRaw = parseInt(document.getElementById('cachetsInput')?.value, 10);
const cachets = isTechnicien() ? 0 : (isNaN(cachetsRaw) ? 0 : Math.max(0, cachetsRaw));
const type = document.querySelector('input[name="type"]:checked')?.value || 'brut';
const abattement = getAbattementInfo();
const plafondUrssaf = getPlafondUrssaf();
let brut, net, costEmployer;
if (type === 'brut') {
brut = montant;
({ net, costEmployer } = calculateSalaries(brut, cachets, heures, plafondUrssaf));
} else if (type === 'net') {
const targetNet = montant;
brut = findBrutFromNet(targetNet, cachets, heures, plafondUrssaf);
({ net, costEmployer } = calculateSalaries(brut, cachets, heures, plafondUrssaf));
} else if (type === 'cost') {
const targetCE = montant;
brut = findBrutFromCostEmployer(targetCE, cachets, heures, plafondUrssaf);
({ net, costEmployer } = calculateSalaries(brut, cachets, heures, plafondUrssaf));
}
const { contributions, totals } = buildContributionsStructured(brut, cachets, heures, plafondUrssaf);
const convEl = document.getElementById("conventionSelect");
const convention = convEl?.selectedOptions?.[0]?.text || convEl?.value || "";
const statut = document.getElementById("statutSelect")?.value || "";
const cat = document.getElementById('categorieSelect')?.value || 'artiste';
window.odentasSimulation = {
profile: {
role: (cat === 'technicien') ? "Technicien" : ((statut === "cadre") ? "Artiste cadre" : "Artiste"),
convention: convention || "—",
// 👇 nouveau
abattement_applique: abattement.applique,
abattement_taux: abattement.taux, // 0, 18/19, ou 21
abattement_code: abattement.code, // "drama" | "musique" | null
abattement_label: abattement.label // texte lisible pour PDF
},
inputs: {
brut_total: fmtEuro(brut) + " €",
jours: String(nbJours || 0),
cachets: String(cachets || 0),
heures: String(heures || 0),
periode,
mode_saisie: type,
montant_saisi: fmtEuro(montant) + " €",
// 👇 si tu préfères côté "inputs"
abattement: {
applique: abattement.applique,
taux: abattement.taux,
code: abattement.code,
label: abattement.label
}
},
results: {
kpis: {
brut: fmtEuro(brut) + " €",
net_avant_pas: fmtEuro(net) + " €",
cout_total_employeur: fmtEuro(costEmployer) + " €",
total_part_salariale: totals.salariale,
total_part_employeur: totals.employeur
},
totaux_parts: {
total_part_salariale: totals.salariale,
total_part_employeur: totals.employeur
},
contributions
},
cta: { titre: "Externalisez votre paie", sous_titre: "Frais douverture offerts jusquau 30/09" },
branding: { logo_url: "https://odentas.fr/static/logo.png" },
legal: { disclaimer: "Cette simulation est fournie à titre indicatif sur la base des informations saisies." },
contact: { email: "bonjour@odentas.fr", site: "odentas.fr" }
};
console.debug("odentasSimulation ready:", window.odentasSimulation);
}catch(err){ console.error("build odentasSimulation failed:", err); }
}, 0);
});
})();
</script>
<style>
/* styles simples, à adapter si besoin */
.simulateur label{ display:block; margin-top:.75rem; font-weight:600; }
.simulateur input, .simulateur select{ width:100%; max-width:380px; padding:.5rem; }
.options{ margin:.75rem 0; display:flex; gap:1rem; flex-wrap:wrap; }
.result-table, .cotisations-table{ width:100%; border-collapse:collapse; margin-top:10px; }
.result-table th, .result-table td, .cotisations-table th, .cotisations-table td{
border:1px solid #e5e7eb; padding:.5rem; text-align:right;
}
.result-table th:first-child, .result-table td:first-child,
.cotisations-table th:first-child, .cotisations-table td:first-child{ text-align:left; }
</style>