1876 lines
70 KiB
HTML
1876 lines
70 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Simulateur Paie</title>
|
||
<!-- Bootstrap 5 Bundle avec Popper -->
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"></script>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" />
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||
</head>
|
||
<body style="margin:0;padding:20px;background:#f9fafb;font-family:system-ui,-apple-system,sans-serif;">
|
||
|
||
<div class="simulateur">
|
||
|
||
<!-- Section 1: CCN, Catégorie, Statut -->
|
||
<div class="form-section">
|
||
<label for="conventionSelect">
|
||
Convention Collective
|
||
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
|
||
data-bs-content="Choisissez votre Convention Collective pour intégrer dans le calcul vos éventuelles cotisations conventionnelles obligatoires.">
|
||
<i class="fa fa-info-circle"></i>
|
||
</span>
|
||
</label>
|
||
<select id="conventionSelect">
|
||
<option value="1285">1285 – Entreprises Artistiques & Culturelles (CCNEAC)</option>
|
||
<option value="3090">3090 – Spectacle Vivant Privé (CCNSVP)</option>
|
||
<option value="1518">1518 – ÉCLAT (ex-Animation)</option>
|
||
<option value="1922">1922 – Radiodiffusion</option>
|
||
<option value="2121">2121 – Édition phonographique</option>
|
||
<option value="2412">2412 – Production de films d'animation</option>
|
||
<option value="2642">2642 – Production audiovisuelle</option>
|
||
<option value="3097">3097 – Production cinématographique</option>
|
||
<option value="3241">3241 – Télédiffusion</option>
|
||
<option value="3252">3252 – Entreprises au service de la création et de l'événement</option>
|
||
</select>
|
||
|
||
<label for="categorieSelect">Catégorie</label>
|
||
<select id="categorieSelect">
|
||
<option value="artiste" selected>Artiste (Annexe 10)</option>
|
||
<option value="technicien">Technicien (Annexe 8)</option>
|
||
</select>
|
||
|
||
<label for="statutSelect">Statut
|
||
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
|
||
data-bs-content="Le statut 'Artiste cadre' concerne des professions comme 'Metteur en scène', 'Chorégraphe', 'Réalisateur', 'Chef des choeurs', 'Chef d'orchestre'.">
|
||
<i class="fa fa-info-circle"></i>
|
||
</span>
|
||
</label>
|
||
<select id="statutSelect">
|
||
<option value="non-cadre" selected>Artiste non-cadre</option>
|
||
<option value="cadre">Artiste cadre</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Section 2: Abattement (uniquement Artiste) -->
|
||
<div class="form-section" style="display: none;" id="abattementSectionWrapper">
|
||
<div id="abattementSection">
|
||
<label>Votre salarié a-t-il choisi de bénéficier de l'abattement pour frais professionnels ?
|
||
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
|
||
data-bs-content="Abattement pour frais professionnels des artistes (dispositif en extinction progressive jusqu'au 01/01/2032).">
|
||
<i class="fa fa-info-circle"></i>
|
||
</span>
|
||
</label>
|
||
|
||
<div class="d-flex mb-3" style="gap:10px;">
|
||
<div>
|
||
<input type="radio" name="abattement" id="abattementOui" value="oui">
|
||
<label for="abattementOui">Oui</label>
|
||
</div>
|
||
<div>
|
||
<input type="radio" name="abattement" id="abattementNon" value="non" checked>
|
||
<label for="abattementNon">Non</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="professionBlock" style="display:none;">
|
||
<label for="professionSelect">Profession du salarié</label>
|
||
<select id="professionSelect">
|
||
<option value="">-- Sélectionnez une profession --</option>
|
||
<option value="drama">Artiste dramatique / lyrique / cinématographique / chorégraphique / de cirque (21%)</option>
|
||
<option value="musique">Artiste musicien / choriste / chef d'orchestre (18%)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 3: Cachets, Heures, Dates -->
|
||
<div class="form-section">
|
||
<div id="cachetsGroup">
|
||
<label for="cachetsInput">Nombre de cachets
|
||
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
|
||
data-bs-content="Saisissez le nombre de cachets de représentation et/ou de répétitions.">
|
||
<i class="fa fa-info-circle"></i>
|
||
</span>
|
||
</label>
|
||
<input type="number" id="cachetsInput" step="1" placeholder="Ex : 10">
|
||
</div>
|
||
|
||
<label for="heuresInput">Nombre d'heures
|
||
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
|
||
data-bs-content="Saisissez le nombre d'heures de travail (répétitions rémunérées à l'heure).">
|
||
<i class="fa fa-info-circle"></i>
|
||
</span>
|
||
</label>
|
||
<input type="number" id="heuresInput" step="0.1" placeholder="Ex : 35">
|
||
|
||
<label for="datesInput">Dates de travail
|
||
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
|
||
data-bs-content="Sélectionnez les dates précises (contrats multi-mois non supportés).">
|
||
<i class="fa fa-info-circle"></i>
|
||
</span>
|
||
</label>
|
||
<input type="text" id="datesInput" placeholder="Cliquez pour sélectionner des dates" readonly>
|
||
<p id="daysCount">Veuillez sélectionner les jours de travail.</p>
|
||
</div>
|
||
|
||
<!-- Section 4: Rémunération et Type -->
|
||
<div class="form-section">
|
||
<label for="montantInput">Montant total de la rémunération (€)
|
||
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
|
||
data-bs-content="Saisissez le point de départ puis choisissez le type (Brut / Net avant PAS / Coût employeur).">
|
||
<i class="fa fa-info-circle"></i>
|
||
</span>
|
||
</label>
|
||
<input type="number" id="montantInput" step="0.01" placeholder="Ex: 2000">
|
||
|
||
<div class="options">
|
||
<label><input type="radio" name="type" value="brut" checked> Salaire Brut</label>
|
||
<label><input type="radio" name="type" value="net"> Salaire Net avant PAS
|
||
<span tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover"
|
||
data-bs-content="Le net à payer dépend du taux de PAS communiqué par l'administration fiscale.">
|
||
<i class="fa fa-info-circle"></i>
|
||
</span>
|
||
</label>
|
||
<label><input type="radio" name="type" value="cost"> Coût Total Employeur</label>
|
||
</div>
|
||
</div>
|
||
|
||
<button id="calcBtn">Calculer</button>
|
||
</div>
|
||
|
||
<!-- Alerts -->
|
||
<div id="alerts"></div>
|
||
|
||
<!-- Résultats -->
|
||
<div class="result">
|
||
<h4>Résultat de la simulation</h4>
|
||
<div id="result"></div>
|
||
</div>
|
||
|
||
<!-- Détail des cotisations -->
|
||
<div class="detail-table" id="detailTable"></div>
|
||
|
||
<!-- 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('abattementSectionWrapper');
|
||
const statutLabel = document.querySelector('label[for="statutSelect"]');
|
||
const statutSelect = document.getElementById('statutSelect');
|
||
const info = document.getElementById('info');
|
||
const h1 = document.querySelector('h1');
|
||
|
||
if (isTechnicien()){
|
||
if (cachetsGroup) cachetsGroup.style.display = 'none';
|
||
if (abattementSection) abattementSection.style.display = 'none';
|
||
if (statutLabel) statutLabel.style.display = 'none';
|
||
if (statutSelect) statutSelect.style.display = 'none';
|
||
if (h1) h1.innerHTML = 'Simulateur paie Technicien <br>CDDU Intermittent du Spectacle (Annexe 8)';
|
||
if (info) info.textContent = 'Ce simulateur est paramétré pour les professions de l’Annexe 8 (techniciens).';
|
||
} else {
|
||
if (cachetsGroup) cachetsGroup.style.display = '';
|
||
if (abattementSection) abattementSection.style.display = ''; // contrôlé ensuite par Oui/Non
|
||
if (statutLabel) statutLabel.style.display = '';
|
||
if (statutSelect) statutSelect.style.display = '';
|
||
if (h1) h1.innerHTML = 'Simulateur paie Artiste <br>CDDU Intermittent du Spectacle (Annexe 10)';
|
||
if (info) info.innerHTML = 'Ce simulateur n\'est pas adapté aux contrats des professions de l\'Annexe 8 (techniciens du spectacle).<br> Reportez-vous au simulateur Technicien si besoin.';
|
||
}
|
||
}
|
||
|
||
/* ===== Dates helpers ===== */
|
||
function getSelectedDatesArray() {
|
||
const datesStr = document.getElementById("datesInput").value;
|
||
if (!datesStr) return [];
|
||
const arr = datesStr.split(",").map(s => new Date(s.trim())).filter(d => !isNaN(d));
|
||
return arr.sort((a, b) => a - b);
|
||
}
|
||
|
||
function getPrevoyanceBase(brutTotal) {
|
||
const dates = getSelectedDatesArray();
|
||
const nbDates = dates.length || 0;
|
||
if (nbDates === 0) return 0;
|
||
|
||
// Nombre de jours « retenus » pour la prévoyance :
|
||
// - Si des cachets sont saisis (artistes), on retient le nombre de jours avec cachet,
|
||
// donc min(cachets, nbDates).
|
||
// - Sinon (heures), on retient toutes les dates sélectionnées.
|
||
const cachetsRaw = parseInt(document.getElementById('cachetsInput')?.value, 10);
|
||
const cachets = (isNaN(cachetsRaw) || cachetsRaw < 0 || isTechnicien()) ? 0 : cachetsRaw;
|
||
|
||
const daysCounted = (cachets > 0) ? Math.min(cachets, nbDates) : nbDates;
|
||
|
||
const plafond = PLAFOND_JOUR_SS * daysCounted;
|
||
return Math.min(brutTotal, plafond);
|
||
}
|
||
|
||
/* ===== Abattement (Artiste uniquement) ===== */
|
||
function getAbattementFactor() {
|
||
if (isTechnicien()) return 1; // pas d'abattement pour techniciens
|
||
let factor = 1;
|
||
const ab = document.querySelector('input[name="abattement"]:checked');
|
||
if(ab && ab.value === "oui") {
|
||
const prof = document.getElementById("professionSelect").value;
|
||
if(prof === "drama") factor = 0.79; // -21%
|
||
else if(prof === "musique") factor = 0.82; // -18%
|
||
}
|
||
return factor;
|
||
}
|
||
|
||
/* ===== Plafond URSSAF (assiette TA) ===== */
|
||
function getPlafondUrssaf() {
|
||
const datesStr = document.getElementById("datesInput").value;
|
||
if (!datesStr) return 0;
|
||
|
||
const datesArray = datesStr
|
||
.split(",")
|
||
.map(s => new Date(s.trim()))
|
||
.filter(d => !isNaN(d))
|
||
.sort((a, b) => a - b);
|
||
|
||
if (datesArray.length === 0) return 0;
|
||
|
||
const first = datesArray[0];
|
||
const last = datesArray[datesArray.length - 1];
|
||
|
||
const year = first.getFullYear();
|
||
const month = first.getMonth();
|
||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||
|
||
const diffDays = Math.round((last - first) / (1000 * 60 * 60 * 24)) + 1;
|
||
|
||
const cat = document.getElementById('categorieSelect')?.value || 'artiste';
|
||
|
||
// < 5 jours : règles différentes artiste vs technicien
|
||
if (diffDays < 5) {
|
||
if (cat === 'technicien') {
|
||
// Technicien : PHSS × 4,366 × nbJours
|
||
return PHSS * PHSS_TO_DAILY * diffDays;
|
||
} else {
|
||
// Artiste : PHSS × nbJours × 12
|
||
return PHSS * diffDays * 12;
|
||
}
|
||
}
|
||
|
||
// ≥ 5 jours : PMSS prorata durée
|
||
return PMSS * (diffDays / daysInMonth);
|
||
}
|
||
|
||
// ===== IRC Tranche 1 Max (T1 cap, AGIRC-ARRCO) =====
|
||
function getIrcT1Max(){
|
||
// Mode intermittent ajusté : 12 × PMSS × (span / (daysInMonth × 0.95)), plafonné à 12 × PMSS
|
||
const dates = getSelectedDatesArray();
|
||
if (!dates.length) return 0;
|
||
const first = dates[0], last = dates[dates.length-1];
|
||
const diffDays = Math.round((last - first) / (1000*60*60*24)) + 1;
|
||
const y = first.getFullYear();
|
||
const m = first.getMonth();
|
||
const daysInMonth = new Date(y, m + 1, 0).getDate();
|
||
const adjusted = PMSS * 12 * (diffDays / (daysInMonth * 0.95));
|
||
return Math.min(adjusted, PMSS * 12);
|
||
}
|
||
// ===== Helper: split IRC base between T1 and T2 (avant abattement) =====
|
||
function getIrcBasesBeforeAbattement(brut){
|
||
const t1Max = getIrcT1Max();
|
||
const baseT1 = Math.min(brut, Math.max(0, t1Max));
|
||
const baseT2 = Math.max(0, brut - Math.max(0, t1Max));
|
||
return { baseT1, baseT2 };
|
||
}
|
||
|
||
/* ===== Compléments maladie / AF jour par jour ===== */
|
||
const HOURS_PER_CACHET_FOR_COMPLEMENT = 8; // pour les seuils
|
||
const HOURS_PER_DAY_FOR_COMPLEMENT_IF_CACHET = 6.7; // forfait si cachet ce jour
|
||
const COMPLEMENT_MALADIE_BASE_MODE = 'FULL_IF_ANY';
|
||
const COMPLEMENT_AF_BASE_MODE = 'FULL_IF_ANY';
|
||
|
||
function distributeCachetsAcrossDays(dates, cachets) {
|
||
const n = dates.length;
|
||
const dist = Array(n).fill(0);
|
||
for (let i = 0; i < cachets; i++) dist[i % n] += 1;
|
||
return dist;
|
||
}
|
||
|
||
function getAssietteChomageMaxPourMoisCourant() {
|
||
const datesStr = document.getElementById("datesInput").value;
|
||
if (!datesStr) return Infinity;
|
||
|
||
const dates = datesStr
|
||
.split(",")
|
||
.map(s => new Date(s.trim()))
|
||
.filter(d => !isNaN(d))
|
||
.sort((a, b) => a - b);
|
||
|
||
if (!dates.length) return Infinity;
|
||
|
||
const first = dates[0], last = dates[dates.length - 1];
|
||
const diffDays = Math.round((last - first) / (1000 * 60 * 60 * 24)) + 1;
|
||
const nbJours = dates.length;
|
||
const y = dates[0].getFullYear();
|
||
const m = dates[0].getMonth();
|
||
const daysInMonth = new Date(y, m + 1, 0).getDate();
|
||
|
||
// Ajout : nombre de cachets et nbJoursChomage (max des deux)
|
||
const cachetsRaw = parseInt(document.getElementById('cachetsInput')?.value, 10);
|
||
const cachets = (isNaN(cachetsRaw) || cachetsRaw < 0 || isTechnicien()) ? 0 : cachetsRaw;
|
||
const nbJoursChomage = Math.max(nbJours, cachets);
|
||
|
||
// Règle attendue :
|
||
// - < 5 jours : plafond chômage = 4 × plafond URSSAF de la période
|
||
// - ≥ 5 jours : plafond chômage = 4 × PMSS × (span en jours / daysInMonth)
|
||
if (diffDays < 5) {
|
||
const plafUrssaf = getPlafondUrssaf();
|
||
return 4 * plafUrssaf;
|
||
}
|
||
|
||
return 4 * PMSS * (diffDays / daysInMonth);
|
||
}
|
||
|
||
// === Debug panel helper for chômage assiette max
|
||
function getChomageDebugInfo() {
|
||
const dates = getSelectedDatesArray();
|
||
if (!dates.length) return null;
|
||
const first = dates[0], last = dates[dates.length-1];
|
||
const diffDays = Math.round((last - first) / (1000*60*60*24)) + 1;
|
||
const nbJours = dates.length;
|
||
const y = dates[0].getFullYear();
|
||
const m = dates[0].getMonth();
|
||
const daysInMonth = new Date(y, m + 1, 0).getDate();
|
||
const cachetsRaw = parseInt(document.getElementById('cachetsInput')?.value, 10);
|
||
const cachets = (isNaN(cachetsRaw) || cachetsRaw < 0 || isTechnicien()) ? 0 : cachetsRaw;
|
||
const nbJoursChomage = Math.max(nbJours, cachets);
|
||
const plafondShort = 4 * getPlafondUrssaf();
|
||
// Use diffDays for plafondLong as per new rule
|
||
const plafondLong = 4 * PMSS * (diffDays / daysInMonth);
|
||
const assiette = (diffDays < 5) ? plafondShort : plafondLong;
|
||
return { diffDays, nbJours, cachets, nbJoursChomage, daysInMonth, assiette, branch: (diffDays < 5) ? '<5 jours' : '>=5 jours', usedRatioDays: (diffDays < 5) ? null : diffDays };
|
||
}
|
||
|
||
// Taux complémentaires selon catégorie
|
||
function getComplementsRates(cat){
|
||
if (cat === "technicien") return { maladie: 6.0, af: 1.8 };
|
||
return { maladie: 4.2, af: 1.26 };
|
||
}
|
||
|
||
function computeDailyComplementsFromBrut(brutTotal, cachets, heures) {
|
||
const dates = getSelectedDatesArray();
|
||
if (!dates.length || (cachets <= 0 && (!heures || heures <= 0))) {
|
||
return { compMaladieAmount: 0, compAFAmount: 0, details: [], baseMaladieTotal: 0, baseAFTotal: 0 };
|
||
}
|
||
|
||
const HOURS_EQ_FOR_DISTRIBUTION = 7; // pour ventiler le brut
|
||
const cachetsPerDay = distributeCachetsAcrossDays(dates, cachets);
|
||
const hoursPerDay = (heures && heures > 0) ? (heures / dates.length) : 0;
|
||
|
||
const totalEqHours =
|
||
(cachetsPerDay.reduce((a, b) => a + b, 0) * HOURS_EQ_FOR_DISTRIBUTION) +
|
||
(hoursPerDay * dates.length);
|
||
|
||
if (totalEqHours === 0) {
|
||
return { compMaladieAmount: 0, compAFAmount: 0, details: [], baseMaladieTotal: 0, baseAFTotal: 0 };
|
||
}
|
||
|
||
const brutPerEqHour = brutTotal / totalEqHours;
|
||
|
||
let compMaladieAmount = 0;
|
||
let compAFAmount = 0;
|
||
const details = [];
|
||
|
||
const cat = document.getElementById('categorieSelect')?.value || 'artiste';
|
||
const { maladie: TAUX_COMPLEMENT_MALADIE, af: TAUX_COMPLEMENT_AF } = getComplementsRates(cat);
|
||
|
||
// 👉 facteur d’abattement (1 pour techniciens)
|
||
const factor = getAbattementFactor();
|
||
|
||
const isCachetThreshold = (cJour, hJour) => {
|
||
if (cJour > 0) return Math.max(HOURS_PER_DAY_FOR_COMPLEMENT_IF_CACHET, hJour);
|
||
return hJour;
|
||
};
|
||
|
||
for (let i = 0; i < dates.length; i++) {
|
||
const cJour = cachetsPerDay[i];
|
||
const hJour = hoursPerDay;
|
||
|
||
// Remu « contractuelle » ventilée sur la journée
|
||
const remuJour = brutPerEqHour * ((cJour * HOURS_EQ_FOR_DISTRIBUTION) + hJour);
|
||
|
||
// 👉 Assiette « cotisable » pour les compléments :
|
||
// - Artiste : appliquer l’abattement (factor)
|
||
// - Technicien : factor = 1 → inchangé
|
||
const remuJourAssiette = remuJour * factor;
|
||
|
||
// Seuils (toujours calculés sur l’assiette cotisable du jour)
|
||
const eqHoursForThresholds = isCachetThreshold(cJour, hJour);
|
||
const seuilMaladie = SMIC_HORAIRE * eqHoursForThresholds * 2.5;
|
||
const seuilAF = SMIC_HORAIRE * eqHoursForThresholds * 3.5;
|
||
|
||
let baseMaladie = 0, baseAF = 0;
|
||
|
||
// Déclenchement uniquement si l’assiette abattue dépasse les seuils
|
||
if (remuJourAssiette > seuilMaladie) baseMaladie = remuJourAssiette;
|
||
if (remuJourAssiette > seuilAF) baseAF = remuJourAssiette;
|
||
|
||
const montantMaladie = baseMaladie * (TAUX_COMPLEMENT_MALADIE / 100);
|
||
const montantAF = baseAF * (TAUX_COMPLEMENT_AF / 100);
|
||
|
||
compMaladieAmount += montantMaladie;
|
||
compAFAmount += montantAF;
|
||
|
||
details.push({
|
||
date: dates[i].toISOString().slice(0,10),
|
||
cachets: cJour,
|
||
heures: hJour,
|
||
remuJour, // info : brut ventilé
|
||
remuJourAssiette, // assiette après abattement (clé)
|
||
seuilMaladie,
|
||
seuilAF,
|
||
baseMaladie,
|
||
baseAF,
|
||
montantMaladie,
|
||
montantAF
|
||
});
|
||
}
|
||
|
||
// Totaux d’assiette (pour affichage)
|
||
let baseMaladieTotal = details.reduce((s,d)=> s + d.baseMaladie, 0);
|
||
let baseAFTotal = details.reduce((s,d)=> s + d.baseAF, 0);
|
||
|
||
const hasDayMaladie = baseMaladieTotal > 0;
|
||
const hasDayAF = baseAFTotal > 0;
|
||
|
||
// 👉 Si tu gardes le mode FULL_IF_ANY, l’assiette du complément
|
||
// doit aussi être « abattue » pour les artistes.
|
||
if (COMPLEMENT_MALADIE_BASE_MODE === 'FULL_IF_ANY' && hasDayMaladie) {
|
||
baseMaladieTotal = brutTotal * factor;
|
||
compMaladieAmount = baseMaladieTotal * (TAUX_COMPLEMENT_MALADIE / 100);
|
||
}
|
||
if (COMPLEMENT_AF_BASE_MODE === 'FULL_IF_ANY' && hasDayAF) {
|
||
baseAFTotal = brutTotal * factor;
|
||
compAFAmount = baseAFTotal * (TAUX_COMPLEMENT_AF / 100);
|
||
}
|
||
|
||
return { compMaladieAmount, compAFAmount, details, baseMaladieTotal, baseAFTotal };
|
||
}
|
||
/* ===== Cotisations par code (profils Artiste / Technicien) ===== */
|
||
function baseLibelle(code, cat){
|
||
const isTech = (cat === 'technicien');
|
||
const L = {
|
||
"contrib_solidarite": isTech ? "Contribution Solidarité" : "Contribution Solidarité artiste",
|
||
"maladie": isTech ? "Assurance maladie" : "Assurance maladie artiste",
|
||
"vieillesse": isTech ? "Assurance vieillesse" : "Assurance vieillesse artiste",
|
||
"alloc_fam": isTech ? "Allocations familiales" : "Allocations familiales artiste",
|
||
"at": "Accident du travail",
|
||
"vieillesse_ta": isTech ? "Assurance vieillesse tranche A" : "Assurance vieillesse tranche A artiste",
|
||
"fnal_plaf": isTech ? "FNAL plafonné" : "FNAL artiste plafonné",
|
||
"maj_chomage": isTech ? "Maj. chômage int. < 3 mois" : "Majoration chômage int. moins 3 mois",
|
||
"chomage": "Assurance chômage intermittent",
|
||
"ags": "AGS intermittent",
|
||
"retraite_t1": isTech ? "Retraite non-cadre Int. T1" : "Retraite artiste Tranche 1",
|
||
"ceg_t1": isTech ? "CEG non-cadre Int. T1" : "CEG artiste intermittent Tranche 1",
|
||
"retraite_t2": isTech ? "Retraite non-cadre Int. T2" : "Retraite artiste Tranche 2",
|
||
"ceg_t2": isTech ? "CEG non-cadre Int. T2" : "CEG artiste intermittent Tranche 2",
|
||
"cet": isTech ? "CET non-cadre Int." : "CET artiste intermittent",
|
||
"prevoyance_ta": isTech ? "Prévoyance non-cadre Int. TA" : "Prévoyance non-cadre Artiste Int. TA",
|
||
"conges_spectacles": "Congés spectacles",
|
||
"medecine_t1": isTech ? "Médecine du travail int. T1" : "Médecine du travail int.",
|
||
"cfpc_conv": "Congé formation part conventionnelle",
|
||
"cfp_ta": "CFP-TA",
|
||
"paritarisme": "Financement du paritarisme",
|
||
"csg_deductible": "CSG déductible",
|
||
"csg_imposable": "CSG imposable",
|
||
"rds": "RDS imposable",
|
||
"casc_svp": "CASC-SVP",
|
||
"fnas": "FNAS artiste intermittent",
|
||
"fcap": "FCAP"
|
||
};
|
||
return L[code] || code;
|
||
}
|
||
|
||
function getProfileRates(cat){
|
||
if (cat === 'technicien'){
|
||
return {
|
||
sal: {
|
||
contrib_solidarite: 0, maladie: 0, vieillesse: 0.4, alloc_fam: 0, at: 0,
|
||
vieillesse_ta: 6.9, fnal_plaf: 0, maj_chomage: 0, chomage: 2.4, ags: 0,
|
||
retraite_t1: 3.93, ceg_t1: 0.86, prevoyance_ta: 0.12, conges_spectacles: 0,
|
||
medecine_t1: 0, cfpc_conv: 0, cfp_ta: 0, paritarisme: 0,
|
||
csg_deductible: 6.8, csg_imposable: 2.4, rds: 0.5
|
||
},
|
||
emp: {
|
||
contrib_solidarite: 0.3, maladie: 7.0, vieillesse: 2.02, alloc_fam: 3.45, at: 1.69,
|
||
vieillesse_ta: 8.55, fnal_plaf: 0.1, maj_chomage: 0.5, chomage: 9.0, ags: 0.25,
|
||
retraite_t1: 3.94, ceg_t1: 1.29, prevoyance_ta: 1.04, conges_spectacles: 15.5,
|
||
medecine_t1: 0.32, cfpc_conv: 0.1, cfp_ta: 2.0, paritarisme: 0.016,
|
||
csg_deductible: 0, csg_imposable: 0, rds: 0,
|
||
casc_svp: 0.4, fnas: 1.45, fcap: 0.25
|
||
}
|
||
};
|
||
}
|
||
// Artiste
|
||
return {
|
||
sal: {
|
||
contrib_solidarite: 0, maladie: 0, vieillesse: 0.28, alloc_fam: 0, at: 0,
|
||
vieillesse_ta: 4.83, fnal_plaf: 0, maj_chomage: 0, chomage: 2.4, ags: 0,
|
||
ceg_t1: 0.86, retraite_t1: 4.44, retraite_t2: 10.79, ceg_t2: 1.08,
|
||
cet: 0.14,
|
||
prevoyance_ta: 0.12, conges_spectacles: 0,
|
||
casc_svp: 0, fnas: 0, fcap: 0, medecine_t1: 0, cfpc_conv: 0, cfp_ta: 0,
|
||
paritarisme: 0, csg_deductible: 6.8, csg_imposable: 2.4, rds: 0.5
|
||
},
|
||
emp: {
|
||
contrib_solidarite: 0.3, maladie: 4.90, vieillesse: 1.41, alloc_fam: 2.42, at: 1.18,
|
||
vieillesse_ta: 5.99, fnal_plaf: 0.07, maj_chomage: 0.5, chomage: 9.0, ags: 0.25,
|
||
ceg_t1: 1.29, retraite_t1: 4.45, retraite_t2: 10.80, ceg_t2: 1.62,
|
||
cet: 0.21,
|
||
prevoyance_ta: 1.04, conges_spectacles: 15.5,
|
||
casc_svp: 0.4, fnas: 1.45, fcap: 0.25, medecine_t1: 0.32, cfpc_conv: 0.1,
|
||
cfp_ta: 2.0, paritarisme: 0.016, csg_deductible: 0, csg_imposable: 0, rds: 0
|
||
}
|
||
};
|
||
}
|
||
|
||
function getCotisations(){
|
||
const cat = document.getElementById('categorieSelect')?.value || 'artiste';
|
||
const cc = document.getElementById('conventionSelect')?.value;
|
||
const rates = getProfileRates(cat);
|
||
|
||
let codes = [
|
||
"contrib_solidarite","maladie","vieillesse","alloc_fam","at",
|
||
"vieillesse_ta","fnal_plaf","maj_chomage","chomage","ags",
|
||
"retraite_t1","ceg_t1","prevoyance_ta","conges_spectacles",
|
||
"medecine_t1","cfpc_conv","cfp_ta","paritarisme",
|
||
"csg_deductible","csg_imposable","rds"
|
||
];
|
||
// Pour ARTISTE, insérer T2 après ceg_t1, puis CET
|
||
if (cat === 'artiste') {
|
||
const idx = codes.indexOf("ceg_t1");
|
||
if (idx !== -1) {
|
||
codes.splice(idx + 1, 0, "retraite_t2","ceg_t2","cet");
|
||
}
|
||
}
|
||
if (cc === "3090") codes.push("casc_svp"); // CCNSVP
|
||
if (cc === "1285") { codes.push("fnas","fcap"); } // CCNEAC
|
||
|
||
const sal = [], emp = [];
|
||
for (const code of codes){
|
||
sal.push({ code, libelle: baseLibelle(code, cat), taux: (rates.sal[code] ?? 0) });
|
||
emp.push({ code, libelle: baseLibelle(code, cat), taux: (rates.emp[code] ?? 0) });
|
||
}
|
||
|
||
// Statut cadre pour ARTISTE
|
||
const statut = document.getElementById("statutSelect")?.value;
|
||
if (cat === 'artiste' && statut === 'cadre'){
|
||
const rSal = sal.find(x=>x.code==="retraite_t1");
|
||
const rEmp = emp.find(x=>x.code==="retraite_t1");
|
||
if (rSal){ rSal.libelle = "Retraite cadre Tranche 1"; rSal.taux = 3.93; }
|
||
if (rEmp){ rEmp.libelle = "Retraite cadre Tranche 1"; rEmp.taux = 3.94; }
|
||
|
||
const pSal = sal.find(x=>x.code==="prevoyance_ta");
|
||
const pEmp = emp.find(x=>x.code==="prevoyance_ta");
|
||
if (pSal){ pSal.libelle = "Prévoyance cadre Int TA"; pSal.taux = 0.12; }
|
||
if (pEmp){ pEmp.libelle = "Prévoyance cadre Int TA"; pEmp.taux = 1.62; }
|
||
|
||
sal.push({ code:"apec", libelle:"APEC", taux:0.024 });
|
||
emp.push({ code:"apec", libelle:"APEC", taux:0.036 });
|
||
}
|
||
|
||
return { sal, emp, cat };
|
||
}
|
||
|
||
/* ===== Somme des contributions employeur incluses dans l'assiette CSG/CRDS ===== */
|
||
function employerContribsForCSGBase(brut){
|
||
const { emp } = getCotisations();
|
||
const dates = getSelectedDatesArray();
|
||
if (!dates.length) return 0;
|
||
|
||
const prevBase = getPrevoyanceBase(brut);
|
||
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
|
||
|
||
let total = 0;
|
||
for (const e of emp){
|
||
let amount = 0;
|
||
switch (e.code){
|
||
// ✅ Inclure : chômage / maj. chômage / AGS sur leur assiette plafonnée
|
||
case 'chomage':
|
||
case 'maj_chomage':
|
||
case 'ags': {
|
||
const base = Math.min(brut, assietteChomageMax);
|
||
amount = base * (e.taux / 100);
|
||
break;
|
||
}
|
||
// ✅ Inclure : prévoyance TA (sur base plafonnée jour)
|
||
case 'prevoyance_ta':
|
||
amount = prevBase * (e.taux / 100);
|
||
break;
|
||
// ✅ Inclure : contributions conventionnelles simples
|
||
case 'cfpc_conv':
|
||
case 'paritarisme':
|
||
case 'apec':
|
||
amount = brut * (e.taux / 100);
|
||
break;
|
||
|
||
// ❌ Exclure : IRC (AGIRC-ARRCO) T1/T2 (retraite & CEG), FNAL, maladie, vieillesse, AF, AT, etc.
|
||
default:
|
||
break;
|
||
}
|
||
total += amount;
|
||
}
|
||
return total;
|
||
}
|
||
|
||
/* ===== Prévoyance employeur ===== */
|
||
function prevoyanceEmployerAmount(brut) {
|
||
const { emp } = getCotisations();
|
||
const cot = emp.find(e => e.code === "prevoyance_ta");
|
||
const basePrev = getPrevoyanceBase(brut);
|
||
return basePrev * (cot ? (cot.taux / 100) : 0);
|
||
}
|
||
|
||
/* ===== Net (part salariale) ===== */
|
||
function calculateNet(brut, plafondUrssaf) {
|
||
const { sal } = getCotisations();
|
||
let totalDeduction = 0;
|
||
const prevEmp = prevoyanceEmployerAmount(brut);
|
||
const factor = getAbattementFactor();
|
||
const nonAbattementCodes = [
|
||
"maj_chomage","chomage","ags","conges_spectacles",
|
||
"csg_deductible","csg_imposable","rds",
|
||
"prevoyance_ta",
|
||
"cet"
|
||
];
|
||
|
||
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
|
||
|
||
sal.forEach(c => {
|
||
let base;
|
||
let appliedAbattement = false;
|
||
if (c.code === "vieillesse_ta") {
|
||
base = Math.min(brut * factor, plafondUrssaf); // abattement AVANT plafonnement
|
||
appliedAbattement = true;
|
||
} else if (c.code === "fnal_plaf") {
|
||
base = Math.min(brut * factor, plafondUrssaf) * 1.115; // abattement AVANT plafonnement
|
||
appliedAbattement = true;
|
||
} else if (["chomage","maj_chomage","ags"].includes(c.code)) {
|
||
base = Math.min(brut, assietteChomageMax);
|
||
} else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
|
||
base = getPrevoyanceBase(brut);
|
||
} else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) {
|
||
const incEmp = employerContribsForCSGBase(brut);
|
||
base = (brut + incEmp) * 0.9825;
|
||
} else if (c.code === "retraite_t1" || c.code === "ceg_t1") {
|
||
const { baseT1 } = getIrcBasesBeforeAbattement(brut);
|
||
base = baseT1; // abattement éventuel appliqué plus bas si autorisé
|
||
} else if (c.code === "retraite_t2" || c.code === "ceg_t2") {
|
||
const { baseT2 } = getIrcBasesBeforeAbattement(brut);
|
||
base = baseT2; // abattement éventuel appliqué plus bas si autorisé
|
||
} else if (c.code === "cet") {
|
||
base = brut;
|
||
appliedAbattement = true;
|
||
} else {
|
||
base = brut;
|
||
}
|
||
if (!appliedAbattement && !nonAbattementCodes.includes(c.code)) {
|
||
base = base * factor;
|
||
}
|
||
totalDeduction += base * (c.taux / 100);
|
||
});
|
||
return brut - totalDeduction;
|
||
}
|
||
|
||
/* ===== Charges employeur (avec abattement éventuel + Fillon ventilée) ===== */
|
||
function computeEmployerCharges(brut, cachets, heures, plafondUrssaf) {
|
||
const { sal, emp } = getCotisations();
|
||
let totalCharges = 0;
|
||
const prevEmp = prevoyanceEmployerAmount(brut);
|
||
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
|
||
const factor = getAbattementFactor();
|
||
const nonAbattementCodes = [
|
||
"maj_chomage","chomage","ags","conges_spectacles",
|
||
"csg_deductible","csg_imposable","rds",
|
||
"prevoyance_ta",
|
||
"cet"
|
||
];
|
||
|
||
// Fusionne codes salariaux/patronaux pour calculer la part patronale
|
||
const merged = sal.map(cs => {
|
||
const ce = emp.find(e => e.code === cs.code);
|
||
return {
|
||
code: cs.code,
|
||
libelle: cs.libelle,
|
||
tauxSalarial: cs.taux,
|
||
tauxPatronal: ce ? ce.taux : 0
|
||
};
|
||
});
|
||
|
||
// Somme des charges patronales "brutes"
|
||
merged.forEach(c => {
|
||
let base;
|
||
let appliedAbattement = false;
|
||
if (c.code === "vieillesse_ta") {
|
||
base = Math.min(brut * factor, plafondUrssaf); // abattement AVANT plafonnement
|
||
appliedAbattement = true;
|
||
} else if (c.code === "fnal_plaf") {
|
||
base = Math.min(brut * factor, plafondUrssaf) * 1.115; // abattement AVANT plafonnement
|
||
appliedAbattement = true;
|
||
} else if (["fnas","fcap"].includes(c.code)) {
|
||
base = brut; // jamais d’abattement
|
||
appliedAbattement = true; // bloque toute ré-appl. du factor
|
||
} else if (["chomage","maj_chomage","ags"].includes(c.code)) {
|
||
base = Math.min(brut, assietteChomageMax);
|
||
} else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
|
||
base = getPrevoyanceBase(brut);
|
||
} else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) {
|
||
const incEmp = employerContribsForCSGBase(brut);
|
||
base = (brut + incEmp) * 0.9825;
|
||
} else if (c.code === "retraite_t1" || c.code === "ceg_t1") {
|
||
const { baseT1 } = getIrcBasesBeforeAbattement(brut);
|
||
base = baseT1; // abattement éventuel appliqué plus bas si autorisé
|
||
} else if (c.code === "retraite_t2" || c.code === "ceg_t2") {
|
||
const { baseT2 } = getIrcBasesBeforeAbattement(brut);
|
||
base = baseT2; // abattement éventuel appliqué plus bas si autorisé
|
||
} else if (c.code === "cet") {
|
||
base = brut;
|
||
appliedAbattement = true;
|
||
} else {
|
||
base = brut;
|
||
}
|
||
if (!appliedAbattement && !nonAbattementCodes.includes(c.code)) {
|
||
base = base * factor;
|
||
}
|
||
totalCharges += base * (c.tauxPatronal / 100);
|
||
});
|
||
|
||
// Ajout des compléments (patronal pur)
|
||
const dates = getSelectedDatesArray();
|
||
if (dates.length) {
|
||
const { compMaladieAmount, compAFAmount } = computeDailyComplementsFromBrut(brut, cachets, heures);
|
||
totalCharges += compMaladieAmount + compAFAmount;
|
||
}
|
||
|
||
// —— Réduction Fillon (TECH uniquement, <50 ETP) ——
|
||
if (isTechnicien() && brut > 0 && heures > 0) {
|
||
// Objet de travail
|
||
let lf = { brut, heures, total: 0, urssaf: 0, chomage: 0, irc: 0 };
|
||
|
||
if (typeof computeFillonVentilated === "function") {
|
||
const res = computeFillonVentilated(brut, heures);
|
||
lf = { brut, heures, total: res.total, urssaf: res.urssaf, chomage: res.chomage, irc: res.irc };
|
||
} else if (typeof computeFillonAmount === "function") {
|
||
// Fallback global si la ventilation n'est pas dispo
|
||
const t = computeFillonAmount(brut, heures, /*isGe50=*/false);
|
||
lf = { brut, heures, total: t, urssaf: t, chomage: 0, irc: 0 };
|
||
}
|
||
|
||
if (lf.total > 0) {
|
||
totalCharges = Math.max(0, totalCharges - lf.total);
|
||
}
|
||
window.lastFillon = lf;
|
||
} else {
|
||
window.lastFillon = { brut, heures, total: 0, urssaf: 0, chomage: 0, irc: 0 };
|
||
}
|
||
|
||
return totalCharges;
|
||
}
|
||
|
||
/* ===== Réduction Fillon – constantes & fonctions de base (< 50 ETP) ===== */
|
||
const FILLON_T_LT50 = 0.3193; // T pour entreprises < 50 ETP (FNAL 0,10%)
|
||
const FILLON_NEUTRALISATION = 16/15; // facteur de neutralisation pratique
|
||
|
||
/**
|
||
* Coefficient Fillon (C) – techniciens uniquement
|
||
* @param {number} brut - rémunération brute
|
||
* @param {number} heures - nombre d'heures
|
||
* @param {boolean} isGe50 - true si ≥50 ETP (ici inutile, on mettra false)
|
||
* @returns {number} C >= 0
|
||
*/
|
||
function computeFillonCoefficient(brut, heures, isGe50){
|
||
if (!isTechnicien() || !(brut > 0) || !(heures > 0)) return 0;
|
||
|
||
const smicRef = heures * SMIC_HORAIRE;
|
||
// Tous tes clients sont <50 → on fige T = FILLON_T_LT50
|
||
const T = FILLON_T_LT50;
|
||
|
||
let C = (T / 0.6) * (1.6 * (smicRef / brut) - 1);
|
||
if (C < 0) C = 0;
|
||
|
||
// Borne théorique haute quand brut ≈ SMIC
|
||
const Cmax = (T / 0.6) * 0.6;
|
||
if (C > Cmax) C = Cmax;
|
||
|
||
return C;
|
||
}
|
||
|
||
/**
|
||
* Montant global de réduction Fillon (fallback global sans ventilation)
|
||
* @param {number} brut
|
||
* @param {number} heures
|
||
* @param {boolean} isGe50 - ignoré ici (on reste <50)
|
||
* @returns {number} montant ≥ 0
|
||
*/
|
||
function computeFillonAmount(brut, heures, isGe50){
|
||
const C = computeFillonCoefficient(brut, heures, false);
|
||
if (C <= 0) return 0;
|
||
return Math.max(0, brut * C * FILLON_NEUTRALISATION);
|
||
}
|
||
|
||
/**
|
||
* Ventilation URSSAF / CHOMAGE / IRC de la réduction Fillon (<50 ETP)
|
||
* - URSSAF = maladie + vieillesse (plaf. & déplaf.) + alloc_fam + FNAL + AT + CSA
|
||
* - CHOMAGE = assurance chômage UNIQUEMENT (exclut AGS et majorations CDD)
|
||
* - IRC = retraite complémentaire (TU1 + CEG sur T1) avec borne 6,01% du brut
|
||
*/
|
||
function round2(x){ return Math.round((x + Number.EPSILON) * 100) / 100; }
|
||
|
||
function computeFillonVentilated(brut, heures){
|
||
if (!isTechnicien() || brut <= 0 || heures <= 0) return { total: 0, urssaf: 0, chomage: 0, irc: 0 };
|
||
|
||
// Coefficient + total (clients <50 ETP)
|
||
const C = computeFillonCoefficient(brut, heures, false);
|
||
if (C <= 0) return { total: 0, urssaf: 0, chomage: 0, irc: 0 };
|
||
let reste = round2(brut * C * FILLON_NEUTRALISATION);
|
||
|
||
// Assiettes/bases “réelles”
|
||
const { emp } = getCotisations();
|
||
const plafondUrssaf = getPlafondUrssaf();
|
||
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
|
||
|
||
// — Montants “à payer” par bac (employeur), avant réduction —
|
||
// URSSAF : maladie + vieillesse (plaf. & déplaf.) + AF + FNAL + AT/MP ✅ inclut AT
|
||
const urssafCodes = ["maladie","vieillesse","vieillesse_ta","alloc_fam","fnal_plaf","at"];
|
||
let urssafDue = 0;
|
||
for (const e of emp){
|
||
if (!urssafCodes.includes(e.code)) continue;
|
||
let base = brut;
|
||
if (e.code === "vieillesse_ta") base = Math.min(brut, plafondUrssaf);
|
||
if (e.code === "fnal_plaf") base = Math.min(brut, plafondUrssaf) * 1.115;
|
||
urssafDue += base * (e.taux / 100);
|
||
}
|
||
urssafDue = round2(urssafDue);
|
||
|
||
// IRC (AGIRC-ARRCO) : retraite T1 + CEG T1, plafonné à 6,01 % du brut
|
||
const ircCodes = ["retraite_t1","ceg_t1"];
|
||
let ircDue = 0;
|
||
for (const e of emp){
|
||
if (!ircCodes.includes(e.code)) continue;
|
||
ircDue += brut * (e.taux / 100);
|
||
}
|
||
const ircCap = round2(brut * 0.0601);
|
||
ircDue = Math.min(round2(ircDue), ircCap); // applique la borne 6,01 %
|
||
|
||
// Chômage : uniquement la cotisation “chomage” (exclut AGS et maj. CDD)
|
||
let chomageDue = 0;
|
||
for (const e of emp){
|
||
if (e.code !== "chomage") continue;
|
||
const base = Math.min(brut, assietteChomageMax);
|
||
chomageDue += base * (e.taux / 100);
|
||
}
|
||
chomageDue = round2(chomageDue);
|
||
|
||
// — Imputation et arrondis, dans l’ordre sPAIEctacle —
|
||
// 1) Chômage d’abord
|
||
const choRed = Math.min(reste, chomageDue);
|
||
reste = round2(reste - choRed);
|
||
|
||
// 2) Puis URSSAF (incluant AT)
|
||
const urssafRed = Math.min(reste, urssafDue);
|
||
reste = round2(reste - urssafRed);
|
||
|
||
// 3) Enfin IRC (borne déjà appliquée)
|
||
const ircRed = Math.min(reste, ircDue);
|
||
reste = round2(reste - ircRed);
|
||
|
||
return { total: round2(brut * C * FILLON_NEUTRALISATION), urssaf: urssafRed, chomage: choRed, irc: ircRed };
|
||
}
|
||
function calculateCostEmployer(brut, cachets, heures, plafondUrssaf) {
|
||
return brut + computeEmployerCharges(brut, cachets, heures, plafondUrssaf);
|
||
}
|
||
|
||
/* ===== Combos & inversions ===== */
|
||
function calculateSalaries(brut, cachets, heures, plafondUrssaf) {
|
||
const net = calculateNet(brut, plafondUrssaf);
|
||
const costEmployer = calculateCostEmployer(brut, cachets, heures, plafondUrssaf);
|
||
return { net, costEmployer };
|
||
}
|
||
|
||
function findBrutFromCostEmployer(targetCostEmployer, cachets, heures, plafondUrssaf) {
|
||
const tolerance = 0.0001;
|
||
let low = targetCostEmployer / 3;
|
||
let high = targetCostEmployer * 2;
|
||
|
||
while (calculateCostEmployer(high, cachets, heures, plafondUrssaf) < targetCostEmployer) {
|
||
high *= 2;
|
||
}
|
||
|
||
for (let i = 0; i < 80 && (high - low) > tolerance; i++) {
|
||
const mid = (low + high) / 2;
|
||
const ce = calculateCostEmployer(mid, cachets, heures, plafondUrssaf);
|
||
if (ce < targetCostEmployer) low = mid; else high = mid;
|
||
}
|
||
return (low + high) / 2;
|
||
}
|
||
|
||
function findBrutFromNet(targetNet, cachets, heures, plafondUrssaf) {
|
||
const tolerance = 0.001;
|
||
let low = targetNet;
|
||
let high = targetNet * 2;
|
||
while (calculateSalaries(high, cachets, heures, plafondUrssaf).net < targetNet) high *= 2;
|
||
|
||
while (high - low > tolerance) {
|
||
const mid = (low + high) / 2;
|
||
const { net } = calculateSalaries(mid, cachets, heures, plafondUrssaf);
|
||
if (net < targetNet) low = mid; else high = mid;
|
||
}
|
||
return (low + high) / 2;
|
||
}
|
||
|
||
/* ===== Table de détail ===== */
|
||
function generateDetailTable(brut, cachets, heures, plafondUrssaf) {
|
||
const prevEmp = prevoyanceEmployerAmount(brut);
|
||
const factor = getAbattementFactor();
|
||
const nonAbattementCodes = [
|
||
"maj_chomage","chomage","ags","conges_spectacles",
|
||
"csg_deductible","csg_imposable","rds",
|
||
"prevoyance_ta",
|
||
"cet"
|
||
];
|
||
const { sal, emp, cat } = getCotisations();
|
||
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
|
||
|
||
let merged = sal.map(cs => {
|
||
const ce = emp.find(e => e.code === cs.code);
|
||
return {
|
||
code: cs.code,
|
||
libelle: cs.libelle,
|
||
tauxSalarial: cs.taux,
|
||
tauxPatronal: ce ? ce.taux : 0
|
||
};
|
||
});
|
||
|
||
let html = `<h4>Détail des cotisations</h4>
|
||
<table class="cotisations-table">
|
||
<tr>
|
||
<th>Cotisation</th>
|
||
<th>Taux Salarial (%)</th>
|
||
<th>Taux Patronal (%)</th>
|
||
<th>Base (€)</th>
|
||
<th>Montant Salarial (€)</th>
|
||
<th>Montant Patronal (€)</th>
|
||
</tr>`;
|
||
|
||
merged.forEach(c => {
|
||
let base;
|
||
let appliedAbattement = false;
|
||
if (c.code === "vieillesse_ta") {
|
||
base = Math.min(brut * factor, plafondUrssaf); // abattement AVANT plafonnement
|
||
appliedAbattement = true;
|
||
} else if (c.code === "fnal_plaf") {
|
||
base = Math.min(brut * factor, plafondUrssaf) * 1.115; // abattement AVANT plafonnement
|
||
appliedAbattement = true;
|
||
} else if (["fnas","fcap"].includes(c.code)) {
|
||
base = brut; // jamais d’abattement
|
||
appliedAbattement = true; // bloque toute ré-appl. du factor
|
||
} else if (["chomage","maj_chomage","ags"].includes(c.code)) {
|
||
base = Math.min(brut, assietteChomageMax);
|
||
} else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
|
||
base = getPrevoyanceBase(brut);
|
||
} else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) {
|
||
const incEmp = employerContribsForCSGBase(brut);
|
||
base = (brut + incEmp) * 0.9825;
|
||
} else if (c.code === "retraite_t1" || c.code === "ceg_t1") {
|
||
const { baseT1 } = getIrcBasesBeforeAbattement(brut);
|
||
base = baseT1; // abattement éventuel appliqué plus bas si autorisé
|
||
} else if (c.code === "retraite_t2" || c.code === "ceg_t2") {
|
||
const { baseT2 } = getIrcBasesBeforeAbattement(brut);
|
||
base = baseT2; // abattement éventuel appliqué plus bas si autorisé
|
||
} else if (c.code === "cet") {
|
||
base = brut;
|
||
appliedAbattement = true;
|
||
} else {
|
||
base = brut;
|
||
}
|
||
if (!appliedAbattement && !nonAbattementCodes.includes(c.code)) {
|
||
base = base * factor;
|
||
}
|
||
|
||
const montantSalarial = base * (c.tauxSalarial / 100);
|
||
const montantPatronal = base * (c.tauxPatronal / 100);
|
||
|
||
html += `<tr>
|
||
<td>${c.libelle}</td>
|
||
<td>${fmtPct(c.tauxSalarial)}</td>
|
||
<td>${fmtPct(c.tauxPatronal)}</td>
|
||
<td>${fmtEuro(base)}</td>
|
||
<td>${fmtEuro(montantSalarial)}</td>
|
||
<td>${fmtEuro(montantPatronal)}</td>
|
||
</tr>`;
|
||
});
|
||
|
||
// 👉 ICI : ajout des lignes Fillon si technicien
|
||
if (isTechnicien() && window.lastFillon && window.lastFillon.total > 0) {
|
||
const { total } = window.lastFillon;
|
||
html += `
|
||
<tr>
|
||
<td>Réduction Fillon totale</td>
|
||
<td>—</td><td>—</td><td>${fmtEuro(brut)}</td>
|
||
<td>0,00</td><td>-${fmtEuro(total)}</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
|
||
// Compléments en bas
|
||
const dates = getSelectedDatesArray();
|
||
if (dates.length) {
|
||
const { baseMaladieTotal, baseAFTotal, compMaladieAmount, compAFAmount } =
|
||
computeDailyComplementsFromBrut(brut, cachets, heures);
|
||
|
||
const modeLabelMaladie = (COMPLEMENT_MALADIE_BASE_MODE === 'FULL_IF_ANY') ? 'assiette contrat' : 'récap jours';
|
||
const modeLabelAF = (COMPLEMENT_AF_BASE_MODE === 'FULL_IF_ANY') ? 'assiette contrat' : 'récap jours';
|
||
const compLabelMaladie = (cat === 'technicien') ? "Complément maladie" : "Complément maladie artiste";
|
||
const compLabelAF = (cat === 'technicien') ? "Complément AF" : "Complément AF artiste";
|
||
|
||
const { maladie: TAUX_COMPLEMENT_MALADIE, af: TAUX_COMPLEMENT_AF } = getComplementsRates(cat);
|
||
|
||
html += `
|
||
<tr>
|
||
<td>${compLabelMaladie} (${modeLabelMaladie})</td>
|
||
<td>0,00 %</td>
|
||
<td>${fmtPct(TAUX_COMPLEMENT_MALADIE)}</td>
|
||
<td>${fmtEuro(baseMaladieTotal)}</td>
|
||
<td>0,00</td>
|
||
<td>${fmtEuro(compMaladieAmount)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>${compLabelAF} (${modeLabelAF})</td>
|
||
<td>0,00 %</td>
|
||
<td>${fmtPct(TAUX_COMPLEMENT_AF)}</td>
|
||
<td>${fmtEuro(baseAFTotal)}</td>
|
||
<td>0,00</td>
|
||
<td>${fmtEuro(compAFAmount)}</td>
|
||
</tr>`;
|
||
}
|
||
|
||
html += `</table>`;
|
||
return html;
|
||
}
|
||
|
||
/* ===== Structure pour export PDF (contributions + totaux) ===== */
|
||
function buildContributionsStructured(brut, cachets, heures, plafondUrssaf){
|
||
const prevEmp = prevoyanceEmployerAmount(brut);
|
||
const factor = getAbattementFactor();
|
||
const nonAbattementCodes = [
|
||
"maj_chomage","chomage","ags","conges_spectacles",
|
||
"csg_deductible","csg_imposable","rds",
|
||
"prevoyance_ta",
|
||
"cet"
|
||
];
|
||
const { sal, emp, cat } = getCotisations();
|
||
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
|
||
|
||
let merged = sal.map(cs => {
|
||
const ce = emp.find(e => e.code === cs.code);
|
||
return {
|
||
code: cs.code,
|
||
libelle: cs.libelle,
|
||
tauxSalarial: cs.taux,
|
||
tauxPatronal: ce ? ce.taux : 0
|
||
};
|
||
});
|
||
|
||
// Ajout des compléments comme lignes dédiées (patronal)
|
||
const dates = getSelectedDatesArray();
|
||
let compMaladie = 0, compAF = 0, baseMaladieTot = 0, baseAFTot = 0;
|
||
if (dates.length){
|
||
const comp = computeDailyComplementsFromBrut(brut, cachets, heures);
|
||
compMaladie = comp.compMaladieAmount;
|
||
compAF = comp.compAFAmount;
|
||
baseMaladieTot = comp.baseMaladieTotal;
|
||
baseAFTot = comp.baseAFTotal;
|
||
const compLabelMaladie = (cat === 'technicien') ? "Complément maladie" : "Complément maladie artiste";
|
||
const compLabelAF = (cat === 'technicien') ? "Complément AF" : "Complément AF artiste";
|
||
const { maladie: TAUX_COMPLEMENT_MALADIE, af: TAUX_COMPLEMENT_AF } = getComplementsRates(cat);
|
||
merged.push({ code:"comp_maladie", libelle: compLabelMaladie, tauxSalarial: 0, tauxPatronal: TAUX_COMPLEMENT_MALADIE, _isComplement: "maladie", _base: baseMaladieTot, _montantPat: compMaladie });
|
||
merged.push({ code:"comp_af", libelle: compLabelAF, tauxSalarial: 0, tauxPatronal: TAUX_COMPLEMENT_AF, _isComplement: "af", _base: baseAFTot, _montantPat: compAF });
|
||
}
|
||
|
||
// ——— Réduction Fillon totale (une seule ligne) ———
|
||
let fillonTotal = 0;
|
||
if (isTechnicien()) {
|
||
// priorise la valeur calculée pendant computeEmployerCharges
|
||
if (window.lastFillon && window.lastFillon.total > 0) {
|
||
fillonTotal = window.lastFillon.total;
|
||
} else if (typeof computeFillonVentilated === "function") {
|
||
const { total } = computeFillonVentilated(brut, heures);
|
||
fillonTotal = total || 0;
|
||
} else if (typeof computeFillonAmount === "function") {
|
||
fillonTotal = computeFillonAmount(brut, heures, /*isGe50=*/false) || 0;
|
||
}
|
||
}
|
||
|
||
// Si on a une réduction, on l’ajoute comme ligne dédiée (montant patronal négatif)
|
||
if (fillonTotal > 0) {
|
||
merged.push({
|
||
code: "fillon_total",
|
||
libelle: "Réduction Fillon totale",
|
||
tauxSalarial: 0,
|
||
tauxPatronal: 0,
|
||
_isFillon: true,
|
||
_base: brut, // pour affichage de la base dans le PDF
|
||
_montantPat: -fillonTotal // négatif côté employeur
|
||
});
|
||
}
|
||
|
||
let lignes = [];
|
||
let totalSal = 0;
|
||
let totalPat = 0;
|
||
|
||
for (const c of merged){
|
||
let base;
|
||
let appliedAbattement = false;
|
||
|
||
// ✅ Cas spécial : ligne "Réduction Fillon totale"
|
||
if (c._isFillon) {
|
||
const montantSalarial = 0;
|
||
const montantPatronal = c._montantPat; // négatif
|
||
totalSal += montantSalarial;
|
||
totalPat += montantPatronal;
|
||
|
||
lignes.push({
|
||
libelle: c.libelle,
|
||
assiette: fmtEuro(c._base),
|
||
taux_salarial: "—",
|
||
taux_patronal: "—",
|
||
montant_salarial: fmtEuro(montantSalarial),
|
||
montant_patronal: fmtEuro(montantPatronal)
|
||
});
|
||
continue;
|
||
}
|
||
|
||
// ✅ Cas spécial : compléments (déjà calculés)
|
||
if (c._isComplement){
|
||
base = c._base;
|
||
const montantSalarial = 0;
|
||
const montantPatronal = c._montantPat;
|
||
totalSal += montantSalarial;
|
||
totalPat += montantPatronal;
|
||
lignes.push({
|
||
libelle: c.libelle,
|
||
assiette: fmtEuro(base),
|
||
taux_salarial: fmtPct(c.tauxSalarial),
|
||
taux_patronal: fmtPct(c.tauxPatronal),
|
||
montant_salarial: fmtEuro(montantSalarial),
|
||
montant_patronal: fmtEuro(montantPatronal)
|
||
});
|
||
continue;
|
||
}
|
||
|
||
// — Cas général —
|
||
if (["chomage","maj_chomage","ags"].includes(c.code)) {
|
||
base = Math.min(brut, assietteChomageMax);
|
||
} else if (c.code === "vieillesse_ta") {
|
||
base = Math.min(brut * factor, plafondUrssaf); // abattement AVANT plafonnement
|
||
appliedAbattement = true;
|
||
} else if (c.code === "fnal_plaf") {
|
||
base = Math.min(brut * factor, plafondUrssaf) * 1.115; // abattement AVANT plafonnement
|
||
appliedAbattement = true;
|
||
} else if (["fnas","fcap"].includes(c.code)) {
|
||
base = brut; // jamais d’abattement
|
||
appliedAbattement = true; // bloque toute ré-appl. du factor
|
||
} else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
|
||
base = getPrevoyanceBase(brut);
|
||
} else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) {
|
||
const incEmp = employerContribsForCSGBase(brut);
|
||
base = (brut + incEmp) * 0.9825;
|
||
} else if (c.code === "retraite_t1" || c.code === "ceg_t1") {
|
||
const { baseT1 } = getIrcBasesBeforeAbattement(brut);
|
||
base = baseT1; // abattement éventuel appliqué plus bas si autorisé
|
||
} else if (c.code === "retraite_t2" || c.code === "ceg_t2") {
|
||
const { baseT2 } = getIrcBasesBeforeAbattement(brut);
|
||
base = baseT2; // abattement éventuel appliqué plus bas si autorisé
|
||
} else if (c.code === "cet") {
|
||
base = brut;
|
||
appliedAbattement = true;
|
||
} else {
|
||
base = brut;
|
||
}
|
||
if (!appliedAbattement && !nonAbattementCodes.includes(c.code)) {
|
||
base = base * factor;
|
||
}
|
||
|
||
const montantSalarial = base * (c.tauxSalarial / 100);
|
||
const montantPatronal = base * (c.tauxPatronal / 100);
|
||
|
||
totalSal += montantSalarial;
|
||
totalPat += montantPatronal;
|
||
|
||
lignes.push({
|
||
libelle: c.libelle,
|
||
assiette: fmtEuro(base),
|
||
taux_salarial: fmtPct(c.tauxSalarial),
|
||
taux_patronal: fmtPct(c.tauxPatronal),
|
||
montant_salarial: fmtEuro(montantSalarial),
|
||
montant_patronal: fmtEuro(montantPatronal)
|
||
});
|
||
}
|
||
|
||
const contributions = [{ groupe: "Cotisations et contributions", lignes }];
|
||
|
||
return {
|
||
contributions,
|
||
totals: {
|
||
salariale: fmtEuro(totalSal),
|
||
employeur: fmtEuro(totalPat)
|
||
}
|
||
};
|
||
}
|
||
|
||
/* ===== Bouton Calculer ===== */
|
||
document.getElementById('calcBtn').addEventListener('click', function() {
|
||
// Vérif multi-mois
|
||
const datesStr = document.getElementById("datesInput").value;
|
||
if (datesStr) {
|
||
const datesArray = datesStr.split(",").map(s => new Date(s.trim()));
|
||
const firstMonth = datesArray[0].getMonth();
|
||
const firstYear = datesArray[0].getFullYear();
|
||
const multiMonth = datesArray.some(d => d.getMonth() !== firstMonth || d.getFullYear() !== firstYear);
|
||
if (multiMonth) {
|
||
swal("Simulation impossible", "Le simulateur n'est pas encore prévu pour les contrats multi-mois. Contactez-nous pour une simulation personnalisée.", "warning");
|
||
return;
|
||
}
|
||
}
|
||
|
||
const montant = parseFloat(document.getElementById('montantInput').value);
|
||
const heures = parseFloat(document.getElementById('heuresInput').value) || 0;
|
||
const cachetsRaw = parseInt(document.getElementById('cachetsInput')?.value, 10);
|
||
const cachets = isTechnicien() ? 0 : (isNaN(cachetsRaw) ? 0 : Math.max(0, cachetsRaw));
|
||
if (isNaN(montant) || montant <= 0) {
|
||
alert("Veuillez entrer un montant valide.");
|
||
return;
|
||
}
|
||
|
||
const plafondUrssaf = getPlafondUrssaf();
|
||
const type = document.querySelector('input[name="type"]:checked').value;
|
||
|
||
let brut, net, costEmployer;
|
||
|
||
if (type === 'brut') {
|
||
brut = montant;
|
||
({ net, costEmployer } = calculateSalaries(brut, cachets, heures, plafondUrssaf));
|
||
} else if (type === 'net') {
|
||
const targetNet = montant;
|
||
brut = findBrutFromNet(targetNet, cachets, heures, plafondUrssaf);
|
||
({ net, costEmployer } = calculateSalaries(brut, cachets, heures, plafondUrssaf));
|
||
} else if (type === 'cost') {
|
||
const targetCE = montant;
|
||
brut = findBrutFromCostEmployer(targetCE, cachets, heures, plafondUrssaf);
|
||
({ net, costEmployer } = calculateSalaries(brut, cachets, heures, plafondUrssaf));
|
||
}
|
||
|
||
// 📊 Envoyer un événement PostHog enrichi via postMessage au parent
|
||
try {
|
||
// Récupération de toutes les données du formulaire
|
||
const ccnElement = document.getElementById('conventionSelect');
|
||
const ccnValue = ccnElement?.value || '';
|
||
const ccnText = ccnElement?.options[ccnElement.selectedIndex]?.text || '';
|
||
|
||
const categorieValue = isTechnicien() ? 'technicien' : 'artiste';
|
||
const statutValue = document.getElementById('statutSelect')?.value || 'non-cadre';
|
||
|
||
// Abattement
|
||
const abattementChecked = document.querySelector('input[name="abattement"]:checked')?.value || 'non';
|
||
const professionValue = document.getElementById('professionSelect')?.value || '';
|
||
|
||
// Dates
|
||
const datesInput = document.getElementById('datesInput')?.value || '';
|
||
const datesArray = datesInput ? datesInput.split(',').map(d => d.trim()).filter(Boolean) : [];
|
||
|
||
window.parent.postMessage({
|
||
type: 'simulateur_calculation',
|
||
data: {
|
||
// Paramètres du formulaire
|
||
ccn_id: ccnValue,
|
||
ccn_nom: ccnText,
|
||
categorie: categorieValue,
|
||
statut: statutValue,
|
||
abattement_active: abattementChecked === 'oui',
|
||
abattement_profession: professionValue,
|
||
|
||
// Cachets, heures, dates
|
||
cachets: cachets,
|
||
heures: heures,
|
||
dates_travail: datesArray,
|
||
nombre_jours: datesArray.length,
|
||
|
||
// Montant saisi et type
|
||
montant_saisi: montant,
|
||
type_calcul: type, // 'brut', 'net', ou 'cost'
|
||
|
||
// Résultats calculés
|
||
resultat_brut: parseFloat(brut.toFixed(2)),
|
||
resultat_net: parseFloat(net.toFixed(2)),
|
||
resultat_cost: parseFloat(costEmployer.toFixed(2)),
|
||
|
||
// Métadonnées
|
||
plafond_urssaf: plafondUrssaf,
|
||
timestamp: new Date().toISOString()
|
||
}
|
||
}, '*');
|
||
|
||
console.log('📊 PostHog: Événement simulateur_calculation enrichi envoyé');
|
||
} catch (e) {
|
||
console.error('Erreur lors de l\'envoi de l\'événement PostHog:', e);
|
||
}
|
||
|
||
// —— Alerts & hard stop on extreme simulations ——
|
||
const alertsEl = document.getElementById('alerts');
|
||
if (alertsEl) alertsEl.innerHTML = '';
|
||
|
||
// Hard stop > 100 000 €
|
||
if (brut > 100000) {
|
||
try {
|
||
swal({
|
||
title: "Montant très élevé",
|
||
text: "Cette simulation dépasse le périmètre de l'outil. Merci de nous contacter pour confirmer ou valider la simulation.",
|
||
icon: "warning",
|
||
buttons: {
|
||
cancel: "Fermer",
|
||
support: {
|
||
text: "Contacter le support",
|
||
value: "support",
|
||
closeModal: true
|
||
}
|
||
}
|
||
}).then(value => {
|
||
if (value === "support") window.location.href = "/support";
|
||
});
|
||
} catch(e){
|
||
// Fallback simple si SweetAlert indisponible
|
||
if (confirm("Montant très élevé. Ouvrir la page support ?")) window.location.href = "/support";
|
||
}
|
||
// Stop calculation rendering
|
||
document.getElementById('result').innerHTML = '';
|
||
document.getElementById('detailTable').innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
// Alerte non bloquante > 10 000 €
|
||
if (brut > 10000) {
|
||
if (alertsEl) {
|
||
alertsEl.innerHTML = `
|
||
<div class="alert alert-warning" role="alert" style="margin:16px 0">
|
||
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
|
||
<div>
|
||
<strong>Simulation extrême :</strong> le montant saisi est très élevé.\n
|
||
Nous vous invitons à nous contacter pour confirmer ou valider la simulation.
|
||
</div>
|
||
<a href="/support" class="btn btn-sm btn-primary">Contacter le support</a>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
}
|
||
|
||
// Ajout du debug chômage
|
||
const dbg = getChomageDebugInfo();
|
||
const dbgHtml = dbg ? `
|
||
<div style="margin-top:12px;padding:8px;border:1px dashed #cbd5e1;border-radius:8px;background:#f8fafc">
|
||
<div style="font-weight:600;margin-bottom:4px">Debug chômage</div>
|
||
<div>Branche: <strong>${dbg.branch}</strong> — Jours sélectionnés: <strong>${dbg.nbJours}</strong>, Cachets: <strong>${dbg.cachets}</strong>, Jours retenus chômage: <strong>${dbg.nbJoursChomage}</strong></div>
|
||
<div>Jours dans le mois: <strong>${dbg.daysInMonth}</strong>, DiffDays (span): <strong>${dbg.diffDays}</strong></div>
|
||
<div>Assiette chômage max calculée: <strong>${fmtEuro(dbg.assiette)}</strong> €</div>
|
||
</div>` : '';
|
||
|
||
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>
|
||
${dbgHtml}
|
||
`;
|
||
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>
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.simulateur {
|
||
margin: 0 auto !important;
|
||
padding: 24px !important;
|
||
background: #ffffff !important;
|
||
border-radius: 12px !important;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.03) !important;
|
||
color: #1e293b !important;
|
||
}
|
||
|
||
.simulateur label {
|
||
display: block !important;
|
||
margin-top: 16px !important;
|
||
margin-bottom: 6px !important;
|
||
font-size: 0.875rem !important;
|
||
font-weight: 500 !important;
|
||
color: #334155 !important;
|
||
}
|
||
|
||
.simulateur label .fa-info-circle {
|
||
color: #6366f1 !important;
|
||
margin-left: 4px !important;
|
||
font-size: 0.875rem !important;
|
||
}
|
||
|
||
.simulateur input,
|
||
.simulateur select {
|
||
width: 100% !important;
|
||
padding: 10px 12px !important;
|
||
margin-top: 0 !important;
|
||
font-size: 0.9375rem !important;
|
||
border: 1.5px solid #e2e8f0 !important;
|
||
border-radius: 8px !important;
|
||
background: #ffffff !important;
|
||
color: #1e293b !important;
|
||
transition: all 0.15s ease !important;
|
||
}
|
||
|
||
.simulateur input:focus,
|
||
.simulateur select:focus {
|
||
outline: none !important;
|
||
border-color: #6366f1 !important;
|
||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1) !important;
|
||
}
|
||
|
||
.simulateur input::placeholder {
|
||
color: #94a3b8 !important;
|
||
}
|
||
|
||
.simulateur .options {
|
||
display: flex !important;
|
||
flex-wrap: wrap !important;
|
||
gap: 12px !important;
|
||
margin-top: 12px !important;
|
||
}
|
||
|
||
.simulateur .options label {
|
||
display: inline-flex !important;
|
||
align-items: center !important;
|
||
margin: 0 !important;
|
||
padding: 8px 16px !important;
|
||
background: #f8fafc !important;
|
||
border: 1.5px solid #e2e8f0 !important;
|
||
border-radius: 8px !important;
|
||
font-size: 0.875rem !important;
|
||
font-weight: 500 !important;
|
||
color: #475569 !important;
|
||
cursor: pointer !important;
|
||
transition: all 0.15s ease !important;
|
||
}
|
||
|
||
.simulateur .options label:hover {
|
||
background: #f1f5f9 !important;
|
||
border-color: #cbd5e1 !important;
|
||
}
|
||
|
||
.simulateur .options input[type="radio"] {
|
||
width: auto !important;
|
||
margin-right: 8px !important;
|
||
margin-top: 0 !important;
|
||
accent-color: #6366f1 !important;
|
||
}
|
||
|
||
.simulateur .options input[type="radio"]:checked + label,
|
||
.simulateur .options label:has(input[type="radio"]:checked) {
|
||
background: #eef2ff !important;
|
||
border-color: #6366f1 !important;
|
||
color: #4338ca !important;
|
||
}
|
||
|
||
.simulateur button {
|
||
width: 100% !important;
|
||
padding: 12px 24px !important;
|
||
margin-top: 24px !important;
|
||
font-size: 0.9375rem !important;
|
||
font-weight: 600 !important;
|
||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
|
||
color: #ffffff !important;
|
||
border: none !important;
|
||
border-radius: 10px !important;
|
||
cursor: pointer !important;
|
||
transition: all 0.2s ease !important;
|
||
box-shadow: 0 4px 6px -1px rgba(99, 102, 241, 0.2), 0 2px 4px -1px rgba(99, 102, 241, 0.1) !important;
|
||
}
|
||
|
||
.simulateur button:hover {
|
||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%) !important;
|
||
transform: translateY(-1px) !important;
|
||
box-shadow: 0 6px 8px -1px rgba(99, 102, 241, 0.3), 0 4px 6px -1px rgba(99, 102, 241, 0.15) !important;
|
||
}
|
||
|
||
.simulateur button:active {
|
||
transform: translateY(0) !important;
|
||
}
|
||
|
||
.result, .detail-table {
|
||
margin: 24px auto !important;
|
||
background: #ffffff !important;
|
||
padding: 20px !important;
|
||
border-radius: 12px !important;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.03) !important;
|
||
color: #1e293b !important;
|
||
}
|
||
|
||
.result h4 {
|
||
margin: 0 0 16px 0 !important;
|
||
font-size: 1.125rem !important;
|
||
font-weight: 600 !important;
|
||
color: #1e293b !important;
|
||
}
|
||
|
||
table {
|
||
width: 100% !important;
|
||
border-collapse: separate !important;
|
||
border-spacing: 0 !important;
|
||
font-size: 0.875rem !important;
|
||
color: #1e293b !important;
|
||
border-radius: 8px !important;
|
||
overflow: hidden !important;
|
||
}
|
||
|
||
table th, table td {
|
||
border: 1px solid #e2e8f0 !important;
|
||
padding: 10px 12px !important;
|
||
text-align: left !important;
|
||
color: #1e293b !important;
|
||
}
|
||
|
||
table th {
|
||
background: #f8fafc !important;
|
||
font-weight: 600 !important;
|
||
font-size: 0.8125rem !important;
|
||
text-transform: uppercase !important;
|
||
letter-spacing: 0.025em !important;
|
||
color: #475569 !important;
|
||
}
|
||
|
||
table tr:hover {
|
||
background: #f8fafc !important;
|
||
}
|
||
|
||
/* Customisation du calendrier Flatpickr */
|
||
.flatpickr-calendar {
|
||
font-size: 14px !important;
|
||
border-radius: 12px !important;
|
||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
|
||
border: 1px solid #e2e8f0 !important;
|
||
}
|
||
|
||
.flatpickr-month {
|
||
background: #f8fafc !important;
|
||
border-bottom: 1px solid #e2e8f0 !important;
|
||
color: #1e293b !important;
|
||
}
|
||
|
||
.flatpickr-day.selected,
|
||
.flatpickr-day.startRange,
|
||
.flatpickr-day.endRange {
|
||
background: #6366f1 !important;
|
||
border-color: #6366f1 !important;
|
||
color: #ffffff !important;
|
||
}
|
||
|
||
.flatpickr-day:hover {
|
||
background: #eef2ff !important;
|
||
border-color: #c7d2fe !important;
|
||
}
|
||
|
||
.result-table {
|
||
font-size: 1rem !important;
|
||
background: linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 100%) !important;
|
||
border: 2px solid #86efac !important;
|
||
border-radius: 8px !important;
|
||
}
|
||
|
||
.result-table th {
|
||
background: linear-gradient(135deg, #dcfce7 0%, #d1fae5 100%) !important;
|
||
font-weight: 700 !important;
|
||
color: #166534 !important;
|
||
}
|
||
|
||
.result-table td {
|
||
font-weight: 600 !important;
|
||
color: #15803d !important;
|
||
}
|
||
|
||
.cotisations-table {
|
||
font-size: 0.875rem !important;
|
||
background: #ffffff !important;
|
||
border: 1px solid #e2e8f0 !important;
|
||
}
|
||
|
||
.cotisations-table th {
|
||
background: #f8fafc !important;
|
||
}
|
||
|
||
.cotisations-table td {
|
||
font-weight: normal !important;
|
||
}
|
||
|
||
#daysCount {
|
||
font-size: 0.8125rem !important;
|
||
color: #64748b !important;
|
||
margin-top: 8px !important;
|
||
font-style: italic !important;
|
||
}
|
||
|
||
#info {
|
||
font-size: 0.8125rem !important;
|
||
color: #64748b !important;
|
||
line-height: 1.5 !important;
|
||
font-style: italic !important;
|
||
text-align: center !important;
|
||
margin-top: 16px !important;
|
||
}
|
||
|
||
/* Sections avec espacement */
|
||
.form-section {
|
||
padding: 16px !important;
|
||
background: #f8fafc !important;
|
||
border-radius: 8px !important;
|
||
margin-top: 16px !important;
|
||
border: 1px solid #e2e8f0 !important;
|
||
}
|
||
|
||
.form-section:first-of-type {
|
||
margin-top: 0 !important;
|
||
}
|
||
|
||
.form-section > label:first-child {
|
||
margin-top: 0 !important;
|
||
}
|
||
|
||
.form-section > div:first-child > label:first-child {
|
||
margin-top: 0 !important;
|
||
}
|
||
|
||
#abattementSection,
|
||
#cachetsGroup,
|
||
#professionBlock {
|
||
padding: 0 !important;
|
||
background: transparent !important;
|
||
border-radius: 0 !important;
|
||
margin-top: 12px !important;
|
||
border: none !important;
|
||
}
|
||
|
||
#professionBlock {
|
||
margin-top: 12px !important;
|
||
}
|
||
|
||
.d-flex {
|
||
display: flex !important;
|
||
gap: 16px !important;
|
||
}
|
||
|
||
.d-flex > div {
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
gap: 8px !important;
|
||
}
|
||
|
||
.d-flex input[type="radio"] {
|
||
width: auto !important;
|
||
accent-color: #6366f1 !important;
|
||
}
|
||
|
||
.d-flex label {
|
||
margin: 0 !important;
|
||
font-weight: 500 !important;
|
||
cursor: pointer !important;
|
||
}
|
||
</style></body></html>
|