jeremy 4 meses atrás
pai
commit
79f92d287c

+ 5 - 0
backend/src/db.js

@@ -119,6 +119,11 @@ try { db.exec('ALTER TABLE charges ADD COLUMN recoverable INTEGER NOT NULL DEFAU
 // Add charge_regularization column to payments if missing
 try { db.exec('ALTER TABLE payments ADD COLUMN charge_regularization REAL DEFAULT 0'); } catch {}
 
+// Prorata columns on payments
+try { db.exec('ALTER TABLE payments ADD COLUMN is_prorata INTEGER DEFAULT 0'); } catch {}
+try { db.exec('ALTER TABLE payments ADD COLUMN prorata_days INTEGER'); } catch {}
+try { db.exec('ALTER TABLE payments ADD COLUMN prorata_total_days INTEGER'); } catch {}
+
 // Regularizations from annual exercise closure
 db.exec(`
   CREATE TABLE IF NOT EXISTS charge_regularizations (

+ 6 - 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, charge_regularization } = req.body;
+  const { lease_id, period_month, period_year, rent_paid, charges_paid, payment_date, payment_method, notes, charge_regularization,
+          is_prorata, prorata_days, prorata_total_days } = req.body;
   if (!lease_id || !period_month || !period_year || rent_paid === undefined || !payment_date)
     return res.status(400).json({ error: 'Champs requis manquants' });
 
@@ -39,10 +40,11 @@ router.post('/', (req, res) => {
   const reg = 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, charge_regularization)
-    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+    INSERT INTO payments (lease_id, user_id, period_month, period_year, rent_paid, charges_paid, payment_date, payment_method, notes, charge_regularization, is_prorata, prorata_days, prorata_total_days)
+    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
   `).run(lease_id, req.userId, period_month, period_year, rent_paid, charges_paid || 0,
-    payment_date, payment_method || 'virement', notes || null, reg);
+    payment_date, payment_method || 'virement', notes || null, reg,
+    is_prorata ? 1 : 0, prorata_days || null, prorata_total_days || null);
 
   // Mark regularization as applied
   if (reg !== 0) {

+ 6 - 1
backend/src/routes/receipts.js

@@ -91,7 +91,12 @@ router.get('/:paymentId', (req, res) => {
     doc.moveDown(0.6);
   };
 
-  drawRow('Loyer hors charges', payment.rent_paid);
+  const isProrata = payment.is_prorata && payment.prorata_days && payment.prorata_total_days;
+  if (isProrata) {
+    drawRow(`Loyer au prorata (${payment.prorata_days}/${payment.prorata_total_days} jours)`, payment.rent_paid);
+  } else {
+    drawRow('Loyer hors charges', payment.rent_paid);
+  }
   drawRow('Charges locatives', payment.charges_paid);
 
   const reg = payment.charge_regularization || 0;

+ 73 - 1
frontend/src/pages/LeaseDetail.jsx

@@ -25,6 +25,8 @@ function Modal({ title, onClose, children }) {
   )
 }
 
+const daysInMonth = (month, year) => new Date(year, month, 0).getDate()
+
 export default function LeaseDetail() {
   const { id } = useParams()
   const [lease, setLease] = useState(null)
@@ -46,6 +48,9 @@ export default function LeaseDetail() {
     payment_method: 'virement',
     notes: '',
     apply_regularization: false,
+    is_prorata: false,
+    prorata_days: '',
+    prorata_total_days: '',
   })
 
   const [revForm, setRevForm] = useState({
@@ -78,6 +83,9 @@ export default function LeaseDetail() {
         ...form,
         lease_id: Number(id),
         charge_regularization: form.apply_regularization ? pendingReg.total : 0,
+        is_prorata: form.is_prorata ? 1 : 0,
+        prorata_days: form.is_prorata ? form.prorata_days : null,
+        prorata_total_days: form.is_prorata ? form.prorata_total_days : null,
       }
       await api.post('/payments', payload)
       toast.success('Paiement enregistré !')
@@ -284,7 +292,10 @@ export default function LeaseDetail() {
                     <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 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-900">
+                      {fmt(p.rent_paid)}
+                      {p.is_prorata ? <span className="ml-1 text-xs text-amber-600" title={`Prorata ${p.prorata_days}/${p.prorata_total_days} j`}>⅟</span> : null}
+                    </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 text-sm">
                       {reg !== 0
@@ -350,6 +361,67 @@ export default function LeaseDetail() {
               </div>
             </div>
 
+            {/* Prorata */}
+            <label className="flex items-center gap-3 cursor-pointer select-none">
+              <input type="checkbox" checked={form.is_prorata}
+                onChange={e => {
+                  const enabled = e.target.checked
+                  const total = daysInMonth(form.period_month, form.period_year)
+                  setForm(f => ({
+                    ...f,
+                    is_prorata: enabled,
+                    prorata_total_days: enabled ? total : '',
+                    prorata_days: enabled ? total : '',
+                    rent_paid: enabled ? lease.rent_amount : lease.rent_amount,
+                    charges_paid: enabled ? lease.charges_amount : lease.charges_amount,
+                  }))
+                }} />
+              <span className="text-sm font-medium text-gray-700">Mois incomplet — calculer au prorata</span>
+            </label>
+
+            {form.is_prorata && (
+              <div className="bg-amber-50 border border-amber-200 rounded-xl p-4 space-y-3">
+                <p className="text-xs text-amber-700 font-medium">Loyer et charges recalculés au prorata des jours occupés</p>
+                <div className="grid grid-cols-2 gap-3">
+                  <div>
+                    <label className="label text-xs">Jours occupés *</label>
+                    <input className="input" type="number" min="1"
+                      max={daysInMonth(form.period_month, form.period_year)}
+                      value={form.prorata_days}
+                      onChange={e => {
+                        const days = Number(e.target.value)
+                        const total = form.prorata_total_days || daysInMonth(form.period_month, form.period_year)
+                        const ratio = days / total
+                        setForm(f => ({
+                          ...f,
+                          prorata_days: days,
+                          rent_paid: (lease.rent_amount * ratio).toFixed(2),
+                          charges_paid: (lease.charges_amount * ratio).toFixed(2),
+                        }))
+                      }} />
+                  </div>
+                  <div>
+                    <label className="label text-xs">Jours dans le mois</label>
+                    <input className="input" type="number" min="28" max="31" value={form.prorata_total_days}
+                      onChange={e => {
+                        const total = Number(e.target.value)
+                        const days = form.prorata_days || total
+                        const ratio = days / total
+                        setForm(f => ({
+                          ...f,
+                          prorata_total_days: total,
+                          rent_paid: (lease.rent_amount * ratio).toFixed(2),
+                          charges_paid: (lease.charges_amount * ratio).toFixed(2),
+                        }))
+                      }} />
+                  </div>
+                </div>
+                <p className="text-xs text-amber-600">
+                  Loyer plein : {fmt(lease.rent_amount)} · Charges pleines : {fmt(lease.charges_amount)}
+                </p>
+              </div>
+            )}
+
             {pendingReg.pending.length > 0 && (
               <label className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer ${form.apply_regularization ? (pendingReg.total >= 0 ? 'bg-green-50 border-green-300' : 'bg-orange-50 border-orange-300') : 'bg-gray-50 border-gray-200'}`}>
                 <input type="checkbox" className="mt-0.5" checked={form.apply_regularization}