Checkpoint: Masse salariale enrichie : import Excel administrateur côté serveur, projections annuelles 2025-2027 importées du fichier de référence, réel 2026 exclusivement calculé sur les liasses archivées, conservation des montants de référence et des centimes, éditions limitées aux primes et évolutions de l'exercice courant/futur. 74 tests, TypeScript et build validés.
All checks were successful
Validation applicative / TypeScript, tests et build (push) Successful in 5m21s
All checks were successful
Validation applicative / TypeScript, tests et build (push) Successful in 5m21s
This commit is contained in:
@@ -6,7 +6,7 @@ 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";
|
||||
import { Banknote, CalendarDays, CircleAlert, FileSpreadsheet, LockKeyhole, ShieldCheck, Upload, Users } from "lucide-react";
|
||||
|
||||
const ANNEES = [2025, 2026, 2027] as const;
|
||||
|
||||
@@ -49,6 +49,8 @@ export default function MasseSalariale() {
|
||||
const [editingKey, setEditingKey] = useState<string | null>(null);
|
||||
const [primeDraft, setPrimeDraft] = useState("");
|
||||
const [evolutionDraft, setEvolutionDraft] = useState("");
|
||||
const [workbookFile, setWorkbookFile] = useState<File | null>(null);
|
||||
const [isImportingWorkbook, setIsImportingWorkbook] = useState(false);
|
||||
const utils = trpc.useUtils();
|
||||
const { data, isLoading, isError } = trpc.masseSalariale.get.useQuery();
|
||||
const saveAdjustments = trpc.masseSalariale.setForecastAdjustments.useMutation({
|
||||
@@ -106,6 +108,32 @@ export default function MasseSalariale() {
|
||||
});
|
||||
};
|
||||
|
||||
const importWorkbook = async () => {
|
||||
if (!workbookFile) {
|
||||
toast.error("Choisissez le fichier Excel de référence avant de lancer l'import.");
|
||||
return;
|
||||
}
|
||||
if (!workbookFile.name.toLowerCase().endsWith(".xlsx") || workbookFile.size > 5 * 1024 * 1024) {
|
||||
toast.error("Le fichier doit être un .xlsx de 5 Mo maximum.");
|
||||
return;
|
||||
}
|
||||
setIsImportingWorkbook(true);
|
||||
try {
|
||||
const payload = new FormData();
|
||||
payload.append("file", workbookFile);
|
||||
const response = await fetch("/api/masse-salariale/previsionnels", { method: "POST", body: payload, credentials: "include" });
|
||||
const result = await response.json() as { salaries?: number; remunerations?: number; error?: string };
|
||||
if (!response.ok) throw new Error(result.error ?? "L'import a échoué.");
|
||||
await utils.masseSalariale.get.invalidate();
|
||||
setWorkbookFile(null);
|
||||
toast.success(`Fichier de référence importé : ${result.salaries ?? 0} salarié(s), ${result.remunerations ?? 0} prévision(s).`);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "L'import a échoué.");
|
||||
} finally {
|
||||
setIsImportingWorkbook(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex bg-background">
|
||||
<AppSidebar collapsed={sidebarCollapsed} onToggle={() => setSidebarCollapsed((value) => !value)} />
|
||||
@@ -124,6 +152,11 @@ export default function MasseSalariale() {
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-auto px-6 py-5 space-y-5">
|
||||
<section className="bg-card border border-border rounded-xl shadow-sm px-5 py-4 flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex items-start gap-3"><div className="w-9 h-9 rounded-lg bg-emerald-500/15 flex items-center justify-center flex-shrink-0"><FileSpreadsheet className="w-4 h-4 text-emerald-600" /></div><div><h2 className="font-semibold text-foreground">Fichier prévisionnel de référence</h2><p className="text-xs text-muted-foreground mt-0.5">Administrateur uniquement. L'import remplace les prévisions 2025–2027, sans modifier les liasses de paie ni les réels déjà archivés.</p></div></div>
|
||||
<div className="flex flex-col sm:flex-row gap-2 sm:items-center"><input aria-label="Fichier Excel de masse salariale" type="file" accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" onChange={(event) => setWorkbookFile(event.target.files?.[0] ?? null)} disabled={isImportingWorkbook} className="max-w-72 text-xs file:mr-3 file:rounded-md file:border-0 file:bg-emerald-50 file:px-3 file:py-2 file:text-xs file:font-semibold file:text-emerald-700 hover:file:bg-emerald-100" /><button onClick={importWorkbook} disabled={!workbookFile || isImportingWorkbook} className="inline-flex items-center justify-center gap-2 rounded-lg bg-emerald-600 px-3 py-2 text-xs font-semibold text-white hover:bg-emerald-700 disabled:cursor-not-allowed disabled:opacity-50"><Upload className="w-3.5 h-3.5" />{isImportingWorkbook ? "Import en cours…" : "Importer le fichier"}</button></div>
|
||||
</section>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-3">
|
||||
{ANNEES.map((annee) => {
|
||||
const total = totals.get(annee);
|
||||
|
||||
Reference in New Issue
Block a user