Explorar el Código

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 hace 4 meses
padre
commit
14e0e6ea1a
Se han modificado 2 ficheros con 97 adiciones y 1 borrados
  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);
   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
 // Annual bilan for a property
 router.get('/bilan/:property_id/:year', async (req, res) => {
 router.get('/bilan/:property_id/:year', async (req, res) => {
   const { property_id, year } = req.params;
   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 [cancelingLeaseId, setCancelingLeaseId] = useState(null)
   const [loading, setLoading] = useState(false)
   const [loading, setLoading] = useState(false)
   const [editCharge, setEditCharge] = useState(null)
   const [editCharge, setEditCharge] = useState(null)
+  const [deductibleSummary, setDeductibleSummary] = useState(null)
+  const [showDeductible, setShowDeductible] = useState(false)
 
 
   const emptyForm = {
   const emptyForm = {
     property_id: '',
     property_id: '',
@@ -79,8 +81,16 @@ export default function Charges() {
     setCharges(data)
     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(() => { loadProperties() }, [])
-  useEffect(() => { if (filterProperty) loadCharges() }, [filterProperty, filterYear])
+  useEffect(() => { if (filterProperty) { loadCharges(); loadDeductibleSummary() } }, [filterProperty, filterYear])
 
 
   const openAdd = () => {
   const openAdd = () => {
     setEditCharge(null)
     setEditCharge(null)
@@ -415,6 +425,62 @@ export default function Charges() {
         )}
         )}
       </div>
       </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 list */}
       {charges.length === 0 ? (
       {charges.length === 0 ? (
         <div className="card text-center py-12">
         <div className="card text-center py-12">