Selection de la plateforma avant import

This commit is contained in:
2026-07-05 10:40:33 +02:00
parent 1f75878d87
commit 186ffc2596
+73 -21
View File
@@ -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() {
<label>Module cible</label>
<select value={module} onChange={e => {
const v = e.target.value;
const stillScopable = v === 'depots_retraits' || v === 'investissements' || v === 'remboursements';
setModule(v);
setPreview(null); setMapping({}); setResult(null); setErr(null);
if (!stillScopable) setScopePlateformeId('');
if (v !== 'investissements' && activeMode === 'dossier') setActiveMode(null);
}}>
{Object.entries(MODULES).map(([k, v]) => (
@@ -839,6 +876,18 @@ export default function ImportsSection() {
))}
</select>
</div>
{platformScopable && (
<div style={{ flex: '0 0 auto', width: 280 }}>
<label>Plateforme</label>
<select value={scopePlateformeId} onChange={e => {
setScopePlateformeId(e.target.value);
setPreview(null); setMapping({}); setDefaults({}); setResult(null); setErr(null);
}}>
<option value="">Toutes les plateformes (multi-plateforme)</option>
{plats.map(p => <option key={p.id} value={p.id}>{p.nom}</option>)}
</select>
</div>
)}
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 14, flexWrap: 'wrap' }}>
@@ -896,6 +945,7 @@ export default function ImportsSection() {
<h3 style={{ margin: '0 0 4px' }}>Étape 2 - Précisez le fichier source</h3>
<p className="text-muted" style={{ margin: '0 0 16px', fontSize: 'var(--fs-sm)' }}>
Importez des données depuis un fichier Excel, CSV ou JSON pour le module <strong>{def?.label ?? module}</strong>.
{scopePlateforme && <> Toutes les lignes seront associées à <strong>{scopePlateforme.nom}</strong>.</>}
</p>
<div className="row">
<div style={{ flex: 2 }}>
@@ -935,6 +985,7 @@ export default function ImportsSection() {
Utilisez un assistant IA pour convertir l'export de votre plateforme (CSV, Excel, PDF) en données structurées,
sans mappage manuel des colonnes. Module cible actuel : <strong>{def?.label ?? module}</strong>
{' '}(modifiable en section 1 ci-dessus).
{scopePlateforme && <> Import limité à <strong>{scopePlateforme.nom}</strong> : le prompt généré ci-dessous en tient compte.</>}
</p>
<IaImportPasteBlock
moduleLabel={def?.label ?? module}
@@ -947,7 +998,8 @@ export default function ImportsSection() {
module={module}
moduleLabel={def?.label ?? module}
plats={plats}
investissements={investissements}
investissements={scopedInvestissements}
scopePlateforme={scopePlateforme}
open={iaPromptOpen}
setOpen={setIaPromptOpen}
/>
@@ -1020,7 +1072,7 @@ export default function ImportsSection() {
{preview.headers.map(h => <option key={h} value={h}>{h}</option>)}
</select>
{NAME_RESOLVABLE_FIELDS.has(t) && mapping[t] && (() => {
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(
preview.sampleRows
@@ -1061,7 +1113,7 @@ export default function ImportsSection() {
) : t === 'investissement_id' ? (
<select value={defaults[t] || ''} onChange={e => setDefaults({ ...defaults, [t]: e.target.value })}>
<option value="">—</option>
{investissements.map(i => <option key={i.id} value={i.id}>{i.nom_projet}</option>)}
{scopedInvestissements.map(i => <option key={i.id} value={i.id}>{i.nom_projet}</option>)}
</select>
) : (
<input value={defaults[t] || ''} onChange={e => setDefaults({ ...defaults, [t]: e.target.value })}