Selection de la plateforma avant import
This commit is contained in:
@@ -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) {
|
||||
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 })}
|
||||
|
||||
Reference in New Issue
Block a user