Checkpoint: Correction du bug d'affichage de la modale : déplacement du rendu hors de la sidebar (overflow:hidden) via un ImportModalContext + rendu au niveau racine dans App.tsx. Mise à jour du prompt par défaut avec les règles précises du format ISI-APP réel (code depuis libellé 8 car., nom depuis entité, type fixe/portable depuis libellé, vétusté depuis date mise en service ou date achat). Mise à jour du parsing handleInventaireFile pour utiliser ces règles avec comptage fixes/portables dans le résumé.
This commit is contained in:
@@ -54,30 +54,40 @@ const DEFAULT_IMPORT_PROMPT = `# Processus d'import de l'inventaire ISI-APP
|
||||
- Encodage : UTF-8 ou Windows-1252
|
||||
- Première ligne : en-têtes de colonnes
|
||||
|
||||
## Colonnes obligatoires
|
||||
- **Code établissement** : identifiant unique (ex : 1063CRD, 2013ANG…)
|
||||
Noms acceptés : code, code_etab, code_etablissement, code etablissement
|
||||
- **Nom établissement** : libellé complet
|
||||
Noms acceptés : nom, nom_etab, nom_etablissement, etablissement, libelle_etab
|
||||
## Extraction du code établissement
|
||||
- Colonne source : "libellé" (1ère colonne)
|
||||
- Règle : prendre les 8 premiers caractères du libellé
|
||||
- Exemple : libellé "1083MAS-F4833" → code établissement "1083MAS"
|
||||
- Exemple : libellé "3069UDA-P5191" → code établissement "3069UDA"
|
||||
|
||||
## Colonnes optionnelles
|
||||
- type : fixe / portable / bureau / laptop
|
||||
- date_achat : date d'achat du poste (JJ/MM/AAAA)
|
||||
- modele : modèle du matériel
|
||||
- fabricant : marque (Lenovo, Dell, HP…)
|
||||
- groupe : groupement ou association de rattachement
|
||||
- ville : commune de l'établissement
|
||||
## Extraction du nom établissement
|
||||
- Colonne source : "entité"
|
||||
- Utiliser la valeur telle quelle comme nom de l'établissement
|
||||
|
||||
## Détection du type de poste (fixe / portable)
|
||||
- Colonne source : "libellé"
|
||||
- PC fixe : le libellé contient "-F" ou "-FIX" après le code établissement
|
||||
Exemple : 1083SYL-F4833 → PC Fixe
|
||||
- PC portable : le libellé contient "-P" ou "-POR" après le code établissement
|
||||
Exemple : 3069UDA-P5191 → PC Portable
|
||||
|
||||
## Calcul de la vétusté
|
||||
- Utiliser en priorité la colonne "date de mise en service"
|
||||
- Si cette colonne est vide ou absente, utiliser la colonne "date d'achat"
|
||||
- L'âge en années est calculé depuis cette date jusqu'à la date de référence (01/01/2027)
|
||||
|
||||
## Règles de traitement
|
||||
1. Les établissements uniques sont extraits et enregistrés dans la liste des établissements
|
||||
1. Les établissements uniques (code + nom) sont extraits et enregistrés dans la liste des établissements
|
||||
2. Les établissements déjà présents ne sont pas écrasés (fusion intelligente)
|
||||
3. Les lignes sans code ou sans nom sont ignorées
|
||||
4. Les doublons de postes sont conservés (un poste = une ligne)
|
||||
3. Les lignes sans libellé ou sans entité sont ignorées
|
||||
4. Chaque ligne = un poste (un poste = une ligne)
|
||||
|
||||
## Comportement attendu
|
||||
- Créer ou mettre à jour la liste des établissements (code + nom)
|
||||
- Créer ou mettre à jour la liste des établissements (code 8 car. + nom depuis "entité")
|
||||
- Classifier chaque poste en fixe ou portable selon le libellé
|
||||
- Calculer l'âge à partir de la date de mise en service (ou date d'achat si vide)
|
||||
- Enregistrer les métadonnées du fichier importé (nom, date, nb lignes)
|
||||
- Afficher un résumé : nb lignes traitées, nb établissements détectés, nb nouveaux`;
|
||||
- Afficher un résumé : nb lignes traitées, nb établissements détectés, nb fixes, nb portables`;
|
||||
|
||||
export function loadImportPrompt(): string {
|
||||
try {
|
||||
@@ -276,53 +286,92 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
|
||||
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)
|
||||
// ── Format ISI-APP : extraction par libellé et entité ──────────────────
|
||||
// Colonne "libellé" : contient le code poste (ex: 1083MAS-F4833)
|
||||
// → code établissement = 8 premiers caractères avant le tiret
|
||||
// → type = F/FIX → fixe, P/POR → portable
|
||||
// Colonne "entité" : nom de l'établissement
|
||||
// Colonne "date de mise en service" (priorité) ou "date d'achat" : pour la vétusté
|
||||
|
||||
const libelleIdx = rawHeaders.findIndex(h =>
|
||||
h === 'libellé' || h === 'libelle' || h === 'label' ||
|
||||
h.includes('libellé') || h.includes('libelle')
|
||||
);
|
||||
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 entiteIdx = rawHeaders.findIndex(h =>
|
||||
h === 'entité' || h === 'entite' || h === 'entity' ||
|
||||
h.includes('entité') || h.includes('entite')
|
||||
);
|
||||
const dateMiseEnServiceIdx = rawHeaders.findIndex(h =>
|
||||
h.includes('mise en service') || h.includes('miseenservice') || h.includes('mise_en_service') ||
|
||||
h.includes('service')
|
||||
);
|
||||
const dateAchatIdx = rawHeaders.findIndex(h =>
|
||||
h.includes('achat') || h.includes('purchase') || h.includes('date_achat')
|
||||
);
|
||||
const groupeIdx = rawHeaders.findIndex(h =>
|
||||
h.includes('groupe') || h.includes('association') || h.includes('entite') || h.includes('entité')
|
||||
h.includes('groupe') || h.includes('association') || h.includes('groupement')
|
||||
);
|
||||
const villeIdx = rawHeaders.findIndex(h =>
|
||||
h.includes('ville') || h.includes('commune') || h.includes('localite') || h.includes('localité')
|
||||
);
|
||||
|
||||
if (codeIdx === -1 || nomIdx === -1) {
|
||||
// Fallback : si pas de colonne libellé/entité, chercher code/nom classiques
|
||||
const codeIdxFallback = libelleIdx === -1 ? rawHeaders.findIndex(h =>
|
||||
h === 'code' || h.includes('code_etab') || h.includes('code etablissement') ||
|
||||
(h.startsWith('code') && h.length < 20)
|
||||
) : -1;
|
||||
const nomIdxFallback = entiteIdx === -1 ? rawHeaders.findIndex(h =>
|
||||
h === 'nom' || h.includes('nom_etab') || h.includes('nom etablissement') ||
|
||||
h.includes('etablissement') || (h.startsWith('nom') && h.length < 20)
|
||||
) : -1;
|
||||
|
||||
const useIsiFormat = libelleIdx !== -1 && entiteIdx !== -1;
|
||||
const useClassicFormat = !useIsiFormat && codeIdxFallback !== -1 && nomIdxFallback !== -1;
|
||||
|
||||
if (!useIsiFormat && !useClassicFormat) {
|
||||
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".'
|
||||
`Colonnes introuvables. En-têtes détectés : ${rawHeaders.slice(0, 10).join(', ')}…\n` +
|
||||
'Format ISI-APP attendu : colonnes "libellé" et "entité". ' +
|
||||
'Format alternatif : colonnes "code" et "nom".'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Compteurs pour le résumé
|
||||
let nbFixes = 0;
|
||||
let nbPortables = 0;
|
||||
|
||||
// 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();
|
||||
|
||||
let code = '';
|
||||
let nom = '';
|
||||
|
||||
if (useIsiFormat) {
|
||||
// Format ISI-APP : code = 8 premiers caractères du libellé (avant le tiret)
|
||||
const libelle = cols[libelleIdx]?.trim() || '';
|
||||
// Extraire le code établissement : tout ce qui précède le premier tiret, max 8 car.
|
||||
const dashIdx = libelle.indexOf('-');
|
||||
code = dashIdx > 0 ? libelle.substring(0, dashIdx) : libelle.substring(0, 8);
|
||||
code = code.trim();
|
||||
nom = cols[entiteIdx]?.trim() || '';
|
||||
|
||||
// Détecter le type de poste depuis le libellé
|
||||
const afterDash = dashIdx > 0 ? libelle.substring(dashIdx + 1).toUpperCase() : '';
|
||||
if (afterDash.startsWith('F') || afterDash.startsWith('FIX')) {
|
||||
nbFixes++;
|
||||
} else if (afterDash.startsWith('P') || afterDash.startsWith('POR')) {
|
||||
nbPortables++;
|
||||
}
|
||||
} else {
|
||||
// Format classique : code et nom dans des colonnes dédiées
|
||||
code = cols[codeIdxFallback]?.trim() || '';
|
||||
nom = cols[nomIdxFallback]?.trim() || '';
|
||||
}
|
||||
|
||||
if (!code || !nom) continue;
|
||||
if (!etabMap.has(code)) {
|
||||
etabMap.set(code, {
|
||||
@@ -369,19 +418,24 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
|
||||
size: file.size,
|
||||
nbLignes: lines.length - 1,
|
||||
nbEtablissements: etabMap.size,
|
||||
nbFixes: useIsiFormat ? nbFixes : null,
|
||||
nbPortables: useIsiFormat ? nbPortables : null,
|
||||
format: useIsiFormat ? 'isi-app' : 'classique',
|
||||
}));
|
||||
|
||||
const details = [
|
||||
`${lines.length - 1} ligne${lines.length > 2 ? 's' : ''} de postes`,
|
||||
`${lines.length - 1} poste${lines.length > 2 ? '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,
|
||||
useIsiFormat && nbFixes > 0 ? `${nbFixes} fixes` : null,
|
||||
useIsiFormat && nbPortables > 0 ? `${nbPortables} portables` : null,
|
||||
].filter(Boolean).join(' — ');
|
||||
|
||||
setInventaireStatus('success');
|
||||
setInventaireMsg(`"${file.name}" importé — ${details}`);
|
||||
toast.success('Inventaire importé', {
|
||||
description: `${etabMap.size} établissements extraits et enregistrés dans la liste.`,
|
||||
description: `${etabMap.size} établissements extraits${useIsiFormat ? ` — ${nbFixes} fixes, ${nbPortables} portables` : ''}.`,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Import inventaire:', err);
|
||||
|
||||
Reference in New Issue
Block a user