|
@@ -30,6 +30,7 @@ export default function LeaseDetail() {
|
|
|
const [lease, setLease] = useState(null)
|
|
const [lease, setLease] = useState(null)
|
|
|
const [payments, setPayments] = useState([])
|
|
const [payments, setPayments] = useState([])
|
|
|
const [revisions, setRevisions] = useState([])
|
|
const [revisions, setRevisions] = useState([])
|
|
|
|
|
+ const [pendingReg, setPendingReg] = useState({ pending: [], total: 0 })
|
|
|
const [showModal, setShowModal] = useState(false)
|
|
const [showModal, setShowModal] = useState(false)
|
|
|
const [showRevisionModal, setShowRevisionModal] = useState(false)
|
|
const [showRevisionModal, setShowRevisionModal] = useState(false)
|
|
|
const [loading, setLoading] = useState(false)
|
|
const [loading, setLoading] = useState(false)
|
|
@@ -43,7 +44,8 @@ export default function LeaseDetail() {
|
|
|
charges_paid: '',
|
|
charges_paid: '',
|
|
|
payment_date: now.toISOString().slice(0, 10),
|
|
payment_date: now.toISOString().slice(0, 10),
|
|
|
payment_method: 'virement',
|
|
payment_method: 'virement',
|
|
|
- notes: ''
|
|
|
|
|
|
|
+ notes: '',
|
|
|
|
|
+ apply_regularization: false,
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const [revForm, setRevForm] = useState({
|
|
const [revForm, setRevForm] = useState({
|
|
@@ -54,14 +56,16 @@ export default function LeaseDetail() {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const load = async () => {
|
|
const load = async () => {
|
|
|
- const [l, p, r] = await Promise.all([
|
|
|
|
|
|
|
+ const [l, p, r, reg] = await Promise.all([
|
|
|
api.get(`/leases/${id}`),
|
|
api.get(`/leases/${id}`),
|
|
|
api.get(`/payments?lease_id=${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)
|
|
setLease(l.data)
|
|
|
setPayments(p.data)
|
|
setPayments(p.data)
|
|
|
setRevisions(r.data)
|
|
setRevisions(r.data)
|
|
|
|
|
+ setPendingReg(reg.data)
|
|
|
setForm(f => ({ ...f, rent_paid: l.data.rent_amount, charges_paid: l.data.charges_amount }))
|
|
setForm(f => ({ ...f, rent_paid: l.data.rent_amount, charges_paid: l.data.charges_amount }))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -70,7 +74,12 @@ export default function LeaseDetail() {
|
|
|
const handlePayment = async e => {
|
|
const handlePayment = async e => {
|
|
|
e.preventDefault(); setLoading(true)
|
|
e.preventDefault(); setLoading(true)
|
|
|
try {
|
|
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é !')
|
|
toast.success('Paiement enregistré !')
|
|
|
setShowModal(false); load()
|
|
setShowModal(false); load()
|
|
|
} catch (err) { toast.error(err.response?.data?.error || 'Erreur') }
|
|
} catch (err) { toast.error(err.response?.data?.error || 'Erreur') }
|
|
@@ -229,6 +238,24 @@ export default function LeaseDetail() {
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ {/* Bannière régularisation en attente */}
|
|
|
|
|
+ {pendingReg.pending.length > 0 && (
|
|
|
|
|
+ <div className={`mb-4 rounded-xl border p-4 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>
|
|
|
|
|
+ <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).join(' · ')}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p className="text-xs text-gray-400 mt-1">Sera appliquée automatiquement lors du prochain paiement enregistré.</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
{payments.length === 0 ? (
|
|
{payments.length === 0 ? (
|
|
|
<div className="card text-center py-10">
|
|
<div className="card text-center py-10">
|
|
|
<p className="text-gray-400">Aucun paiement enregistré pour ce bail.</p>
|
|
<p className="text-gray-400">Aucun paiement enregistré pour ce bail.</p>
|
|
@@ -243,25 +270,38 @@ export default function LeaseDetail() {
|
|
|
<th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Mode</th>
|
|
<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">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">Charges</th>
|
|
|
- <th className="text-right px-6 py-3 text-sm font-semibold text-gray-600">Total</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">Net</th>
|
|
|
<th className="px-6 py-3"></th>
|
|
<th className="px-6 py-3"></th>
|
|
|
</tr>
|
|
</tr>
|
|
|
</thead>
|
|
</thead>
|
|
|
<tbody className="divide-y divide-gray-50">
|
|
<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 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 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>
|
|
|
|
|
- </td>
|
|
|
|
|
- </tr>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ {payments.map(p => {
|
|
|
|
|
+ const reg = p.charge_regularization || 0
|
|
|
|
|
+ const net = p.rent_paid + p.charges_paid - reg
|
|
|
|
|
+ return (
|
|
|
|
|
+ <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 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 text-sm">
|
|
|
|
|
+ {reg !== 0
|
|
|
|
|
+ ? <span className={`font-semibold ${reg > 0 ? 'text-green-600' : 'text-orange-600'}`}
|
|
|
|
|
+ title={reg > 0 ? 'Trop-perçu déduit' : 'Complément appelé'}>
|
|
|
|
|
+ {reg > 0 ? `−${fmt(reg)}` : `+${fmt(Math.abs(reg))}`}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ : <span className="text-gray-300">—</span>}
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td className="px-6 py-4 text-right font-semibold text-green-700">{fmt(net)}</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>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ )
|
|
|
|
|
+ })}
|
|
|
</tbody>
|
|
</tbody>
|
|
|
</table>
|
|
</table>
|
|
|
</div>
|
|
</div>
|
|
@@ -309,8 +349,28 @@ export default function LeaseDetail() {
|
|
|
<input className="input" value={form.notes} onChange={e => setForm(f => ({ ...f, notes: e.target.value }))} placeholder="..." />
|
|
<input className="input" value={form.notes} onChange={e => setForm(f => ({ ...f, notes: e.target.value }))} placeholder="..." />
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</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}
|
|
|
|
|
+ onChange={e => setForm(f => ({ ...f, apply_regularization: e.target.checked }))} />
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-sm font-semibold text-gray-800">
|
|
|
|
|
+ {pendingReg.total >= 0
|
|
|
|
|
+ ? `Appliquer la régularisation : −${fmt(pendingReg.total)} (trop-perçu à déduire)`
|
|
|
|
|
+ : `Appliquer la régularisation : +${fmt(Math.abs(pendingReg.total))} (complément à appeler)`}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p className="text-xs text-gray-500 mt-0.5">Suite à la clôture des charges de l'exercice</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
<div className="bg-blue-50 rounded-lg p-3 text-sm text-blue-700">
|
|
<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))}
|
|
|
|
|
|
|
+ <strong>Total à encaisser :</strong>{' '}
|
|
|
|
|
+ {fmt(
|
|
|
|
|
+ Number(form.rent_paid || 0) + Number(form.charges_paid || 0) -
|
|
|
|
|
+ (form.apply_regularization ? (pendingReg.total || 0) : 0)
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
<div className="flex gap-3 pt-2">
|
|
<div className="flex gap-3 pt-2">
|
|
|
<button type="button" onClick={() => setShowModal(false)} className="btn-secondary flex-1">Annuler</button>
|
|
<button type="button" onClick={() => setShowModal(false)} className="btn-secondary flex-1">Annuler</button>
|