HTML 82.1%
Python 14.6%
TypeScript 1.9%
CSS 1%
JavaScript 0.5%
1/**2 * =============================================================================3 * Job·Ka — Groupe KA4 * Auteur : Simon-Pierre Boucher5 * Contact : contact@spboucher.ai6 * Fichier : frontend/src/fiche/SalaryCard.tsx7 * Rôle : « Salaire et marché » — KPI (salaire affiché, médiane de la8 * catégorie, fourchette P25–P75), jauge de position du point médian9 * de l'offre dans la fenêtre interquartile, historique du salaire10 * affiché, méthodologie. Jamais d'estimation : sans salaire publié,11 * la carte le dit et montre seulement le marché.12 * Créé : 2026-09-0713 * =============================================================================14 */15import { Job, compactMoney, formatSalary, yearlyEquiv } from "../api";16import { Ico } from "../components/Icons";17import { SalaryCapsule } from "./JobHero";18import { ComparaisonSalaire } from "./synthese";19import { Accordion, SectionCard, StatTile, NBSP } from "./ui";2021function Gauge({ p25, p75, mid }: { p25: number; p75: number; mid: number }) {22 const lo = Math.min(p25 * 0.75, mid * 0.95), hi = Math.max(p75 * 1.25, mid * 1.05);23 const W = 320, H = 54;24 const x = (v: number) => 8 + ((Math.min(Math.max(v, lo), hi) - lo) / (hi - lo)) * (W - 16);25 const anchor = (px: number) => (px < 60 ? "start" : px > W - 60 ? "end" : "middle");26 return (27 <div className="jk-gauge">28 <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`Fenêtre P25–P75 de ${compactMoney(p25)} à ${compactMoney(p75)}, cette offre à ${compactMoney(mid)} par an`}>29 <rect className="jk-gauge-track" x="8" y="24" width={W - 16} height="8" rx="4" />30 <rect className="jk-gauge-box" x={x(p25)} y="22" width={Math.max(4, x(p75) - x(p25))} height="12" rx="4" />31 <text className="jk-gauge-txt" x={x(p25)} y="12" textAnchor={anchor(x(p25))}>P25 {compactMoney(p25)}</text>32 <text className="jk-gauge-txt" x={x(p75)} y="12" textAnchor={anchor(x(p75))}>P75 {compactMoney(p75)}</text>33 <circle className="jk-gauge-me" cx={x(mid)} cy="28" r="7" />34 <text className="jk-gauge-txt me" x={x(mid)} y="52" textAnchor={anchor(x(mid))}>cette offre ≈ {compactMoney(mid)}/an</text>35 </svg>36 </div>37 );38}3940export default function SalaryCard({ job, cmp }: { job: Job; cmp: ComparaisonSalaire | null }) {41 const ctx = job.salary_context;42 const salaire = formatSalary(job);43 const yearly = yearlyEquiv(job);44 const hist = (job.salary_history ?? []).filter((h) => h.s_min != null);45 if (!salaire && !ctx) return null;46 return (47 <SectionCard id="salaire" title="Salaire et marché" icon={<Ico name="scale" size={18} />} aside={cmp && <SalaryCapsule cmp={cmp} short />}>48 <div className="jk-kpis cols-3">49 <StatTile accent value={salaire ?? "Non affiché"} label={salaire ? (yearly ? `Salaire affiché · ${yearly}` : "Salaire affiché par l'employeur") : "L'employeur n'a pas publié de salaire"} />50 {ctx && <StatTile value={`${compactMoney(ctx.median)}/an`} label={`Médiane · ${ctx.category || "toutes catégories"}`} />}51 {ctx && <StatTile value={`${compactMoney(ctx.p25)} – ${compactMoney(ctx.p75)}`} label={`Fourchette P25–P75 · ${ctx.n.toLocaleString("fr-CA")} offres`} />}52 </div>53 {ctx && ctx.job_mid != null && <Gauge p25={ctx.p25} p75={ctx.p75} mid={ctx.job_mid} />}54 {ctx && ctx.job_mid == null && salaire == null && (55 <p className="jk-note info">Sans salaire publié, Job·Ka n'estime rien : seule la référence du marché de la catégorie est affichée.</p>56 )}57 {hist.length > 1 && (58 <>59 <h3 className="jk-card-sub jk-subtitle">Évolution du salaire affiché</h3>60 <ul className="jk-tl">61 {hist.map((h, i) => (62 <li key={i}>63 <span className="jk-tl-date">{new Date(h.ts * 1000).toLocaleDateString("fr-CA", { day: "numeric", month: "short", year: "numeric" })}</span>64 <b>{formatSalary({ salary_min: h.s_min, salary_max: h.s_max, salary_unit: h.unit })}</b>65 </li>66 ))}67 </ul>68 </>69 )}70 <Accordion title="Méthodologie" small>71 <p>72 Le salaire est celui publié par l'employeur (jamais estimé). La référence de marché est calculée par Job·Ka73 sur les offres actives de la même catégorie qui affichent un salaire, converties en équivalent annuel :74 médiane et quartiles P25–P75 ; l'écart compare le point médian de la fourchette de l'offre à cette médiane.75 Les avantages, primes et horaires ne sont pas pris en compte.{NBSP}76 </p>77 </Accordion>78 </SectionCard>79 );80}81