Tenants.jsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import { useEffect, useState } from 'react'
  2. import api from '../api'
  3. import toast from 'react-hot-toast'
  4. function Modal({ title, onClose, children }) {
  5. return (
  6. <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
  7. <div className="bg-white rounded-2xl shadow-xl w-full max-w-lg">
  8. <div className="flex items-center justify-between p-6 border-b">
  9. <h2 className="text-lg font-semibold">{title}</h2>
  10. <button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-2xl leading-none">&times;</button>
  11. </div>
  12. <div className="p-6">{children}</div>
  13. </div>
  14. </div>
  15. )
  16. }
  17. const emptyForm = { first_name: '', last_name: '', email: '', phone: '' }
  18. export default function Tenants() {
  19. const [tenants, setTenants] = useState([])
  20. const [showModal, setShowModal] = useState(false)
  21. const [form, setForm] = useState(emptyForm)
  22. const [editing, setEditing] = useState(null)
  23. const [loading, setLoading] = useState(false)
  24. const load = () => api.get('/tenants').then(r => setTenants(r.data))
  25. useEffect(() => { load() }, [])
  26. const openAdd = () => { setForm(emptyForm); setEditing(null); setShowModal(true) }
  27. const openEdit = t => {
  28. setForm({ first_name: t.first_name, last_name: t.last_name, email: t.email || '', phone: t.phone || '' })
  29. setEditing(t.id); setShowModal(true)
  30. }
  31. const handleSubmit = async e => {
  32. e.preventDefault(); setLoading(true)
  33. try {
  34. if (editing) await api.put(`/tenants/${editing}`, form)
  35. else await api.post('/tenants', form)
  36. toast.success(editing ? 'Locataire modifié !' : 'Locataire ajouté !')
  37. setShowModal(false); load()
  38. } catch (err) { toast.error(err.response?.data?.error || 'Erreur') }
  39. finally { setLoading(false) }
  40. }
  41. const handleDelete = async id => {
  42. if (!confirm('Supprimer ce locataire ?')) return
  43. try { await api.delete(`/tenants/${id}`); toast.success('Locataire supprimé'); load() }
  44. catch (err) { toast.error(err.response?.data?.error || 'Erreur') }
  45. }
  46. return (
  47. <div>
  48. <div className="flex items-center justify-between mb-8">
  49. <div>
  50. <h1 className="text-2xl font-bold text-gray-900">👤 Locataires</h1>
  51. <p className="text-gray-500 mt-1">{tenants.length} locataire(s) enregistré(s)</p>
  52. </div>
  53. <button onClick={openAdd} className="btn-primary">+ Ajouter un locataire</button>
  54. </div>
  55. {tenants.length === 0 ? (
  56. <div className="card text-center py-16">
  57. <div className="text-5xl mb-4">👤</div>
  58. <p className="text-gray-500">Aucun locataire enregistré.</p>
  59. </div>
  60. ) : (
  61. <div className="card overflow-hidden p-0">
  62. <table className="w-full">
  63. <thead className="bg-gray-50 border-b">
  64. <tr>
  65. <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Nom</th>
  66. <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Email</th>
  67. <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Téléphone</th>
  68. <th className="px-6 py-3"></th>
  69. </tr>
  70. </thead>
  71. <tbody className="divide-y divide-gray-50">
  72. {tenants.map(t => (
  73. <tr key={t.id} className="hover:bg-gray-50">
  74. <td className="px-6 py-4 font-medium text-gray-900">{t.first_name} {t.last_name}</td>
  75. <td className="px-6 py-4 text-gray-600">{t.email || '—'}</td>
  76. <td className="px-6 py-4 text-gray-600">{t.phone || '—'}</td>
  77. <td className="px-6 py-4 text-right space-x-3">
  78. <button onClick={() => openEdit(t)} className="text-gray-400 hover:text-blue-600">✏️</button>
  79. <button onClick={() => handleDelete(t.id)} className="text-gray-400 hover:text-red-600">🗑️</button>
  80. </td>
  81. </tr>
  82. ))}
  83. </tbody>
  84. </table>
  85. </div>
  86. )}
  87. {showModal && (
  88. <Modal title={editing ? 'Modifier le locataire' : 'Ajouter un locataire'} onClose={() => setShowModal(false)}>
  89. <form onSubmit={handleSubmit} className="space-y-4">
  90. <div className="grid grid-cols-2 gap-4">
  91. <div>
  92. <label className="label">Prénom *</label>
  93. <input className="input" required value={form.first_name} onChange={e => setForm(f => ({ ...f, first_name: e.target.value }))} />
  94. </div>
  95. <div>
  96. <label className="label">Nom *</label>
  97. <input className="input" required value={form.last_name} onChange={e => setForm(f => ({ ...f, last_name: e.target.value }))} />
  98. </div>
  99. <div className="col-span-2">
  100. <label className="label">Email</label>
  101. <input className="input" type="email" value={form.email} onChange={e => setForm(f => ({ ...f, email: e.target.value }))} />
  102. </div>
  103. <div className="col-span-2">
  104. <label className="label">Téléphone</label>
  105. <input className="input" value={form.phone} onChange={e => setForm(f => ({ ...f, phone: e.target.value }))} />
  106. </div>
  107. </div>
  108. <div className="flex gap-3 pt-2">
  109. <button type="button" onClick={() => setShowModal(false)} className="btn-secondary flex-1">Annuler</button>
  110. <button type="submit" disabled={loading} className="btn-primary flex-1">{loading ? '...' : 'Enregistrer'}</button>
  111. </div>
  112. </form>
  113. </Modal>
  114. )}
  115. </div>
  116. )
  117. }