jeremy 4 mesiacov pred
rodič
commit
8ff2f8ad0c

+ 25 - 0
backend/src/db.js

@@ -116,4 +116,29 @@ db.exec(`
 // Add recoverable column to charges if missing
 try { db.exec('ALTER TABLE charges ADD COLUMN recoverable INTEGER NOT NULL DEFAULT 1'); } catch {}
 
+// Add prorata + regularization columns to payments if missing
+['is_prorata INTEGER DEFAULT 0',
+ 'prorata_days INTEGER',
+ 'prorata_total_days INTEGER',
+ 'charge_regularization REAL DEFAULT 0'
+].forEach(col => {
+  try { db.exec(`ALTER TABLE payments ADD COLUMN ${col}`); } catch {}
+});
+
+// Charge regularizations from annual bilan closure
+db.exec(`
+  CREATE TABLE IF NOT EXISTS charge_regularizations (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    lease_id INTEGER NOT NULL,
+    user_id INTEGER NOT NULL,
+    year INTEGER NOT NULL,
+    amount REAL NOT NULL,
+    notes TEXT,
+    applied_payment_id INTEGER,
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    FOREIGN KEY (lease_id) REFERENCES leases(id),
+    FOREIGN KEY (user_id) REFERENCES users(id)
+  )
+`);
+
 module.exports = db;

+ 61 - 0
backend/src/routes/charges.js

@@ -213,6 +213,67 @@ router.get('/bilan/:property_id/:year', (req, res) => {
   });
 });
 
+// Close annual bilan: create charge_regularization records for each tenant
+router.post('/bilan/:property_id/:year/close', (req, res) => {
+  const { property_id, year } = req.params;
+
+  const prop = db.prepare('SELECT * FROM properties WHERE id = ? AND user_id = ?').get(property_id, req.userId);
+  if (!prop) return res.status(403).json({ error: 'Bien non autorisé' });
+
+  // Check not already closed
+  const alreadyClosed = db.prepare(
+    'SELECT id FROM charge_regularizations WHERE user_id = ? AND year = ? AND lease_id IN (SELECT id FROM leases WHERE property_id = ?)'
+  ).get(req.userId, year, property_id);
+  if (alreadyClosed) return res.status(409).json({ error: `Le bilan ${year} a déjà été clôturé pour ce bien` });
+
+  // Re-compute bilan
+  const totalChargesRow = db.prepare(
+    'SELECT SUM(amount) as total FROM charges WHERE property_id = ? AND year = ? AND user_id = ? AND recoverable = 1'
+  ).get(property_id, year, req.userId);
+  const totalChargesReelles = totalChargesRow?.total || 0;
+
+  const leases = db.prepare(`
+    SELECT l.*, t.first_name, t.last_name FROM leases l
+    JOIN tenants t ON l.tenant_id = t.id
+    WHERE l.property_id = ? AND l.user_id = ?
+      AND (l.start_date <= ? AND (l.end_date IS NULL OR l.end_date >= ?))
+  `).all(property_id, req.userId, `${year}-12-31`, `${year}-01-01`);
+
+  if (leases.length === 0) return res.status(400).json({ error: 'Aucun locataire actif sur cette période' });
+
+  const leaseBilans = leases.map(lease => {
+    const provisions = db.prepare(
+      'SELECT SUM(charges_paid) as total FROM payments WHERE lease_id = ? AND period_year = ?'
+    ).get(lease.id, year);
+    return { lease_id: lease.id, tenant: `${lease.first_name} ${lease.last_name}`, provisions_percues: provisions?.total || 0 };
+  });
+
+  const totalProvisions = leaseBilans.reduce((s, l) => s + l.provisions_percues, 0);
+
+  const insertReg = db.prepare(
+    'INSERT INTO charge_regularizations (lease_id, user_id, year, amount, notes) VALUES (?, ?, ?, ?, ?)'
+  );
+
+  const closeAll = db.transaction(() => {
+    const results = [];
+    for (const l of leaseBilans) {
+      const ratio = totalProvisions > 0 ? l.provisions_percues / totalProvisions : 1 / leaseBilans.length;
+      const quote_part = totalChargesReelles * ratio;
+      // trop_percu > 0 = bailleur doit rembourser → crédit pour locataire (négatif sur prochain loyer)
+      // trop_percu < 0 = locataire doit payer en plus → débit
+      const trop_percu = Math.round((l.provisions_percues - quote_part) * 100) / 100;
+      // Stored as: positive = credit tenant (réduction loyer), negative = debit tenant (supplément)
+      insertReg.run(l.lease_id, req.userId, parseInt(year), trop_percu,
+        `Régularisation charges ${year} — quote-part ${Math.round(ratio * 10000) / 100}%`);
+      results.push({ lease_id: l.lease_id, tenant: l.tenant, trop_percu });
+    }
+    return results;
+  });
+
+  const results = closeAll();
+  res.json({ success: true, year: parseInt(year), regularizations: results });
+});
+
 // Export PDF bilan + invoices
 router.get('/bilan/:property_id/:year/pdf', async (req, res) => {
   const { property_id, year } = req.params;

+ 31 - 4
backend/src/routes/payments.js

@@ -24,7 +24,8 @@ router.get('/', (req, res) => {
 });
 
 router.post('/', (req, res) => {
-  const { lease_id, period_month, period_year, rent_paid, charges_paid, payment_date, payment_method, notes } = req.body;
+  const { lease_id, period_month, period_year, rent_paid, charges_paid, payment_date, payment_method, notes,
+    is_prorata, prorata_days, prorata_total_days, charge_regularization } = req.body;
   if (!lease_id || !period_month || !period_year || rent_paid === undefined || !payment_date)
     return res.status(400).json({ error: 'Champs requis manquants' });
 
@@ -36,10 +37,25 @@ router.post('/', (req, res) => {
   ).get(lease_id, period_month, period_year);
   if (existing) return res.status(409).json({ error: 'Un paiement existe déjà pour cette période' });
 
+  const regularization = parseFloat(charge_regularization || 0);
+
   const result = db.prepare(`
-    INSERT INTO payments (lease_id, user_id, period_month, period_year, rent_paid, charges_paid, payment_date, payment_method, notes)
-    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
-  `).run(lease_id, req.userId, period_month, period_year, rent_paid, charges_paid || 0, payment_date, payment_method || 'virement', notes || null);
+    INSERT INTO payments (lease_id, user_id, period_month, period_year, rent_paid, charges_paid, payment_date, payment_method, notes,
+      is_prorata, prorata_days, prorata_total_days, charge_regularization)
+    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+  `).run(lease_id, req.userId, period_month, period_year, rent_paid, charges_paid || 0,
+    payment_date, payment_method || 'virement', notes || null,
+    is_prorata ? 1 : 0, prorata_days || null, prorata_total_days || null, regularization);
+
+  // If a regularization was applied, mark it as used
+  if (regularization !== 0) {
+    db.prepare(`
+      UPDATE charge_regularizations SET applied_payment_id = ?
+      WHERE lease_id = ? AND applied_payment_id IS NULL
+      ORDER BY created_at ASC LIMIT 1
+    `).run(result.lastInsertRowid, lease_id);
+  }
+
   res.status(201).json({ id: result.lastInsertRowid });
 });
 
@@ -50,6 +66,17 @@ router.delete('/:id', (req, res) => {
   res.json({ success: true });
 });
 
+// Pending charge regularization for a lease
+router.get('/regularization/:lease_id', (req, res) => {
+  const lease = db.prepare('SELECT id FROM leases WHERE id = ? AND user_id = ?').get(req.params.lease_id, req.userId);
+  if (!lease) return res.status(404).json({ error: 'Bail non trouvé' });
+  const rows = db.prepare(
+    'SELECT * FROM charge_regularizations WHERE lease_id = ? AND applied_payment_id IS NULL ORDER BY year DESC'
+  ).all(req.params.lease_id);
+  const total = rows.reduce((s, r) => s + r.amount, 0);
+  res.json({ pending: rows, total: Math.round(total * 100) / 100 });
+});
+
 // Dashboard stats
 router.get('/stats', (req, res) => {
   const stats = db.prepare(`

+ 23 - 7
backend/src/routes/receipts.js

@@ -80,22 +80,38 @@ router.get('/:paymentId', (req, res) => {
   doc.fontSize(12).font('Helvetica-Bold').text('DÉTAIL DU RÈGLEMENT');
   doc.moveDown(0.5);
 
-  const tableTop = doc.y;
   const col1 = 50, col2 = 400;
 
-  const drawRow = (label, value, bold = false) => {
+  const drawRow = (label, value, bold = false, color = '#000000') => {
     const y = doc.y;
-    doc.font(bold ? 'Helvetica-Bold' : 'Helvetica').fontSize(11);
+    doc.font(bold ? 'Helvetica-Bold' : 'Helvetica').fontSize(11).fillColor(color);
     doc.text(label, col1, y);
     doc.text(`${value.toFixed(2)} €`, col2, y, { width: 100, align: 'right' });
+    doc.fillColor('#000000');
     doc.moveDown(0.6);
   };
 
-  drawRow('Loyer hors charges', payment.rent_paid);
-  drawRow('Charges', payment.charges_paid);
+  const isProrata = payment.is_prorata && payment.prorata_days && payment.prorata_total_days;
+  if (isProrata) {
+    drawRow(`Loyer hors charges (prorata ${payment.prorata_days}/${payment.prorata_total_days} jours)`, payment.rent_paid);
+    drawRow(`Charges (prorata ${payment.prorata_days}/${payment.prorata_total_days} jours)`, payment.charges_paid);
+  } else {
+    drawRow('Loyer hors charges', payment.rent_paid);
+    drawRow('Charges', payment.charges_paid);
+  }
+
+  const reg = payment.charge_regularization || 0;
+  if (reg !== 0) {
+    const regLabel = reg > 0
+      ? `Régularisation de charges (trop-perçu déduit)`
+      : `Régularisation de charges (complément dû)`;
+    drawRow(regLabel, -reg, false, reg > 0 ? '#15803d' : '#c2410c');
+  }
+
   doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor('#e5e7eb').lineWidth(0.5).stroke();
   doc.moveDown(0.3);
-  drawRow('TOTAL', total, true);
+  const netTotal = total - reg;
+  drawRow('TOTAL NET', netTotal, true);
   doc.moveDown(0.5);
 
   // Mode et date de paiement
@@ -116,7 +132,7 @@ router.get('/:paymentId', (req, res) => {
   doc.font('Helvetica').fontSize(10).fillColor('#6b7280')
     .text(
       `Je soussigné(e) ${ownerFullName}, bailleur, donne quittance à ${payment.tenant_first_name} ${payment.tenant_last_name} ` +
-      `pour la somme de ${total.toFixed(2)} € correspondant au paiement du loyer et des charges du logement situé ` +
+      `pour la somme de ${netTotal.toFixed(2)} € correspondant au paiement du loyer et des charges du logement situé ` +
       `${payment.property_address}, ${payment.property_zip} ${payment.property_city}, ` +
       `pour la période de ${monthName} ${payment.period_year}.`,
       { align: 'justify' }

+ 20 - 3
frontend/src/pages/Charges.jsx

@@ -47,6 +47,7 @@ export default function Charges() {
   const [bilan, setBilan] = useState(null)
   const [bilanLoading, setBilanLoading] = useState(false)
   const [pdfLoading, setPdfLoading] = useState(false)
+  const [closeLoading, setCloseLoading] = useState(false)
   const [loading, setLoading] = useState(false)
   const [editCharge, setEditCharge] = useState(null)
 
@@ -312,6 +313,18 @@ export default function Charges() {
     finally { setPdfLoading(false) }
   }
 
+  const closeBilan = async () => {
+    if (!filterProperty || !filterYear) return
+    if (!confirm(`Clôturer définitivement le bilan ${filterYear} ? Cela créera les régularisations de charges pour chaque locataire et ne peut pas être annulé.`)) return
+    setCloseLoading(true)
+    try {
+      const { data } = await api.post(`/charges/bilan/${filterProperty}/${filterYear}/close`)
+      toast.success(`Bilan ${filterYear} clôturé ! ${data.regularizations.length} régularisation(s) créée(s).`)
+      setShowBilan(false)
+    } catch (err) { toast.error(err.response?.data?.error || 'Erreur lors de la clôture') }
+    finally { setCloseLoading(false) }
+  }
+
   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)
@@ -512,14 +525,18 @@ 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 gap-2">
+            <div className="flex justify-end gap-2 flex-wrap">
               <button onClick={printBilan}
                 className="btn-secondary flex items-center gap-2 text-sm">
                 🖨️ Aperçu / Imprimer
               </button>
               <button onClick={downloadPdf} disabled={pdfLoading}
-                className="btn-primary flex items-center gap-2 text-sm">
-                {pdfLoading ? '⏳ Génération…' : '📥 Télécharger PDF complet (avec factures)'}
+                className="btn-secondary flex items-center gap-2 text-sm">
+                {pdfLoading ? '⏳ Génération…' : '📥 PDF complet (avec factures)'}
+              </button>
+              <button onClick={closeBilan} disabled={closeLoading}
+                className="flex items-center gap-2 text-sm px-4 py-2 rounded-lg bg-orange-600 hover:bg-orange-700 text-white font-medium transition-colors">
+                {closeLoading ? '⏳…' : '✅ Clôturer le bilan'}
               </button>
             </div>
             {/* Summary cards */}

+ 141 - 9
frontend/src/pages/LeaseDetail.jsx

@@ -30,6 +30,7 @@ export default function LeaseDetail() {
   const [lease, setLease] = useState(null)
   const [payments, setPayments] = useState([])
   const [revisions, setRevisions] = useState([])
+  const [pendingReg, setPendingReg] = useState({ pending: [], total: 0 })
   const [showModal, setShowModal] = useState(false)
   const [showRevisionModal, setShowRevisionModal] = useState(false)
   const [loading, setLoading] = useState(false)
@@ -43,7 +44,12 @@ export default function LeaseDetail() {
     charges_paid: '',
     payment_date: now.toISOString().slice(0, 10),
     payment_method: 'virement',
-    notes: ''
+    notes: '',
+    is_prorata: false,
+    prorata_days: '',
+    prorata_total_days: '',
+    apply_regularization: false,
+    charge_regularization: 0,
   })
 
   const [revForm, setRevForm] = useState({
@@ -54,23 +60,50 @@ export default function LeaseDetail() {
   })
 
   const load = async () => {
-    const [l, p, r] = await Promise.all([
+    const [l, p, r, reg] = await Promise.all([
       api.get(`/leases/${id}`),
       api.get(`/payments?lease_id=${id}`),
-      api.get(`/leases/${id}/revisions`)
+      api.get(`/leases/${id}/revisions`),
+      api.get(`/payments/regularization/${id}`),
     ])
     setLease(l.data)
     setPayments(p.data)
     setRevisions(r.data)
+    setPendingReg(reg.data)
     setForm(f => ({ ...f, rent_paid: l.data.rent_amount, charges_paid: l.data.charges_amount }))
   }
 
   useEffect(() => { load() }, [id])
 
+  // Prorata calculation
+  const calcProrata = (baseAmount, days, totalDays) => {
+    if (!days || !totalDays || totalDays === 0) return baseAmount
+    return Math.round((baseAmount / totalDays) * days * 100) / 100
+  }
+
+  const updateProrata = (updatedForm) => {
+    if (!updatedForm.is_prorata) return updatedForm
+    const days = parseInt(updatedForm.prorata_days) || 0
+    const total = parseInt(updatedForm.prorata_total_days) || 0
+    if (!days || !total) return updatedForm
+    const baseRent = lease?.rent_amount || 0
+    const baseCharges = lease?.charges_amount || 0
+    return {
+      ...updatedForm,
+      rent_paid: calcProrata(baseRent, days, total),
+      charges_paid: calcProrata(baseCharges, days, total),
+    }
+  }
+
   const handlePayment = async e => {
     e.preventDefault(); setLoading(true)
     try {
-      await api.post('/payments', { ...form, lease_id: Number(id) })
+      const payload = {
+        ...form,
+        lease_id: Number(id),
+        charge_regularization: form.apply_regularization ? pendingReg.total : 0,
+      }
+      await api.post('/payments', payload)
       toast.success('Paiement enregistré !')
       setShowModal(false); load()
     } catch (err) { toast.error(err.response?.data?.error || 'Erreur') }
@@ -229,6 +262,23 @@ export default function LeaseDetail() {
         )}
       </div>
 
+      {/* Régularisation en attente */}
+      {pendingReg.pending.length > 0 && (
+        <div className={`mb-4 rounded-xl p-4 border flex items-start gap-3 ${pendingReg.total >= 0 ? 'bg-green-50 border-green-200' : 'bg-orange-50 border-orange-200'}`}>
+          <span className="text-2xl">{pendingReg.total >= 0 ? '✅' : '⚠️'}</span>
+          <div className="flex-1">
+            <p className={`font-semibold text-sm ${pendingReg.total >= 0 ? 'text-green-800' : 'text-orange-800'}`}>
+              {pendingReg.total >= 0
+                ? `Régularisation de charges en attente : ${fmt(pendingReg.total)} à déduire du prochain loyer`
+                : `Régularisation de charges en attente : ${fmt(Math.abs(pendingReg.total))} à appeler sur le prochain loyer`}
+            </p>
+            <p className="text-xs text-gray-500 mt-0.5">
+              {pendingReg.pending.map(r => `${r.notes} (${fmt(r.amount)})`).join(' · ')}
+            </p>
+          </div>
+        </div>
+      )}
+
       {payments.length === 0 ? (
         <div className="card text-center py-10">
           <p className="text-gray-400">Aucun paiement enregistré pour ce bail.</p>
@@ -243,6 +293,7 @@ export default function LeaseDetail() {
                 <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Mode</th>
                 <th className="text-right px-6 py-3 text-sm font-semibold text-gray-600">Loyer</th>
                 <th className="text-right px-6 py-3 text-sm font-semibold text-gray-600">Charges</th>
+                <th className="text-right px-6 py-3 text-sm font-semibold text-gray-600">Régul.</th>
                 <th className="text-right px-6 py-3 text-sm font-semibold text-gray-600">Total</th>
                 <th className="px-6 py-3"></th>
               </tr>
@@ -250,12 +301,24 @@ export default function LeaseDetail() {
             <tbody className="divide-y divide-gray-50">
               {payments.map(p => (
                 <tr key={p.id} className="hover:bg-gray-50">
-                  <td className="px-6 py-4 font-medium text-gray-900">{MONTHS[p.period_month-1]} {p.period_year}</td>
+                  <td className="px-6 py-4 font-medium text-gray-900">
+                    {MONTHS[p.period_month-1]} {p.period_year}
+                    {p.is_prorata ? <span className="ml-1 text-xs bg-yellow-100 text-yellow-700 px-1.5 py-0.5 rounded-full font-medium">prorata {p.prorata_days}/{p.prorata_total_days}j</span> : null}
+                  </td>
                   <td className="px-6 py-4 text-gray-600">{fmtDate(p.payment_date)}</td>
                   <td className="px-6 py-4 text-gray-600 capitalize">{p.payment_method}</td>
                   <td className="px-6 py-4 text-right text-gray-900">{fmt(p.rent_paid)}</td>
                   <td className="px-6 py-4 text-right text-gray-600">{fmt(p.charges_paid)}</td>
-                  <td className="px-6 py-4 text-right font-semibold text-green-700">{fmt(p.rent_paid + p.charges_paid)}</td>
+                  <td className="px-6 py-4 text-right text-sm">
+                    {p.charge_regularization !== 0 && p.charge_regularization != null
+                      ? <span className={p.charge_regularization > 0 ? 'text-green-600' : 'text-orange-600'}>
+                          {p.charge_regularization > 0 ? '-' : '+'}{fmt(Math.abs(p.charge_regularization))}
+                        </span>
+                      : <span className="text-gray-300">—</span>}
+                  </td>
+                  <td className="px-6 py-4 text-right font-semibold text-green-700">
+                    {fmt(p.rent_paid + p.charges_paid - (p.charge_regularization > 0 ? p.charge_regularization : 0) + (p.charge_regularization < 0 ? Math.abs(p.charge_regularization) : 0))}
+                  </td>
                   <td className="px-6 py-4 text-right space-x-2">
                     <button onClick={() => downloadReceipt(p.id)} className="text-blue-600 hover:text-blue-800 text-sm font-medium" title="Télécharger la quittance PDF">📄 Quittance</button>
                     <button onClick={() => handleDeletePayment(p.id)} className="text-gray-400 hover:text-red-600 ml-2">🗑️</button>
@@ -283,6 +346,46 @@ export default function LeaseDetail() {
                 <input className="input" type="number" min="2000" max="2099" value={form.period_year}
                   onChange={e => setForm(f => ({ ...f, period_year: Number(e.target.value) }))} />
               </div>
+            </div>
+
+            {/* Prorata */}
+            <div className="border border-gray-200 rounded-xl p-4 space-y-3">
+              <label className="flex items-center gap-3 cursor-pointer">
+                <input type="checkbox" className="w-4 h-4" checked={form.is_prorata}
+                  onChange={e => {
+                    const checked = e.target.checked
+                    const daysInMonth = new Date(form.period_year, form.period_month, 0).getDate()
+                    const next = { ...form, is_prorata: checked, prorata_total_days: checked ? daysInMonth : '', prorata_days: checked ? daysInMonth : '' }
+                    setForm(updateProrata(next))
+                  }} />
+                <span className="text-sm font-medium text-gray-700">Appliquer un prorata (entrée/sortie en cours de mois)</span>
+              </label>
+              {form.is_prorata && (
+                <div className="grid grid-cols-2 gap-3 pt-1">
+                  <div>
+                    <label className="label">Jours occupés *</label>
+                    <input className="input" type="number" min="1" max="31" value={form.prorata_days}
+                      onChange={e => {
+                        const next = { ...form, prorata_days: e.target.value }
+                        setForm(updateProrata(next))
+                      }} />
+                  </div>
+                  <div>
+                    <label className="label">Jours dans le mois</label>
+                    <input className="input" type="number" min="1" max="31" value={form.prorata_total_days}
+                      onChange={e => {
+                        const next = { ...form, prorata_total_days: e.target.value }
+                        setForm(updateProrata(next))
+                      }} />
+                  </div>
+                  <p className="col-span-2 text-xs text-blue-600">
+                    Loyer calculé : {fmt(calcProrata(lease?.rent_amount || 0, form.prorata_days, form.prorata_total_days))} + charges : {fmt(calcProrata(lease?.charges_amount || 0, form.prorata_days, form.prorata_total_days))}
+                  </p>
+                </div>
+              )}
+            </div>
+
+            <div className="grid grid-cols-2 gap-4">
               <div>
                 <label className="label">Loyer encaissé (€) *</label>
                 <input className="input" type="number" step="0.01" min="0" required value={form.rent_paid}
@@ -309,9 +412,38 @@ export default function LeaseDetail() {
                 <input className="input" value={form.notes} onChange={e => setForm(f => ({ ...f, notes: e.target.value }))} placeholder="..." />
               </div>
             </div>
-            <div className="bg-blue-50 rounded-lg p-3 text-sm text-blue-700">
-              <strong>Total à encaisser :</strong> {fmt(Number(form.rent_paid || 0) + Number(form.charges_paid || 0))}
-            </div>
+
+            {/* Régularisation en attente */}
+            {pendingReg.pending.length > 0 && (
+              <div className={`border rounded-xl p-3 ${pendingReg.total >= 0 ? 'border-green-200 bg-green-50' : 'border-orange-200 bg-orange-50'}`}>
+                <label className="flex items-start gap-3 cursor-pointer">
+                  <input type="checkbox" className="w-4 h-4 mt-0.5" checked={form.apply_regularization}
+                    onChange={e => setForm(f => ({ ...f, apply_regularization: e.target.checked }))} />
+                  <div>
+                    <p className={`text-sm font-medium ${pendingReg.total >= 0 ? 'text-green-800' : 'text-orange-800'}`}>
+                      {pendingReg.total >= 0
+                        ? `Appliquer la régularisation de charges : -${fmt(pendingReg.total)} sur ce loyer`
+                        : `Appliquer la régularisation de charges : +${fmt(Math.abs(pendingReg.total))} sur ce loyer`}
+                    </p>
+                    <p className="text-xs text-gray-500 mt-0.5">{pendingReg.pending.map(r => r.notes).join(' · ')}</p>
+                  </div>
+                </label>
+              </div>
+            )}
+
+            {(() => {
+              const base = Number(form.rent_paid || 0) + Number(form.charges_paid || 0)
+              const reg = form.apply_regularization ? pendingReg.total : 0
+              const net = base - reg
+              return (
+                <div className="bg-blue-50 rounded-lg p-3 text-sm text-blue-700 space-y-1">
+                  <div className="flex justify-between"><span>Loyer + charges :</span><strong>{fmt(base)}</strong></div>
+                  {form.apply_regularization && <div className="flex justify-between"><span>Régularisation :</span><strong className={reg >= 0 ? 'text-green-700' : 'text-orange-700'}>{reg >= 0 ? '-' : '+'}{fmt(Math.abs(reg))}</strong></div>}
+                  <div className="flex justify-between border-t border-blue-200 pt-1"><span><strong>Total à encaisser :</strong></span><strong>{fmt(net)}</strong></div>
+                </div>
+              )
+            })()}
+
             <div className="flex gap-3 pt-2">
               <button type="button" onClick={() => setShowModal(false)} className="btn-secondary flex-1">Annuler</button>
               <button type="submit" disabled={loading} className="btn-primary flex-1">{loading ? '...' : 'Enregistrer'}</button>