Evite les doublons
This commit is contained in:
@@ -2075,4 +2075,15 @@ console.log('[DB] Migrations 2FA OK');
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Migration : compteur de doublons ignorés sur imports ──────────────────
|
||||||
|
// Distingue les lignes ignorées car identiques à une ligne déjà en base
|
||||||
|
// (rows_duplicates) des lignes ignorées pour une autre raison (rows_skipped
|
||||||
|
// reste le total ; rows_duplicates est un sous-ensemble informatif).
|
||||||
|
{
|
||||||
|
const importsCols = db.prepare('PRAGMA table_info(imports)').all().map(c => c.name);
|
||||||
|
if (!importsCols.includes('rows_duplicates')) {
|
||||||
|
db.exec('ALTER TABLE imports ADD COLUMN rows_duplicates INTEGER NOT NULL DEFAULT 0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default db;
|
export default db;
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ router.post('/apply', (req, res, next) => {
|
|||||||
invNameMap = new Map(invRows.map(i => [normalizeName(i.nom_projet), i.id]));
|
invNameMap = new Map(invRows.map(i => [normalizeName(i.nom_projet), i.id]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let inserted = 0, skipped = 0;
|
let inserted = 0, skipped = 0, duplicates = 0;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
const tx = db.transaction(() => {
|
const tx = db.transaction(() => {
|
||||||
@@ -153,22 +153,48 @@ router.post('/apply', (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (module === 'depots_retraits') {
|
if (module === 'depots_retraits') {
|
||||||
|
const plateformeId = resolveRefId(v('plateforme_id'), platIdSet, platNameMap, 'Plateforme');
|
||||||
|
const dateOperation = normaliseDate(v('date_operation'));
|
||||||
|
const type = normaliseType(v('type'));
|
||||||
|
const montant = num(v('montant'));
|
||||||
|
|
||||||
|
// Anti-doublon : même investisseur + plateforme + date + type + montant
|
||||||
|
const dup = db.prepare(`
|
||||||
|
SELECT id FROM depots_retraits
|
||||||
|
WHERE investisseur_id = ? AND plateforme_id = ? AND date_operation = ?
|
||||||
|
AND type = ? AND ABS(montant - ?) < 0.005
|
||||||
|
LIMIT 1
|
||||||
|
`).get(req.investisseur.id, plateformeId, dateOperation, type, montant);
|
||||||
|
if (dup) { duplicates++; continue; }
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO depots_retraits
|
INSERT INTO depots_retraits
|
||||||
(investisseur_id, plateforme_id, date_operation, type, montant, libelle, reference, source)
|
(investisseur_id, plateforme_id, date_operation, type, montant, libelle, reference, source)
|
||||||
VALUES (?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?)
|
||||||
`).run(
|
`).run(
|
||||||
req.investisseur.id,
|
req.investisseur.id,
|
||||||
resolveRefId(v('plateforme_id'), platIdSet, platNameMap, 'Plateforme'),
|
plateformeId,
|
||||||
normaliseDate(v('date_operation')),
|
dateOperation,
|
||||||
normaliseType(v('type')),
|
type,
|
||||||
num(v('montant')),
|
montant,
|
||||||
v('libelle') || null,
|
v('libelle') || null,
|
||||||
v('reference') || null,
|
v('reference') || null,
|
||||||
srcLabel,
|
srcLabel,
|
||||||
);
|
);
|
||||||
|
|
||||||
} else if (module === 'investissements') {
|
} else if (module === 'investissements') {
|
||||||
|
const plateformeId = resolveRefId(v('plateforme_id'), platIdSet, platNameMap, 'Plateforme');
|
||||||
|
const nomProjet = String(v('nom_projet'));
|
||||||
|
const dateSouscription = normaliseDate(v('date_souscription'));
|
||||||
|
|
||||||
|
// Anti-doublon : même investisseur + plateforme + nom du projet + date de souscription
|
||||||
|
const dup = db.prepare(`
|
||||||
|
SELECT id FROM investissements
|
||||||
|
WHERE investisseur_id = ? AND plateforme_id = ? AND nom_projet = ? AND date_souscription = ?
|
||||||
|
LIMIT 1
|
||||||
|
`).get(req.investisseur.id, plateformeId, nomProjet, dateSouscription);
|
||||||
|
if (dup) { duplicates++; continue; }
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO investissements
|
INSERT INTO investissements
|
||||||
(investisseur_id, plateforme_id, nom_projet, emetteur, date_souscription,
|
(investisseur_id, plateforme_id, nom_projet, emetteur, date_souscription,
|
||||||
@@ -177,10 +203,10 @@ router.post('/apply', (req, res, next) => {
|
|||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
`).run(
|
`).run(
|
||||||
req.investisseur.id,
|
req.investisseur.id,
|
||||||
resolveRefId(v('plateforme_id'), platIdSet, platNameMap, 'Plateforme'),
|
plateformeId,
|
||||||
String(v('nom_projet')),
|
nomProjet,
|
||||||
v('emetteur') || null,
|
v('emetteur') || null,
|
||||||
normaliseDate(v('date_souscription')),
|
dateSouscription,
|
||||||
v('date_premiere_echeance') ? normaliseDate(v('date_premiere_echeance')) : null,
|
v('date_premiere_echeance') ? normaliseDate(v('date_premiere_echeance')) : null,
|
||||||
v('date_cible') ? normaliseDate(v('date_cible')) : null,
|
v('date_cible') ? normaliseDate(v('date_cible')) : null,
|
||||||
num(v('montant_investi')),
|
num(v('montant_investi')),
|
||||||
@@ -194,6 +220,8 @@ router.post('/apply', (req, res, next) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
} else if (module === 'remboursements') {
|
} else if (module === 'remboursements') {
|
||||||
|
const investissementId = resolveRefId(v('investissement_id'), invIdSet, invNameMap, 'Investissement');
|
||||||
|
const dateRemb = normaliseDate(v('date_remb'));
|
||||||
const capital = num(v('capital'));
|
const capital = num(v('capital'));
|
||||||
const cashback = num(v('cashback'));
|
const cashback = num(v('cashback'));
|
||||||
const bruts = num(v('interets_bruts'));
|
const bruts = num(v('interets_bruts'));
|
||||||
@@ -201,14 +229,24 @@ router.post('/apply', (req, res, next) => {
|
|||||||
const pf = num(v('prelev_forfaitaire'));
|
const pf = num(v('prelev_forfaitaire'));
|
||||||
const interets_nets = Math.round((bruts - ps - pf) * 100) / 100;
|
const interets_nets = Math.round((bruts - ps - pf) * 100) / 100;
|
||||||
const net_recu = Math.round((capital + cashback + interets_nets) * 100) / 100;
|
const net_recu = Math.round((capital + cashback + interets_nets) * 100) / 100;
|
||||||
|
|
||||||
|
// Anti-doublon : même investissement + date + capital + intérêts bruts
|
||||||
|
const dup = db.prepare(`
|
||||||
|
SELECT id FROM remboursements
|
||||||
|
WHERE investissement_id = ? AND date_remb = ?
|
||||||
|
AND ABS(capital - ?) < 0.005 AND ABS(interets_bruts - ?) < 0.005
|
||||||
|
LIMIT 1
|
||||||
|
`).get(investissementId, dateRemb, capital, bruts);
|
||||||
|
if (dup) { duplicates++; continue; }
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO remboursements
|
INSERT INTO remboursements
|
||||||
(investissement_id, date_remb, capital, cashback, interets_bruts, prelev_sociaux,
|
(investissement_id, date_remb, capital, cashback, interets_bruts, prelev_sociaux,
|
||||||
prelev_forfaitaire, interets_nets, net_recu, statut, source)
|
prelev_forfaitaire, interets_nets, net_recu, statut, source)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||||
`).run(
|
`).run(
|
||||||
resolveRefId(v('investissement_id'), invIdSet, invNameMap, 'Investissement'),
|
investissementId,
|
||||||
normaliseDate(v('date_remb')),
|
dateRemb,
|
||||||
capital, cashback, bruts, ps, pf, interets_nets, net_recu,
|
capital, cashback, bruts, ps, pf, interets_nets, net_recu,
|
||||||
v('statut') || 'paye',
|
v('statut') || 'paye',
|
||||||
srcLabel,
|
srcLabel,
|
||||||
@@ -226,8 +264,8 @@ router.post('/apply', (req, res, next) => {
|
|||||||
v('url') || null,
|
v('url') || null,
|
||||||
v('notes') || null,
|
v('notes') || null,
|
||||||
);
|
);
|
||||||
// changes = 0 means the row was ignored (nom already exists)
|
// changes = 0 means the row was ignored (nom already exists) — doublon, pas une erreur
|
||||||
if (r.changes === 0) throw new Error(`Plateforme "${nom}" existe déjà — ignorée`);
|
if (r.changes === 0) { duplicates++; continue; }
|
||||||
|
|
||||||
} else if (module === 'taux_pfu') {
|
} else if (module === 'taux_pfu') {
|
||||||
const annee = parseInt(v('annee'), 10);
|
const annee = parseInt(v('annee'), 10);
|
||||||
@@ -258,8 +296,8 @@ router.post('/apply', (req, res, next) => {
|
|||||||
tx();
|
tx();
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO imports (user_id, investisseur_id, module, filename, rows_total, rows_inserted, rows_skipped, mapping_json)
|
INSERT INTO imports (user_id, investisseur_id, module, filename, rows_total, rows_inserted, rows_skipped, rows_duplicates, mapping_json)
|
||||||
VALUES (?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?,?)
|
||||||
`).run(
|
`).run(
|
||||||
req.user.id,
|
req.user.id,
|
||||||
req.investisseur?.id ?? null,
|
req.investisseur?.id ?? null,
|
||||||
@@ -268,13 +306,14 @@ router.post('/apply', (req, res, next) => {
|
|||||||
rows.length,
|
rows.length,
|
||||||
inserted,
|
inserted,
|
||||||
skipped,
|
skipped,
|
||||||
|
duplicates,
|
||||||
JSON.stringify(mapping),
|
JSON.stringify(mapping),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clean up temp file
|
// Clean up temp file
|
||||||
try { fs.unlinkSync(tempPath); } catch { /* */ }
|
try { fs.unlinkSync(tempPath); } catch { /* */ }
|
||||||
|
|
||||||
res.json({ inserted, skipped, total: rows.length, errors: errors.slice(0, 50) });
|
res.json({ inserted, skipped, duplicates, total: rows.length, errors: errors.slice(0, 50) });
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -406,7 +406,11 @@ export default function ImportsSection() {
|
|||||||
});
|
});
|
||||||
setResult({
|
setResult({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: `✔ Import terminé : ${r.inserted} / ${r.total} lignes insérées${r.skipped > 0 ? `, ${r.skipped} ignorées` : ''}.${r.errors?.length > 0 ? ` (${r.errors.length} avertissement(s))` : ''}`,
|
msg: `✔ Import terminé : ${r.inserted} / ${r.total} lignes insérées`
|
||||||
|
+ (r.duplicates > 0 ? `, ${r.duplicates} doublon(s) ignoré(s)` : '')
|
||||||
|
+ (r.skipped > 0 ? `, ${r.skipped} ignorée(s)` : '')
|
||||||
|
+ '.'
|
||||||
|
+ (r.errors?.length > 0 ? ` (${r.errors.length} avertissement(s))` : ''),
|
||||||
});
|
});
|
||||||
setPreview(null); setFile(null); setMapping({}); setDefaults({});
|
setPreview(null); setFile(null); setMapping({}); setDefaults({});
|
||||||
api.get('/imports/history').then(setHistory).catch(() => {});
|
api.get('/imports/history').then(setHistory).catch(() => {});
|
||||||
@@ -602,12 +606,12 @@ export default function ImportsSection() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th><th>Module</th><th>Fichier</th>
|
<th>Date</th><th>Module</th><th>Fichier</th>
|
||||||
<th className="num">Total</th><th className="num">OK</th><th className="num">KO</th>
|
<th className="num">Total</th><th className="num">OK</th><th className="num">Doublons</th><th className="num">KO</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{history.length === 0 && (
|
{history.length === 0 && (
|
||||||
<tr><td colSpan={6} className="text-muted" style={{ textAlign: 'center' }}>Aucun import</td></tr>
|
<tr><td colSpan={7} className="text-muted" style={{ textAlign: 'center' }}>Aucun import</td></tr>
|
||||||
)}
|
)}
|
||||||
{history.map(h => (
|
{history.map(h => (
|
||||||
<tr key={h.id}>
|
<tr key={h.id}>
|
||||||
@@ -616,6 +620,7 @@ export default function ImportsSection() {
|
|||||||
<td className="text-muted" style={{ fontSize: 11 }}>{h.filename}</td>
|
<td className="text-muted" style={{ fontSize: 11 }}>{h.filename}</td>
|
||||||
<td className="num">{h.rows_total}</td>
|
<td className="num">{h.rows_total}</td>
|
||||||
<td className="num" style={{ color: 'var(--success)' }}>{h.rows_inserted}</td>
|
<td className="num" style={{ color: 'var(--success)' }}>{h.rows_inserted}</td>
|
||||||
|
<td className="num" style={{ color: h.rows_duplicates > 0 ? 'var(--text-muted)' : undefined }}>{h.rows_duplicates ?? 0}</td>
|
||||||
<td className="num" style={{ color: h.rows_skipped > 0 ? 'var(--warning)' : undefined }}>{h.rows_skipped}</td>
|
<td className="num" style={{ color: h.rows_skipped > 0 ? 'var(--warning)' : undefined }}>{h.rows_skipped}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user