jeremy преди 4 месеца
родител
ревизия
e4d93e56ea
променени са 1 файла, в които са добавени 131 реда и са изтрити 0 реда
  1. 131 0
      frontend/src/pages/Charges.jsx

+ 131 - 0
frontend/src/pages/Charges.jsx

@@ -150,6 +150,131 @@ export default function Charges() {
     finally { setBilanLoading(false) }
   }
 
+  const printBilan = () => {
+    if (!bilan) return
+    const catLabel = v => CATEGORIES.find(c => c.value === v)?.label || v
+    const fmtEur = v => `${Number(v).toFixed(2)} €`
+    const solde = bilan.total_provisions_percues - bilan.total_charges_reelles
+    const isPos = solde >= 0
+
+    const tenantsRows = bilan.bilans.map(b => {
+      const pos = b.trop_percu >= 0
+      return `
+        <div class="tenant-block">
+          <div class="tenant-header">
+            <div>
+              <strong>${b.tenant_name}</strong>
+              ${b.tenant_email ? `<br><span class="small">${b.tenant_email}</span>` : ''}
+            </div>
+            <span class="badge ${pos ? 'badge-green' : 'badge-orange'}">${pos ? '↩ Remboursement' : '↑ Appel de fonds'}</span>
+          </div>
+          <table class="inner-table">
+            <tr>
+              <td>Provisions perçues</td>
+              <td class="amount">${fmtEur(b.provisions_percues)}</td>
+            </tr>
+            <tr>
+              <td>Quote-part des charges réelles (${b.ratio_percent}%)</td>
+              <td class="amount">${fmtEur(b.quote_part_charges)}</td>
+            </tr>
+            <tr class="${pos ? 'row-green' : 'row-orange'}">
+              <td><strong>${pos ? 'Trop-perçu à restituer' : 'Solde restant dû'}</strong></td>
+              <td class="amount"><strong>${fmtEur(Math.abs(b.trop_percu))}</strong></td>
+            </tr>
+          </table>
+        </div>`
+    }).join('')
+
+    const catRows = bilan.charges_by_category.map(c => {
+      const pct = bilan.total_charges_reelles > 0 ? (c.total / bilan.total_charges_reelles * 100).toFixed(1) : 0
+      return `<tr><td>${catLabel(c.category)}</td><td class="amount">${fmtEur(c.total)}</td><td class="pct">${pct}%</td></tr>`
+    }).join('')
+
+    const html = `<!DOCTYPE html>
+<html lang="fr">
+<head>
+<meta charset="UTF-8">
+<title>Bilan de charges ${bilan.year} — ${bilan.property?.name}</title>
+<style>
+  * { box-sizing: border-box; margin: 0; padding: 0; }
+  body { font-family: Arial, sans-serif; font-size: 13px; color: #1a1a1a; padding: 40px; max-width: 800px; margin: auto; }
+  h1 { font-size: 22px; font-weight: bold; margin-bottom: 4px; }
+  h2 { font-size: 15px; font-weight: 600; margin: 24px 0 10px; border-bottom: 1px solid #e5e7eb; padding-bottom: 6px; }
+  .subtitle { color: #6b7280; font-size: 12px; margin-bottom: 28px; }
+  .summary { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-bottom: 24px; }
+  .card { border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px; text-align: center; }
+  .card .label { font-size: 11px; color: #6b7280; margin-bottom: 4px; }
+  .card .value { font-size: 20px; font-weight: bold; }
+  .card.red .value { color: #dc2626; }
+  .card.blue .value { color: #2563eb; }
+  .card.green { border-color: #bbf7d0; background: #f0fdf4; }
+  .card.green .value { color: #15803d; }
+  .card.orange { border-color: #fed7aa; background: #fff7ed; }
+  .card.orange .value { color: #c2410c; }
+  .card .note { font-size: 11px; color: #9ca3af; margin-top: 4px; }
+  table { width: 100%; border-collapse: collapse; }
+  table th { text-align: left; padding: 8px 10px; background: #f9fafb; font-size: 12px; color: #374151; }
+  table td { padding: 8px 10px; border-bottom: 1px solid #f3f4f6; }
+  td.amount { text-align: right; font-weight: 600; }
+  td.pct { text-align: right; color: #6b7280; font-size: 11px; }
+  .tenant-block { border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px; margin-bottom: 12px; }
+  .tenant-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
+  .tenant-header strong { font-size: 14px; }
+  .small { font-size: 11px; color: #9ca3af; }
+  .badge { font-size: 11px; font-weight: 600; padding: 3px 8px; border-radius: 99px; }
+  .badge-green { background: #dcfce7; color: #15803d; }
+  .badge-orange { background: #ffedd5; color: #c2410c; }
+  .inner-table td { padding: 6px 8px; border-bottom: 1px solid #f3f4f6; font-size: 12px; }
+  .row-green td { background: #f0fdf4; color: #15803d; }
+  .row-orange td { background: #fff7ed; color: #c2410c; }
+  .footer { margin-top: 40px; padding-top: 16px; border-top: 1px solid #e5e7eb; font-size: 11px; color: #9ca3af; text-align: center; }
+  @media print {
+    body { padding: 20px; }
+    .no-print { display: none; }
+  }
+</style>
+</head>
+<body>
+  <h1>Bilan annuel de charges ${bilan.year}</h1>
+  <p class="subtitle">${bilan.property?.name} — ${bilan.property?.address || ''}</p>
+
+  <div class="summary">
+    <div class="card">
+      <div class="label">Charges réelles</div>
+      <div class="value red">${fmtEur(bilan.total_charges_reelles)}</div>
+      <div class="note">${bilan.charges_count} facture${bilan.charges_count > 1 ? 's' : ''}</div>
+    </div>
+    <div class="card">
+      <div class="label">Provisions perçues</div>
+      <div class="value blue">${fmtEur(bilan.total_provisions_percues)}</div>
+    </div>
+    <div class="card ${isPos ? 'green' : 'orange'}">
+      <div class="label">${isPos ? 'Trop-perçu global' : 'Solde insuffisant'}</div>
+      <div class="value">${isPos ? '+' : '-'}${fmtEur(Math.abs(solde))}</div>
+    </div>
+  </div>
+
+  ${bilan.charges_by_category.length > 0 ? `
+  <h2>Répartition des charges par catégorie</h2>
+  <table>
+    <thead><tr><th>Catégorie</th><th style="text-align:right">Montant</th><th style="text-align:right">%</th></tr></thead>
+    <tbody>${catRows}</tbody>
+    <tfoot><tr><td><strong>Total</strong></td><td class="amount"><strong>${fmtEur(bilan.total_charges_reelles)}</strong></td><td></td></tr></tfoot>
+  </table>` : ''}
+
+  ${bilan.bilans.length > 0 ? `<h2>Décompte par locataire</h2>${tenantsRows}` : ''}
+
+  <div class="footer">Document généré le ${new Date().toLocaleDateString('fr-FR')} — Bilan de régularisation de charges ${bilan.year}</div>
+
+  <script>window.onload = () => window.print()<\/script>
+</body>
+</html>`
+
+    const w = window.open('', '_blank')
+    w.document.write(html)
+    w.document.close()
+  }
+
   const fmt = v => `${Number(v).toFixed(2)} €`
   const fmtDate = d => d ? new Date(d + 'T00:00:00').toLocaleDateString('fr-FR') : '—'
   const totalAmount = charges.reduce((s, c) => s + c.amount, 0)
@@ -327,6 +452,12 @@ export default function Charges() {
       {showBilan && bilan && (
         <Modal title={`📊 Bilan annuel ${bilan.year} — ${bilan.property?.name}`} onClose={() => setShowBilan(false)}>
           <div className="space-y-6">
+            <div className="flex justify-end">
+              <button onClick={printBilan}
+                className="btn-primary flex items-center gap-2 text-sm">
+                🖨️ Imprimer / Exporter PDF
+              </button>
+            </div>
             {/* Summary cards */}
             <div className="grid grid-cols-2 gap-3">
               <div className="bg-red-50 border border-red-100 rounded-xl p-4 text-center">