1255 lines
No EOL
49 KiB
HTML
1255 lines
No EOL
49 KiB
HTML
<!-- 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 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>
|
||
|
||
<!-- 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 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 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 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 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 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" // ✅ 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 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;
|
||
|
||
// ✅ 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 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);
|
||
});
|
||
})();
|
||
</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> |