Checkpoint: Import inventaire ISI-APP enrichi : le fichier CSV est maintenant parsé pour extraire automatiquement les établissements uniques (code + nom) et les enregistrer dans budgetsi_etablissements. Fusion intelligente avec les établissements existants (sans écrasement). Message de résultat détaillé (nb lignes, nb établissements détectés, nb nouveaux/mis à jour). Accepte de nombreuses variantes de nommage de colonnes (code_etab, code_etablissement, nom_etab, etablissement…). Message d'erreur explicite avec affichage des en-têtes détectés si les colonnes sont introuvables.

This commit is contained in:
Manus
2026-06-04 14:28:26 +00:00
parent 4ff5228ac3
commit ade7471956

View File

@@ -155,26 +155,142 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
if (!open) return null;
// ── Inventaire ISI-APP ──────────────────────────────────────────────────────
// Colonnes attendues dans l'export ISI-APP :
// code_etab (ou code_etablissement, code etab…) — obligatoire
// nom_etab (ou nom_etablissement, libelle etab, etablissement…) — obligatoire
// type (fixe / portable / bureau / laptop…)
// date_achat, modele, fabricant — optionnels
const handleInventaireFile = async (file: File) => {
try {
// Validation basique du format
if (!file.name.match(/\.(xlsx|xls|csv)$/i)) {
setInventaireStatus('error');
setInventaireMsg('Format non supporté. Utilisez .xlsx, .xls ou .csv');
return;
}
// Stocker le nom du fichier importé
const text = await file.text();
const lines = text.split('\n').filter(l => l.trim());
if (lines.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, ''));
// Recherche des colonnes code et nom établissement
// Accepte de nombreuses variantes de nommage ISI-APP
const codeIdx = rawHeaders.findIndex(h =>
h === 'code' ||
h.includes('code_etab') ||
h.includes('code etab') ||
h.includes('code_etablissement') ||
h.includes('code etablissement') ||
h.includes('codeétab') ||
h.includes('codeétablissement') ||
(h.startsWith('code') && h.length < 20)
);
const nomIdx = rawHeaders.findIndex(h =>
h === 'nom' ||
h.includes('nom_etab') ||
h.includes('nom etab') ||
h.includes('nom_etablissement') ||
h.includes('nom etablissement') ||
h.includes('libelle_etab') ||
h.includes('libelle etab') ||
h.includes('établissement') ||
h.includes('etablissement') ||
h.includes('nométab') ||
(h.startsWith('nom') && h.length < 20)
);
const groupeIdx = rawHeaders.findIndex(h =>
h.includes('groupe') || h.includes('association') || h.includes('entite') || h.includes('entité')
);
const villeIdx = rawHeaders.findIndex(h =>
h.includes('ville') || h.includes('commune') || h.includes('localite') || h.includes('localité')
);
if (codeIdx === -1 || nomIdx === -1) {
setInventaireStatus('error');
setInventaireMsg(
`Colonnes établissement introuvables. En-têtes détectés : ${rawHeaders.slice(0, 8).join(', ')}\n` +
'Attendu : une colonne contenant "code" et une contenant "nom" ou "etablissement".'
);
return;
}
// 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, ''));
const code = cols[codeIdx]?.trim();
const nom = cols[nomIdx]?.trim();
if (!code || !nom) continue;
if (!etabMap.has(code)) {
etabMap.set(code, {
nom,
groupe: groupeIdx >= 0 ? cols[groupeIdx]?.trim() || undefined : undefined,
ville: villeIdx >= 0 ? cols[villeIdx]?.trim() || undefined : undefined,
});
}
}
if (etabMap.size === 0) {
setInventaireStatus('error');
setInventaireMsg('Aucun établissement extrait. Vérifiez que les colonnes code/nom contiennent des données.');
return;
}
// Fusion avec les établissements déjà enregistrés (sans écraser les données existantes)
const existing = loadEtablissements();
const existingMap = new Map(existing.map(e => [e.code, e]));
let nbNouveaux = 0;
let nbMisAJour = 0;
etabMap.forEach((info, code) => {
if (existingMap.has(code)) {
// Mise à jour du nom si vide, conservation du reste
const ex = existingMap.get(code)!;
if (!ex.nom && info.nom) {
existingMap.set(code, { ...ex, nom: info.nom });
nbMisAJour++;
}
} else {
existingMap.set(code, { code, nom: info.nom, groupe: info.groupe, ville: info.ville, actif: true });
nbNouveaux++;
}
});
const merged = Array.from(existingMap.values());
saveEtablissements(merged);
// Enregistrement des métadonnées du fichier importé
localStorage.setItem('budgetsi_inventaire_import', JSON.stringify({
filename: file.name,
date: new Date().toISOString(),
size: file.size,
nbLignes: lines.length - 1,
nbEtablissements: etabMap.size,
}));
const details = [
`${lines.length - 1} ligne${lines.length > 2 ? 's' : ''} de postes`,
`${etabMap.size} établissement${etabMap.size > 1 ? 's' : ''} détectés`,
nbNouveaux > 0 ? `${nbNouveaux} nouveau${nbNouveaux > 1 ? 'x' : ''}` : null,
nbMisAJour > 0 ? `${nbMisAJour} mis à jour` : null,
].filter(Boolean).join(' — ');
setInventaireStatus('success');
setInventaireMsg(`Fichier "${file.name}" importé avec succès`);
toast.success('Inventaire importé', { description: `Fichier ${file.name} enregistré` });
} catch {
setInventaireMsg(`"${file.name}" importé ${details}`);
toast.success('Inventaire importé', {
description: `${etabMap.size} établissements extraits et enregistrés dans la liste.`,
});
} catch (err) {
console.error('Import inventaire:', err);
setInventaireStatus('error');
setInventaireMsg('Erreur lors de l\'import du fichier');
setInventaireMsg('Erreur lors de la lecture du fichier. Vérifiez le format CSV.');
}
};
@@ -346,8 +462,12 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
<div className="text-sm text-blue-800">
<p className="font-semibold mb-1">Inventaire des postes (ISI-APP)</p>
<p className="text-xs text-blue-700">
Importez l'export de l'inventaire depuis ISI-APP au format Excel (.xlsx) ou CSV (.csv).
Les colonnes attendues sont : <strong>Code établissement, Nom, Type (fixe/portable), Date achat, Modèle</strong>.
Importez l'export de l'inventaire depuis ISI-APP au format CSV (.csv).
Colonnes <strong>obligatoires</strong> : <strong>code établissement</strong> et <strong>nom établissement</strong> (variantes acceptées : <em>code_etab, code_etablissement, nom_etab, etablissement</em>).
Colonnes optionnelles : type, date_achat, modele, fabricant, groupe, ville.
</p>
<p className="text-xs text-blue-600 mt-1.5 font-medium">
La liste des établissements sera automatiquement créée/mise à jour à partir de cet import.
</p>
</div>
</div>