Checkpoint: Audit de robustesse et maintenabilité : calcul dynamique des tendances OPEX par véritable année N-1, filtrage/validation centralisés des codes établissement, transactions d'import et d'écriture, contraintes uniques CAPEX/OPEX avec déduplication, suppression effective de postes OPEX, nettoyage des gabarits et scripts morts, chargement différé XLSX, documentation métier et 22 tests passants.

This commit is contained in:
Manus
2026-08-17 20:50:33 +00:00
parent 31cdb257fb
commit 615dd46059
29 changed files with 1449 additions and 3307 deletions

View File

@@ -1,10 +1,9 @@
// ImportModal.tsx — Fenêtre d'import de données (Inventaire, Établissements, Utilisateurs)
// Design: Corporate Modernism — Itinova Budget SI
import { useState, useRef, useEffect, useCallback } from 'react';
import { useState, useRef, useEffect } from 'react';
import { trpc } from '@/lib/trpc';
import { useAnnee, getInventaireStorageKey } from '../contexts/AnneeContext';
import * as XLSX from 'xlsx';
import { useAnnee } from '../contexts/AnneeContext';
import {
Upload,
X,
@@ -28,6 +27,9 @@ import { toast } from 'sonner';
async function fileToRows(file: File): Promise<string[][]> {
const isXlsx = file.name.match(/\.(xlsx|xls)$/i);
if (isXlsx) {
// XLSX pèse plusieurs centaines de Ko : il n'est requis qu'à l'ouverture
// d'un fichier d'import et ne doit pas ralentir l'affichage initial.
const XLSX = await import('xlsx');
const buffer = await file.arrayBuffer();
const wb = XLSX.read(buffer, { type: 'array', cellDates: true });
const ws = wb.Sheets[wb.SheetNames[0]];
@@ -418,10 +420,6 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
h === 'fabricant' || h === 'marque' || h === 'brand' ||
h.includes('fabricant') || h.includes('marque')
);
const statutIdx = rawHeaders.findIndex(h =>
h === 'statut' || h === 'status' || h.includes('statut')
);
// Compteurs pour le résumé
let nbFixes = 0;
let nbPortables = 0;
@@ -543,7 +541,7 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
});
// ── Envoyer à la base de données via tRPC ──────────────────────────────────
const result = await importInventaireMutation.mutateAsync({ annee, filename: file.name, postes: postesPayload });
await importInventaireMutation.mutateAsync({ annee, filename: file.name, postes: postesPayload });
// ── Upsert établissements dans la BDD ──────────────────────────────────────
for (const [code, info] of Array.from(etabData.entries())) {
@@ -552,7 +550,6 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
} catch { /* ignore les erreurs d'upsert établissement */ }
}
const nbNouveaux = result.nbEtablissements;
const details = [
`${rows.length - 1} poste${rows.length - 1 > 1 ? 's' : ''}`,
`${etabData.size} établissement${etabData.size > 1 ? 's' : ''} détectés`,