feat(fiscal): export/import ZIP référentiel fiscal
- Remplace le dropdown CSV/XLS/JSON de PfuSection par Exporter/Importer ZIP - GET /api/pfu/export-zip : ZIP avec manifest, pfu.json, taux_credit_impot.json - POST /api/pfu/import-zip : upsert PFU (par annee) et TCI (par nom_pays/code_pays) - ResultBanner pour le résultat de l'import dans PfuSection
This commit is contained in:
+150
-13
@@ -1,15 +1,158 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
import multer from 'multer';
|
||||||
import db from '../db/index.js';
|
import db from '../db/index.js';
|
||||||
|
import { createZip, readZip } from '../utils/zip.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
/* GET /api/pfu — liste triée par année */
|
const zipStorage = multer.memoryStorage();
|
||||||
|
const zipUpload = multer({ storage: zipStorage, limits: { fileSize: 5 * 1024 * 1024 } });
|
||||||
|
|
||||||
|
/* GET /api/pfu */
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
const rows = db.prepare('SELECT * FROM taux_pfu ORDER BY annee ASC').all();
|
const rows = db.prepare('SELECT * FROM taux_pfu ORDER BY annee ASC').all();
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* POST /api/pfu — ajouter une année */
|
/* GET /api/pfu/export-zip — export ZIP fiscal (PFU + crédit d'impôt) */
|
||||||
|
router.get('/export-zip', (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const pfu = db.prepare('SELECT * FROM taux_pfu ORDER BY annee ASC').all();
|
||||||
|
const tci = db.prepare('SELECT * FROM taux_credit_impot ORDER BY nom_pays ASC').all();
|
||||||
|
|
||||||
|
const manifest = {
|
||||||
|
version: '1.0',
|
||||||
|
app: 'crowdlending',
|
||||||
|
exported_at: new Date().toISOString(),
|
||||||
|
type: 'fiscal-referentiel',
|
||||||
|
};
|
||||||
|
|
||||||
|
const pfuData = pfu.map(r => ({
|
||||||
|
annee: r.annee,
|
||||||
|
impot_revenu: r.impot_revenu,
|
||||||
|
csg: r.csg,
|
||||||
|
crds: r.crds,
|
||||||
|
solidarite: r.solidarite,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const tciData = tci.map(r => ({
|
||||||
|
nom_pays: r.nom_pays,
|
||||||
|
code_pays: r.code_pays,
|
||||||
|
div_taux: r.div_taux,
|
||||||
|
div_taux_alt: r.div_taux_alt,
|
||||||
|
div_taux_alt_label: r.div_taux_alt_label,
|
||||||
|
div_exclusif_residence: r.div_exclusif_residence,
|
||||||
|
int_taux: r.int_taux,
|
||||||
|
int_taux_alt: r.int_taux_alt,
|
||||||
|
int_taux_alt_label: r.int_taux_alt_label,
|
||||||
|
int_exclusif_residence: r.int_exclusif_residence,
|
||||||
|
notice: r.notice,
|
||||||
|
statut_convention: r.statut_convention,
|
||||||
|
date_suspension: r.date_suspension,
|
||||||
|
ref_boi: r.ref_boi,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const entries = [
|
||||||
|
{ name: 'manifest.json', data: JSON.stringify(manifest, null, 2) },
|
||||||
|
{ name: 'pfu.json', data: JSON.stringify(pfuData, null, 2) },
|
||||||
|
{ name: 'taux_credit_impot.json', data: JSON.stringify(tciData, null, 2) },
|
||||||
|
];
|
||||||
|
|
||||||
|
const zipBuf = createZip(entries);
|
||||||
|
const slug = 'fiscal-referentiel-' + new Date().toISOString().slice(0, 10);
|
||||||
|
res.setHeader('Content-Type', 'application/zip');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename="${slug}.zip"`);
|
||||||
|
res.send(zipBuf);
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
/* POST /api/pfu/import-zip — import ZIP fiscal (PFU + crédit d'impôt) */
|
||||||
|
router.post('/import-zip', zipUpload.single('file'), (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (!req.file) return res.status(400).json({ error: 'Fichier ZIP manquant' });
|
||||||
|
const zipEntries = readZip(req.file.buffer);
|
||||||
|
|
||||||
|
const pfuEntry = zipEntries.find(e => e.name === 'pfu.json');
|
||||||
|
const tciEntry = zipEntries.find(e => e.name === 'taux_credit_impot.json');
|
||||||
|
if (!pfuEntry && !tciEntry) {
|
||||||
|
return res.status(400).json({ error: 'ZIP invalide : aucun fichier reconnu' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let pfuCreated = 0, pfuUpdated = 0;
|
||||||
|
let tciCreated = 0, tciUpdated = 0;
|
||||||
|
|
||||||
|
const tx = db.transaction(() => {
|
||||||
|
if (pfuEntry) {
|
||||||
|
const rows = JSON.parse(pfuEntry.data.toString('utf8'));
|
||||||
|
for (const r of rows) {
|
||||||
|
if (!r.annee) continue;
|
||||||
|
const ps = Number(r.csg || 0) + Number(r.crds || 0) + Number(r.solidarite || 0);
|
||||||
|
const total = Number(r.impot_revenu || 0) + ps;
|
||||||
|
const ex = db.prepare('SELECT id FROM taux_pfu WHERE annee = ?').get(Number(r.annee));
|
||||||
|
if (ex) {
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE taux_pfu SET pfu_total=?, impot_revenu=?, prelev_sociaux=?, csg=?, crds=?, solidarite=?, updated_at=datetime('now') WHERE id=?`
|
||||||
|
).run(total, Number(r.impot_revenu), ps, Number(r.csg), Number(r.crds), Number(r.solidarite), ex.id);
|
||||||
|
pfuUpdated++;
|
||||||
|
} else {
|
||||||
|
db.prepare(
|
||||||
|
'INSERT INTO taux_pfu (annee, pfu_total, impot_revenu, prelev_sociaux, csg, crds, solidarite) VALUES (?,?,?,?,?,?,?)'
|
||||||
|
).run(Number(r.annee), total, Number(r.impot_revenu), ps, Number(r.csg), Number(r.crds), Number(r.solidarite));
|
||||||
|
pfuCreated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tciEntry) {
|
||||||
|
const rows = JSON.parse(tciEntry.data.toString('utf8'));
|
||||||
|
for (const r of rows) {
|
||||||
|
if (!r.nom_pays) continue;
|
||||||
|
const ex = db.prepare(
|
||||||
|
'SELECT id FROM taux_credit_impot WHERE LOWER(nom_pays) = LOWER(?) OR (code_pays IS NOT NULL AND code_pays = ?)'
|
||||||
|
).get(r.nom_pays, r.code_pays || '');
|
||||||
|
const vals = [
|
||||||
|
r.nom_pays, r.code_pays || null,
|
||||||
|
r.div_taux ?? null, r.div_taux_alt ?? null, r.div_taux_alt_label || null,
|
||||||
|
r.div_exclusif_residence ? 1 : 0,
|
||||||
|
r.int_taux ?? null, r.int_taux_alt ?? null, r.int_taux_alt_label || null,
|
||||||
|
r.int_exclusif_residence ? 1 : 0,
|
||||||
|
r.notice || null, r.statut_convention || 'active',
|
||||||
|
r.date_suspension || null, r.ref_boi || null,
|
||||||
|
];
|
||||||
|
if (ex) {
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE taux_credit_impot SET
|
||||||
|
nom_pays=?, code_pays=?,
|
||||||
|
div_taux=?, div_taux_alt=?, div_taux_alt_label=?, div_exclusif_residence=?,
|
||||||
|
int_taux=?, int_taux_alt=?, int_taux_alt_label=?, int_exclusif_residence=?,
|
||||||
|
notice=?, statut_convention=?, date_suspension=?, ref_boi=?,
|
||||||
|
updated_at=datetime('now') WHERE id=?`
|
||||||
|
).run(...vals, ex.id);
|
||||||
|
tciUpdated++;
|
||||||
|
} else {
|
||||||
|
db.prepare(
|
||||||
|
`INSERT INTO taux_credit_impot
|
||||||
|
(nom_pays, code_pays, div_taux, div_taux_alt, div_taux_alt_label, div_exclusif_residence,
|
||||||
|
int_taux, int_taux_alt, int_taux_alt_label, int_exclusif_residence,
|
||||||
|
notice, statut_convention, date_suspension, ref_boi)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
||||||
|
).run(...vals);
|
||||||
|
tciCreated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tx();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
ok: true,
|
||||||
|
pfu: { created: pfuCreated, updated: pfuUpdated },
|
||||||
|
tci: { created: tciCreated, updated: tciUpdated },
|
||||||
|
});
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
/* POST /api/pfu */
|
||||||
router.post('/', (req, res) => {
|
router.post('/', (req, res) => {
|
||||||
const { annee, impot_revenu, csg, crds, solidarite } = req.body;
|
const { annee, impot_revenu, csg, crds, solidarite } = req.body;
|
||||||
if (!annee || impot_revenu == null || csg == null || crds == null || solidarite == null) {
|
if (!annee || impot_revenu == null || csg == null || crds == null || solidarite == null) {
|
||||||
@@ -18,13 +161,9 @@ router.post('/', (req, res) => {
|
|||||||
const prelev_sociaux = Number(csg) + Number(crds) + Number(solidarite);
|
const prelev_sociaux = Number(csg) + Number(crds) + Number(solidarite);
|
||||||
const pfu_total = Number(impot_revenu) + prelev_sociaux;
|
const pfu_total = Number(impot_revenu) + prelev_sociaux;
|
||||||
try {
|
try {
|
||||||
const stmt = db.prepare(
|
const result = db.prepare(
|
||||||
'INSERT INTO taux_pfu (annee, pfu_total, impot_revenu, prelev_sociaux, csg, crds, solidarite) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
'INSERT INTO taux_pfu (annee, pfu_total, impot_revenu, prelev_sociaux, csg, crds, solidarite) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||||
);
|
).run(Number(annee), pfu_total, Number(impot_revenu), prelev_sociaux, Number(csg), Number(crds), Number(solidarite));
|
||||||
const result = stmt.run(
|
|
||||||
Number(annee), pfu_total, Number(impot_revenu), prelev_sociaux,
|
|
||||||
Number(csg), Number(crds), Number(solidarite)
|
|
||||||
);
|
|
||||||
res.status(201).json(db.prepare('SELECT * FROM taux_pfu WHERE id = ?').get(result.lastInsertRowid));
|
res.status(201).json(db.prepare('SELECT * FROM taux_pfu WHERE id = ?').get(result.lastInsertRowid));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message?.includes('UNIQUE')) return res.status(409).json({ error: `L'année ${annee} existe déjà.` });
|
if (e.message?.includes('UNIQUE')) return res.status(409).json({ error: `L'année ${annee} existe déjà.` });
|
||||||
@@ -32,7 +171,7 @@ router.post('/', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* PUT /api/pfu/:id — modifier */
|
/* PUT /api/pfu/:id */
|
||||||
router.put('/:id', (req, res) => {
|
router.put('/:id', (req, res) => {
|
||||||
const { annee, impot_revenu, csg, crds, solidarite } = req.body;
|
const { annee, impot_revenu, csg, crds, solidarite } = req.body;
|
||||||
if (!annee || impot_revenu == null || csg == null || crds == null || solidarite == null) {
|
if (!annee || impot_revenu == null || csg == null || crds == null || solidarite == null) {
|
||||||
@@ -44,10 +183,8 @@ router.put('/:id', (req, res) => {
|
|||||||
db.prepare(
|
db.prepare(
|
||||||
`UPDATE taux_pfu SET annee=?, pfu_total=?, impot_revenu=?, prelev_sociaux=?,
|
`UPDATE taux_pfu SET annee=?, pfu_total=?, impot_revenu=?, prelev_sociaux=?,
|
||||||
csg=?, crds=?, solidarite=?, updated_at=datetime('now') WHERE id=?`
|
csg=?, crds=?, solidarite=?, updated_at=datetime('now') WHERE id=?`
|
||||||
).run(
|
).run(Number(annee), pfu_total, Number(impot_revenu), prelev_sociaux,
|
||||||
Number(annee), pfu_total, Number(impot_revenu), prelev_sociaux,
|
Number(csg), Number(crds), Number(solidarite), Number(req.params.id));
|
||||||
Number(csg), Number(crds), Number(solidarite), Number(req.params.id)
|
|
||||||
);
|
|
||||||
const row = db.prepare('SELECT * FROM taux_pfu WHERE id = ?').get(Number(req.params.id));
|
const row = db.prepare('SELECT * FROM taux_pfu WHERE id = ?').get(Number(req.params.id));
|
||||||
if (!row) return res.status(404).json({ error: 'Introuvable' });
|
if (!row) return res.status(404).json({ error: 'Introuvable' });
|
||||||
res.json(row);
|
res.json(row);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
import ConfirmModal from '../components/ConfirmModal.jsx';
|
import ConfirmModal from '../components/ConfirmModal.jsx';
|
||||||
|
import ResultBanner from '../components/ResultBanner.jsx';
|
||||||
import Modal from '../components/Modal.jsx';
|
import Modal from '../components/Modal.jsx';
|
||||||
import CountrySelect, { COUNTRIES } from '../components/CountrySelect.jsx';
|
import CountrySelect, { COUNTRIES } from '../components/CountrySelect.jsx';
|
||||||
|
|
||||||
@@ -790,50 +791,11 @@ function TciPromptBlock({ rows }) {
|
|||||||
/* ═══════════════════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════════════════
|
||||||
SECTION PFU — Flat Tax
|
SECTION PFU — Flat Tax
|
||||||
═══════════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════════ */
|
||||||
function dlBlob(content, filename, type) {
|
function IconDownload() {
|
||||||
const blob = new Blob([content], { type });
|
return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>;
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url; a.download = filename; a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
}
|
||||||
|
function IconUpload() {
|
||||||
function pfuToCSV(rows) {
|
return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>;
|
||||||
const BOM = '\uFEFF'; const sep = ';';
|
|
||||||
const q = v => `"${String(v ?? '').replace(/"/g, '""')}"`;
|
|
||||||
const headers = ['Année', 'PFU total (%)', 'IR (%)', 'PS total (%)', 'CSG (%)', 'CRDS (%)', 'Solidarité (%)'];
|
|
||||||
const data = rows.map(r => [r.annee, r.pfu_total, r.impot_revenu, r.prelev_sociaux, r.csg ?? 9.2, r.crds ?? 0.5, r.solidarite ?? 7.5]);
|
|
||||||
return BOM + [headers, ...data].map(r => r.map(q).join(sep)).join('\r\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function pfuToXLS(rows) {
|
|
||||||
const esc = v => String(v ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
||||||
const cell = (v, t = 'String') => `<Cell><Data ss:Type="${t}">${esc(v)}</Data></Cell>`;
|
|
||||||
const mkRow = cells => ` <Row>${cells.join('')}</Row>`;
|
|
||||||
const header = mkRow(['Année','PFU total (%)','IR (%)','PS total (%)','CSG (%)','CRDS (%)','Solidarité (%)'].map(h => cell(h)));
|
|
||||||
const dataRows = rows.map(r => mkRow([
|
|
||||||
cell(r.annee, 'Number'), cell(r.pfu_total, 'Number'), cell(r.impot_revenu, 'Number'),
|
|
||||||
cell(r.prelev_sociaux, 'Number'), cell(r.csg ?? 9.2, 'Number'), cell(r.crds ?? 0.5, 'Number'), cell(r.solidarite ?? 7.5, 'Number'),
|
|
||||||
])).join('\n');
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<?mso-application progid="Excel.Sheet"?>
|
|
||||||
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
|
|
||||||
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
|
|
||||||
<Styles><Style ss:ID="h"><Font ss:Bold="1"/></Style></Styles>
|
|
||||||
<Worksheet ss:Name="Flat Tax PFU">
|
|
||||||
<Table>
|
|
||||||
${header}
|
|
||||||
${dataRows}
|
|
||||||
</Table>
|
|
||||||
</Worksheet>
|
|
||||||
</Workbook>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pfuToJSON(rows) {
|
|
||||||
return JSON.stringify(rows.map(r => ({
|
|
||||||
annee: r.annee, pfu_total: r.pfu_total, impot_revenu: r.impot_revenu,
|
|
||||||
prelev_sociaux: r.prelev_sociaux, csg: r.csg ?? 9.2, crds: r.crds ?? 0.5, solidarite: r.solidarite ?? 7.5,
|
|
||||||
})), null, 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyPfu = { annee: '', impot_revenu: '', csg: '', crds: '', solidarite: '' };
|
const emptyPfu = { annee: '', impot_revenu: '', csg: '', crds: '', solidarite: '' };
|
||||||
@@ -843,33 +805,6 @@ function fmtPct(v) {
|
|||||||
return `${Number(v).toFixed(1).replace('.', ',')} %`;
|
return `${Number(v).toFixed(1).replace('.', ',')} %`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PfuExportDropdown({ disabled, onCSV, onXLS, onJSON }) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
return (
|
|
||||||
<div style={{ position: 'relative' }}>
|
|
||||||
<button disabled={disabled} onClick={() => setOpen(o => !o)}
|
|
||||||
style={{ padding: '7px 12px', borderRadius: 6, border: '1px solid var(--border)', fontSize: 13, fontWeight: 600, cursor: disabled ? 'not-allowed' : 'pointer', background: 'var(--surface-2)', color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: 5 }}>
|
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
||||||
Exporter
|
|
||||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
|
||||||
</button>
|
|
||||||
{open && (
|
|
||||||
<>
|
|
||||||
<div onClick={() => setOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 999 }} />
|
|
||||||
<div style={{ position: 'absolute', top: '100%', right: 0, marginTop: 4, zIndex: 1000, background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 8, boxShadow: '0 4px 16px rgba(0,0,0,.12)', minWidth: 120, overflow: 'hidden' }}>
|
|
||||||
{[['CSV', onCSV], ['XLS', onXLS], ['JSON', onJSON]].map(([label, fn]) => (
|
|
||||||
<button key={label} onClick={() => { fn(); setOpen(false); }}
|
|
||||||
style={{ display: 'block', width: '100%', padding: '9px 14px', background: 'none', border: 'none', cursor: 'pointer', textAlign: 'left', fontSize: 13 }}>
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PfuDetailPanel({ row, onEdit }) {
|
function PfuDetailPanel({ row, onEdit }) {
|
||||||
if (!row) return (
|
if (!row) return (
|
||||||
<div className="dr-detail dr-detail-empty">
|
<div className="dr-detail dr-detail-empty">
|
||||||
@@ -917,6 +852,8 @@ function PfuSection() {
|
|||||||
const [showNewPfu, setShowNewPfu] = useState(false);
|
const [showNewPfu, setShowNewPfu] = useState(false);
|
||||||
const [confirmDel, setConfirmDel] = useState(null);
|
const [confirmDel, setConfirmDel] = useState(null);
|
||||||
const [err, setErr] = useState(null);
|
const [err, setErr] = useState(null);
|
||||||
|
const [zipResult, setZipResult] = useState(null);
|
||||||
|
const zipImportRef = useRef(null);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
const rows = await api.get('/pfu');
|
const rows = await api.get('/pfu');
|
||||||
@@ -959,6 +896,36 @@ function PfuSection() {
|
|||||||
} catch (e) { setErr(e.message); }
|
} catch (e) { setErr(e.message); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExportZip = async () => {
|
||||||
|
try {
|
||||||
|
const blob = await api.blob('/pfu/export-zip');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `fiscal-referentiel-${new Date().toISOString().slice(0, 10)}.zip`;
|
||||||
|
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (e) { setZipResult({ ok: false, msg: e.message }); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImportZip = async (file) => {
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('file', file);
|
||||||
|
const r = await api.upload('/pfu/import-zip', fd);
|
||||||
|
const pfu = r.pfu?.created || r.pfu?.updated
|
||||||
|
? `PFU : ${r.pfu.created} créé(s), ${r.pfu.updated} mis à jour`
|
||||||
|
: null;
|
||||||
|
const tci = r.tci?.created || r.tci?.updated
|
||||||
|
? `Crédit d'impôt : ${r.tci.created} créé(s), ${r.tci.updated} mis à jour`
|
||||||
|
: null;
|
||||||
|
setZipResult({ ok: true, msg: [pfu, tci].filter(Boolean).join(' — ') || 'Import terminé.' });
|
||||||
|
await load();
|
||||||
|
} catch (e) { setZipResult({ ok: false, msg: e.message }); }
|
||||||
|
finally { if (zipImportRef.current) zipImportRef.current.value = ''; }
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dr-mouvements-layout">
|
<div className="dr-mouvements-layout">
|
||||||
@@ -972,18 +939,27 @@ function PfuSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||||
<PfuExportDropdown
|
<button
|
||||||
disabled={pfuRows.length === 0}
|
onClick={handleExportZip} disabled={pfuRows.length === 0}
|
||||||
onCSV={() => dlBlob(pfuToCSV(pfuRows), 'flat-tax-pfu.csv', 'text/csv;charset=utf-8')}
|
title="Exporter le référentiel fiscal (PFU + crédit d'impôt) en ZIP"
|
||||||
onXLS={() => dlBlob(pfuToXLS(pfuRows), 'flat-tax-pfu.xls', 'application/vnd.ms-excel')}
|
style={{ padding: '7px 14px', borderRadius: 6, border: '1px solid var(--border)', fontSize: 13, fontWeight: 600, cursor: 'pointer', background: 'var(--surface-2)', color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
onJSON={() => dlBlob(pfuToJSON(pfuRows), 'flat-tax-pfu.json', 'application/json')}
|
<IconDownload /> Exporter
|
||||||
/>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => zipImportRef.current?.click()}
|
||||||
|
title="Importer un fichier ZIP de référentiel fiscal"
|
||||||
|
style={{ padding: '7px 14px', borderRadius: 6, border: '1px solid var(--border)', fontSize: 13, fontWeight: 600, cursor: 'pointer', background: 'var(--surface-2)', color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<IconUpload /> Importer
|
||||||
|
</button>
|
||||||
|
<input ref={zipImportRef} type="file" accept=".zip" style={{ display: 'none' }}
|
||||||
|
onChange={e => handleImportZip(e.target.files?.[0])} />
|
||||||
<button className="primary" onClick={() => { setShowNewPfu(true); setErr(null); }}>
|
<button className="primary" onClick={() => { setShowNewPfu(true); setErr(null); }}>
|
||||||
+ Ajouter
|
+ Ajouter
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{zipResult && <ResultBanner result={zipResult} onDismiss={() => setZipResult(null)} style={{ marginBottom: 12 }} />}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user