Correctif bases spécifiques simulateur

This commit is contained in:
odentas 2025-10-16 00:03:58 +02:00
parent 2497a80f4d
commit dd4e8d17b1
4 changed files with 525 additions and 81 deletions

View file

@ -33,23 +33,63 @@ PostHog
### Nom de l'événement ### Nom de l'événement
`simulateur_calculation` `simulateur_calculation`
### Propriétés ### Propriétés complètes
| Propriété | Type | Description | Exemple | | Propriété | Type | Description | Exemple |
|-----------|------|-------------|---------| |-----------|------|-------------|---------|
| **Paramètres du formulaire** ||||
| `ccn_id` | string | ID de la CCN sélectionnée | `"1285"` |
| `ccn_nom` | string | Nom complet de la CCN | `"1285 Entreprises Artistiques & Culturelles (CCNEAC)"` |
| `categorie` | string | Catégorie du salarié | `"artiste"` ou `"technicien"` | | `categorie` | string | Catégorie du salarié | `"artiste"` ou `"technicien"` |
| `type_calcul` | string | Type de calcul | `"brut"`, `"net"` ou `"cost"` | | `statut` | string | Statut professionnel | `"non-cadre"` ou `"cadre"` |
| `montant` | number | Montant saisi | `2000` | | `abattement_active` | boolean | Abattement pour frais pro activé | `true` ou `false` |
| `timestamp` | string | Date/heure de l'événement | `"2025-10-15T14:30:00.000Z"` | | `abattement_profession` | string | Type de profession (si abattement) | `"drama"`, `"musique"` ou `""` |
| **Cachets, heures et dates** ||||
| `cachets` | number | Nombre de cachets | `10` |
| `heures` | number | Nombre d'heures | `35` |
| `dates_travail` | array | Liste des dates de travail | `["2025-10-15", "2025-10-16"]` |
| `nombre_jours` | number | Nombre de jours travaillés | `2` |
| **Montant et type de calcul** ||||
| `montant_saisi` | number | Montant saisi par l'utilisateur | `2000` |
| `type_calcul` | string | Type de calcul effectué | `"brut"`, `"net"` ou `"cost"` |
| **Résultats calculés** ||||
| `resultat_brut` | number | Salaire brut calculé | `2000.00` |
| `resultat_net` | number | Salaire net avant PAS calculé | `1580.50` |
| `resultat_cost` | number | Coût total employeur calculé | `2456.80` |
| **Métadonnées** ||||
| `plafond_urssaf` | number | Plafond URSSAF appliqué | `3864` |
| `timestamp` | string | Date/heure de la simulation | `"2025-10-16T14:30:00.000Z"` |
### Exemple d'événement ### Exemple d'événement complet
```javascript ```javascript
{ {
event: 'simulateur_calculation', event: 'simulateur_calculation',
properties: { properties: {
// Paramètres du formulaire
ccn_id: '1285',
ccn_nom: '1285 Entreprises Artistiques & Culturelles (CCNEAC)',
categorie: 'artiste', categorie: 'artiste',
statut: 'non-cadre',
abattement_active: true,
abattement_profession: 'drama',
// Cachets, heures et dates
cachets: 10,
heures: 35,
dates_travail: ['2025-10-15', '2025-10-16', '2025-10-17'],
nombre_jours: 3,
// Montant et type de calcul
montant_saisi: 2000,
type_calcul: 'brut', type_calcul: 'brut',
montant: 2000,
timestamp: '2025-10-15T14:30:00.000Z' // Résultats calculés
resultat_brut: 2000.00,
resultat_net: 1580.50,
resultat_cost: 2456.80,
// Métadonnées
plafond_urssaf: 3864,
timestamp: '2025-10-16T14:30:00.000Z'
} }
} }
``` ```
@ -57,17 +97,56 @@ PostHog
## Fichiers modifiés ## Fichiers modifiés
### 1. `/public/simulateur-embed.html` ### 1. `/public/simulateur-embed.html`
**Ligne 1120+** : Ajout de l'envoi du message via `postMessage` dans le gestionnaire du bouton "Calculer" **Ligne 1326+** : Ajout de l'envoi du message enrichi via `postMessage` dans le gestionnaire du bouton "Calculer"
```javascript ```javascript
// 📊 Envoyer un événement PostHog via postMessage au parent // 📊 Envoyer un événement PostHog enrichi via postMessage au parent
try { 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({ window.parent.postMessage({
type: 'simulateur_calculation', type: 'simulateur_calculation',
data: { data: {
categorie: isTechnicien() ? 'technicien' : 'artiste', // Paramètres du formulaire
type: document.querySelector('input[name="type"]:checked').value, ccn_id: ccnValue,
montant: montant 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,
// 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()
} }
}, '*'); }, '*');
} catch (e) { } catch (e) {
@ -76,28 +155,49 @@ try {
``` ```
### 2. `/app/(app)/simulateur/page.tsx` ### 2. `/app/(app)/simulateur/page.tsx`
**Lignes 1-35** : Ajout du hook PostHog et de l'écouteur de messages **Lignes 12-50** : Mise à jour du hook PostHog pour capturer toutes les propriétés enrichies
```typescript ```typescript
import { usePostHog } from 'posthog-js/react'; useEffect(() => {
const handleMessage = (event: MessageEvent) => {
export default function SimulateurPage() { if (event.data?.type === 'simulateur_calculation') {
const posthog = usePostHog(); const data = event.data.data;
// 📊 Écouter les messages de l'iframe pour tracker les calculs posthog?.capture('simulateur_calculation', {
useEffect(() => { // Paramètres du formulaire
const handleMessage = (event: MessageEvent) => { ccn_id: data.ccn_id,
if (event.data?.type === 'simulateur_calculation') { ccn_nom: data.ccn_nom,
const { categorie, type, montant } = event.data.data; categorie: data.categorie,
statut: data.statut,
abattement_active: data.abattement_active,
abattement_profession: data.abattement_profession,
posthog?.capture('simulateur_calculation', { // Cachets, heures, dates
categorie, cachets: data.cachets,
type_calcul: type, heures: data.heures,
montant, dates_travail: data.dates_travail,
timestamp: new Date().toISOString() nombre_jours: data.nombre_jours,
});
} // Montant saisi et type
}; montant_saisi: data.montant_saisi,
type_calcul: data.type_calcul,
// Résultats calculés
resultat_brut: data.resultat_brut,
resultat_net: data.resultat_net,
resultat_cost: data.resultat_cost,
// Métadonnées
plafond_urssaf: data.plafond_urssaf,
timestamp: data.timestamp
});
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [posthog]);
```
window.addEventListener('message', handleMessage); window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage);
@ -136,7 +236,17 @@ Targeting:
### Dans la console du navigateur ### Dans la console du navigateur
Lors d'un clic sur "Calculer", vous devriez voir : Lors d'un clic sur "Calculer", vous devriez voir :
``` ```
📊 PostHog: Événement simulateur_calculation envoyé { categorie: 'artiste', type: 'brut', montant: 2000 } 📊 PostHog: Événement simulateur_calculation enrichi envoyé {
categorie: 'artiste',
ccn: '1285 Entreprises Artistiques & Culturelles (CCNEAC)',
type_calcul: 'brut',
montant_saisi: 2000,
resultats: {
brut: 2000,
net: 1580.5,
cost: 2456.8
}
}
``` ```
### Dans PostHog ### Dans PostHog
@ -168,27 +278,81 @@ Lors d'un clic sur "Calculer", vous devriez voir :
## Évolutions futures ## Évolutions futures
### Idées d'améliorations ### Analyses possibles avec les données enrichies
- Tracker également les modifications de paramètres (CCN, catégorie, etc.) Avec toutes ces propriétés, vous pouvez maintenant analyser :
- Ajouter le temps passé sur le simulateur
- Tracker les exports PDF (si fonctionnalité ajoutée)
- Ajouter des événements pour les erreurs de validation
### Autres métriques possibles - **Par CCN** : Quelle convention collective est la plus utilisée ?
```javascript - **Par catégorie** : Artiste vs Technicien - ratio d'utilisation
// Exemple d'événements supplémentaires - **Par type de calcul** : Les utilisateurs partent-ils plutôt du brut, net ou coût ?
posthog.capture('simulateur_ccn_changed', { ccn: '1285' }); - **Abattement** : Combien d'utilisateurs utilisent l'abattement pour frais professionnels ?
posthog.capture('simulateur_export_pdf', { format: 'pdf' }); - **Montants moyens** : Salaire brut moyen, net moyen, coût moyen par CCN
posthog.capture('simulateur_error', { error_type: 'invalid_amount' }); - **Heures et cachets** : Patterns d'utilisation (nombre moyen de cachets, heures par simulation)
- **Périodes** : Analyse temporelle des simulations (jours, semaines, mois)
### Exemples de requêtes PostHog
**Salaire brut moyen par CCN** :
```sql
SELECT
ccn_nom,
AVG(resultat_brut) as brut_moyen,
COUNT(*) as nb_simulations
FROM events
WHERE event = 'simulateur_calculation'
GROUP BY ccn_nom
ORDER BY nb_simulations DESC
``` ```
**Répartition Artiste vs Technicien** :
```sql
SELECT
categorie,
COUNT(*) as total,
AVG(resultat_brut) as brut_moyen
FROM events
WHERE event = 'simulateur_calculation'
GROUP BY categorie
```
**Taux d'utilisation de l'abattement** :
```sql
SELECT
abattement_active,
COUNT(*) as total,
(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER ()) as pourcentage
FROM events
WHERE event = 'simulateur_calculation'
AND categorie = 'artiste'
GROUP BY abattement_active
```
### Idées d'améliorations futures
- Tracker les exports PDF (si fonctionnalité ajoutée)
- Ajouter le temps passé sur le simulateur
- Tracker les erreurs de validation
- Capturer le device type (mobile/desktop)
- Ajouter des événements pour les modifications de paramètres sans calcul
## Notes techniques ## Notes techniques
- ✅ Le simulateur est maintenant actif dans la sidebar - ✅ Le simulateur est maintenant actif dans la sidebar
- ✅ Les événements PostHog sont trackés pour analyse - ✅ Les événements PostHog sont trackés avec **toutes les données de simulation**
- ✅ Compatible avec les surveys PostHog - ✅ Compatible avec les surveys PostHog
- ✅ Aucune dépendance externe supplémentaire requise - ✅ Aucune dépendance externe supplémentaire requise
- ✅ Fonctionne en mode production et développement - ✅ Fonctionne en mode production et développement
- ✅ **Données enrichies** : CCN, catégorie, statut, abattement, cachets, heures, dates, montants et résultats complets
## Données capturées (résumé)
| Catégorie | Données |
|-----------|---------|
| **CCN** | ID et nom complet de la convention |
| **Profil** | Catégorie (artiste/technicien), statut (cadre/non-cadre) |
| **Abattement** | Activation et type de profession |
| **Volumes** | Cachets, heures, dates de travail, nombre de jours |
| **Calcul** | Montant saisi, type de calcul (brut/net/cost) |
| **Résultats** | Brut, Net avant PAS, Coût Total Employeur |
| **Contexte** | Plafond URSSAF, timestamp |
## Date de mise en place ## Date de mise en place
15 octobre 2025 - **Première version** : 15 octobre 2025 (tracking basique)
- **Version enrichie** : 16 octobre 2025 (toutes les données)

View file

@ -14,17 +14,49 @@ export default function SimulateurPage() {
const handleMessage = (event: MessageEvent) => { const handleMessage = (event: MessageEvent) => {
// Vérifier que le message vient de notre iframe // Vérifier que le message vient de notre iframe
if (event.data?.type === 'simulateur_calculation') { if (event.data?.type === 'simulateur_calculation') {
const { categorie, type, montant } = event.data.data; const data = event.data.data;
// Envoyer l'événement à PostHog // Envoyer l'événement enrichi à PostHog
posthog?.capture('simulateur_calculation', { posthog?.capture('simulateur_calculation', {
categorie, // Paramètres du formulaire
type_calcul: type, ccn_id: data.ccn_id,
montant, ccn_nom: data.ccn_nom,
timestamp: new Date().toISOString() categorie: data.categorie,
statut: data.statut,
abattement_active: data.abattement_active,
abattement_profession: data.abattement_profession,
// Cachets, heures, dates
cachets: data.cachets,
heures: data.heures,
dates_travail: data.dates_travail,
nombre_jours: data.nombre_jours,
// Montant saisi et type
montant_saisi: data.montant_saisi,
type_calcul: data.type_calcul,
// Résultats calculés
resultat_brut: data.resultat_brut,
resultat_net: data.resultat_net,
resultat_cost: data.resultat_cost,
// Métadonnées
plafond_urssaf: data.plafond_urssaf,
timestamp: data.timestamp
}); });
console.log('📊 PostHog: Événement simulateur_calculation envoyé', { categorie, type, montant }); console.log('📊 PostHog: Événement simulateur_calculation enrichi envoyé', {
categorie: data.categorie,
ccn: data.ccn_nom,
type_calcul: data.type_calcul,
montant_saisi: data.montant_saisi,
resultats: {
brut: data.resultat_brut,
net: data.resultat_net,
cost: data.resultat_cost
}
});
} }
}; };

View file

@ -458,7 +458,7 @@ export default function Sidebar({ clientInfo, isStaff = false, mobile = false, o
<DisabledMenuItem <DisabledMenuItem
icon={Scale} icon={Scale}
label="Minima CCN" label="Minima CCN"
tooltipMessage="Le simulateur et les minima seront de retour dans quelques jours." tooltipMessage="Les minima CCN seront de retour dans quelques jours."
/> />
<Link href="/simulateur" onClick={() => onNavigate && onNavigate()} className={`block px-3 py-2 rounded-xl text-sm transition truncate ${ <Link href="/simulateur" onClick={() => onNavigate && onNavigate()} className={`block px-3 py-2 rounded-xl text-sm transition truncate ${
isActivePath(pathname, "/simulateur") ? "bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-200 text-slate-700 font-semibold" : "hover:bg-slate-50" isActivePath(pathname, "/simulateur") ? "bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-200 text-slate-700 font-semibold" : "hover:bg-slate-50"

View file

@ -141,6 +141,9 @@
<button id="calcBtn">Calculer</button> <button id="calcBtn">Calculer</button>
</div> </div>
<!-- Alerts -->
<div id="alerts"></div>
<!-- Résultats --> <!-- Résultats -->
<div class="result"> <div class="result">
<h4>Résultat de la simulation</h4> <h4>Résultat de la simulation</h4>
@ -314,6 +317,27 @@ function getPlafondUrssaf() {
return PMSS * (diffDays / daysInMonth); 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 ===== */ /* ===== Compléments maladie / AF jour par jour ===== */
const HOURS_PER_CACHET_FOR_COMPLEMENT = 8; // pour les seuils 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 HOURS_PER_DAY_FOR_COMPLEMENT_IF_CACHET = 6.7; // forfait si cachet ce jour
@ -346,15 +370,40 @@ function getAssietteChomageMaxPourMoisCourant() {
const m = dates[0].getMonth(); const m = dates[0].getMonth();
const daysInMonth = new Date(y, m + 1, 0).getDate(); 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 : // Règle attendue :
// - < 5 jours : plafond chômage = 4 × plafond URSSAF de la période // - < 5 jours : plafond chômage = 4 × plafond URSSAF de la période
// - ≥ 5 jours : plafond chômage = 4 × PMSS × (nbJours / daysInMonth) // - ≥ 5 jours : plafond chômage = 4 × PMSS × (span en jours / daysInMonth)
if (diffDays < 5) { if (diffDays < 5) {
const plafUrssaf = getPlafondUrssaf(); const plafUrssaf = getPlafondUrssaf();
return 4 * plafUrssaf; return 4 * plafUrssaf;
} }
return 4 * PMSS * (nbJours / daysInMonth); 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 // Taux complémentaires selon catégorie
@ -478,6 +527,9 @@ function baseLibelle(code, cat){
"ags": "AGS intermittent", "ags": "AGS intermittent",
"retraite_t1": isTech ? "Retraite non-cadre Int. T1" : "Retraite artiste Tranche 1", "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", "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", "prevoyance_ta": isTech ? "Prévoyance non-cadre Int. TA" : "Prévoyance non-cadre Artiste Int. TA",
"conges_spectacles": "Congés spectacles", "conges_spectacles": "Congés spectacles",
"medecine_t1": isTech ? "Médecine du travail int. T1" : "Médecine du travail int.", "medecine_t1": isTech ? "Médecine du travail int. T1" : "Médecine du travail int.",
@ -514,19 +566,23 @@ function getProfileRates(cat){
} }
}; };
} }
// Artiste = base existante // Artiste
return { return {
sal: { sal: {
contrib_solidarite: 0, maladie: 0, vieillesse: 0.28, alloc_fam: 0, at: 0, 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, 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, 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, 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 paritarisme: 0, csg_deductible: 6.8, csg_imposable: 2.4, rds: 0.5
}, },
emp: { emp: {
contrib_solidarite: 0.3, maladie: 4.90, vieillesse: 1.41, alloc_fam: 2.42, at: 1.18, 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, 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, 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, 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 cfp_ta: 2.0, paritarisme: 0.016, csg_deductible: 0, csg_imposable: 0, rds: 0
} }
@ -538,13 +594,20 @@ function getCotisations(){
const cc = document.getElementById('conventionSelect')?.value; const cc = document.getElementById('conventionSelect')?.value;
const rates = getProfileRates(cat); const rates = getProfileRates(cat);
const codes = [ let codes = [
"contrib_solidarite","maladie","vieillesse","alloc_fam","at", "contrib_solidarite","maladie","vieillesse","alloc_fam","at",
"vieillesse_ta","fnal_plaf","maj_chomage","chomage","ags", "vieillesse_ta","fnal_plaf","maj_chomage","chomage","ags",
"retraite_t1","ceg_t1","prevoyance_ta","conges_spectacles", "retraite_t1","ceg_t1","prevoyance_ta","conges_spectacles",
"medecine_t1","cfpc_conv","cfp_ta","paritarisme", "medecine_t1","cfpc_conv","cfp_ta","paritarisme",
"csg_deductible","csg_imposable","rds" "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 === "3090") codes.push("casc_svp"); // CCNSVP
if (cc === "1285") { codes.push("fnas","fcap"); } // CCNEAC if (cc === "1285") { codes.push("fnas","fcap"); } // CCNEAC
@ -574,6 +637,47 @@ function getCotisations(){
return { sal, emp, cat }; 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 ===== */ /* ===== Prévoyance employeur ===== */
function prevoyanceEmployerAmount(brut) { function prevoyanceEmployerAmount(brut) {
const { emp } = getCotisations(); const { emp } = getCotisations();
@ -591,7 +695,8 @@ function calculateNet(brut, plafondUrssaf) {
const nonAbattementCodes = [ const nonAbattementCodes = [
"maj_chomage","chomage","ags","conges_spectacles", "maj_chomage","chomage","ags","conges_spectacles",
"csg_deductible","csg_imposable","rds", "csg_deductible","csg_imposable","rds",
"prevoyance_ta" // ✅ ne pas abattre la prévoyance "prevoyance_ta",
"cet"
]; ];
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant(); const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
@ -610,7 +715,17 @@ const nonAbattementCodes = [
} else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) { } else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
base = getPrevoyanceBase(brut); base = getPrevoyanceBase(brut);
} else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) { } else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) {
base = (brut + prevEmp) * 0.9825; 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 { } else {
base = brut; base = brut;
} }
@ -632,7 +747,8 @@ function computeEmployerCharges(brut, cachets, heures, plafondUrssaf) {
const nonAbattementCodes = [ const nonAbattementCodes = [
"maj_chomage","chomage","ags","conges_spectacles", "maj_chomage","chomage","ags","conges_spectacles",
"csg_deductible","csg_imposable","rds", "csg_deductible","csg_imposable","rds",
"prevoyance_ta" // ✅ ne pas abattre la prévoyance "prevoyance_ta",
"cet"
]; ];
// Fusionne codes salariaux/patronaux pour calculer la part patronale // Fusionne codes salariaux/patronaux pour calculer la part patronale
@ -664,7 +780,17 @@ const nonAbattementCodes = [
} else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) { } else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
base = getPrevoyanceBase(brut); base = getPrevoyanceBase(brut);
} else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) { } else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) {
base = (brut + prevEmp) * 0.9825; 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 { } else {
base = brut; base = brut;
} }
@ -864,7 +990,8 @@ function generateDetailTable(brut, cachets, heures, plafondUrssaf) {
const nonAbattementCodes = [ const nonAbattementCodes = [
"maj_chomage","chomage","ags","conges_spectacles", "maj_chomage","chomage","ags","conges_spectacles",
"csg_deductible","csg_imposable","rds", "csg_deductible","csg_imposable","rds",
"prevoyance_ta" // ✅ ne pas abattre la prévoyance "prevoyance_ta",
"cet"
]; ];
const { sal, emp, cat } = getCotisations(); const { sal, emp, cat } = getCotisations();
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant(); const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
@ -907,7 +1034,17 @@ const nonAbattementCodes = [
} else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) { } else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
base = getPrevoyanceBase(brut); base = getPrevoyanceBase(brut);
} else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) { } else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) {
base = (brut + prevEmp) * 0.9825; 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 { } else {
base = brut; base = brut;
} }
@ -983,7 +1120,8 @@ function buildContributionsStructured(brut, cachets, heures, plafondUrssaf){
const nonAbattementCodes = [ const nonAbattementCodes = [
"maj_chomage","chomage","ags","conges_spectacles", "maj_chomage","chomage","ags","conges_spectacles",
"csg_deductible","csg_imposable","rds", "csg_deductible","csg_imposable","rds",
"prevoyance_ta" // ✅ ne pas abattre la prévoyance "prevoyance_ta",
"cet"
]; ];
const { sal, emp, cat } = getCotisations(); const { sal, emp, cat } = getCotisations();
const assietteChomageMax = getAssietteChomageMaxPourMoisCourant(); const assietteChomageMax = getAssietteChomageMaxPourMoisCourant();
@ -1100,7 +1238,17 @@ if (fillonTotal > 0) {
} else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) { } else if (c.code === "prevoyance_ta" || (c.libelle && c.libelle.includes("Prévoyance"))) {
base = getPrevoyanceBase(brut); base = getPrevoyanceBase(brut);
} else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) { } else if (["csg_deductible","csg_imposable","rds"].includes(c.code)) {
base = (brut + prevoyanceEmployerAmount(brut)) * 0.9825; 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 { } else {
base = brut; base = brut;
} }
@ -1159,20 +1307,6 @@ document.getElementById('calcBtn').addEventListener('click', function() {
return; return;
} }
// 📊 Envoyer un événement PostHog via postMessage au parent
try {
window.parent.postMessage({
type: 'simulateur_calculation',
data: {
categorie: isTechnicien() ? 'technicien' : 'artiste',
type: document.querySelector('input[name="type"]:checked').value,
montant: montant
}
}, '*');
} catch (e) {
console.error('Erreur lors de l\'envoi de l\'événement PostHog:', e);
}
const plafondUrssaf = getPlafondUrssaf(); const plafondUrssaf = getPlafondUrssaf();
const type = document.querySelector('input[name="type"]:checked').value; const type = document.querySelector('input[name="type"]:checked').value;
@ -1191,6 +1325,119 @@ document.getElementById('calcBtn').addEventListener('click', function() {
({ net, costEmployer } = calculateSalaries(brut, 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 = ` const resultTable = `
<table class="result-table"> <table class="result-table">
<tr> <tr>
@ -1205,6 +1452,7 @@ document.getElementById('calcBtn').addEventListener('click', function() {
</tr> </tr>
</table> </table>
<p>Plafond URSSAF : ${fmtEuro(plafondUrssaf)} €</p> <p>Plafond URSSAF : ${fmtEuro(plafondUrssaf)} €</p>
${dbgHtml}
`; `;
document.getElementById('result').innerHTML = resultTable; document.getElementById('result').innerHTML = resultTable;
document.getElementById('detailTable').innerHTML = generateDetailTable(brut, cachets, heures, plafondUrssaf); document.getElementById('detailTable').innerHTML = generateDetailTable(brut, cachets, heures, plafondUrssaf);