|
|
@@ -29,8 +29,11 @@ export default function LeaseDetail() {
|
|
|
const { id } = useParams()
|
|
|
const [lease, setLease] = useState(null)
|
|
|
const [payments, setPayments] = useState([])
|
|
|
+ const [revisions, setRevisions] = useState([])
|
|
|
const [showModal, setShowModal] = useState(false)
|
|
|
+ const [showRevisionModal, setShowRevisionModal] = useState(false)
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
+ const [revLoading, setRevLoading] = useState(false)
|
|
|
|
|
|
const now = new Date()
|
|
|
const [form, setForm] = useState({
|
|
|
@@ -43,10 +46,22 @@ export default function LeaseDetail() {
|
|
|
notes: ''
|
|
|
})
|
|
|
|
|
|
+ const [revForm, setRevForm] = useState({
|
|
|
+ effective_date: now.toISOString().slice(0, 10),
|
|
|
+ rent_amount: '',
|
|
|
+ charges_amount: '',
|
|
|
+ notes: ''
|
|
|
+ })
|
|
|
+
|
|
|
const load = async () => {
|
|
|
- const [l, p] = await Promise.all([api.get(`/leases/${id}`), api.get(`/payments?lease_id=${id}`)])
|
|
|
+ const [l, p, r] = await Promise.all([
|
|
|
+ api.get(`/leases/${id}`),
|
|
|
+ api.get(`/payments?lease_id=${id}`),
|
|
|
+ api.get(`/leases/${id}/revisions`)
|
|
|
+ ])
|
|
|
setLease(l.data)
|
|
|
setPayments(p.data)
|
|
|
+ setRevisions(r.data)
|
|
|
setForm(f => ({ ...f, rent_paid: l.data.rent_amount, charges_paid: l.data.charges_amount }))
|
|
|
}
|
|
|
|
|
|
@@ -68,6 +83,23 @@ export default function LeaseDetail() {
|
|
|
catch { toast.error('Erreur') }
|
|
|
}
|
|
|
|
|
|
+ const handleRevision = async e => {
|
|
|
+ e.preventDefault(); setRevLoading(true)
|
|
|
+ try {
|
|
|
+ await api.post(`/leases/${id}/revisions`, revForm)
|
|
|
+ toast.success('Révision enregistrée !')
|
|
|
+ setShowRevisionModal(false)
|
|
|
+ load()
|
|
|
+ } catch (err) { toast.error(err.response?.data?.error || 'Erreur') }
|
|
|
+ finally { setRevLoading(false) }
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleDeleteRevision = async revId => {
|
|
|
+ if (!confirm('Supprimer cette révision ?')) return
|
|
|
+ try { await api.delete(`/leases/${id}/revisions/${revId}`); toast.success('Révision supprimée'); load() }
|
|
|
+ catch { toast.error('Erreur') }
|
|
|
+ }
|
|
|
+
|
|
|
const downloadReceipt = payId => {
|
|
|
const token = localStorage.getItem('token')
|
|
|
const link = document.createElement('a')
|
|
|
@@ -108,7 +140,15 @@ export default function LeaseDetail() {
|
|
|
<p className="text-gray-600 mt-1">{lease.property_name}</p>
|
|
|
<p className="text-gray-500 text-sm">{lease.property_address}, {lease.property_zip} {lease.property_city}</p>
|
|
|
</div>
|
|
|
- <span className={lease.active ? 'badge-green' : 'badge-red'}>{lease.active ? 'Actif' : 'Clôturé'}</span>
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
+ {lease.active && (
|
|
|
+ <button onClick={() => {
|
|
|
+ setRevForm({ effective_date: now.toISOString().slice(0, 10), rent_amount: String(lease.rent_amount), charges_amount: String(lease.charges_amount), notes: '' })
|
|
|
+ setShowRevisionModal(true)
|
|
|
+ }} className="btn-secondary text-sm">📈 Réviser le loyer</button>
|
|
|
+ )}
|
|
|
+ <span className={lease.active ? 'badge-green' : 'badge-red'}>{lease.active ? 'Actif' : 'Clôturé'}</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6 pt-6 border-t">
|
|
|
@@ -143,6 +183,44 @@ export default function LeaseDetail() {
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ {/* Historique des révisions */}
|
|
|
+ {revisions.length > 0 && (
|
|
|
+ <div className="card mb-6">
|
|
|
+ <h2 className="text-base font-semibold text-gray-800 mb-3">📈 Historique des révisions de loyer</h2>
|
|
|
+ <div className="divide-y divide-gray-50">
|
|
|
+ {revisions.map((r, i) => {
|
|
|
+ const isNext = r.effective_date > now.toISOString().slice(0, 10)
|
|
|
+ return (
|
|
|
+ <div key={r.id} className="flex items-center justify-between py-3">
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
+ {i === 0 && !isNext && (
|
|
|
+ <span className="text-xs bg-green-100 text-green-700 font-semibold px-2 py-0.5 rounded-full">En vigueur</span>
|
|
|
+ )}
|
|
|
+ {isNext && (
|
|
|
+ <span className="text-xs bg-blue-100 text-blue-700 font-semibold px-2 py-0.5 rounded-full">À venir</span>
|
|
|
+ )}
|
|
|
+ {i > 0 && !isNext && (
|
|
|
+ <span className="text-xs bg-gray-100 text-gray-500 font-semibold px-2 py-0.5 rounded-full">Ancien</span>
|
|
|
+ )}
|
|
|
+ <div>
|
|
|
+ <p className="text-sm font-medium text-gray-900">
|
|
|
+ Loyer {fmt(r.rent_amount)} + charges {fmt(r.charges_amount)}
|
|
|
+ <span className="text-gray-400 font-normal ml-1">= {fmt(r.rent_amount + r.charges_amount)}/mois</span>
|
|
|
+ </p>
|
|
|
+ <p className="text-xs text-gray-500">
|
|
|
+ Applicable à partir du {fmtDate(r.effective_date)}
|
|
|
+ {r.notes && ` — ${r.notes}`}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <button onClick={() => handleDeleteRevision(r.id)} className="text-gray-300 hover:text-red-500 ml-4">🗑️</button>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
{/* Paiements */}
|
|
|
<div className="flex items-center justify-between mb-4">
|
|
|
<h2 className="text-xl font-bold text-gray-900">💰 Historique des paiements</h2>
|
|
|
@@ -241,6 +319,57 @@ export default function LeaseDetail() {
|
|
|
</form>
|
|
|
</Modal>
|
|
|
)}
|
|
|
+
|
|
|
+ {/* Modal révision loyer */}
|
|
|
+ {showRevisionModal && (
|
|
|
+ <Modal title="📈 Réviser le loyer" onClose={() => setShowRevisionModal(false)}>
|
|
|
+ <form onSubmit={handleRevision} className="space-y-4">
|
|
|
+ <div>
|
|
|
+ <label className="label">Date d'effet de la révision *</label>
|
|
|
+ <input className="input" type="date" required value={revForm.effective_date}
|
|
|
+ onChange={e => setRevForm(f => ({ ...f, effective_date: e.target.value }))} />
|
|
|
+ <p className="text-xs text-gray-400 mt-1">
|
|
|
+ Si la date est aujourd'hui ou passée, le loyer en vigueur sera mis à jour immédiatement.
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ <div className="grid grid-cols-2 gap-4">
|
|
|
+ <div>
|
|
|
+ <label className="label">Nouveau loyer HC (€) *</label>
|
|
|
+ <input className="input" type="number" step="0.01" min="0" required value={revForm.rent_amount}
|
|
|
+ onChange={e => setRevForm(f => ({ ...f, rent_amount: e.target.value }))} />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label className="label">Nouvelles charges (€)</label>
|
|
|
+ <input className="input" type="number" step="0.01" min="0" value={revForm.charges_amount}
|
|
|
+ onChange={e => setRevForm(f => ({ ...f, charges_amount: e.target.value }))} />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {revForm.rent_amount && (
|
|
|
+ <div className="bg-blue-50 rounded-lg p-3 text-sm text-blue-800">
|
|
|
+ <strong>Nouveau total mensuel :</strong> {fmt(Number(revForm.rent_amount || 0) + Number(revForm.charges_amount || 0))}
|
|
|
+ {lease && (
|
|
|
+ <span className="ml-2 text-blue-500">
|
|
|
+ ({Number(revForm.rent_amount) - lease.rent_amount >= 0 ? '+' : ''}{fmt(Number(revForm.rent_amount) - lease.rent_amount)} loyer,{' '}
|
|
|
+ {Number(revForm.charges_amount) - lease.charges_amount >= 0 ? '+' : ''}{fmt(Number(revForm.charges_amount) - lease.charges_amount)} charges)
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <div>
|
|
|
+ <label className="label">Notes (IRL, motif…)</label>
|
|
|
+ <input className="input" value={revForm.notes}
|
|
|
+ onChange={e => setRevForm(f => ({ ...f, notes: e.target.value }))}
|
|
|
+ placeholder="Ex : Révision IRL T3 2024, indice 142.6" />
|
|
|
+ </div>
|
|
|
+ <div className="flex gap-3 pt-2">
|
|
|
+ <button type="button" onClick={() => setShowRevisionModal(false)} className="btn-secondary flex-1">Annuler</button>
|
|
|
+ <button type="submit" disabled={revLoading} className="btn-primary flex-1">
|
|
|
+ {revLoading ? '...' : 'Enregistrer la révision'}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </Modal>
|
|
|
+ )}
|
|
|
</div>
|
|
|
)
|
|
|
}
|