Checkpoint: Ajout du support XLSX natif via la librairie SheetJS (xlsx). Un helper fileToRows() unifie la lecture CSV et XLSX : pour XLSX, lecture via arrayBuffer + XLSX.read + sheet_to_json ; pour CSV, détection du séparateur et split classique. Le parsing ISI-APP fonctionne désormais identiquement sur les deux formats. Les métadonnées d'import incluent le format détecté (isi-app ou classique).

This commit is contained in:
Manus
2026-06-04 15:10:48 +00:00
parent b917aeaa56
commit af5d4fd703
3 changed files with 101 additions and 10 deletions

View File

@@ -2,6 +2,7 @@
// Design: Corporate Modernism — Itinova Budget SI
import { useState, useRef, useEffect } from 'react';
import * as XLSX from 'xlsx';
import {
Upload,
X,
@@ -21,6 +22,25 @@ import {
} from 'lucide-react';
import { toast } from 'sonner';
// ─── Helper : lecture fichier → tableau de lignes (CSV ou XLSX) ────────────────
async function fileToRows(file: File): Promise<string[][]> {
const isXlsx = file.name.match(/\.(xlsx|xls)$/i);
if (isXlsx) {
const buffer = await file.arrayBuffer();
const wb = XLSX.read(buffer, { type: 'array', cellDates: true });
const ws = wb.Sheets[wb.SheetNames[0]];
// sheet_to_json avec header:1 retourne un tableau de tableaux
const rows = XLSX.utils.sheet_to_json<string[]>(ws, { header: 1, defval: '' });
return rows.map(r => r.map(c => String(c ?? '').trim()));
} else {
// CSV : détection du séparateur
const text = await file.text();
const lines = text.split('\n').filter(l => l.trim());
const sep = lines[0].includes(';') ? ';' : ',';
return lines.map(l => l.split(sep).map(c => c.trim().replace(/"/g, '').replace(/[\r\n]/g, '')));
}
}
// ─── Types ────────────────────────────────────────────────────────────────────
export interface Etablissement {
@@ -274,17 +294,15 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
return;
}
const text = await file.text();
const lines = text.split('\n').filter(l => l.trim());
if (lines.length < 2) {
// Lecture unifiée CSV + XLSX via SheetJS
const rows = await fileToRows(file);
if (rows.length < 2) {
setInventaireStatus('error');
setInventaireMsg('Fichier vide ou format invalide (moins de 2 lignes)');
return;
}
// Détection du séparateur
const sep = lines[0].includes(';') ? ';' : ',';
const rawHeaders = lines[0].split(sep).map(h => h.trim().toLowerCase().replace(/"/g, '').replace(/[\r\n]/g, ''));
const rawHeaders = rows[0].map(h => h.toLowerCase().replace(/[\r\n]/g, ''));
// ── Format ISI-APP : extraction par libellé et entité ──────────────────
// Colonne "libellé" : contient le code poste (ex: 1083MAS-F4833)
@@ -344,8 +362,7 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
// Extraction des établissements uniques depuis l'inventaire
const etabMap = new Map<string, { nom: string; groupe?: string; ville?: string }>();
for (const line of lines.slice(1)) {
const cols = line.split(sep).map(c => c.trim().replace(/"/g, '').replace(/[\r\n]/g, ''));
for (const cols of rows.slice(1)) {
let code = '';
let nom = '';
@@ -412,11 +429,12 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
saveEtablissements(merged);
// Enregistrement des métadonnées du fichier importé
const nbLignes = rows.length - 1;
localStorage.setItem('budgetsi_inventaire_import', JSON.stringify({
filename: file.name,
date: new Date().toISOString(),
size: file.size,
nbLignes: lines.length - 1,
nbLignes,
nbEtablissements: etabMap.size,
nbFixes: useIsiFormat ? nbFixes : null,
nbPortables: useIsiFormat ? nbPortables : null,
@@ -424,7 +442,7 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
}));
const details = [
`${lines.length - 1} poste${lines.length > 2 ? 's' : ''}`,
`${nbLignes} poste${nbLignes > 1 ? 's' : ''}`,
`${etabMap.size} établissement${etabMap.size > 1 ? 's' : ''} détectés`,
nbNouveaux > 0 ? `${nbNouveaux} nouveau${nbNouveaux > 1 ? 'x' : ''}` : null,
nbMisAJour > 0 ? `${nbMisAJour} mis à jour` : null,