|
|
@@ -3,7 +3,21 @@ import api from '../api'
|
|
|
import toast from 'react-hot-toast'
|
|
|
import { useAuth } from '../context/AuthContext'
|
|
|
|
|
|
-export default function Profile() {
|
|
|
+function Modal({ title, onClose, children }) {
|
|
|
+ return (
|
|
|
+ <div className="fixed inset-0 bg-black/50 flex items-end sm:items-center justify-center z-50 p-0 sm:p-4">
|
|
|
+ <div className="bg-white rounded-t-2xl sm:rounded-2xl shadow-xl w-full sm:max-w-lg max-h-[92vh] flex flex-col">
|
|
|
+ <div className="flex items-center justify-between p-4 sm:p-6 border-b shrink-0">
|
|
|
+ <h2 className="text-lg font-semibold">{title}</h2>
|
|
|
+ <button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-2xl leading-none">×</button>
|
|
|
+ </div>
|
|
|
+ <div className="p-4 sm:p-6 overflow-y-auto">{children}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function ProfileTab() {
|
|
|
const { user, setUser } = useAuth()
|
|
|
const [form, setForm] = useState({ first_name: '', last_name: '', email: '', address: '', phone: '', name: '' })
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
@@ -49,34 +63,167 @@ export default function Profile() {
|
|
|
)
|
|
|
|
|
|
return (
|
|
|
- <div className="max-w-xl">
|
|
|
- <div className="mb-6">
|
|
|
- <h1 className="text-2xl font-bold text-gray-900">👤 Mon profil bailleur</h1>
|
|
|
- <p className="text-gray-500 text-sm mt-1">Ces informations apparaîtront dans vos quittances et bilans de charges</p>
|
|
|
+ <div className="card">
|
|
|
+ <form onSubmit={handleSubmit} className="space-y-4">
|
|
|
+ <div className="grid grid-cols-2 gap-4">
|
|
|
+ {field('Prénom', 'first_name', 'text', 'Jean')}
|
|
|
+ {field('Nom', 'last_name', 'text', 'Dupont')}
|
|
|
+ </div>
|
|
|
+ {field('Adresse postale', 'address', 'text', '12 rue de la Paix, 75001 Paris')}
|
|
|
+ {field('Adresse e-mail *', 'email', 'email', 'vous@exemple.fr')}
|
|
|
+ {field('Numéro de téléphone', 'phone', 'tel', '06 00 00 00 00')}
|
|
|
+
|
|
|
+ <div className="pt-2 border-t border-gray-100">
|
|
|
+ <p className="text-xs text-gray-400 mb-3">
|
|
|
+ Le champ "Nom d'affichage" est utilisé si prénom/nom ne sont pas renseignés.
|
|
|
+ </p>
|
|
|
+ {field("Nom d'affichage *", 'name', 'text', 'Jean Dupont')}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button type="submit" disabled={loading} className="btn-primary w-full">
|
|
|
+ {loading ? '...' : '💾 Enregistrer le profil'}
|
|
|
+ </button>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function CategoriesTab() {
|
|
|
+ const [categories, setCategories] = useState([])
|
|
|
+ const [showModal, setShowModal] = useState(false)
|
|
|
+ const [editCat, setEditCat] = useState(null)
|
|
|
+ const [form, setForm] = useState({ value: '', label: '', icon: '📦' })
|
|
|
+
|
|
|
+ const load = () => api.get('/categories').then(r => setCategories(r.data))
|
|
|
+ useEffect(() => { load() }, [])
|
|
|
+
|
|
|
+ const openAdd = () => { setForm({ value: '', label: '', icon: '📦' }); setEditCat(null); setShowModal(true) }
|
|
|
+ const openEdit = (cat) => { setForm({ value: cat.value, label: cat.label, icon: cat.icon }); setEditCat(cat); setShowModal(true) }
|
|
|
+
|
|
|
+ const handleSubmit = async (e) => {
|
|
|
+ e.preventDefault()
|
|
|
+ try {
|
|
|
+ if (editCat) {
|
|
|
+ await api.put(`/categories/${editCat.id}`, { label: form.label, icon: form.icon })
|
|
|
+ toast.success('Catégorie modifiée')
|
|
|
+ } else {
|
|
|
+ await api.post('/categories', form)
|
|
|
+ toast.success('Catégorie ajoutée')
|
|
|
+ }
|
|
|
+ setShowModal(false)
|
|
|
+ load()
|
|
|
+ } catch (err) {
|
|
|
+ toast.error(err.response?.data?.error || 'Erreur')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleDelete = async (cat) => {
|
|
|
+ if (!confirm(`Supprimer la catégorie "${cat.label}" ?`)) return
|
|
|
+ try {
|
|
|
+ await api.delete(`/categories/${cat.id}`)
|
|
|
+ toast.success('Catégorie supprimée')
|
|
|
+ load()
|
|
|
+ } catch (err) {
|
|
|
+ toast.error(err.response?.data?.error || 'Erreur')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const move = async (index, direction) => {
|
|
|
+ const newCats = [...categories]
|
|
|
+ const swapIdx = index + direction
|
|
|
+ if (swapIdx < 0 || swapIdx >= newCats.length) return
|
|
|
+ ;[newCats[index], newCats[swapIdx]] = [newCats[swapIdx], newCats[index]]
|
|
|
+ setCategories(newCats)
|
|
|
+ try {
|
|
|
+ await api.put('/categories/reorder', { order: newCats.map(c => c.id) })
|
|
|
+ } catch {
|
|
|
+ load()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div>
|
|
|
+ <div className="flex items-center justify-between mb-4">
|
|
|
+ <p className="text-sm text-gray-500">Gérez les catégories utilisées pour classer les charges</p>
|
|
|
+ <button onClick={openAdd} className="btn-primary text-sm">+ Ajouter</button>
|
|
|
</div>
|
|
|
|
|
|
- <div className="card">
|
|
|
- <form onSubmit={handleSubmit} className="space-y-4">
|
|
|
- <div className="grid grid-cols-2 gap-4">
|
|
|
- {field('Prénom', 'first_name', 'text', 'Jean')}
|
|
|
- {field('Nom', 'last_name', 'text', 'Dupont')}
|
|
|
- </div>
|
|
|
- {field('Adresse postale', 'address', 'text', '12 rue de la Paix, 75001 Paris')}
|
|
|
- {field('Adresse e-mail *', 'email', 'email', 'vous@exemple.fr')}
|
|
|
- {field('Numéro de téléphone', 'phone', 'tel', '06 00 00 00 00')}
|
|
|
-
|
|
|
- <div className="pt-2 border-t border-gray-100">
|
|
|
- <p className="text-xs text-gray-400 mb-3">
|
|
|
- Le champ "Nom d'affichage" est utilisé si prénom/nom ne sont pas renseignés.
|
|
|
- </p>
|
|
|
- {field("Nom d'affichage *", 'name', 'text', 'Jean Dupont')}
|
|
|
+ <div className="card divide-y">
|
|
|
+ {categories.length === 0 ? (
|
|
|
+ <p className="text-gray-400 text-center py-8">Aucune catégorie</p>
|
|
|
+ ) : categories.map((cat, i) => (
|
|
|
+ <div key={cat.id} className="flex items-center gap-3 py-3 px-1">
|
|
|
+ <div className="flex flex-col gap-0.5">
|
|
|
+ <button onClick={() => move(i, -1)} disabled={i === 0} className="text-gray-300 hover:text-gray-600 text-xs disabled:opacity-30">▲</button>
|
|
|
+ <button onClick={() => move(i, 1)} disabled={i === categories.length - 1} className="text-gray-300 hover:text-gray-600 text-xs disabled:opacity-30">▼</button>
|
|
|
+ </div>
|
|
|
+ <span className="text-lg">{cat.icon}</span>
|
|
|
+ <div className="flex-1 min-w-0">
|
|
|
+ <p className="font-medium text-gray-800 text-sm">{cat.label}</p>
|
|
|
+ <p className="text-xs text-gray-400">{cat.value}</p>
|
|
|
+ </div>
|
|
|
+ <button onClick={() => openEdit(cat)} className="text-gray-400 hover:text-blue-600 text-sm">✏️</button>
|
|
|
+ <button onClick={() => handleDelete(cat)} className="text-gray-400 hover:text-red-600 text-sm">🗑️</button>
|
|
|
</div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
|
|
|
- <button type="submit" disabled={loading} className="btn-primary w-full">
|
|
|
- {loading ? '...' : '💾 Enregistrer le profil'}
|
|
|
+ {showModal && (
|
|
|
+ <Modal title={editCat ? 'Modifier la catégorie' : 'Ajouter une catégorie'} onClose={() => setShowModal(false)}>
|
|
|
+ <form onSubmit={handleSubmit} className="space-y-4">
|
|
|
+ <div>
|
|
|
+ <label className="label">Identifiant technique *</label>
|
|
|
+ <input className="input" required value={form.value} disabled={!!editCat}
|
|
|
+ onChange={e => setForm(f => ({ ...f, value: e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '_') }))}
|
|
|
+ placeholder="ex: taxe_fonciere" />
|
|
|
+ {editCat && <p className="text-xs text-gray-400 mt-1">L'identifiant ne peut pas être modifié</p>}
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label className="label">Libellé *</label>
|
|
|
+ <input className="input" required value={form.label} onChange={e => setForm(f => ({ ...f, label: e.target.value }))} placeholder="ex: Taxe foncière" />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label className="label">Icône (emoji)</label>
|
|
|
+ <input className="input" value={form.icon} onChange={e => setForm(f => ({ ...f, icon: e.target.value }))} placeholder="📦" />
|
|
|
+ </div>
|
|
|
+ <div className="flex gap-3 pt-2">
|
|
|
+ <button type="button" onClick={() => setShowModal(false)} className="btn-secondary flex-1">Annuler</button>
|
|
|
+ <button type="submit" className="btn-primary flex-1">Enregistrer</button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </Modal>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+const TABS = [
|
|
|
+ { id: 'profile', label: '👤 Profil' },
|
|
|
+ { id: 'categories', label: '🏷️ Catégories de charges' },
|
|
|
+]
|
|
|
+
|
|
|
+export default function Profile() {
|
|
|
+ const [tab, setTab] = useState('profile')
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="max-w-2xl">
|
|
|
+ <div className="mb-6">
|
|
|
+ <h1 className="text-2xl font-bold text-gray-900">⚙️ Paramètres</h1>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex gap-1 mb-6 border-b">
|
|
|
+ {TABS.map(t => (
|
|
|
+ <button key={t.id} onClick={() => setTab(t.id)}
|
|
|
+ className={`px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px ${
|
|
|
+ tab === t.id ? 'border-blue-600 text-blue-700' : 'border-transparent text-gray-500 hover:text-gray-700'
|
|
|
+ }`}>
|
|
|
+ {t.label}
|
|
|
</button>
|
|
|
- </form>
|
|
|
+ ))}
|
|
|
</div>
|
|
|
+
|
|
|
+ {tab === 'profile' && <ProfileTab />}
|
|
|
+ {tab === 'categories' && <CategoriesTab />}
|
|
|
</div>
|
|
|
)
|
|
|
}
|