|
@@ -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,12 @@ 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: '',
|
|
|
|
|
+ is_prorata: false,
|
|
|
|
|
+ prorata_days: '',
|
|
|
|
|
+ prorata_total_days: '',
|
|
|
|
|
+ apply_regularization: false,
|
|
|
|
|
+ charge_regularization: 0,
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const [revForm, setRevForm] = useState({
|
|
const [revForm, setRevForm] = useState({
|
|
@@ -54,23 +60,50 @@ 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 }))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
useEffect(() => { load() }, [id])
|
|
useEffect(() => { load() }, [id])
|
|
|
|
|
|
|
|
|
|
+ // Prorata calculation
|
|
|
|
|
+ const calcProrata = (baseAmount, days, totalDays) => {
|
|
|
|
|
+ if (!days || !totalDays || totalDays === 0) return baseAmount
|
|
|
|
|
+ return Math.round((baseAmount / totalDays) * days * 100) / 100
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const updateProrata = (updatedForm) => {
|
|
|
|
|
+ if (!updatedForm.is_prorata) return updatedForm
|
|
|
|
|
+ const days = parseInt(updatedForm.prorata_days) || 0
|
|
|
|
|
+ const total = parseInt(updatedForm.prorata_total_days) || 0
|
|
|
|
|
+ if (!days || !total) return updatedForm
|
|
|
|
|
+ const baseRent = lease?.rent_amount || 0
|
|
|
|
|
+ const baseCharges = lease?.charges_amount || 0
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...updatedForm,
|
|
|
|
|
+ rent_paid: calcProrata(baseRent, days, total),
|
|
|
|
|
+ charges_paid: calcProrata(baseCharges, days, total),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
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 +262,23 @@ export default function LeaseDetail() {
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ {/* Régularisation en attente */}
|
|
|
|
|
+ {pendingReg.pending.length > 0 && (
|
|
|
|
|
+ <div className={`mb-4 rounded-xl p-4 border 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 className="flex-1">
|
|
|
|
|
+ <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} (${fmt(r.amount)})`).join(' · ')}
|
|
|
|
|
+ </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,6 +293,7 @@ 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">Régul.</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">Total</th>
|
|
|
<th className="px-6 py-3"></th>
|
|
<th className="px-6 py-3"></th>
|
|
|
</tr>
|
|
</tr>
|
|
@@ -250,12 +301,24 @@ export default function LeaseDetail() {
|
|
|
<tbody className="divide-y divide-gray-50">
|
|
<tbody className="divide-y divide-gray-50">
|
|
|
{payments.map(p => (
|
|
{payments.map(p => (
|
|
|
<tr key={p.id} className="hover:bg-gray-50">
|
|
<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 font-medium text-gray-900">
|
|
|
|
|
+ {MONTHS[p.period_month-1]} {p.period_year}
|
|
|
|
|
+ {p.is_prorata ? <span className="ml-1 text-xs bg-yellow-100 text-yellow-700 px-1.5 py-0.5 rounded-full font-medium">prorata {p.prorata_days}/{p.prorata_total_days}j</span> : null}
|
|
|
|
|
+ </td>
|
|
|
<td className="px-6 py-4 text-gray-600">{fmtDate(p.payment_date)}</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-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)}</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-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 text-sm">
|
|
|
|
|
+ {p.charge_regularization !== 0 && p.charge_regularization != null
|
|
|
|
|
+ ? <span className={p.charge_regularization > 0 ? 'text-green-600' : 'text-orange-600'}>
|
|
|
|
|
+ {p.charge_regularization > 0 ? '-' : '+'}{fmt(Math.abs(p.charge_regularization))}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ : <span className="text-gray-300">—</span>}
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td className="px-6 py-4 text-right font-semibold text-green-700">
|
|
|
|
|
+ {fmt(p.rent_paid + p.charges_paid - (p.charge_regularization > 0 ? p.charge_regularization : 0) + (p.charge_regularization < 0 ? Math.abs(p.charge_regularization) : 0))}
|
|
|
|
|
+ </td>
|
|
|
<td className="px-6 py-4 text-right space-x-2">
|
|
<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={() => 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>
|
|
<button onClick={() => handleDeletePayment(p.id)} className="text-gray-400 hover:text-red-600 ml-2">🗑️</button>
|
|
@@ -283,6 +346,46 @@ export default function LeaseDetail() {
|
|
|
<input className="input" type="number" min="2000" max="2099" value={form.period_year}
|
|
<input className="input" type="number" min="2000" max="2099" value={form.period_year}
|
|
|
onChange={e => setForm(f => ({ ...f, period_year: Number(e.target.value) }))} />
|
|
onChange={e => setForm(f => ({ ...f, period_year: Number(e.target.value) }))} />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Prorata */}
|
|
|
|
|
+ <div className="border border-gray-200 rounded-xl p-4 space-y-3">
|
|
|
|
|
+ <label className="flex items-center gap-3 cursor-pointer">
|
|
|
|
|
+ <input type="checkbox" className="w-4 h-4" checked={form.is_prorata}
|
|
|
|
|
+ onChange={e => {
|
|
|
|
|
+ const checked = e.target.checked
|
|
|
|
|
+ const daysInMonth = new Date(form.period_year, form.period_month, 0).getDate()
|
|
|
|
|
+ const next = { ...form, is_prorata: checked, prorata_total_days: checked ? daysInMonth : '', prorata_days: checked ? daysInMonth : '' }
|
|
|
|
|
+ setForm(updateProrata(next))
|
|
|
|
|
+ }} />
|
|
|
|
|
+ <span className="text-sm font-medium text-gray-700">Appliquer un prorata (entrée/sortie en cours de mois)</span>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ {form.is_prorata && (
|
|
|
|
|
+ <div className="grid grid-cols-2 gap-3 pt-1">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="label">Jours occupés *</label>
|
|
|
|
|
+ <input className="input" type="number" min="1" max="31" value={form.prorata_days}
|
|
|
|
|
+ onChange={e => {
|
|
|
|
|
+ const next = { ...form, prorata_days: e.target.value }
|
|
|
|
|
+ setForm(updateProrata(next))
|
|
|
|
|
+ }} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="label">Jours dans le mois</label>
|
|
|
|
|
+ <input className="input" type="number" min="1" max="31" value={form.prorata_total_days}
|
|
|
|
|
+ onChange={e => {
|
|
|
|
|
+ const next = { ...form, prorata_total_days: e.target.value }
|
|
|
|
|
+ setForm(updateProrata(next))
|
|
|
|
|
+ }} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p className="col-span-2 text-xs text-blue-600">
|
|
|
|
|
+ Loyer calculé : {fmt(calcProrata(lease?.rent_amount || 0, form.prorata_days, form.prorata_total_days))} + charges : {fmt(calcProrata(lease?.charges_amount || 0, form.prorata_days, form.prorata_total_days))}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="grid grid-cols-2 gap-4">
|
|
|
<div>
|
|
<div>
|
|
|
<label className="label">Loyer encaissé (€) *</label>
|
|
<label className="label">Loyer encaissé (€) *</label>
|
|
|
<input className="input" type="number" step="0.01" min="0" required value={form.rent_paid}
|
|
<input className="input" type="number" step="0.01" min="0" required value={form.rent_paid}
|
|
@@ -309,9 +412,38 @@ 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>
|
|
|
- <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))}
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ {/* Régularisation en attente */}
|
|
|
|
|
+ {pendingReg.pending.length > 0 && (
|
|
|
|
|
+ <div className={`border rounded-xl p-3 ${pendingReg.total >= 0 ? 'border-green-200 bg-green-50' : 'border-orange-200 bg-orange-50'}`}>
|
|
|
|
|
+ <label className="flex items-start gap-3 cursor-pointer">
|
|
|
|
|
+ <input type="checkbox" className="w-4 h-4 mt-0.5" checked={form.apply_regularization}
|
|
|
|
|
+ onChange={e => setForm(f => ({ ...f, apply_regularization: e.target.checked }))} />
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className={`text-sm font-medium ${pendingReg.total >= 0 ? 'text-green-800' : 'text-orange-800'}`}>
|
|
|
|
|
+ {pendingReg.total >= 0
|
|
|
|
|
+ ? `Appliquer la régularisation de charges : -${fmt(pendingReg.total)} sur ce loyer`
|
|
|
|
|
+ : `Appliquer la régularisation de charges : +${fmt(Math.abs(pendingReg.total))} sur ce loyer`}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p className="text-xs text-gray-500 mt-0.5">{pendingReg.pending.map(r => r.notes).join(' · ')}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {(() => {
|
|
|
|
|
+ const base = Number(form.rent_paid || 0) + Number(form.charges_paid || 0)
|
|
|
|
|
+ const reg = form.apply_regularization ? pendingReg.total : 0
|
|
|
|
|
+ const net = base - reg
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="bg-blue-50 rounded-lg p-3 text-sm text-blue-700 space-y-1">
|
|
|
|
|
+ <div className="flex justify-between"><span>Loyer + charges :</span><strong>{fmt(base)}</strong></div>
|
|
|
|
|
+ {form.apply_regularization && <div className="flex justify-between"><span>Régularisation :</span><strong className={reg >= 0 ? 'text-green-700' : 'text-orange-700'}>{reg >= 0 ? '-' : '+'}{fmt(Math.abs(reg))}</strong></div>}
|
|
|
|
|
+ <div className="flex justify-between border-t border-blue-200 pt-1"><span><strong>Total à encaisser :</strong></span><strong>{fmt(net)}</strong></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>
|
|
|
<button type="submit" disabled={loading} className="btn-primary flex-1">{loading ? '...' : 'Enregistrer'}</button>
|
|
<button type="submit" disabled={loading} className="btn-primary flex-1">{loading ? '...' : 'Enregistrer'}</button>
|