Browse Source

feat: tableau récapitulatif des charges déductibles sur 10 ans

Ajout d'un endpoint API GET /charges/deductible-summary/:property_id
qui retourne la somme des charges déductibles par année sur 10 ans.

Ajout d'un tableau dépliable sur la page Charges (visible quand un bien
est sélectionné) affichant année par année le montant déductible,
le nombre de factures, et le total cumulé sur 10 ans.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jeremy 4 tháng trước cách đây
mục cha
commit
14e0e6ea1a
2 tập tin đã thay đổi với 97 bổ sung1 xóa
  1. 30 0
      backend/src/routes/charges.js
  2. 67 1
      frontend/src/pages/Charges.jsx

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

@@ -195,6 +195,36 @@ router.get('/:id/invoice', async (req, res) => {
   res.sendFile(filePath);
 });
 
+// Deductible charges summary over 10 years for a property
+router.get('/deductible-summary/:property_id', async (req, res) => {
+  const { property_id } = req.params;
+
+  const prop = (await pool.query('SELECT * 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 currentYear = new Date().getFullYear();
+  const startYear = currentYear - 9;
+
+  const rows = (await pool.query(`
+    SELECT year, SUM(amount) as total, COUNT(*) as count
+    FROM charges
+    WHERE property_id = $1 AND user_id = $2 AND recoverable_type = 'deductible'
+      AND year >= $3 AND year <= $4
+    GROUP BY year
+    ORDER BY year ASC
+  `, [property_id, req.userId, startYear, currentYear])).rows;
+
+  const byYear = Object.fromEntries(rows.map(r => [r.year, { total: round2(parseFloat(r.total)), count: parseInt(r.count) }]));
+  const years = [];
+  for (let y = startYear; y <= currentYear; y++) {
+    years.push({ year: y, total: byYear[y]?.total || 0, count: byYear[y]?.count || 0 });
+  }
+
+  const grandTotal = round2(years.reduce((s, y) => s + y.total, 0));
+
+  res.json({ property: prop, start_year: startYear, end_year: currentYear, years, grand_total: grandTotal });
+});
+
 // Annual bilan for a property
 router.get('/bilan/:property_id/:year', async (req, res) => {
   const { property_id, year } = req.params;

+ 67 - 1
frontend/src/pages/Charges.jsx

@@ -51,6 +51,8 @@ export default function Charges() {
   const [cancelingLeaseId, setCancelingLeaseId] = useState(null)
   const [loading, setLoading] = useState(false)
   const [editCharge, setEditCharge] = useState(null)
+  const [deductibleSummary, setDeductibleSummary] = useState(null)
+  const [showDeductible, setShowDeductible] = useState(false)
 
   const emptyForm = {
     property_id: '',
@@ -79,8 +81,16 @@ export default function Charges() {
     setCharges(data)
   }
 
+  const loadDeductibleSummary = async () => {
+    if (!filterProperty) { setDeductibleSummary(null); return }
+    try {
+      const { data } = await api.get(`/charges/deductible-summary/${filterProperty}`)
+      setDeductibleSummary(data)
+    } catch { setDeductibleSummary(null) }
+  }
+
   useEffect(() => { loadProperties() }, [])
-  useEffect(() => { if (filterProperty) loadCharges() }, [filterProperty, filterYear])
+  useEffect(() => { if (filterProperty) { loadCharges(); loadDeductibleSummary() } }, [filterProperty, filterYear])
 
   const openAdd = () => {
     setEditCharge(null)
@@ -415,6 +425,62 @@ export default function Charges() {
         )}
       </div>
 
+      {/* Deductible summary table (10 years) */}
+      {filterProperty && deductibleSummary && (
+        <div className="card mb-6">
+          <button
+            onClick={() => setShowDeductible(v => !v)}
+            className="w-full flex items-center justify-between text-left"
+          >
+            <div className="flex items-center gap-2">
+              <span className="text-lg">💼</span>
+              <div>
+                <h3 className="text-sm font-semibold text-purple-800">Charges déductibles des impôts — 10 ans</h3>
+                <p className="text-xs text-purple-500">
+                  Total cumulé : <span className="font-bold">{fmt(deductibleSummary.grand_total)}</span>
+                </p>
+              </div>
+            </div>
+            <span className={`text-gray-400 transition-transform ${showDeductible ? 'rotate-180' : ''}`}>▼</span>
+          </button>
+          {showDeductible && (
+            <div className="mt-4 overflow-x-auto">
+              <table className="w-full text-sm">
+                <thead>
+                  <tr className="bg-purple-50 border-b border-purple-100">
+                    <th className="text-left px-4 py-2 text-purple-700 font-semibold">Année</th>
+                    <th className="text-right px-4 py-2 text-purple-700 font-semibold">Montant déductible</th>
+                    <th className="text-right px-4 py-2 text-purple-700 font-semibold">Nb factures</th>
+                  </tr>
+                </thead>
+                <tbody className="divide-y divide-purple-50">
+                  {deductibleSummary.years.map(y => (
+                    <tr key={y.year} className={`hover:bg-purple-50/50 ${String(y.year) === filterYear ? 'bg-purple-50 font-semibold' : ''}`}>
+                      <td className="px-4 py-2 text-gray-700">{y.year}</td>
+                      <td className={`px-4 py-2 text-right ${y.total > 0 ? 'text-purple-700' : 'text-gray-300'}`}>
+                        {y.total > 0 ? fmt(y.total) : '—'}
+                      </td>
+                      <td className={`px-4 py-2 text-right ${y.count > 0 ? 'text-gray-600' : 'text-gray-300'}`}>
+                        {y.count > 0 ? y.count : '—'}
+                      </td>
+                    </tr>
+                  ))}
+                </tbody>
+                <tfoot className="bg-purple-50 border-t-2 border-purple-200">
+                  <tr>
+                    <td className="px-4 py-2 font-bold text-purple-800">Total (10 ans)</td>
+                    <td className="px-4 py-2 text-right font-bold text-purple-800">{fmt(deductibleSummary.grand_total)}</td>
+                    <td className="px-4 py-2 text-right font-bold text-gray-600">
+                      {deductibleSummary.years.reduce((s, y) => s + y.count, 0)}
+                    </td>
+                  </tr>
+                </tfoot>
+              </table>
+            </div>
+          )}
+        </div>
+      )}
+
       {/* Charges list */}
       {charges.length === 0 ? (
         <div className="card text-center py-12">