|
@@ -67,6 +67,38 @@ export default function LeaseDetail() {
|
|
|
charges_amount: '',
|
|
charges_amount: '',
|
|
|
notes: ''
|
|
notes: ''
|
|
|
})
|
|
})
|
|
|
|
|
+ const [irlCurrent, setIrlCurrent] = useState({ value: null, loading: false, quarter: null, year: null })
|
|
|
|
|
+
|
|
|
|
|
+ // Retourne le trimestre (1-4) d'une date ISO
|
|
|
|
|
+ const getQuarter = dateStr => Math.ceil((new Date(dateStr).getMonth() + 1) / 3)
|
|
|
|
|
+
|
|
|
|
|
+ // Ouvre le modal révision et tente de récupérer l'IRL du trimestre de référence pour l'année en cours
|
|
|
|
|
+ const openRevisionModal = async () => {
|
|
|
|
|
+ setRevForm({ effective_date: now.toISOString().slice(0, 10), rent_amount: String(lease.rent_amount), charges_amount: String(lease.charges_amount), notes: '' })
|
|
|
|
|
+ setIrlCurrent({ value: null, loading: false, quarter: null, year: null })
|
|
|
|
|
+
|
|
|
|
|
+ if (lease.irl_reference_quarter) {
|
|
|
|
|
+ const q = lease.irl_reference_quarter
|
|
|
|
|
+ const y = now.getFullYear()
|
|
|
|
|
+ setIrlCurrent(s => ({ ...s, loading: true, quarter: q, year: y }))
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { data } = await api.get(`/irl?year=${y}&quarter=${q}`)
|
|
|
|
|
+ setIrlCurrent({ value: data.value, loading: false, quarter: q, year: y, configured: data.configured })
|
|
|
|
|
+ // Pré-calculer le loyer si IRL de référence connu
|
|
|
|
|
+ if (data.value && lease.irl_reference_value) {
|
|
|
|
|
+ const ratio = data.value / lease.irl_reference_value
|
|
|
|
|
+ setRevForm(f => ({
|
|
|
|
|
+ ...f,
|
|
|
|
|
+ rent_amount: (lease.rent_amount * ratio).toFixed(2),
|
|
|
|
|
+ notes: `Révision IRL T${q} ${y} — indice ${data.value} / ref ${lease.irl_reference_value}`
|
|
|
|
|
+ }))
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ setIrlCurrent(s => ({ ...s, loading: false }))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ setShowRevisionModal(true)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
const load = async () => {
|
|
const load = async () => {
|
|
|
const [l, p, r, reg, props, ten, own] = await Promise.all([
|
|
const [l, p, r, reg, props, ten, own] = await Promise.all([
|
|
@@ -163,6 +195,9 @@ export default function LeaseDetail() {
|
|
|
charges_amount: String(lease.charges_amount),
|
|
charges_amount: String(lease.charges_amount),
|
|
|
deposit_amount: String(lease.deposit_amount),
|
|
deposit_amount: String(lease.deposit_amount),
|
|
|
payment_day: String(lease.payment_day),
|
|
payment_day: String(lease.payment_day),
|
|
|
|
|
+ irl_reference_value: lease.irl_reference_value ? String(lease.irl_reference_value) : '',
|
|
|
|
|
+ irl_reference_quarter: lease.irl_reference_quarter ? String(lease.irl_reference_quarter) : '',
|
|
|
|
|
+ irl_reference_year: lease.irl_reference_year ? String(lease.irl_reference_year) : '',
|
|
|
})
|
|
})
|
|
|
setShowEditLeaseModal(true)
|
|
setShowEditLeaseModal(true)
|
|
|
}
|
|
}
|
|
@@ -180,6 +215,9 @@ export default function LeaseDetail() {
|
|
|
deposit_amount: parseFloat(leaseForm.deposit_amount || 0),
|
|
deposit_amount: parseFloat(leaseForm.deposit_amount || 0),
|
|
|
payment_day: parseInt(leaseForm.payment_day || 1),
|
|
payment_day: parseInt(leaseForm.payment_day || 1),
|
|
|
active: lease.active,
|
|
active: lease.active,
|
|
|
|
|
+ irl_reference_value: leaseForm.irl_reference_value ? parseFloat(leaseForm.irl_reference_value) : null,
|
|
|
|
|
+ irl_reference_quarter: leaseForm.irl_reference_quarter ? parseInt(leaseForm.irl_reference_quarter) : null,
|
|
|
|
|
+ irl_reference_year: leaseForm.irl_reference_year ? parseInt(leaseForm.irl_reference_year) : null,
|
|
|
})
|
|
})
|
|
|
toast.success('Bail mis à jour !')
|
|
toast.success('Bail mis à jour !')
|
|
|
setShowEditLeaseModal(false)
|
|
setShowEditLeaseModal(false)
|
|
@@ -247,10 +285,7 @@ export default function LeaseDetail() {
|
|
|
</div>
|
|
</div>
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
|
{lease.active && (
|
|
{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</button>
|
|
|
|
|
|
|
+ <button onClick={openRevisionModal} className="btn-secondary text-sm">📈 Réviser</button>
|
|
|
)}
|
|
)}
|
|
|
<button onClick={openEditLease} className="btn-secondary text-sm">✏️ Modifier</button>
|
|
<button onClick={openEditLease} className="btn-secondary text-sm">✏️ Modifier</button>
|
|
|
<span className={lease.active ? 'badge-green' : 'badge-red'}>{lease.active ? 'Actif' : 'Clôturé'}</span>
|
|
<span className={lease.active ? 'badge-green' : 'badge-red'}>{lease.active ? 'Actif' : 'Clôturé'}</span>
|
|
@@ -286,6 +321,15 @@ export default function LeaseDetail() {
|
|
|
<p className="text-sm text-gray-500">Échéance</p>
|
|
<p className="text-sm text-gray-500">Échéance</p>
|
|
|
<p className="font-semibold text-gray-900">Le {lease.payment_day} du mois</p>
|
|
<p className="font-semibold text-gray-900">Le {lease.payment_day} du mois</p>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ {lease.irl_reference_value && (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-sm text-gray-500">IRL de référence</p>
|
|
|
|
|
+ <p className="font-semibold text-gray-900">
|
|
|
|
|
+ {lease.irl_reference_value}
|
|
|
|
|
+ <span className="text-xs text-gray-400 font-normal ml-1">(T{lease.irl_reference_quarter} {lease.irl_reference_year})</span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* Propriétaire bailleur */}
|
|
{/* Propriétaire bailleur */}
|
|
@@ -738,6 +782,41 @@ export default function LeaseDetail() {
|
|
|
Si la date est aujourd'hui ou passée, le loyer en vigueur sera mis à jour immédiatement.
|
|
Si la date est aujourd'hui ou passée, le loyer en vigueur sera mis à jour immédiatement.
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Bloc IRL */}
|
|
|
|
|
+ {lease.irl_reference_quarter ? (
|
|
|
|
|
+ <div className="bg-indigo-50 border border-indigo-100 rounded-xl p-4 space-y-2">
|
|
|
|
|
+ <p className="text-xs font-semibold text-indigo-700 uppercase tracking-wide">Indice IRL</p>
|
|
|
|
|
+ <div className="grid grid-cols-2 gap-3 text-sm">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-gray-500 text-xs">IRL de référence (T{lease.irl_reference_quarter} {lease.irl_reference_year})</p>
|
|
|
|
|
+ <p className="font-bold text-gray-800">{lease.irl_reference_value ?? '—'}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-gray-500 text-xs">IRL T{irlCurrent.quarter} {irlCurrent.year} (en cours)</p>
|
|
|
|
|
+ {irlCurrent.loading
|
|
|
|
|
+ ? <p className="text-indigo-500 text-xs">⏳ Récupération…</p>
|
|
|
|
|
+ : irlCurrent.value !== null
|
|
|
|
|
+ ? <p className="font-bold text-indigo-700">{irlCurrent.value}</p>
|
|
|
|
|
+ : <p className="text-gray-400 text-xs">{irlCurrent.configured === false ? 'INSEE non configuré' : 'Indice non disponible'}</p>}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {irlCurrent.value && lease.irl_reference_value && (
|
|
|
|
|
+ <p className="text-xs text-indigo-600 pt-1 border-t border-indigo-100">
|
|
|
|
|
+ Variation : <strong>+{((irlCurrent.value / lease.irl_reference_value - 1) * 100).toFixed(2)} %</strong>
|
|
|
|
|
+ {' '}— loyer calculé : <strong>{fmt(lease.rent_amount * irlCurrent.value / lease.irl_reference_value)}</strong>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {!irlCurrent.configured && (
|
|
|
|
|
+ <p className="text-xs text-gray-400">Configurez <code>INSEE_API_TOKEN</code> pour la récupération automatique.</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="bg-gray-50 border border-gray-200 rounded-xl p-3 text-xs text-gray-400">
|
|
|
|
|
+ 💡 Renseignez l'IRL de référence dans les informations du bail pour bénéficier du pré-calcul automatique.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
<div>
|
|
<div>
|
|
|
<label className="label">Nouveau loyer HC (€) *</label>
|
|
<label className="label">Nouveau loyer HC (€) *</label>
|
|
@@ -846,6 +925,52 @@ export default function LeaseDetail() {
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ {/* IRL de référence */}
|
|
|
|
|
+ <div className="border-t pt-4">
|
|
|
|
|
+ <p className="text-sm font-semibold text-gray-700 mb-3">📈 IRL de référence (pour révision annuelle)</p>
|
|
|
|
|
+ <div className="grid grid-cols-3 gap-3">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="label text-xs">Valeur IRL</label>
|
|
|
|
|
+ <div className="flex gap-1">
|
|
|
|
|
+ <input className="input" type="number" step="0.01" placeholder="Ex: 142.06"
|
|
|
|
|
+ value={leaseForm.irl_reference_value}
|
|
|
|
|
+ onChange={e => setLeaseForm(f => ({ ...f, irl_reference_value: e.target.value }))} />
|
|
|
|
|
+ {leaseForm.irl_reference_quarter && leaseForm.irl_reference_year && (
|
|
|
|
|
+ <button type="button" title="Récupérer depuis INSEE"
|
|
|
|
|
+ onClick={async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { data } = await api.get(`/irl?year=${leaseForm.irl_reference_year}&quarter=${leaseForm.irl_reference_quarter}`)
|
|
|
|
|
+ if (data.value) setLeaseForm(f => ({ ...f, irl_reference_value: String(data.value) }))
|
|
|
|
|
+ else toast.error(data.configured === false ? 'INSEE non configuré (INSEE_API_TOKEN)' : 'IRL indisponible pour cette période')
|
|
|
|
|
+ } catch { toast.error('Erreur récupération IRL') }
|
|
|
|
|
+ }}
|
|
|
|
|
+ className="px-2 py-1 bg-indigo-100 hover:bg-indigo-200 text-indigo-700 rounded-lg text-sm transition-colors whitespace-nowrap">
|
|
|
|
|
+ 🔄
|
|
|
|
|
+ </button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="label text-xs">Trimestre (1-4)</label>
|
|
|
|
|
+ <select className="input" value={leaseForm.irl_reference_quarter}
|
|
|
|
|
+ onChange={e => setLeaseForm(f => ({ ...f, irl_reference_quarter: e.target.value }))}>
|
|
|
|
|
+ <option value="">—</option>
|
|
|
|
|
+ <option value="1">T1 (jan-mars)</option>
|
|
|
|
|
+ <option value="2">T2 (avr-juin)</option>
|
|
|
|
|
+ <option value="3">T3 (juil-sep)</option>
|
|
|
|
|
+ <option value="4">T4 (oct-déc)</option>
|
|
|
|
|
+ </select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="label text-xs">Année</label>
|
|
|
|
|
+ <input className="input" type="number" min="2000" max="2100" placeholder={now.getFullYear()}
|
|
|
|
|
+ value={leaseForm.irl_reference_year}
|
|
|
|
|
+ onChange={e => setLeaseForm(f => ({ ...f, irl_reference_year: e.target.value }))} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p className="text-xs text-gray-400 mt-1">L'IRL du même trimestre de l'année en cours sera utilisé lors de la révision pour pré-calculer le nouveau loyer.</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<div className="flex gap-3 pt-2">
|
|
<div className="flex gap-3 pt-2">
|
|
|
<button type="button" onClick={() => setShowEditLeaseModal(false)} className="btn-secondary flex-1">Annuler</button>
|
|
<button type="button" onClick={() => setShowEditLeaseModal(false)} className="btn-secondary flex-1">Annuler</button>
|
|
|
<button type="submit" disabled={leaseEditLoading} className="btn-primary flex-1">
|
|
<button type="submit" disabled={leaseEditLoading} className="btn-primary flex-1">
|