Checkpoint: Ajout de la projection annuelle prévisionnel-réel dans Masse salariale : import contrôlé du fichier modèle 2025-2027, réel 2026 provenant uniquement des liasses Salaires, ajustements administrateur limités aux primes et évolutions de l'année courante/future, conservation des centimes, modèle de calcul testé et documentation. 72 tests, TypeScript et build validés.

This commit is contained in:
Manus
2026-08-31 10:58:02 +00:00
parent 5c9618b204
commit 28570234d0
17 changed files with 2415 additions and 204 deletions

View File

@@ -1,261 +1,179 @@
// MasseSalariale.tsx — Données de rémunération SANTINOVA (accès administrateur)
// MasseSalariale.tsx — Prévisionnel et réel SANTINOVA, accès administrateur.
import { Fragment, useMemo, useState } from 'react';
import { AppSidebar } from '../components/AppSidebar';
import { MasseSalarialeDataState } from '../components/MasseSalarialeDataState';
import { trpc } from '../lib/trpc';
import { getMasseSalarialeUiState, getMasseSalarialeYearSummary } from '../lib/masseSalariale';
import { toast } from 'sonner';
import {
Banknote,
CalendarDays,
CircleAlert,
ShieldCheck,
Users,
} from 'lucide-react';
import { Fragment, useMemo, useState } from "react";
import { AppSidebar } from "../components/AppSidebar";
import { MasseSalarialeDataState } from "../components/MasseSalarialeDataState";
import { trpc } from "../lib/trpc";
import { getMasseSalarialeUiState } from "../lib/masseSalariale";
import { toast } from "sonner";
import { Banknote, CalendarDays, CircleAlert, LockKeyhole, ShieldCheck, Users } from "lucide-react";
const ANNEES = [2025, 2026, 2027] as const;
function formatEuros(cents?: number | null) {
if (cents === undefined || cents === null) return '—';
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
maximumFractionDigits: 0,
if (cents === undefined || cents === null) return "—";
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
minimumFractionDigits: cents % 100 === 0 ? 0 : 2,
maximumFractionDigits: 2,
}).format(cents / 100);
}
function formatPercent(bps?: number | null) {
if (bps === undefined || bps === null) return '—';
return `${(bps / 100).toLocaleString('fr-FR', { maximumFractionDigits: 2 })} %`;
if (bps === undefined || bps === null) return "—";
return `${(bps / 100).toLocaleString("fr-FR", { maximumFractionDigits: 2 })} %`;
}
function formatDate(isoDate?: string | null) {
if (!isoDate) return '—';
const [year, month, day] = isoDate.split('-');
if (!isoDate) return "—";
const [year, month, day] = isoDate.split("-");
return year && month && day ? `${day}/${month}/${year}` : isoDate;
}
function evolutionKey(salarieId: number, annee: number) {
function keyFor(salarieId: number, annee: number) {
return `${salarieId}-${annee}`;
}
function formatMonths(months: number[] | undefined) {
if (!months?.length) return "—";
return months.length === 12 ? "12 mois" : `${months.length} mois`;
}
/**
* Les revenus annuels sont strictement ceux présents dans les bulletins
* importés. L'évolution, elle, reste une hypothèse utilisateur distincte et
* n'engendre donc jamais un montant 2027 artificiel.
* Les prévisions viennent du classeur de référence. Le réel n'est affiché que
* lorsqu'il provient d'une clôture ou d'une liasse PDF effectivement archivée.
*/
export default function MasseSalariale() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [editingKey, setEditingKey] = useState<string | null>(null);
const [evolutionDraft, setEvolutionDraft] = useState('');
const [primeDraft, setPrimeDraft] = useState("");
const [evolutionDraft, setEvolutionDraft] = useState("");
const utils = trpc.useUtils();
const { data, isLoading, isError } = trpc.masseSalariale.get.useQuery();
const setEvolution = trpc.masseSalariale.setEvolution.useMutation({
const saveAdjustments = trpc.masseSalariale.setForecastAdjustments.useMutation({
onSuccess: async () => {
await utils.masseSalariale.get.invalidate();
setEditingKey(null);
setEvolutionDraft('');
toast.success('Évolution salariale enregistrée');
toast.success("Prévision mise à jour");
},
onError: () => toast.error("L'évolution salariale n'a pas pu être enregistrée"),
onError: (error) => toast.error(error.message || "La prévision n'a pas pu être enregistrée"),
});
const remunerationByKey = useMemo(
() => new Map((data?.remunerations ?? []).map((row) => [evolutionKey(row.salarieId, row.annee), row])),
[data?.remunerations],
const forecastByKey = useMemo(
() => new Map((data?.forecasts ?? []).map((row) => [keyFor(row.salarieId, row.annee), row])),
[data?.forecasts],
);
const evolutionByKey = useMemo(
() => new Map((data?.evolutions ?? []).map((row) => [evolutionKey(row.salarieId, row.annee), row])),
[data?.evolutions],
const actualByKey = useMemo(
() => new Map((data?.actuals ?? []).map((row) => [keyFor(row.salarieId, row.annee), row])),
[data?.actuals],
);
const totals = useMemo(() => new Map(ANNEES.map((annee) => {
const forecasts = (data?.forecasts ?? []).filter((row) => row.annee === annee);
const actuals = (data?.actuals ?? []).filter((row) => row.annee === annee);
return [annee, {
forecast: forecasts.reduce((sum, row) => sum + row.brutAvecPrimesCents, 0),
actual: actuals.reduce((sum, row) => sum + row.brutAvecPrimesCents, 0),
forecastCount: forecasts.length,
actualCount: actuals.length,
actualMonths: Math.max(0, ...actuals.map((row) => row.moisDisponibles.length)),
}];
})), [data?.forecasts, data?.actuals]);
const uiState = getMasseSalarialeUiState(isLoading, isError, data?.salaries.length ?? 0);
const totalsByYear = useMemo(
() => new Map(ANNEES.map((annee) => [
annee,
getMasseSalarialeYearSummary(data?.remunerations ?? [], annee),
])),
[data?.remunerations],
);
const startEvolutionEdit = (salarieId: number, annee: number) => {
const key = evolutionKey(salarieId, annee);
const current = evolutionByKey.get(key)?.evolutionSalarialeBps;
setEditingKey(key);
setEvolutionDraft(current === undefined || current === null ? '' : String(current / 100));
const startEdit = (salarieId: number, annee: number) => {
const forecast = forecastByKey.get(keyFor(salarieId, annee));
if (!forecast?.editable) return;
setEditingKey(keyFor(salarieId, annee));
setPrimeDraft((forecast.primeCents / 100).toFixed(2));
setEvolutionDraft((forecast.evolutionCents / 100).toFixed(2));
};
const saveEvolution = (salarieId: number, annee: number) => {
const normalized = evolutionDraft.trim().replace(',', '.');
const percentage = Number(normalized);
if (!normalized || !Number.isFinite(percentage) || percentage < -100 || percentage > 100) {
toast.error('Saisissez une évolution comprise entre -100 % et 100 %.');
const saveEdit = (salarieId: number, annee: number) => {
const normalize = (value: string) => Number(value.trim().replace(",", "."));
const prime = normalize(primeDraft);
const evolution = normalize(evolutionDraft);
if (!Number.isFinite(prime) || !Number.isFinite(evolution) || Math.abs(prime) > 1_000_000 || Math.abs(evolution) > 1_000_000) {
toast.error("Saisissez des montants compris entre -1 000 000 € et 1 000 000 €.");
return;
}
setEvolution.mutate({ salarieId, annee, evolutionSalarialeBps: Math.round(percentage * 100) });
saveAdjustments.mutate({
salarieId,
annee,
primeCents: Math.round(prime * 100),
evolutionCents: Math.round(evolution * 100),
});
};
return (
<div className="min-h-screen flex bg-background">
<AppSidebar collapsed={sidebarCollapsed} onToggle={() => setSidebarCollapsed((value) => !value)} />
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
<header className="bg-card border-b border-border px-6 py-4 flex-shrink-0">
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3 min-w-0">
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center flex-shrink-0">
<Banknote className="w-5 h-5 text-emerald-600" />
</div>
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center flex-shrink-0"><Banknote className="w-5 h-5 text-emerald-600" /></div>
<div className="min-w-0">
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
Masse salariale SANTINOVA
</h1>
<p className="text-sm text-muted-foreground mt-0.5">
Rémunérations 20252027 et évolutions salariales manuelles
</p>
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: "Sora, sans-serif" }}>Masse salariale SANTINOVA</h1>
<p className="text-sm text-muted-foreground mt-0.5">Prévisionnel et réel annuel, issus du fichier de référence et des liasses archivées.</p>
</div>
</div>
<div className="hidden sm:flex items-center gap-2 text-xs text-emerald-700 bg-emerald-50 border border-emerald-200 rounded-full px-3 py-1.5 whitespace-nowrap">
<ShieldCheck className="w-3.5 h-3.5" />
Accès administrateur
</div>
<div className="hidden sm:flex items-center gap-2 text-xs text-emerald-700 bg-emerald-50 border border-emerald-200 rounded-full px-3 py-1.5 whitespace-nowrap"><ShieldCheck className="w-3.5 h-3.5" />Accès administrateur</div>
</div>
</header>
<div className="flex-1 overflow-auto px-6 py-5 space-y-5">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="grid grid-cols-1 xl:grid-cols-3 gap-3">
{ANNEES.map((annee) => {
const total = totalsByYear.get(annee);
const isProvisional = total?.status === 'cumul_provisoire';
return (
<section key={annee} className="bg-card border border-border rounded-xl shadow-sm p-4">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-sm font-semibold text-foreground">
<CalendarDays className="w-4 h-4 text-emerald-600" />
{annee}
</div>
<span className={`text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded-full ${
annee === 2025
? 'bg-emerald-100 text-emerald-700'
: isProvisional
? 'bg-amber-100 text-amber-700'
: 'bg-muted text-muted-foreground'
}`}>
{total?.status === 'annuel' ? 'Année clôturée' : isProvisional ? 'Cumul au dernier bulletin' : 'À renseigner'}
</span>
</div>
<p className="mt-3 text-2xl font-bold text-foreground">
{formatEuros(total?.annualGrossWithBonusesCents)}
</p>
<p className="mt-1 text-xs text-muted-foreground">
Brut avec primes {total?.salaryCount ?? 0} salarié{(total?.salaryCount ?? 0) > 1 ? 's' : ''}
</p>
</section>
);
const total = totals.get(annee);
const hasActual = (total?.actualCount ?? 0) > 0;
return <section key={annee} className="bg-card border border-border rounded-xl shadow-sm p-4">
<div className="flex items-center gap-2 text-sm font-semibold text-foreground"><CalendarDays className="w-4 h-4 text-emerald-600" />{annee}</div>
<div className="grid grid-cols-2 gap-4 mt-3">
<div><p className="text-[10px] font-semibold uppercase tracking-wide text-emerald-700">Prévisionnel</p><p className="text-xl font-bold text-foreground mt-1">{formatEuros(total?.forecast)}</p><p className="text-xs text-muted-foreground mt-1">{total?.forecastCount ?? 0} salarié(s)</p></div>
<div className="border-l border-border pl-4"><p className="text-[10px] font-semibold uppercase tracking-wide text-sky-700">Réel</p><p className="text-xl font-bold text-foreground mt-1">{hasActual ? formatEuros(total?.actual) : "—"}</p><p className="text-xs text-muted-foreground mt-1">{hasActual ? `${total?.actualCount} salarié(s) · ${formatMonths(total?.actualMonths ? Array.from({ length: total.actualMonths }, (_, index) => index) : [])}` : "Aucune liasse"}</p></div>
</div>
</section>;
})}
</div>
<div className="bg-amber-50 border border-amber-200 rounded-xl px-4 py-3 flex gap-3 text-sm text-amber-900">
<CircleAlert className="w-4 h-4 flex-shrink-0 mt-0.5 text-amber-600" />
<p>
<strong>2025</strong> est issu des cumuls annuels de décembre. <strong>2026</strong> est un cumul provisoire au dernier bulletin disponible pour chaque salarié (janvier à août). Aucun salaire 2027 nest calculé automatiquement ; seules les évolutions peuvent être saisies.
</p>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-xl px-4 py-3 flex gap-3 text-sm text-amber-900"><CircleAlert className="w-4 h-4 flex-shrink-0 mt-0.5 text-amber-600" /><p><strong>Réel 2026 :</strong> cumul strict des liasses mensuelles disponibles, sans annualisation. Le brut hors astreinte retire uniquement les primes dastreinte. Les cellules vertes <strong>Prime</strong> et <strong>Évolution</strong> sont modifiables seulement pour {data?.currentYear ?? "lexercice courant"} et les années suivantes.</p></div>
<section className="bg-card border border-border rounded-xl shadow-sm overflow-hidden">
<div className="px-5 py-4 border-b border-border flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-emerald-500/15 flex items-center justify-center">
<Users className="w-4 h-4 text-emerald-600" />
</div>
<div>
<h2 className="font-semibold text-foreground">Détail par salarié</h2>
<p className="text-xs text-muted-foreground">Cliquez sur une évolution pour la saisir ou la modifier.</p>
</div>
</div>
<div className="px-5 py-4 border-b border-border flex items-center gap-3"><div className="w-8 h-8 rounded-lg bg-emerald-500/15 flex items-center justify-center"><Users className="w-4 h-4 text-emerald-600" /></div><div><h2 className="font-semibold text-foreground">Détail par salarié</h2><p className="text-xs text-muted-foreground">Prévisionnel issu du fichier modèle ; réel issu des clôtures et liasses PDF archivées.</p></div></div>
<MasseSalarialeDataState state={uiState}>
<div className="overflow-auto max-h-[calc(100vh-335px)]">
<table className="w-full min-w-[1900px] text-sm border-collapse">
<div className="overflow-auto max-h-[calc(100vh-350px)]">
<table className="w-full min-w-[4900px] text-sm border-collapse">
<thead className="sticky top-0 z-10 bg-muted/95 backdrop-blur">
<tr className="border-b border-border">
<th rowSpan={2} className="sticky left-0 z-20 bg-muted/95 text-left px-4 py-3 font-semibold text-foreground">Salarié</th>
<th rowSpan={2} className="text-left px-4 py-3 font-semibold text-foreground">Poste</th>
<th rowSpan={2} className="text-left px-4 py-3 font-semibold text-foreground">Embauche</th>
{ANNEES.map((annee) => <th key={annee} colSpan={5} className="text-center px-3 py-2 font-bold text-foreground border-l border-border">{annee}</th>)}
</tr>
<tr className="border-b border-border text-[10px] uppercase tracking-wide text-muted-foreground">
{ANNEES.flatMap((annee) => [
<th key={`${annee}-annual-base`} className="px-3 py-2 text-right font-medium border-l border-border">Annuel hors primes</th>,
<th key={`${annee}-annual-total`} className="px-3 py-2 text-right font-medium">Annuel avec primes</th>,
<th key={`${annee}-monthly`} className="px-3 py-2 text-right font-medium">Mensuel hors primes</th>,
<th key={`${annee}-charges`} className="px-3 py-2 text-right font-medium">Taux charges</th>,
<th key={`${annee}-evolution`} className="px-3 py-2 text-right font-medium">Évolution</th>,
])}
</tr>
<tr className="border-b border-border"><th rowSpan={3} className="sticky left-0 z-20 bg-muted/95 text-left px-4 py-3 font-semibold text-foreground">Salarié</th><th rowSpan={3} className="text-left px-4 py-3 font-semibold text-foreground">Poste</th><th rowSpan={3} className="text-left px-4 py-3 font-semibold text-foreground">Embauche</th>{ANNEES.map((annee) => <th key={annee} colSpan={12} className="text-center px-3 py-2 font-bold text-foreground border-l border-border">{annee}</th>)}</tr>
<tr className="border-b border-border text-[10px] uppercase tracking-wide"><th colSpan={7} className="px-3 py-2 text-center text-emerald-700 border-l border-border">Prévisionnel</th><th colSpan={5} className="px-3 py-2 text-center text-sky-700 border-l border-border">Réel</th>{ANNEES.slice(1).flatMap((annee) => [<th key={`${annee}-p`} colSpan={7} className="px-3 py-2 text-center text-emerald-700 border-l border-border">Prévisionnel</th>, <th key={`${annee}-r`} colSpan={5} className="px-3 py-2 text-center text-sky-700 border-l border-border">Réel</th>])}</tr>
<tr className="border-b border-border text-[10px] uppercase tracking-wide text-muted-foreground">{ANNEES.flatMap((annee) => [
<th key={`${annee}-f-base`} className="px-3 py-2 text-right font-medium border-l border-border">Annuel hors astreinte</th>,
<th key={`${annee}-f-total`} className="px-3 py-2 text-right font-medium">Annuel avec primes</th>,
<th key={`${annee}-f-monthly`} className="px-3 py-2 text-right font-medium">Mensuel hors astreinte</th>,
<th key={`${annee}-f-astreinte`} className="px-3 py-2 text-right font-medium">Astreintes</th>,
<th key={`${annee}-f-charges`} className="px-3 py-2 text-right font-medium">Taux charges</th>,
<th key={`${annee}-f-evolution`} className="px-3 py-2 text-right font-medium">Évolution</th>,
<th key={`${annee}-f-prime`} className="px-3 py-2 text-right font-medium">Prime</th>,
<th key={`${annee}-a-base`} className="px-3 py-2 text-right font-medium border-l border-border">Cumul hors astreinte</th>,
<th key={`${annee}-a-total`} className="px-3 py-2 text-right font-medium">Cumul avec primes</th>,
<th key={`${annee}-a-monthly`} className="px-3 py-2 text-right font-medium">Moy. mensuelle</th>,
<th key={`${annee}-a-astreinte`} className="px-3 py-2 text-right font-medium">Astreintes</th>,
<th key={`${annee}-a-months`} className="px-3 py-2 text-right font-medium">Mois</th>,
])}</tr>
</thead>
<tbody>
{data?.salaries.map((salarie) => (
<tr key={salarie.id} className="border-b border-border/70 hover:bg-muted/30">
<td className="sticky left-0 z-[1] bg-card px-4 py-3 min-w-[220px]">
<p className="font-semibold text-foreground">{salarie.nom} {salarie.prenom}</p>
<p className="text-[11px] text-muted-foreground font-mono">{salarie.matricule}</p>
</td>
<td className="px-4 py-3 text-xs text-foreground min-w-[200px]">{salarie.poste}</td>
<td className="px-4 py-3 text-xs text-muted-foreground whitespace-nowrap">{formatDate(salarie.dateEmbauche)}</td>
{ANNEES.map((annee) => {
const remuneration = remunerationByKey.get(evolutionKey(salarie.id, annee));
const evolution = evolutionByKey.get(evolutionKey(salarie.id, annee));
const key = evolutionKey(salarie.id, annee);
return (
<Fragment key={key}>
<td key={`${key}-annual-base`} className="px-3 py-3 text-right font-medium text-foreground border-l border-border">{formatEuros(remuneration?.salaireAnnuelBrutHorsPrimesCents)}</td>
<td key={`${key}-annual-total`} className="px-3 py-3 text-right font-medium text-foreground">{formatEuros(remuneration?.salaireAnnuelBrutAvecPrimesCents)}</td>
<td key={`${key}-monthly`} className="px-3 py-3 text-right text-foreground">{formatEuros(remuneration?.salaireMensuelBrutHorsPrimesCents)}</td>
<td key={`${key}-charges`} className="px-3 py-3 text-right text-foreground">{formatPercent(remuneration?.tauxChargeBps)}</td>
<td key={`${key}-evolution`} className="px-3 py-2 text-right">
{editingKey === key ? (
<div className="flex items-center justify-end gap-1">
<input
aria-label={`Évolution ${annee} de ${salarie.nom}`}
type="number"
min="-100"
max="100"
step="0.01"
value={evolutionDraft}
onChange={(event) => setEvolutionDraft(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') saveEvolution(salarie.id, annee);
if (event.key === 'Escape') setEditingKey(null);
}}
className="w-20 rounded border border-emerald-400 bg-white px-2 py-1 text-right text-xs focus:outline-none focus:ring-2 focus:ring-emerald-300"
autoFocus
disabled={setEvolution.isPending}
/>
<button
onClick={() => saveEvolution(salarie.id, annee)}
className="text-[11px] font-semibold text-emerald-700 hover:text-emerald-900 disabled:opacity-50"
disabled={setEvolution.isPending}
>
OK
</button>
</div>
) : (
<button
onClick={() => startEvolutionEdit(salarie.id, annee)}
className="min-w-20 rounded px-2 py-1 text-xs font-semibold text-emerald-700 hover:bg-emerald-50 focus:outline-none focus:ring-2 focus:ring-emerald-300"
title="Saisir l'évolution salariale"
>
{formatPercent(evolution?.evolutionSalarialeBps)}
</button>
)}
</td>
</Fragment>
);
})}
</tr>
))}
</tbody>
<tbody>{data?.salaries.map((salarie) => <tr key={salarie.id} className="border-b border-border/70 hover:bg-muted/30"><td className="sticky left-0 z-[1] bg-card px-4 py-3 min-w-[220px]"><p className="font-semibold text-foreground">{salarie.nom} {salarie.prenom}</p><p className="text-[11px] text-muted-foreground font-mono">{salarie.matricule}</p></td><td className="px-4 py-3 text-xs text-foreground min-w-[200px]">{salarie.poste}</td><td className="px-4 py-3 text-xs text-muted-foreground whitespace-nowrap">{formatDate(salarie.dateEmbauche)}</td>{ANNEES.map((annee) => {
const forecast = forecastByKey.get(keyFor(salarie.id, annee));
const actual = actualByKey.get(keyFor(salarie.id, annee));
const editing = editingKey === keyFor(salarie.id, annee);
return <Fragment key={keyFor(salarie.id, annee)}>
<td className="px-3 py-3 text-right font-medium text-foreground border-l border-border">{formatEuros(forecast?.brutHorsAstreinteCents)}</td><td className="px-3 py-3 text-right font-medium text-foreground">{formatEuros(forecast?.brutAvecPrimesCents)}</td><td className="px-3 py-3 text-right text-foreground">{formatEuros(forecast?.brutMensuelHorsAstreinteCents)}</td><td className="px-3 py-3 text-right text-foreground">{formatEuros(forecast?.primeAstreinteCents)}</td><td className="px-3 py-3 text-right text-foreground">{formatPercent(forecast?.tauxChargeBps)}</td>
<td className="px-3 py-2 text-right">{editing ? <input aria-label={`Évolution ${annee} de ${salarie.nom}`} type="number" step="0.01" value={evolutionDraft} onChange={(event) => setEvolutionDraft(event.target.value)} className="w-24 rounded border border-emerald-400 bg-white px-2 py-1 text-right text-xs focus:outline-none focus:ring-2 focus:ring-emerald-300" disabled={saveAdjustments.isPending} /> : forecast?.editable ? <button onClick={() => startEdit(salarie.id, annee)} className="min-w-20 rounded px-2 py-1 text-xs font-semibold text-emerald-700 hover:bg-emerald-50 focus:outline-none focus:ring-2 focus:ring-emerald-300">{formatEuros(forecast.evolutionCents)}</button> : <span className="inline-flex items-center gap-1 text-xs text-muted-foreground"><LockKeyhole className="w-3 h-3" />{formatEuros(forecast?.evolutionCents)}</span>}</td>
<td className="px-3 py-2 text-right">{editing ? <div className="flex items-center justify-end gap-1"><input aria-label={`Prime ${annee} de ${salarie.nom}`} type="number" step="0.01" value={primeDraft} onChange={(event) => setPrimeDraft(event.target.value)} className="w-24 rounded border border-emerald-400 bg-white px-2 py-1 text-right text-xs focus:outline-none focus:ring-2 focus:ring-emerald-300" disabled={saveAdjustments.isPending} /><button onClick={() => saveEdit(salarie.id, annee)} className="text-[11px] font-semibold text-emerald-700 hover:text-emerald-900" disabled={saveAdjustments.isPending}>OK</button></div> : forecast?.editable ? <button onClick={() => startEdit(salarie.id, annee)} className="min-w-20 rounded px-2 py-1 text-xs font-semibold text-emerald-700 hover:bg-emerald-50 focus:outline-none focus:ring-2 focus:ring-emerald-300">{formatEuros(forecast.primeCents)}</button> : <span className="inline-flex items-center gap-1 text-xs text-muted-foreground"><LockKeyhole className="w-3 h-3" />{formatEuros(forecast?.primeCents)}</span>}</td>
<td className="px-3 py-3 text-right font-medium text-foreground border-l border-border">{formatEuros(actual?.brutHorsAstreinteCents)}</td><td className="px-3 py-3 text-right font-medium text-foreground">{formatEuros(actual?.brutAvecPrimesCents)}</td><td className="px-3 py-3 text-right text-foreground">{formatEuros(actual?.brutMensuelHorsAstreinteCents)}</td><td className="px-3 py-3 text-right text-foreground">{formatEuros(actual?.primeAstreinteCents)}</td><td className="px-3 py-3 text-right text-muted-foreground">{formatMonths(actual?.moisDisponibles)}</td>
</Fragment>;
})}</tr>)}</tbody>
</table>
</div>
</MasseSalarialeDataState>