import type { MasseSalarialeImportRemuneration, MasseSalarialeImportSalarie } from "./db"; import XLSX from "xlsx"; type SpreadsheetValue = string | number | Date | null | undefined; function cents(value: SpreadsheetValue): number { if (typeof value === "number") return Math.round(value * 100); if (typeof value !== "string") return 0; const normalized = value.replace(/[\s\u00a0]/g, "").replace(",", "."); const parsed = Number(normalized); return Number.isFinite(parsed) ? Math.round(parsed * 100) : 0; } function bps(value: SpreadsheetValue): number | null { if (value === null || value === undefined || value === "") return null; const numeric = typeof value === "number" ? value : Number(String(value).replace(",", ".")); return Number.isFinite(numeric) ? Math.round(numeric * 10_000) : null; } function isoDate(value: SpreadsheetValue): string | null { if (value instanceof Date && !Number.isNaN(value.valueOf())) return value.toISOString().slice(0, 10); if (typeof value === "number" && Number.isFinite(value)) { const excelDate = new Date(Date.UTC(1899, 11, 30) + Math.round(value * 86_400_000)); return Number.isNaN(excelDate.valueOf()) ? null : excelDate.toISOString().slice(0, 10); } if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) return value; if (typeof value === "string" && /^\d{2}\/\d{2}\/\d{4}$/.test(value)) { const [day, month, year] = value.split("/"); return `${year}-${month}-${day}`; } return null; } function text(value: SpreadsheetValue): string { return typeof value === "string" || typeof value === "number" ? String(value).trim() : ""; } export type MasseSalarialeWorkbookImport = { salaries: MasseSalarialeImportSalarie[]; remunerations: MasseSalarialeImportRemuneration[]; }; /** * Convertit les lignes de la feuille modèle (index Excel 1, en-tête sur la * ligne 5) sans évaluer de formules côté application : les valeurs calculées * et sauvegardées par le fichier fourni servent de référence importée. */ export function parseMasseSalarialeWorkbookRows(rows: SpreadsheetValue[][]): MasseSalarialeWorkbookImport { const salaries: MasseSalarialeImportSalarie[] = []; const remunerations: MasseSalarialeImportRemuneration[] = []; for (const row of rows.slice(5)) { // SheetJS supprime la cellule A vide de certaines lignes. On repère donc // la présence effective de cette cellule plutôt que de supposer un index. const originalColumn = (excelColumn: number) => row[excelColumn + (row[0] === null || row[0] === undefined ? 0 : -1)]; const matricule = text(originalColumn(1)); const nom = text(originalColumn(2)); const prenom = text(originalColumn(3)); const poste = text(originalColumn(4)); const dateEmbauche = isoDate(originalColumn(5)); if (!matricule || !nom || !prenom || !poste || !dateEmbauche) continue; salaries.push({ matricule, nom, prenom, poste, dateEmbauche }); const addForecast = (annee: number, startIndex: number, tauxChargeBps: number | null, periodeReference: string) => { const brutHorsAstreinte = cents(originalColumn(startIndex)); const brutAvecPrimes = cents(originalColumn(startIndex + 1)); const primeAstreinte = cents(originalColumn(startIndex + 2)); const brutMensuel = cents(originalColumn(startIndex + 3)); const evolution = cents(originalColumn(startIndex + 5)); const prime = annee === 2027 ? cents(originalColumn(startIndex + 6)) : 0; if (brutHorsAstreinte === 0 && brutAvecPrimes === 0 && brutMensuel === 0) return; remunerations.push({ matricule, annee, nature: "previsionnel", salaireAnnuelBrutHorsPrimesCents: brutHorsAstreinte, salaireAnnuelBrutAvecPrimesCents: brutAvecPrimes, salaireMensuelBrutHorsPrimesCents: brutMensuel, tauxChargeBps, primeAstreinteCents: primeAstreinte, primeCents: prime, evolutionCents: evolution, primeReferenceCents: prime, evolutionReferenceCents: evolution, periodeReference, statut: annee === 2025 ? "annuel" : "previsionnel", source: "fichier_modele_2026-08-28", }); }; addForecast(2025, 6, bps(originalColumn(10)), "2025-12"); addForecast(2026, 12, bps(originalColumn(16)), "2026-08"); addForecast(2027, 18, null, "2027-01"); } return { salaries, remunerations }; } /** * Lit le fichier exclusivement côté serveur, afin que les données de masse * salariale ne soient ni transformées ni renvoyées par le navigateur. */ export function parseMasseSalarialeWorkbookBuffer(buffer: Buffer): MasseSalarialeWorkbookImport { const workbook = XLSX.read(buffer, { type: "buffer", cellDates: true }); const sheet = workbook.Sheets["Masse salariale"]; if (!sheet) throw new Error("Feuille Masse salariale introuvable"); const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, raw: true, defval: null, }); const parsed = parseMasseSalarialeWorkbookRows(rows); if (parsed.salaries.length === 0 || parsed.remunerations.length === 0) { throw new Error("Le fichier ne contient aucune prévision exploitable"); } return parsed; }