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];
|
const def = MODULES[mod];
|
||||||
if (!def) return '';
|
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 = [];
|
const lines = [];
|
||||||
for (const key of def.required) {
|
for (const key of def.required) lines.push(line(key, true));
|
||||||
const hint = FIELD_HINTS_OVERRIDE[mod]?.[key] ?? FIELD_HINTS[key] ?? '';
|
for (const key of def.optional) lines.push(line(key, false));
|
||||||
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}` : ''}`);
|
|
||||||
}
|
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildReferenceSection(mod, plats, investissements) {
|
function buildReferenceSection(mod, plats, investissements, scopePlateforme) {
|
||||||
if ((mod === 'depots_retraits' || mod === 'investissements') && plats.length > 0) {
|
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(' | ');
|
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`;
|
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) {
|
if (mod === 'remboursements' && investissements.length > 0) {
|
||||||
const names = investissements.map(i => i.nom_projet).join(' | ');
|
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 '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -440,7 +457,7 @@ function IaImportPasteBlock({ moduleLabel, iaJson, setIaJson, iaErr, setIaErr, o
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Bloc collapsible : prompt IA dynamique (champs du module + plateformes/investissements existants) */
|
/** 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 LS_KEY = 'cl_import_ia_prompt_template';
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [prompt, setPrompt] = useState(() => localStorage.getItem(LS_KEY) || DEFAULT_IA_IMPORT_PROMPT);
|
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
|
const resolvedPrompt = () => prompt
|
||||||
.replace('{{MODULE_LABEL}}', moduleLabel)
|
.replace('{{MODULE_LABEL}}', moduleLabel)
|
||||||
.replace('{{FIELDS_LIST}}', buildFieldsList(module))
|
.replace('{{FIELDS_LIST}}', buildFieldsList(module, scopePlateforme))
|
||||||
.replace('{{REFERENCE_SECTION}}', buildReferenceSection(module, plats, investissements));
|
.replace('{{REFERENCE_SECTION}}', buildReferenceSection(module, plats, investissements, scopePlateforme));
|
||||||
|
|
||||||
const startEdit = () => { setDraft(prompt); setEditing(true); };
|
const startEdit = () => { setDraft(prompt); setEditing(true); };
|
||||||
const saveEdit = () => { setPrompt(draft); localStorage.setItem(LS_KEY, draft); setEditing(false); };
|
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.
|
// 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".
|
// Si on arrive via un lien direct (?module=...), on ouvre directement "Fichier source".
|
||||||
const [activeMode, setActiveMode] = useState(() => searchParams.get('module') ? 'fichier' : null);
|
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 [file, setFile] = useState(null);
|
||||||
const [preview, setPreview] = useState(null);
|
const [preview, setPreview] = useState(null);
|
||||||
const [mapping, setMapping] = useState({});
|
const [mapping, setMapping] = useState({});
|
||||||
@@ -691,6 +710,18 @@ export default function ImportsSection() {
|
|||||||
const allTargets = def ? [...def.required, ...def.optional] : [];
|
const allTargets = def ? [...def.required, ...def.optional] : [];
|
||||||
const missingInv = def?.needsInvestisseur && !activeId;
|
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é :
|
* 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.
|
* 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); }
|
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);
|
setMapping(auto);
|
||||||
|
|
||||||
// Pré-remplit la valeur par défaut quand toutes les lignes de l'échantillon
|
// Pré-remplit la valeur par défaut quand toutes les lignes de l'échantillon
|
||||||
@@ -726,7 +760,7 @@ export default function ImportsSection() {
|
|||||||
const autoDefaults = {};
|
const autoDefaults = {};
|
||||||
for (const t of Object.keys(auto)) {
|
for (const t of Object.keys(auto)) {
|
||||||
if (!NAME_RESOLVABLE_FIELDS.has(t)) continue;
|
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 nameKey = t === 'plateforme_id' ? 'nom' : 'nom_projet';
|
||||||
const rawValues = [...new Set(
|
const rawValues = [...new Set(
|
||||||
r.sampleRows.map(row => row[auto[t]]).filter(v => v !== null && v !== undefined && String(v).trim() !== '')
|
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);
|
const match = resolvePreviewMatch(rawValues[0], refList, nameKey);
|
||||||
if (match) autoDefaults[t] = String(match.id);
|
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 }));
|
if (Object.keys(autoDefaults).length > 0) setDefaults(d => ({ ...d, ...autoDefaults }));
|
||||||
} catch (e) { setErr(e.message); }
|
} catch (e) { setErr(e.message); }
|
||||||
finally { setBusy(false); }
|
finally { setBusy(false); }
|
||||||
@@ -830,8 +865,10 @@ export default function ImportsSection() {
|
|||||||
<label>Module cible</label>
|
<label>Module cible</label>
|
||||||
<select value={module} onChange={e => {
|
<select value={module} onChange={e => {
|
||||||
const v = e.target.value;
|
const v = e.target.value;
|
||||||
|
const stillScopable = v === 'depots_retraits' || v === 'investissements' || v === 'remboursements';
|
||||||
setModule(v);
|
setModule(v);
|
||||||
setPreview(null); setMapping({}); setResult(null); setErr(null);
|
setPreview(null); setMapping({}); setResult(null); setErr(null);
|
||||||
|
if (!stillScopable) setScopePlateformeId('');
|
||||||
if (v !== 'investissements' && activeMode === 'dossier') setActiveMode(null);
|
if (v !== 'investissements' && activeMode === 'dossier') setActiveMode(null);
|
||||||
}}>
|
}}>
|
||||||
{Object.entries(MODULES).map(([k, v]) => (
|
{Object.entries(MODULES).map(([k, v]) => (
|
||||||
@@ -839,6 +876,18 @@ export default function ImportsSection() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 8, marginTop: 14, flexWrap: 'wrap' }}>
|
<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>
|
<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)' }}>
|
<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>.
|
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>
|
</p>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div style={{ flex: 2 }}>
|
<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,
|
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>
|
sans mappage manuel des colonnes. Module cible actuel : <strong>{def?.label ?? module}</strong>
|
||||||
{' '}(modifiable en section 1 ci-dessus).
|
{' '}(modifiable en section 1 ci-dessus).
|
||||||
|
{scopePlateforme && <> Import limité à <strong>{scopePlateforme.nom}</strong> : le prompt généré ci-dessous en tient compte.</>}
|
||||||
</p>
|
</p>
|
||||||
<IaImportPasteBlock
|
<IaImportPasteBlock
|
||||||
moduleLabel={def?.label ?? module}
|
moduleLabel={def?.label ?? module}
|
||||||
@@ -947,7 +998,8 @@ export default function ImportsSection() {
|
|||||||
module={module}
|
module={module}
|
||||||
moduleLabel={def?.label ?? module}
|
moduleLabel={def?.label ?? module}
|
||||||
plats={plats}
|
plats={plats}
|
||||||
investissements={investissements}
|
investissements={scopedInvestissements}
|
||||||
|
scopePlateforme={scopePlateforme}
|
||||||
open={iaPromptOpen}
|
open={iaPromptOpen}
|
||||||
setOpen={setIaPromptOpen}
|
setOpen={setIaPromptOpen}
|
||||||
/>
|
/>
|
||||||
@@ -1020,7 +1072,7 @@ export default function ImportsSection() {
|
|||||||
{preview.headers.map(h => <option key={h} value={h}>{h}</option>)}
|
{preview.headers.map(h => <option key={h} value={h}>{h}</option>)}
|
||||||
</select>
|
</select>
|
||||||
{NAME_RESOLVABLE_FIELDS.has(t) && mapping[t] && (() => {
|
{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 nameKey = t === 'plateforme_id' ? 'nom' : 'nom_projet';
|
||||||
const rawValues = [...new Set(
|
const rawValues = [...new Set(
|
||||||
preview.sampleRows
|
preview.sampleRows
|
||||||
@@ -1061,7 +1113,7 @@ export default function ImportsSection() {
|
|||||||
) : t === 'investissement_id' ? (
|
) : t === 'investissement_id' ? (
|
||||||
<select value={defaults[t] || ''} onChange={e => setDefaults({ ...defaults, [t]: e.target.value })}>
|
<select value={defaults[t] || ''} onChange={e => setDefaults({ ...defaults, [t]: e.target.value })}>
|
||||||
<option value="">—</option>
|
<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>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
<input value={defaults[t] || ''} onChange={e => setDefaults({ ...defaults, [t]: e.target.value })}
|
<input value={defaults[t] || ''} onChange={e => setDefaults({ ...defaults, [t]: e.target.value })}
|
||||||
|
|||||||
Reference in New Issue
Block a user