diff --git a/frontend/src/pages/settings/ImportsSection.jsx b/frontend/src/pages/settings/ImportsSection.jsx index 78a367b..57aea0d 100644 --- a/frontend/src/pages/settings/ImportsSection.jsx +++ b/frontend/src/pages/settings/ImportsSection.jsx @@ -340,29 +340,46 @@ const FIELD_HINTS_OVERRIDE = { }, }; -function buildFieldsList(mod) { +function buildFieldsList(mod, scopePlateforme) { const def = MODULES[mod]; if (!def) return ''; + const line = (key, required) => { + if (key === 'plateforme_id' && scopePlateforme) { + return `- ${key} (non nécessaire) : ne l'inclus pas — cet import concerne exclusivement la plateforme ` + + `"${scopePlateforme.nom}" (voir section PLATEFORME ci-dessous).`; + } + const hint = FIELD_HINTS_OVERRIDE[mod]?.[key] ?? FIELD_HINTS[key] ?? ''; + return `- ${key} (${required ? 'obligatoire' : 'optionnel'})${hint ? ` : ${hint}` : ''}`; + }; const lines = []; - for (const key of def.required) { - const hint = FIELD_HINTS_OVERRIDE[mod]?.[key] ?? FIELD_HINTS[key] ?? ''; - lines.push(`- ${key} (obligatoire)${hint ? ` : ${hint}` : ''}`); - } - for (const key of def.optional) { - const hint = FIELD_HINTS_OVERRIDE[mod]?.[key] ?? FIELD_HINTS[key] ?? ''; - lines.push(`- ${key} (optionnel)${hint ? ` : ${hint}` : ''}`); - } + for (const key of def.required) lines.push(line(key, true)); + for (const key of def.optional) lines.push(line(key, false)); return lines.join('\n'); } -function buildReferenceSection(mod, plats, investissements) { - if ((mod === 'depots_retraits' || mod === 'investissements') && plats.length > 0) { - const names = plats.map(p => p.nom).join(' | '); - return `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nPLATEFORMES EXISTANTES (pour plateforme_id)\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nUtilise EXACTEMENT l'un de ces noms si la plateforme correspond à l'une d'elles :\n${names}\n\n⚠️ Si le fichier ne permet pas d'identifier avec certitude de quelle plateforme il provient (nom absent du fichier, logo/en-tête ambigu, etc.), NE DEVINE PAS et NE GÉNÈRE PAS le JSON tout de suite : pose-moi d'abord la question « De quelle plateforme provient ce fichier ? » en ne me proposant QUE ces réponses possibles :\n${names}\nAttends ma réponse, puis utilise le nom choisi comme valeur de plateforme_id pour TOUTES les lignes générées.\n\n`; +function buildReferenceSection(mod, plats, investissements, scopePlateforme) { + if (mod === 'depots_retraits' || mod === 'investissements') { + if (scopePlateforme) { + return `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nPLATEFORME\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nCet import concerne EXCLUSIVEMENT la plateforme "${scopePlateforme.nom}". ` + + `N'inclus PAS de champ plateforme_id dans le JSON généré (il est déjà connu et sera appliqué automatiquement à chaque ligne).\n\n`; + } + if (plats.length > 0) { + const names = plats.map(p => p.nom).join(' | '); + return `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nPLATEFORMES EXISTANTES (pour plateforme_id)\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nUtilise EXACTEMENT l'un de ces noms si la plateforme correspond à l'une d'elles :\n${names}\n\n⚠️ Si le fichier ne permet pas d'identifier avec certitude de quelle plateforme il provient (nom absent du fichier, logo/en-tête ambigu, etc.), NE DEVINE PAS et NE GÉNÈRE PAS le JSON tout de suite : pose-moi d'abord la question « De quelle plateforme provient ce fichier ? » en ne me proposant QUE ces réponses possibles :\n${names}\nAttends ma réponse, puis utilise le nom choisi comme valeur de plateforme_id pour TOUTES les lignes générées.\n\n`; + } } if (mod === 'remboursements' && investissements.length > 0) { const names = investissements.map(i => i.nom_projet).join(' | '); - return `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nINVESTISSEMENTS EXISTANTS (pour investissement_id)\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nUtilise EXACTEMENT l'un de ces noms de projet si le remboursement correspond à l'un d'eux :\n${names}\n\n⚠️ Si le fichier ne permet pas d'identifier avec certitude à quel investissement une ligne correspond, NE DEVINE PAS et NE GÉNÈRE PAS le JSON tout de suite : pose-moi d'abord la question « À quel investissement correspond ce fichier ? » en ne me proposant QUE ces réponses possibles :\n${names}\nAttends ma réponse, puis utilise le nom choisi comme valeur d'investissement_id pour TOUTES les lignes générées.\n\n`; + const scopeNote = scopePlateforme ? ` de la plateforme "${scopePlateforme.nom}"` : ''; + return `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nINVESTISSEMENTS EXISTANTS${scopeNote} (pour investissement_id)\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nPour CHAQUE ligne, recherche dans la liste ci-dessous le nom de projet dont la correspondance est la PLUS PROCHE ` + + `(émetteur, référence de commande/facture, mots-clés communs — même partiels ou approximatifs) et utilise EXACTEMENT ce nom comme ` + + `valeur d'investissement_id. Une correspondance approximative suffit : ne cherche pas une identité parfaite, une similarité claire et ` + + `raisonnable est acceptable. Ne me pose AUCUNE question ligne par ligne — traite tout le fichier directement.\n${names}\n\n` + + `⚠️ Ce n'est que si, après analyse de l'ensemble du fichier, une ou plusieurs lignes ne présentent VRAIMENT aucune ressemblance avec ` + + `l'un de ces noms (aucun mot commun, aucune référence reconnaissable) que tu peux me poser, à la toute fin, UNE SEULE question groupée ` + + `pour l'ensemble de ces lignes (pas une par ligne), du type « Je n'ai pas trouvé de correspondance fiable pour les lignes suivantes : ` + + `[...]. À quel investissement correspondent-elles ? » en ne me proposant QUE ces réponses possibles :\n${names}\n` + + `Sinon, génère directement le JSON complet pour toutes les lignes sans attendre de réponse.\n\n`; } return ''; } @@ -440,7 +457,7 @@ function IaImportPasteBlock({ moduleLabel, iaJson, setIaJson, iaErr, setIaErr, o } /** Bloc collapsible : prompt IA dynamique (champs du module + plateformes/investissements existants) */ -function IaImportPromptBlock({ module, moduleLabel, plats, investissements, open, setOpen }) { +function IaImportPromptBlock({ module, moduleLabel, plats, investissements, scopePlateforme, open, setOpen }) { const LS_KEY = 'cl_import_ia_prompt_template'; const [editing, setEditing] = useState(false); const [prompt, setPrompt] = useState(() => localStorage.getItem(LS_KEY) || DEFAULT_IA_IMPORT_PROMPT); @@ -449,8 +466,8 @@ function IaImportPromptBlock({ module, moduleLabel, plats, investissements, open const resolvedPrompt = () => prompt .replace('{{MODULE_LABEL}}', moduleLabel) - .replace('{{FIELDS_LIST}}', buildFieldsList(module)) - .replace('{{REFERENCE_SECTION}}', buildReferenceSection(module, plats, investissements)); + .replace('{{FIELDS_LIST}}', buildFieldsList(module, scopePlateforme)) + .replace('{{REFERENCE_SECTION}}', buildReferenceSection(module, plats, investissements, scopePlateforme)); const startEdit = () => { setDraft(prompt); setEditing(true); }; const saveEdit = () => { setPrompt(draft); localStorage.setItem(LS_KEY, draft); setEditing(false); }; @@ -647,6 +664,8 @@ export default function ImportsSection() { // null par défaut : seuls "Contexte" et "Historique" sont visibles au chargement. // Si on arrive via un lien direct (?module=...), on ouvre directement "Fichier source". const [activeMode, setActiveMode] = useState(() => searchParams.get('module') ? 'fichier' : null); + // '' = import multi-plateforme (défaut, comportement inchangé) ; sinon id de la plateforme unique visée. + const [scopePlateformeId, setScopePlateformeId] = useState(''); const [file, setFile] = useState(null); const [preview, setPreview] = useState(null); const [mapping, setMapping] = useState({}); @@ -691,6 +710,18 @@ export default function ImportsSection() { const allTargets = def ? [...def.required, ...def.optional] : []; const missingInv = def?.needsInvestisseur && !activeId; + // Un import peut viser une plateforme unique plutôt que multi-plateforme (défaut) — + // pertinent uniquement pour les modules dont les lignes sont rattachées à une plateforme. + const platformScopable = module === 'depots_retraits' || module === 'investissements' || module === 'remboursements'; + const scopePlateforme = platformScopable + ? plats.find(p => String(p.id) === String(scopePlateformeId)) || null + : null; + // Pour Remboursements, restreint la liste de référence (et les résolutions) aux + // investissements de la plateforme ciblée quand une plateforme unique est choisie. + const scopedInvestissements = (module === 'remboursements' && scopePlateforme) + ? investissements.filter(i => String(i.plateforme_id) === String(scopePlateforme.id)) + : investissements; + /** * Analyse un fichier (upload classique OU JSON généré par IA) pour le module ciblé : * upload + auto-mapping des colonnes + pré-remplissage des valeurs par défaut résolues. @@ -719,6 +750,9 @@ export default function ImportsSection() { } if (col) { auto[t] = col; usedHeaders.add(col); } } + // Plateforme unique choisie à l'étape 1 : elle est déjà connue, on ignore toute colonne + // "plateforme" du fichier pour ne jamais la contredire (voir aussi buildReferenceSection/buildFieldsList). + if (scopePlateformeId && targets.includes('plateforme_id')) delete auto.plateforme_id; setMapping(auto); // Pré-remplit la valeur par défaut quand toutes les lignes de l'échantillon @@ -726,7 +760,7 @@ export default function ImportsSection() { const autoDefaults = {}; for (const t of Object.keys(auto)) { if (!NAME_RESOLVABLE_FIELDS.has(t)) continue; - const refList = t === 'plateforme_id' ? plats : investissements; + const refList = t === 'plateforme_id' ? plats : scopedInvestissements; const nameKey = t === 'plateforme_id' ? 'nom' : 'nom_projet'; const rawValues = [...new Set( r.sampleRows.map(row => row[auto[t]]).filter(v => v !== null && v !== undefined && String(v).trim() !== '') @@ -735,6 +769,7 @@ export default function ImportsSection() { const match = resolvePreviewMatch(rawValues[0], refList, nameKey); if (match) autoDefaults[t] = String(match.id); } + if (scopePlateformeId && targets.includes('plateforme_id')) autoDefaults.plateforme_id = scopePlateformeId; if (Object.keys(autoDefaults).length > 0) setDefaults(d => ({ ...d, ...autoDefaults })); } catch (e) { setErr(e.message); } finally { setBusy(false); } @@ -830,8 +865,10 @@ export default function ImportsSection() { + {platformScopable && ( +
Importez des données depuis un fichier Excel, CSV ou JSON pour le module {def?.label ?? module}. + {scopePlateforme && <> Toutes les lignes seront associées à {scopePlateforme.nom}.>}