jeremy 4 месяцев назад
Родитель
Сommit
92c5fdead4
2 измененных файлов с 90 добавлено и 8 удалено
  1. 47 1
      backend/src/routes/charges.js
  2. 43 7
      frontend/src/pages/Charges.jsx

+ 47 - 1
backend/src/routes/charges.js

@@ -247,15 +247,26 @@ router.get('/bilan/:property_id/:year', async (req, res) => {
 
 
   const totalProvisions = leaseBilans.reduce((s, l) => s + l.provisions_percues, 0);
   const totalProvisions = leaseBilans.reduce((s, l) => s + l.provisions_percues, 0);
 
 
+  // Fetch existing closures for this property/year to mark closed leases.
+  const closures = (await pool.query(
+    'SELECT id, lease_id, amount, applied_payment_id FROM charge_regularizations WHERE user_id = $1 AND property_id = $2 AND year = $3',
+    [req.userId, property_id, parseInt(year, 10)]
+  )).rows;
+  const closureByLeaseId = Object.fromEntries(closures.map(c => [c.lease_id, c]));
+
   // Compute each tenant's share based on actual occupancy in the year.
   // Compute each tenant's share based on actual occupancy in the year.
   const bilans = leaseBilans.map(l => {
   const bilans = leaseBilans.map(l => {
     const quote_part_charges = totalChargesReelles * l.occupancy_ratio;
     const quote_part_charges = totalChargesReelles * l.occupancy_ratio;
     const trop_percu = l.provisions_percues - quote_part_charges;
     const trop_percu = l.provisions_percues - quote_part_charges;
+    const closure = closureByLeaseId[l.lease_id] || null;
     return {
     return {
       ...l,
       ...l,
       ratio_percent: l.occupancy_ratio_percent,
       ratio_percent: l.occupancy_ratio_percent,
       quote_part_charges: round2(quote_part_charges),
       quote_part_charges: round2(quote_part_charges),
-      trop_percu: round2(trop_percu)
+      trop_percu: round2(trop_percu),
+      closed: !!closure,
+      regularization_id: closure?.id || null,
+      applied_payment_id: closure?.applied_payment_id || null,
     };
     };
   });
   });
 
 
@@ -612,4 +623,39 @@ router.post('/bilan/:property_id/:year/close', async (req, res) => {
   }
   }
 });
 });
 
 
+// Cancel (undo) a lease closure for a given property/year
+router.delete('/bilan/:property_id/:year/close/:lease_id', async (req, res) => {
+  const { property_id, year, lease_id } = req.params;
+
+  const prop = (await pool.query('SELECT id FROM properties WHERE id = $1 AND user_id = $2', [property_id, req.userId])).rows[0];
+  if (!prop) return res.status(403).json({ error: 'Bien non autorisé' });
+
+  const reg = (await pool.query(
+    'SELECT id, applied_payment_id FROM charge_regularizations WHERE user_id = $1 AND lease_id = $2 AND year = $3 LIMIT 1',
+    [req.userId, parseInt(lease_id, 10), parseInt(year, 10)]
+  )).rows[0];
+
+  if (!reg) return res.status(404).json({ error: 'Aucune clôture trouvée pour ce bail et cette année.' });
+
+  const client = await pool.connect();
+  try {
+    await client.query('BEGIN');
+    // If the regularization was applied to a payment, reset that payment's charge_regularization to 0.
+    if (reg.applied_payment_id) {
+      await client.query(
+        'UPDATE payments SET charge_regularization = 0 WHERE id = $1 AND user_id = $2',
+        [reg.applied_payment_id, req.userId]
+      );
+    }
+    await client.query('DELETE FROM charge_regularizations WHERE id = $1', [reg.id]);
+    await client.query('COMMIT');
+    res.json({ success: true });
+  } catch (e) {
+    await client.query('ROLLBACK');
+    throw e;
+  } finally {
+    client.release();
+  }
+});
+
 module.exports = router;
 module.exports = router;

+ 43 - 7
frontend/src/pages/Charges.jsx

@@ -48,6 +48,7 @@ export default function Charges() {
   const [bilanLoading, setBilanLoading] = useState(false)
   const [bilanLoading, setBilanLoading] = useState(false)
   const [pdfLoading, setPdfLoading] = useState(false)
   const [pdfLoading, setPdfLoading] = useState(false)
   const [closingLeaseId, setClosingLeaseId] = useState(null)
   const [closingLeaseId, setClosingLeaseId] = useState(null)
+  const [cancelingLeaseId, setCancelingLeaseId] = useState(null)
   const [loading, setLoading] = useState(false)
   const [loading, setLoading] = useState(false)
   const [editCharge, setEditCharge] = useState(null)
   const [editCharge, setEditCharge] = useState(null)
 
 
@@ -316,6 +317,28 @@ export default function Charges() {
     finally { setPdfLoading(false) }
     finally { setPdfLoading(false) }
   }
   }
 
 
+  const cancelLeaseExercise = async (leaseBilan) => {
+    if (!filterProperty || !filterYear || !leaseBilan?.lease_id) return
+    const appliedWarning = leaseBilan.applied_payment_id
+      ? '\n\n⚠️ Cette régularisation a déjà été appliquée à un paiement. Ce paiement sera corrigé (montant de régularisation remis à 0).'
+      : ''
+    if (!confirm(
+      `Annuler la clôture du bilan annuel des charges ${filterYear} pour ${leaseBilan.tenant_name} ?` +
+      appliedWarning +
+      '\n\nCette action est irréversible.'
+    )) return
+    setCancelingLeaseId(leaseBilan.lease_id)
+    try {
+      await api.delete(`/charges/bilan/${filterProperty}/${filterYear}/close/${leaseBilan.lease_id}`)
+      toast.success(`Clôture annulée pour ${leaseBilan.tenant_name} (${filterYear})`)
+      await loadBilan()
+    } catch (err) {
+      toast.error(err.response?.data?.error || 'Erreur lors de l\'annulation')
+    } finally {
+      setCancelingLeaseId(null)
+    }
+  }
+
   const closeLeaseExercise = async (leaseBilan) => {
   const closeLeaseExercise = async (leaseBilan) => {
     if (!filterProperty || !filterYear || !leaseBilan?.lease_id) return
     if (!filterProperty || !filterYear || !leaseBilan?.lease_id) return
     if (!confirm(
     if (!confirm(
@@ -709,13 +732,26 @@ export default function Charges() {
                           </div>
                           </div>
                         </div>
                         </div>
                         <div className="mt-3 flex justify-end">
                         <div className="mt-3 flex justify-end">
-                          <button
-                            onClick={() => closeLeaseExercise(b)}
-                            disabled={closingLeaseId === b.lease_id}
-                            className="flex items-center gap-2 text-sm px-3 py-2 rounded-lg bg-orange-500 hover:bg-orange-600 text-white font-semibold transition-colors disabled:opacity-60"
-                          >
-                            {closingLeaseId === b.lease_id ? '⏳ Clôture du bilan…' : '✅ Clôturer le bilan annuel'}
-                          </button>
+                          {b.closed ? (
+                            <div className="flex items-center gap-2 flex-wrap justify-end">
+                              <span className="text-xs text-green-700 bg-green-50 border border-green-200 px-2 py-1 rounded-lg font-medium">✅ Bilan clôturé</span>
+                              <button
+                                onClick={() => cancelLeaseExercise(b)}
+                                disabled={cancelingLeaseId === b.lease_id}
+                                className="flex items-center gap-2 text-sm px-3 py-2 rounded-lg bg-red-500 hover:bg-red-600 text-white font-semibold transition-colors disabled:opacity-60"
+                              >
+                                {cancelingLeaseId === b.lease_id ? '⏳ Annulation…' : '↩ Annuler la clôture'}
+                              </button>
+                            </div>
+                          ) : (
+                            <button
+                              onClick={() => closeLeaseExercise(b)}
+                              disabled={closingLeaseId === b.lease_id}
+                              className="flex items-center gap-2 text-sm px-3 py-2 rounded-lg bg-orange-500 hover:bg-orange-600 text-white font-semibold transition-colors disabled:opacity-60"
+                            >
+                              {closingLeaseId === b.lease_id ? '⏳ Clôture du bilan…' : '✅ Clôturer le bilan annuel'}
+                            </button>
+                          )}
                         </div>
                         </div>
                       </div>
                       </div>
                     )
                     )