Files
jb-compta/indy-mcp/bookmarklet.js
T

100 lines
6.2 KiB
JavaScript

/*
* Bookmarklet « Indy → CSV » — version lisible (documentation).
* Le bouton prêt à installer est dans bookmarklet.html, et la même page est servie
* par l'application à /import/bookmarklet/.
*
* Usage : être connecté sur app.indy.fr, cliquer le favori. Une petite fenêtre
* s'ouvre avec deux SÉLECTEURS DE DATE (format JJ/MM/AAAA). Il récupère ensuite les
* transactions (dans TON navigateur = autorisé, 2FA déjà passée, pas de souci serveur
* ni Cloudflare), construit le CSV au format JB Compta et le copie dans le presse-papier.
*/
(async () => {
// --- mapping compte PCG -> catégorie (identique à compta/indy.py) ---
const CC = [
["701", "Vente de produits finis"], ["707", "Vente de produits finis"],
["706", "Prestation de services"],
["605", "Matériel et outillage"], ["606", "Matériel et outillage"], ["607", "Matériel et outillage"],
["6256", "Frais de repas hors domicile"], ["6253", "Frais de repas hors domicile"],
["626", "Télécom, fournitures, documentation"],
["6181", "Télécom, fournitures, documentation"], ["6183", "Télécom, fournitures, documentation"],
["6136", "Abonnement logiciel"], ["6226", "Abonnement logiciel"],
["646", "Cotisation sociale Urssaf"],
["44551", "TVA payée"], ["44558", "TVA payée"], ["44583", "Remboursement de TVA"],
["108", "Prélèvement personnel"],
];
const CA = [["701", "vente"], ["707", "vente"], ["706", "service_bic"]];
const cat = (n) => { n = String(n || ""); let b = null; for (const [p, c] of CC) { if (n.startsWith(p) && (!b || p.length > b[0].length)) b = [p, c]; } return b ? b[1] : ""; };
const act = (n) => { n = String(n || ""); for (const [p, a] of CA) { if (n.startsWith(p)) return a; } return ""; };
const norm = (tx) => {
const c = tx.totalAmountInCents; if (c == null) return null;
const mt = (c / 100).toFixed(2);
const ss = (tx.subdivisions || []).filter(s => !String((s.accounting_account || {}).number || "").startsWith("512"));
if (!ss.length) return null;
let m = ss[0]; for (const s of ss) if (Math.abs(s.amount_in_cents || 0) > Math.abs(m.amount_in_cents || 0)) m = s;
const num = String((m.accounting_account || {}).number || "");
let rg = "", t2 = 0;
if (m.tva_intracom && m.tva_intracom.type === "eu") { rg = "intracom"; t2 = (m.tva_intracom.rate || 0) / 100; }
else { t2 = (m.tva_rate || 0) / 100; }
const lib = (tx.description || tx.raw_description || "").replace(/;/g, " ");
return [tx.date || "", lib, cat(num), act(num), String(t2), rg, mt].join(";");
};
// --- fenêtre avec deux sélecteurs de date (input type=date -> JJ/MM/AAAA) ---
const mk = (t, css) => { const e = document.createElement(t); if (css) e.style.cssText = css; return e; };
const askDates = () => new Promise(res => {
const t = new Date(), y = t.getFullYear(),
mo = String(t.getMonth() + 1).padStart(2, "0"),
ld = String(new Date(y, t.getMonth() + 1, 0).getDate()).padStart(2, "0");
const ov = mk("div", "position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:2147483647;display:flex;align-items:center;justify-content:center");
const bx = mk("div", "background:#fff;color:#111;padding:20px;border-radius:12px;font-family:sans-serif;min-width:280px");
const h = mk("div", "font-weight:700;margin-bottom:12px"); h.textContent = "Indy vers CSV : choisis la periode";
const l1 = mk("div", "font-size:12px;color:#555"); l1.textContent = "Du";
const i1 = mk("input", "width:100%;margin:4px 0 10px;padding:6px"); i1.type = "date"; i1.value = y + "-" + mo + "-01";
const l2 = mk("div", "font-size:12px;color:#555"); l2.textContent = "Au";
const i2 = mk("input", "width:100%;margin:4px 0 14px;padding:6px"); i2.type = "date"; i2.value = y + "-" + mo + "-" + ld;
const row = mk("div", "text-align:right");
const an = mk("button", "margin-right:8px;padding:6px 12px"); an.textContent = "Annuler";
const ok = mk("button", "background:#38bdf8;border:none;padding:6px 14px;border-radius:6px;cursor:pointer"); ok.textContent = "Recuperer";
row.appendChild(an); row.appendChild(ok);
[h, l1, i1, l2, i2, row].forEach(x => bx.appendChild(x));
ov.appendChild(bx); document.body.appendChild(ov);
ok.onclick = () => { const a = i1.value, b = i2.value; document.body.removeChild(ov); res([a, b]); };
an.onclick = () => { document.body.removeChild(ov); res([null, null]); };
});
const [d1, d2] = await askDates(); if (!d1 || !d2) return;
// --- token Firebase (depuis IndexedDB) ---
const getTok = () => new Promise(r => {
const o = indexedDB.open("firebaseLocalStorageDb");
o.onsuccess = () => { try {
const q = o.result.transaction("firebaseLocalStorage", "readonly").objectStore("firebaseLocalStorage").getAll();
q.onsuccess = () => { const v = (q.result || []).map(x => x.value).find(v => v && v.stsTokenManager && v.stsTokenManager.accessToken); r(v ? v.stsTokenManager.accessToken : null); };
q.onerror = () => r(null);
} catch (e) { r(null); } };
o.onerror = () => r(null);
});
const tk = await getTok();
const H = { "Accept": "application/json", "x-client-app-type": "web" };
if (tk) H["Authorization"] = "Bearer " + tk;
// --- fetch paginé ---
let all = [], pg = 1, tot = 1e9;
try {
while (all.length < tot) {
const u = "/api/transactions/transactions-list?" + new URLSearchParams({ search: "", dateFrom: d1, dateTo: d2, page: pg });
const r = await fetch(u, { headers: H, credentials: "include" });
if (!r.ok) throw new Error("HTTP " + r.status);
const j = await r.json();
tot = j.nbTransactions != null ? j.nbTransactions : (j.transactions || []).length;
const t = j.transactions || []; if (!t.length) break;
all = all.concat(t); pg++; if (pg > 60) break;
}
} catch (e) { alert("Erreur Indy : " + e.message); return; }
const rows = all.map(norm).filter(Boolean);
const NL = String.fromCharCode(10);
const csv = "date;libelle;categorie;activite;taux_tva;regime;montant" + NL + rows.join(NL);
try { await navigator.clipboard.writeText(csv); alert(rows.length + " operations -> CSV copie ! Colle-le dans JB Compta (/import/)."); }
catch (e) { window.prompt("Copie ce CSV (Ctrl+C) :", csv); }
})();