1751 lines
No EOL
68 KiB
HTML
1751 lines
No EOL
68 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>
|
||
|
||
<!-- ===== Odentas • Panneau Simulateur (colonne droite, épuré) ===== -->
|
||
<aside class="od-simu-aside" aria-label="Aide et explications">
|
||
<!-- Carte : résumé / mode d’emploi -->
|
||
<div class="od-card">
|
||
<p>L'équipe Odentas vous propose son simulateur de paie gratuit vous permettant de calculer le coût de recrutement d'un intermittent du spectacle, dans le cadre d'un Contrat à Durée Déterminée d'Usage (CDDU).
|
||
|
||
</p>
|
||
<h3 class="od-card-title">Comment utiliser notre simulateur paie intermittent</h3>
|
||
<ul class="od-list">
|
||
<li>Choisissez votre Convention Collective, l'Annexe et le statut du salarié.</li>
|
||
<li>Indiquez les cachets et/ou heures + les dates travaillées.</li>
|
||
<li>Saisissez un montant puis indiquez s'il s'agit du <em>Brut</em>, <em>Net</em> ou <em>Coût employeur</em>.</li>
|
||
<li>Le résultat s’affiche instantanément (avec détails des cotisations).</li>
|
||
<li>Vous pouvez télécharger le résultat en PDF.</li>
|
||
</ul>
|
||
<p class="od-note">Mise à jour des taux : <strong>2025</strong>. Multi-mois : <a href="/contact/#multi-mois">nous contacter</a>.</p>
|
||
</div>
|
||
|
||
<!-- Carte : CTA -->
|
||
<div class="od-card od-cta" id="od-simu-contact">
|
||
<h3 class="od-card-title">Gagner du temps</h3>
|
||
<p>Contrats, bulletins, déclarations : on gère votre paie spectacle de A à Z.</p>
|
||
<a href="/contact/" class="od-btn">Demander un devis</a>
|
||
</div>
|
||
|
||
<!-- Carte : Badges info + Liens sous forme de mini-cards cliquables -->
|
||
<div class="od-card">
|
||
<h3 class="od-card-title">Ressources utiles</h3>
|
||
|
||
|
||
|
||
<!-- Liens en mini-cards cliquables -->
|
||
<div class="od-minicards">
|
||
<a class="od-minicard" href="/minima-conventions-collectives-spectacle/">
|
||
<span class="od-minititle">Minima des Conventions Collectives</span>
|
||
<span class="od-minitext">Grilles interactives, à jour 2025</span>
|
||
</a>
|
||
<a class="od-minicard" href="/simulateur-paie-spectacle-technicien/">
|
||
<span class="od-minititle">Backstage Paie</span>
|
||
<span class="od-minitext">Notre blog de la paie du spectacle</span>
|
||
</a>
|
||
<a class="od-minicard" href="/externalisation-paie-intermittent-spectacle/">
|
||
<span class="od-minititle">Liste des emplois des Annexes 8 & 10</span>
|
||
<span class="od-minitext">Liste interactive des professions éligibles au CDDU</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Carte : Partage -->
|
||
<div class="od-card">
|
||
<h3 class="od-card-title">Partager cette simulation</h3>
|
||
<div class="od-share">
|
||
<!-- Bouton (dans ta card "Partager cette simulation") -->
|
||
<button id="shareCopyBtn" class="od-btn od-btn-secondary" type="button">
|
||
<i class="fa fa-link" aria-hidden="true"></i> Copier le lien
|
||
</button>
|
||
<a class="od-btn od-btn-secondary" id="od-whatsapp" target="_blank" rel="noopener">WhatsApp</a>
|
||
</div>
|
||
<div class="od-sim-share card">
|
||
<button id="od-download-pdf" class="od-btn" type="button">
|
||
Télécharger en PDF
|
||
</button>
|
||
<div id="od-download-msg" class="od-msg" aria-live="polite"></div>
|
||
</div>
|
||
<p class="od-micro">Le lien reprend vos paramètres (montant, CC, statut…)</p>
|
||
</div>
|
||
|
||
<!-- Carte : Disclaimer -->
|
||
<div class="od-card">
|
||
<h3 class="od-card-title">Limitations du simulateur | Mentions & conditions</h3>
|
||
<p class="od-micro">
|
||
Le simulateur ne prévoit pas les cas particuliers comme :
|
||
mineurs de moins de 16 ans, cumul annuel, éventuelle taxe sur les salaires, éventuelle taxe d'apprentissage, non-résidents fiscaux français, contrats multi-mois.
|
||
<br> <br> Résultats donnés à titre indicatif, sans valeur contractuelle.
|
||
Le simulateur ne couvre pas tous les cas particuliers.
|
||
<a href="#mentions" style="text-decoration:underline !important;color:#111827 !important;">Voir les mentions complètes</a>.
|
||
</p>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Fonts pour Oswald -->
|
||
<link href="https://fonts.googleapis.com/css2?family=Oswald:wght@500;600&display=swap" rel="stylesheet">
|
||
|
||
<style>
|
||
/* Panneau sticky */
|
||
.od-simu-aside{position:sticky !important;top:88px !important}
|
||
|
||
/* Cards */
|
||
.od-card{background:#fff !important;border-radius:14px !important;padding:16px 18px !important;box-shadow:0 8px 24px rgba(0,0,0,.08) !important;margin-bottom:16px !important}
|
||
|
||
/* Titres : Oswald noir */
|
||
.od-card-title{margin:0 0 8px !important;font-size:1.08rem !important;
|
||
font-family:'Oswald',system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Open Sans",sans-serif !important;
|
||
color:#000 !important;letter-spacing:.2px !important}
|
||
|
||
/* Texte */
|
||
.od-note,.od-micro{color:#6b7280 !important;font-size:.9rem !important;margin:.5rem 0 0 !important}
|
||
.od-list{margin:0 !important;padding-left:18px !important}
|
||
.od-list li{margin:.25rem 0 !important}
|
||
|
||
/* CTA */
|
||
.od-cta{border:2px solid #eab308 !important}
|
||
.od-btn{display:inline-block !important;padding:10px 14px !important;border-radius:10px !important;background:#111827 !important;color:#fff !important;text-decoration:none !important}
|
||
.od-btn:hover{opacity:.92 !important}
|
||
.od-btn-secondary{background:#eef2ff !important;color:#111827 !important}
|
||
.od-btn[disabled]{ opacity:.6; cursor:not-allowed }
|
||
|
||
/* Badges info */
|
||
.od-badges{display:flex !important;flex-wrap:wrap !important;gap:8px !important;margin:0 0 12px !important;padding:0 !important;list-style:none !important}
|
||
.od-badges li{background:#f3f4f6 !important;border-radius:999px !important;padding:6px 10px !important;font-size:.9rem !important}
|
||
|
||
/* Mini-cards cliquables (liens) */
|
||
.od-minicards{display:grid !important;grid-template-columns:1fr !important;gap:10px !important}
|
||
.od-minicard{display:block !important;background:#f8fafc !important;border:1px solid #e5e7eb !important;border-radius:12px !important;
|
||
padding:10px 12px !important;text-decoration:none !important;transition:transform .12s ease, box-shadow .12s ease !important}
|
||
.od-minicard:hover{transform:translateY(-1px) !important;box-shadow:0 6px 16px rgba(0,0,0,.08) !important}
|
||
.od-minititle{display:block !important;
|
||
font-family:'Oswald',system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Open Sans",sans-serif !important;
|
||
color:#000 !important;font-size:1rem !important;line-height:1.2 !important}
|
||
.od-minitext{display:block !important;color:#6b7280 !important;font-size:.9rem !important;margin-top:2px !important}
|
||
|
||
/* Partage */
|
||
.od-share{display:flex !important;gap:8px !important}
|
||
|
||
/* Responsive */
|
||
@media (max-width:1024px){
|
||
.od-simu-aside{position:static !important}
|
||
}
|
||
|
||
#shareCopyBtn i{ margin-right:.5rem !important; }
|
||
|
||
/* ===== Odentas • Skin SweetAlert ===== */
|
||
.swal-overlay{ background: rgba(17,24,39,.66) !important; } /* #111827 */
|
||
|
||
.swal-modal.od-swal{
|
||
position: relative !important;
|
||
background: linear-gradient(180deg,#1f2937,#111827) !important;
|
||
color:#E5E7EB !important;
|
||
border-radius:16px !important;
|
||
padding:18px 18px 14px !important;
|
||
border:1px solid #374151 !important;
|
||
box-shadow:0 22px 60px rgba(0,0,0,.35) !important;
|
||
}
|
||
.swal-modal.od-swal::before{
|
||
content:""; position:absolute; left:0; top:0; width:100%; height:4px;
|
||
background:#eab308; border-top-left-radius:16px; border-top-right-radius:16px;
|
||
}
|
||
|
||
.od-swal .swal-title{
|
||
font-family:'Oswald',system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Open Sans",sans-serif !important;
|
||
color:#fff !important; font-size:22px !important; margin:2px 0 6px !important; letter-spacing:.2px !important;
|
||
}
|
||
.od-swal .swal-text{ color:#cbd5e1 !important; font-size:14px !important; line-height:1.45 !important; }
|
||
|
||
.od-swal .swal-footer{
|
||
margin-top:14px !important; display:flex !important; gap:8px !important; justify-content:flex-end !important;
|
||
}
|
||
.od-swal .swal-button{
|
||
border-radius:10px !important; padding:10px 14px !important; font-weight:600 !important; box-shadow:none !important;
|
||
}
|
||
.od-swal .swal-button--confirm{ background:#eab308 !important; color:#111827 !important; }
|
||
.od-swal .swal-button--cancel{ background:#111827 !important; color:#fff !important; border:1px solid #374151 !important; }
|
||
|
||
/* Icône succès un peu plus punchy */
|
||
.od-swal .swal-icon--success{ border-color:#22c55e !important; }
|
||
.od-swal .swal-icon--success__line{ background:#22c55e !important; }
|
||
.od-swal .swal-icon--success__ring{ border:4px solid rgba(34,197,94,.25) !important; }
|
||
|
||
/* Lien affiché dans la modale */
|
||
.od-swal .od-swal-link{
|
||
display:block; margin-top:8px; padding:8px; background:#0b1220; color:#93c5fd;
|
||
border:1px solid #1f2937; border-radius:8px; font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
word-break:break-all;
|
||
}
|
||
|
||
</style>
|
||
|
||
|
||
<script>
|
||
(function(){
|
||
// === Config ===
|
||
const API_URL = "https://itmhabom80.execute-api.eu-west-3.amazonaws.com/simulation-pdf";
|
||
// (Optionnel) si tu as créé l’endpoint de statut :
|
||
const STATUS_URL = ""; // sinon laisse vide ""
|
||
const USE_JWT = false; // mets true si tu veux envoyer un JWT
|
||
const JWT_TOKEN = ""; // ex: window.odJWT
|
||
|
||
const btn = document.getElementById("od-download-pdf");
|
||
const msg = document.getElementById("od-download-msg");
|
||
|
||
function hasSimulation(){
|
||
const s = window.odentasSimulation;
|
||
return !!(s && s.results && s.results.kpis && s.results.contributions);
|
||
}
|
||
|
||
function buildPayload(){
|
||
const s = window.odentasSimulation;
|
||
return {
|
||
profile: s.profile || {},
|
||
inputs: s.inputs || {},
|
||
results: s.results || {},
|
||
cta: s.cta || {},
|
||
branding:s.branding|| {},
|
||
legal: s.legal || {},
|
||
contact: s.contact || {}
|
||
};
|
||
}
|
||
|
||
function setBusy(b){
|
||
if(!btn) return;
|
||
btn.disabled = b;
|
||
btn.textContent = b ? "Génération en cours…" : "Télécharger en PDF";
|
||
}
|
||
function setMsg(t, kind="info"){
|
||
if(!msg) return;
|
||
msg.textContent = t || "";
|
||
msg.style.color = (kind==="error"?"#b91c1c": kind==="ok"?"#065f46":"#111827");
|
||
}
|
||
|
||
async function callGenerate(payload){
|
||
const headers = { "Content-Type":"application/json" };
|
||
if (USE_JWT && JWT_TOKEN) headers["Authorization"] = "Bearer " + JWT_TOKEN;
|
||
|
||
const res = await fetch(API_URL, { method:"POST", headers, body: JSON.stringify(payload) });
|
||
const data = await res.json().catch(()=> ({}));
|
||
return { status: res.status, data };
|
||
}
|
||
|
||
async function pollStatus(docId, maxMs=30000, everyMs=1200){
|
||
const start = Date.now();
|
||
while (Date.now() - start < maxMs){
|
||
const url = STATUS_URL + "?id=" + encodeURIComponent(docId);
|
||
const r = await fetch(url, { method:"GET" });
|
||
const j = await r.json().catch(()=> ({}));
|
||
// Attends un objet { state, download_url }
|
||
if (j && j.download_url){
|
||
return j.download_url;
|
||
}
|
||
await new Promise(res => setTimeout(res, everyMs));
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function onClick(){
|
||
try{
|
||
if(!hasSimulation()){
|
||
setMsg("Aucune simulation à exporter. Lance d’abord un calcul.", "error");
|
||
return;
|
||
}
|
||
setBusy(true); setMsg("Préparation du PDF…");
|
||
|
||
const payload = buildPayload();
|
||
const { status, data } = await callGenerate(payload);
|
||
|
||
// 200 => on a l’URL S3 présignée
|
||
if (status === 200 && data && data.url){
|
||
setMsg("Téléchargement…", "ok");
|
||
window.open(data.url, "_blank");
|
||
setBusy(false);
|
||
return;
|
||
}
|
||
|
||
// 202 => génération en cours ; si tu as un /status, on poll
|
||
if (status === 202 && data && data.processing){
|
||
if (!STATUS_URL){
|
||
setMsg("PDF en cours de génération… réessaye dans quelques secondes.", "info");
|
||
setBusy(false);
|
||
return;
|
||
}
|
||
setMsg("Génération en cours…");
|
||
const dl = await pollStatus(data.id);
|
||
if (dl){
|
||
setMsg("Téléchargement…", "ok");
|
||
window.location.href = dl;
|
||
}else{
|
||
setMsg("Le PDF met trop de temps à se générer. Réessaye ou choisis l’envoi par e-mail.", "error");
|
||
}
|
||
setBusy(false);
|
||
return;
|
||
}
|
||
|
||
// autre cas
|
||
setMsg("Erreur lors de la génération du PDF.", "error");
|
||
console.error("Generate PDF error:", status, data);
|
||
|
||
}catch(e){
|
||
console.error(e);
|
||
setMsg("Erreur réseau. Vérifie ta connexion et réessaie.", "error");
|
||
}finally{
|
||
setBusy(false);
|
||
}
|
||
}
|
||
|
||
if (btn) btn.addEventListener("click", onClick);
|
||
})();
|
||
</script>
|
||
|
||
<script>
|
||
(function(){
|
||
/* ===============================
|
||
* 1) Permalien de simulation
|
||
* =============================== */
|
||
function serializeState(){
|
||
const p = new URLSearchParams();
|
||
const convEl = document.getElementById('conventionSelect');
|
||
p.set('ccn', convEl?.value || '');
|
||
|
||
const catVal = (document.getElementById('categorieSelect')?.value === 'technicien') ? 'tech' : 'art';
|
||
p.set('cat', catVal);
|
||
|
||
if (catVal === 'art'){
|
||
p.set('statut', document.getElementById('statutSelect').value === 'cadre' ? 'cadre' : 'nc');
|
||
const abOui = document.getElementById('abattementOui').checked;
|
||
p.set('ab', abOui ? '1' : '0');
|
||
if (abOui){
|
||
p.set('prof', document.getElementById('professionSelect').value || '');
|
||
}
|
||
const cachets = parseInt(document.getElementById('cachetsInput').value || '0', 10);
|
||
p.set('cachets', String(Math.max(0, cachets)));
|
||
} else {
|
||
p.set('cachets', '0');
|
||
}
|
||
|
||
const heures = parseFloat(document.getElementById('heuresInput').value || '0');
|
||
p.set('heures', isFinite(heures) ? String(heures) : '0');
|
||
|
||
const datesStr = document.getElementById('datesInput').value;
|
||
if (datesStr) p.set('dates', datesStr.replace(/\s+/g,''));
|
||
|
||
const montant = document.getElementById('montantInput').value;
|
||
if (montant) p.set('montant', montant);
|
||
|
||
const type = document.querySelector('input[name="type"]:checked')?.value || 'brut';
|
||
p.set('type', type);
|
||
|
||
p.set('v','1');
|
||
return p.toString();
|
||
}
|
||
function buildShareUrl(){
|
||
const base = location.origin + location.pathname;
|
||
return base + '?' + serializeState();
|
||
}
|
||
|
||
/* ===============================
|
||
* 2) Message & WhatsApp
|
||
* =============================== */
|
||
function buildMessage(){
|
||
const link = (typeof buildShareUrl === 'function') ? buildShareUrl() : location.href;
|
||
const s = window.odentasSimulation;
|
||
const role = (s?.profile?.role) ? ` (${s.profile.role})` : '';
|
||
const header = `Bonjour ! Voici ma simulation de salaire intermittent calculée avec Odentas.fr${role} :`;
|
||
|
||
if (s?.results?.kpis){
|
||
const k = s.results.kpis;
|
||
// Ligne vide après le header + ligne vide avant le lien
|
||
return [
|
||
header,
|
||
'',
|
||
`• Brut : ${k.brut}`,
|
||
`• Net avant PAS : ${k.net_avant_pas}`,
|
||
`• Coût employeur : ${k.cout_total_employeur}`,
|
||
'',
|
||
link
|
||
].join('\n');
|
||
}
|
||
|
||
// Fallback minimal avec aérations
|
||
return [header, '', link].join('\n');
|
||
}
|
||
function normalizePhone(raw){
|
||
if (!raw) return '';
|
||
let s = String(raw).trim().replace(/[\s.\-()]/g,'');
|
||
if (s.startsWith('+')) s = s.slice(1);
|
||
if (s.startsWith('0')) s = '33' + s.slice(1);
|
||
return s;
|
||
}
|
||
function buildWhatsAppUrl(){
|
||
const waBtn = document.getElementById('od-whatsapp');
|
||
const phoneEl = document.getElementById('wa-phone'); // optionnel
|
||
const fromInput = phoneEl && phoneEl.value ? phoneEl.value : '';
|
||
const fromData = waBtn?.dataset?.phone || '';
|
||
const phone = normalizePhone(fromInput || fromData);
|
||
const base = phone ? `https://wa.me/${phone}` : `https://wa.me/`;
|
||
return `${base}?text=${encodeURIComponent(buildMessage())}`;
|
||
}
|
||
|
||
/* ===============================
|
||
* 3) SweetAlert skinné Odentas
|
||
* =============================== */
|
||
function showShareSuccess(link){
|
||
const content = document.createElement('div');
|
||
content.innerHTML = `
|
||
Collez-le où vous voulez, ou partagez-le directement.
|
||
<a class="od-swal-link" href="${link}" target="_blank" rel="noopener">${link}</a>
|
||
`;
|
||
swal({
|
||
title: "Lien copié ✅",
|
||
content,
|
||
icon: null,
|
||
buttons: {
|
||
cancel: { text: "Fermer", visible: true },
|
||
confirm: { text: "Partager via WhatsApp", closeModal: true }
|
||
},
|
||
className: "od-swal",
|
||
closeOnClickOutside: true
|
||
}).then(goWhatsApp => {
|
||
if (!goWhatsApp) return;
|
||
window.open(buildWhatsAppUrl(), "_blank", "noopener");
|
||
});
|
||
}
|
||
|
||
/* ===============================
|
||
* 4) Événements UI
|
||
* =============================== */
|
||
const copyBtn = document.getElementById('shareCopyBtn');
|
||
const waBtn = document.getElementById('od-whatsapp');
|
||
|
||
if(copyBtn){
|
||
copyBtn.addEventListener('click', async ()=>{
|
||
const link = buildShareUrl();
|
||
try {
|
||
await navigator.clipboard.writeText(link);
|
||
} catch(e){
|
||
// fallback
|
||
const ta = document.createElement('textarea');
|
||
ta.value = link; document.body.appendChild(ta); ta.select();
|
||
document.execCommand('copy'); ta.remove();
|
||
}
|
||
showShareSuccess(link);
|
||
});
|
||
}
|
||
|
||
if(waBtn){
|
||
// met à jour le href juste avant d’ouvrir
|
||
waBtn.addEventListener('click', ()=> { waBtn.href = buildWhatsAppUrl(); });
|
||
// initialise une première fois
|
||
waBtn.href = buildWhatsAppUrl();
|
||
}
|
||
|
||
/* ===============================
|
||
* 5) Pré-remplissage depuis l’URL
|
||
* =============================== */
|
||
function applyParamsFromUrl(){
|
||
const qs = new URLSearchParams(location.search);
|
||
if (![...qs].length) return;
|
||
|
||
const ccn = qs.get('ccn');
|
||
if (ccn) document.getElementById('conventionSelect').value = ccn;
|
||
|
||
const cat = qs.get('cat') === 'tech' ? 'technicien' : 'artiste';
|
||
document.getElementById('categorieSelect').value = cat;
|
||
if (typeof toggleUIOnCategorieChange === 'function') toggleUIOnCategorieChange();
|
||
|
||
if (cat === 'artiste'){
|
||
const st = qs.get('statut');
|
||
if (st) document.getElementById('statutSelect').value = (st === 'cadre' ? 'cadre' : 'non-cadre');
|
||
|
||
const ab = qs.get('ab') === '1';
|
||
document.getElementById('abattementOui').checked = ab;
|
||
document.getElementById('abattementNon').checked = !ab;
|
||
document.getElementById('professionBlock').style.display = ab ? 'block' : 'none';
|
||
|
||
const prof = qs.get('prof');
|
||
if (prof) document.getElementById('professionSelect').value = prof;
|
||
|
||
const cachets = qs.get('cachets');
|
||
if (cachets !== null) document.getElementById('cachetsInput').value = cachets;
|
||
}
|
||
|
||
const heures = qs.get('heures');
|
||
if (heures !== null) document.getElementById('heuresInput').value = heures;
|
||
|
||
const dates = qs.get('dates');
|
||
if (dates){
|
||
const fp = document.getElementById('datesInput')._flatpickr;
|
||
if (fp) fp.setDate(dates.split(','), true);
|
||
else document.getElementById('datesInput').value = dates;
|
||
}
|
||
|
||
const montant = qs.get('montant');
|
||
if (montant !== null) document.getElementById('montantInput').value = montant;
|
||
|
||
const type = qs.get('type');
|
||
if (type){
|
||
const r = document.querySelector(`input[name="type"][value="${type}"]`);
|
||
if (r) r.checked = true;
|
||
}
|
||
|
||
// lance le calcul
|
||
const calc = document.getElementById('calcBtn');
|
||
if (calc) calc.click();
|
||
}
|
||
document.addEventListener('DOMContentLoaded', applyParamsFromUrl);
|
||
})();
|
||
</script> |