Owners.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  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-end sm:items-center justify-center z-50 p-0 sm:p-4">
  7. <div className="bg-white rounded-t-2xl sm:rounded-2xl shadow-xl w-full sm:max-w-lg max-h-[92vh] flex flex-col">
  8. <div className="flex items-center justify-between p-4 sm:p-6 border-b shrink-0">
  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-4 sm:p-6 overflow-y-auto">{children}</div>
  13. </div>
  14. </div>
  15. )
  16. }
  17. const emptyForm = {
  18. type: 'physique',
  19. first_name: '', last_name: '',
  20. company_name: '', siret: '',
  21. address: '', zip_code: '', city: '',
  22. email: '', phone: '',
  23. }
  24. export function ownerLabel(o) {
  25. if (!o) return '—'
  26. return o.type === 'morale'
  27. ? o.company_name || '—'
  28. : `${o.first_name || ''} ${o.last_name || ''}`.trim() || '—'
  29. }
  30. export default function Owners() {
  31. const [owners, setOwners] = useState([])
  32. const [showModal, setShowModal] = useState(false)
  33. const [form, setForm] = useState(emptyForm)
  34. const [editing, setEditing] = useState(null)
  35. const [loading, setLoading] = useState(false)
  36. const load = () => api.get('/owners').then(r => setOwners(r.data))
  37. useEffect(() => { load() }, [])
  38. const openAdd = () => { setForm(emptyForm); setEditing(null); setShowModal(true) }
  39. const openEdit = o => {
  40. setForm({
  41. type: o.type || 'physique',
  42. first_name: o.first_name || '', last_name: o.last_name || '',
  43. company_name: o.company_name || '', siret: o.siret || '',
  44. address: o.address || '', zip_code: o.zip_code || '', city: o.city || '',
  45. email: o.email || '', phone: o.phone || '',
  46. })
  47. setEditing(o.id); setShowModal(true)
  48. }
  49. const handleSubmit = async e => {
  50. e.preventDefault(); setLoading(true)
  51. try {
  52. if (editing) await api.put(`/owners/${editing}`, form)
  53. else await api.post('/owners', form)
  54. toast.success(editing ? 'Propriétaire modifié !' : 'Propriétaire ajouté !')
  55. setShowModal(false); load()
  56. } catch (err) { toast.error(err.response?.data?.error || 'Erreur') }
  57. finally { setLoading(false) }
  58. }
  59. const handleDelete = async id => {
  60. if (!confirm('Supprimer ce propriétaire ?')) return
  61. try { await api.delete(`/owners/${id}`); toast.success('Propriétaire supprimé'); load() }
  62. catch (err) { toast.error(err.response?.data?.error || 'Erreur') }
  63. }
  64. const set = field => e => setForm(f => ({ ...f, [field]: e.target.value }))
  65. return (
  66. <div>
  67. <div className="flex items-center justify-between mb-8">
  68. <div>
  69. <h1 className="text-2xl font-bold text-gray-900">🏢 Propriétaires</h1>
  70. <p className="text-gray-500 mt-1">{owners.length} propriétaire(s) enregistré(s)</p>
  71. </div>
  72. <button onClick={openAdd} className="btn-primary">+ Ajouter un propriétaire</button>
  73. </div>
  74. {owners.length === 0 ? (
  75. <div className="card text-center py-16">
  76. <div className="text-5xl mb-4">🏢</div>
  77. <p className="text-gray-500">Aucun propriétaire enregistré.</p>
  78. </div>
  79. ) : (
  80. <div>
  81. {/* Desktop table */}
  82. <div className="card overflow-hidden p-0 hidden md:block">
  83. <table className="w-full">
  84. <thead className="bg-gray-50 border-b">
  85. <tr>
  86. <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Identité</th>
  87. <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Type</th>
  88. <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Email</th>
  89. <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Téléphone</th>
  90. <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Adresse</th>
  91. <th className="px-6 py-3"></th>
  92. </tr>
  93. </thead>
  94. <tbody className="divide-y divide-gray-50">
  95. {owners.map(o => (
  96. <tr key={o.id} className="hover:bg-gray-50">
  97. <td className="px-6 py-4 font-medium text-gray-900">
  98. {ownerLabel(o)}
  99. {o.siret && <span className="ml-2 text-xs text-gray-400">SIRET {o.siret}</span>}
  100. </td>
  101. <td className="px-6 py-4">
  102. <span className={`text-xs px-2 py-0.5 rounded-full font-medium ${o.type === 'morale' ? 'bg-purple-100 text-purple-700' : 'bg-blue-100 text-blue-700'}`}>
  103. {o.type === 'morale' ? 'Personne morale' : 'Personne physique'}
  104. </span>
  105. </td>
  106. <td className="px-6 py-4 text-gray-600">{o.email || '—'}</td>
  107. <td className="px-6 py-4 text-gray-600">{o.phone || '—'}</td>
  108. <td className="px-6 py-4 text-gray-600 text-sm">
  109. {o.address ? `${o.address}${o.zip_code ? ', ' + o.zip_code : ''}${o.city ? ' ' + o.city : ''}` : '—'}
  110. </td>
  111. <td className="px-6 py-4 text-right space-x-3">
  112. <button onClick={() => openEdit(o)} className="text-gray-400 hover:text-blue-600">✏️</button>
  113. <button onClick={() => handleDelete(o.id)} className="text-gray-400 hover:text-red-600">🗑️</button>
  114. </td>
  115. </tr>
  116. ))}
  117. </tbody>
  118. </table>
  119. </div>
  120. {/* Mobile cards */}
  121. <div className="md:hidden space-y-3">
  122. {owners.map(o => (
  123. <div key={o.id} className="card">
  124. <div className="flex items-start justify-between">
  125. <div className="flex-1 min-w-0">
  126. <div className="flex items-center gap-2 flex-wrap">
  127. <p className="font-semibold text-gray-900">{ownerLabel(o)}</p>
  128. <span className={`text-xs px-2 py-0.5 rounded-full font-medium ${o.type === 'morale' ? 'bg-purple-100 text-purple-700' : 'bg-blue-100 text-blue-700'}`}>
  129. {o.type === 'morale' ? 'Morale' : 'Physique'}
  130. </span>
  131. </div>
  132. {o.siret && <p className="text-xs text-gray-400 mt-0.5">SIRET {o.siret}</p>}
  133. {o.email && <p className="text-sm text-gray-500 mt-1">✉️ {o.email}</p>}
  134. {o.phone && <p className="text-sm text-gray-500">📞 {o.phone}</p>}
  135. {o.address && <p className="text-sm text-gray-400">{o.address}{o.zip_code ? `, ${o.zip_code}` : ''}{o.city ? ` ${o.city}` : ''}</p>}
  136. </div>
  137. <div className="flex gap-3 ml-3">
  138. <button onClick={() => openEdit(o)} className="text-gray-400 hover:text-blue-600 text-lg">✏️</button>
  139. <button onClick={() => handleDelete(o.id)} className="text-gray-400 hover:text-red-600 text-lg">🗑️</button>
  140. </div>
  141. </div>
  142. </div>
  143. ))}
  144. </div>
  145. </div>
  146. )}
  147. {showModal && (
  148. <Modal title={editing ? 'Modifier le propriétaire' : 'Ajouter un propriétaire'} onClose={() => setShowModal(false)}>
  149. <form onSubmit={handleSubmit} className="space-y-4">
  150. {/* Type */}
  151. <div className="flex gap-3">
  152. {['physique', 'morale'].map(t => (
  153. <label key={t} className={`flex-1 flex items-center justify-center gap-2 p-3 rounded-xl border-2 cursor-pointer transition-colors ${form.type === t ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-gray-200 text-gray-600 hover:border-gray-300'}`}>
  154. <input type="radio" className="hidden" value={t} checked={form.type === t}
  155. onChange={() => setForm(f => ({ ...f, type: t }))} />
  156. <span className="text-lg">{t === 'physique' ? '👤' : '🏢'}</span>
  157. <span className="text-sm font-medium">{t === 'physique' ? 'Personne physique' : 'Personne morale'}</span>
  158. </label>
  159. ))}
  160. </div>
  161. {/* Identité selon type */}
  162. {form.type === 'physique' ? (
  163. <div className="grid grid-cols-2 gap-4">
  164. <div>
  165. <label className="label">Prénom *</label>
  166. <input className="input" required value={form.first_name} onChange={set('first_name')} />
  167. </div>
  168. <div>
  169. <label className="label">Nom *</label>
  170. <input className="input" required value={form.last_name} onChange={set('last_name')} />
  171. </div>
  172. </div>
  173. ) : (
  174. <div className="grid grid-cols-2 gap-4">
  175. <div className="col-span-2">
  176. <label className="label">Raison sociale *</label>
  177. <input className="input" required value={form.company_name} onChange={set('company_name')} />
  178. </div>
  179. <div className="col-span-2">
  180. <label className="label">SIRET</label>
  181. <input className="input" value={form.siret} onChange={set('siret')} placeholder="XXX XXX XXX XXXXX" />
  182. </div>
  183. </div>
  184. )}
  185. {/* Coordonnées communes */}
  186. <div className="grid grid-cols-2 gap-4">
  187. <div className="col-span-2">
  188. <label className="label">Adresse</label>
  189. <input className="input" value={form.address} onChange={set('address')} />
  190. </div>
  191. <div>
  192. <label className="label">Code postal</label>
  193. <input className="input" value={form.zip_code} onChange={set('zip_code')} />
  194. </div>
  195. <div>
  196. <label className="label">Ville</label>
  197. <input className="input" value={form.city} onChange={set('city')} />
  198. </div>
  199. <div>
  200. <label className="label">Email</label>
  201. <input className="input" type="email" value={form.email} onChange={set('email')} />
  202. </div>
  203. <div>
  204. <label className="label">Téléphone</label>
  205. <input className="input" value={form.phone} onChange={set('phone')} />
  206. </div>
  207. </div>
  208. <div className="flex gap-3 pt-2">
  209. <button type="button" onClick={() => setShowModal(false)} className="btn-secondary flex-1">Annuler</button>
  210. <button type="submit" disabled={loading} className="btn-primary flex-1">{loading ? '...' : 'Enregistrer'}</button>
  211. </div>
  212. </form>
  213. </Modal>
  214. )}
  215. </div>
  216. )
  217. }