Leases.jsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import { useEffect, useState } from 'react'
  2. import { Link } from 'react-router-dom'
  3. import api from '../api'
  4. import toast from 'react-hot-toast'
  5. function Modal({ title, onClose, children }) {
  6. return (
  7. <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 overflow-y-auto">
  8. <div className="bg-white rounded-2xl shadow-xl w-full max-w-lg my-4">
  9. <div className="flex items-center justify-between p-6 border-b">
  10. <h2 className="text-lg font-semibold">{title}</h2>
  11. <button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-2xl leading-none">&times;</button>
  12. </div>
  13. <div className="p-6">{children}</div>
  14. </div>
  15. </div>
  16. )
  17. }
  18. const emptyForm = { property_id: '', tenant_id: '', start_date: '', end_date: '', rent_amount: '', charges_amount: '', deposit_amount: '', payment_day: '1' }
  19. export default function Leases() {
  20. const [leases, setLeases] = useState([])
  21. const [properties, setProperties] = useState([])
  22. const [tenants, setTenants] = useState([])
  23. const [showModal, setShowModal] = useState(false)
  24. const [form, setForm] = useState(emptyForm)
  25. const [loading, setLoading] = useState(false)
  26. const load = () => {
  27. api.get('/leases').then(r => setLeases(r.data))
  28. api.get('/properties').then(r => setProperties(r.data))
  29. api.get('/tenants').then(r => setTenants(r.data))
  30. }
  31. useEffect(() => { load() }, [])
  32. const handleSubmit = async e => {
  33. e.preventDefault(); setLoading(true)
  34. try {
  35. await api.post('/leases', { ...form, property_id: Number(form.property_id), tenant_id: Number(form.tenant_id) })
  36. toast.success('Bail créé !')
  37. setShowModal(false); load()
  38. } catch (err) { toast.error(err.response?.data?.error || 'Erreur') }
  39. finally { setLoading(false) }
  40. }
  41. const handleToggle = async lease => {
  42. try {
  43. await api.put(`/leases/${lease.id}`, {
  44. end_date: lease.end_date, rent_amount: lease.rent_amount, charges_amount: lease.charges_amount,
  45. deposit_amount: lease.deposit_amount, payment_day: lease.payment_day, active: lease.active ? 0 : 1
  46. })
  47. toast.success(lease.active ? 'Bail clôturé' : 'Bail réactivé'); load()
  48. } catch { toast.error('Erreur') }
  49. }
  50. const fmt = v => `${Number(v).toFixed(2)} €`
  51. const fmtDate = d => d ? new Date(d).toLocaleDateString('fr-FR') : '—'
  52. return (
  53. <div>
  54. <div className="flex items-center justify-between mb-8">
  55. <div>
  56. <h1 className="text-2xl font-bold text-gray-900">📋 Baux</h1>
  57. <p className="text-gray-500 mt-1">{leases.filter(l => l.active).length} bail(s) actif(s)</p>
  58. </div>
  59. <button onClick={() => { setForm(emptyForm); setShowModal(true) }} className="btn-primary">+ Nouveau bail</button>
  60. </div>
  61. {leases.length === 0 ? (
  62. <div className="card text-center py-16">
  63. <div className="text-5xl mb-4">📋</div>
  64. <p className="text-gray-500">Aucun bail. Commencez par ajouter vos biens et locataires.</p>
  65. </div>
  66. ) : (
  67. <div className="space-y-3">
  68. {leases.map(l => (
  69. <div key={l.id} className={`card hover:shadow-md transition-shadow ${!l.active ? 'opacity-60' : ''}`}>
  70. <div className="flex items-center justify-between">
  71. <div className="flex items-center gap-4">
  72. <div>
  73. <div className="flex items-center gap-2">
  74. <h3 className="font-semibold text-gray-900">{l.tenant_first_name} {l.tenant_last_name}</h3>
  75. <span className={l.active ? 'badge-green' : 'badge-red'}>{l.active ? 'Actif' : 'Clôturé'}</span>
  76. </div>
  77. <p className="text-sm text-gray-500">{l.property_name} · {l.property_address}, {l.property_city}</p>
  78. <p className="text-sm text-gray-400">Du {fmtDate(l.start_date)}{l.end_date ? ` au ${fmtDate(l.end_date)}` : ''}</p>
  79. </div>
  80. </div>
  81. <div className="flex items-center gap-6">
  82. <div className="text-right">
  83. <p className="font-semibold text-gray-900">{fmt(l.rent_amount + l.charges_amount)}/mois</p>
  84. <p className="text-xs text-gray-400">Loyer {fmt(l.rent_amount)} + Charges {fmt(l.charges_amount)}</p>
  85. </div>
  86. <div className="flex gap-2">
  87. <Link to={`/leases/${l.id}`} className="btn-secondary text-sm px-3 py-1.5">Détail</Link>
  88. <button onClick={() => handleToggle(l)} className={`text-sm px-3 py-1.5 rounded-lg transition-colors ${l.active ? 'bg-red-50 text-red-600 hover:bg-red-100' : 'bg-green-50 text-green-600 hover:bg-green-100'}`}>
  89. {l.active ? 'Clôturer' : 'Réactiver'}
  90. </button>
  91. </div>
  92. </div>
  93. </div>
  94. </div>
  95. ))}
  96. </div>
  97. )}
  98. {showModal && (
  99. <Modal title="Nouveau bail" onClose={() => setShowModal(false)}>
  100. <form onSubmit={handleSubmit} className="space-y-4">
  101. <div>
  102. <label className="label">Bien *</label>
  103. <select className="input" required value={form.property_id} onChange={e => setForm(f => ({ ...f, property_id: e.target.value }))}>
  104. <option value="">Sélectionner un bien</option>
  105. {properties.map(p => <option key={p.id} value={p.id}>{p.name} — {p.city}</option>)}
  106. </select>
  107. </div>
  108. <div>
  109. <label className="label">Locataire *</label>
  110. <select className="input" required value={form.tenant_id} onChange={e => setForm(f => ({ ...f, tenant_id: e.target.value }))}>
  111. <option value="">Sélectionner un locataire</option>
  112. {tenants.map(t => <option key={t.id} value={t.id}>{t.first_name} {t.last_name}</option>)}
  113. </select>
  114. </div>
  115. <div className="grid grid-cols-2 gap-4">
  116. <div>
  117. <label className="label">Date de début *</label>
  118. <input className="input" type="date" required value={form.start_date} onChange={e => setForm(f => ({ ...f, start_date: e.target.value }))} />
  119. </div>
  120. <div>
  121. <label className="label">Date de fin</label>
  122. <input className="input" type="date" value={form.end_date} onChange={e => setForm(f => ({ ...f, end_date: e.target.value }))} />
  123. </div>
  124. <div>
  125. <label className="label">Loyer HC (€) *</label>
  126. <input className="input" type="number" step="0.01" min="0" required value={form.rent_amount} onChange={e => setForm(f => ({ ...f, rent_amount: e.target.value }))} placeholder="800" />
  127. </div>
  128. <div>
  129. <label className="label">Charges (€)</label>
  130. <input className="input" type="number" step="0.01" min="0" value={form.charges_amount} onChange={e => setForm(f => ({ ...f, charges_amount: e.target.value }))} placeholder="80" />
  131. </div>
  132. <div>
  133. <label className="label">Dépôt de garantie (€)</label>
  134. <input className="input" type="number" step="0.01" min="0" value={form.deposit_amount} onChange={e => setForm(f => ({ ...f, deposit_amount: e.target.value }))} placeholder="1600" />
  135. </div>
  136. <div>
  137. <label className="label">Jour de paiement</label>
  138. <input className="input" type="number" min="1" max="28" value={form.payment_day} onChange={e => setForm(f => ({ ...f, payment_day: e.target.value }))} placeholder="1" />
  139. </div>
  140. </div>
  141. <div className="flex gap-3 pt-2">
  142. <button type="button" onClick={() => setShowModal(false)} className="btn-secondary flex-1">Annuler</button>
  143. <button type="submit" disabled={loading} className="btn-primary flex-1">{loading ? '...' : 'Créer le bail'}</button>
  144. </div>
  145. </form>
  146. </Modal>
  147. )}
  148. </div>
  149. )
  150. }