jeremy vor 4 Monaten
Ursprung
Commit
84d4d94b2d

+ 23 - 0
backend/src/db.js

@@ -92,6 +92,29 @@ db.exec(`
   );
 `);
 
+// Owners (bailleur personne physique ou morale)
+db.exec(`
+  CREATE TABLE IF NOT EXISTS owners (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    user_id INTEGER NOT NULL,
+    type TEXT NOT NULL DEFAULT 'physique',
+    first_name TEXT,
+    last_name TEXT,
+    company_name TEXT,
+    siret TEXT,
+    address TEXT,
+    zip_code TEXT,
+    city TEXT,
+    email TEXT,
+    phone TEXT,
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    FOREIGN KEY (user_id) REFERENCES users(id)
+  )
+`);
+
+// Add owner_id to leases if missing
+try { db.exec('ALTER TABLE leases ADD COLUMN owner_id INTEGER REFERENCES owners(id)'); } catch {}
+
 // Add profile columns to users if they don't exist yet
 ['first_name', 'last_name', 'address', 'phone'].forEach(col => {
   try { db.exec(`ALTER TABLE users ADD COLUMN ${col} TEXT`); } catch {}

+ 26 - 10
backend/src/routes/leases.js

@@ -7,12 +7,17 @@ router.use(authMiddleware);
 
 router.get('/', (req, res) => {
   const rows = db.prepare(`
-    SELECT l.*, 
+    SELECT l.*,
       p.name as property_name, p.address as property_address, p.city as property_city, p.zip_code as property_zip,
-      t.first_name as tenant_first_name, t.last_name as tenant_last_name, t.email as tenant_email
+      t.first_name as tenant_first_name, t.last_name as tenant_last_name, t.email as tenant_email,
+      o.type as owner_type, o.first_name as owner_first_name, o.last_name as owner_last_name,
+      o.company_name as owner_company_name, o.siret as owner_siret,
+      o.address as owner_address, o.zip_code as owner_zip, o.city as owner_city,
+      o.email as owner_email, o.phone as owner_phone
     FROM leases l
     JOIN properties p ON l.property_id = p.id
     JOIN tenants t ON l.tenant_id = t.id
+    LEFT JOIN owners o ON l.owner_id = o.id
     WHERE l.user_id = ?
     ORDER BY l.start_date DESC
   `).all(req.userId);
@@ -23,10 +28,15 @@ router.get('/:id', (req, res) => {
   const row = db.prepare(`
     SELECT l.*,
       p.name as property_name, p.address as property_address, p.city as property_city, p.zip_code as property_zip,
-      t.first_name as tenant_first_name, t.last_name as tenant_last_name, t.email as tenant_email
+      t.first_name as tenant_first_name, t.last_name as tenant_last_name, t.email as tenant_email,
+      o.type as owner_type, o.first_name as owner_first_name, o.last_name as owner_last_name,
+      o.company_name as owner_company_name, o.siret as owner_siret,
+      o.address as owner_address, o.zip_code as owner_zip, o.city as owner_city,
+      o.email as owner_email, o.phone as owner_phone
     FROM leases l
     JOIN properties p ON l.property_id = p.id
     JOIN tenants t ON l.tenant_id = t.id
+    LEFT JOIN owners o ON l.owner_id = o.id
     WHERE l.id = ? AND l.user_id = ?
   `).get(req.params.id, req.userId);
   if (!row) return res.status(404).json({ error: 'Bail non trouvé' });
@@ -34,7 +44,7 @@ router.get('/:id', (req, res) => {
 });
 
 router.post('/', (req, res) => {
-  const { property_id, tenant_id, start_date, end_date, rent_amount, charges_amount, deposit_amount, payment_day } = req.body;
+  const { property_id, tenant_id, owner_id, start_date, end_date, rent_amount, charges_amount, deposit_amount, payment_day } = req.body;
   if (!property_id || !tenant_id || !start_date || !rent_amount)
     return res.status(400).json({ error: 'Champs requis manquants' });
 
@@ -42,19 +52,25 @@ router.post('/', (req, res) => {
   if (!prop) return res.status(403).json({ error: 'Bien non autorisé' });
 
   const result = db.prepare(`
-    INSERT INTO leases (user_id, property_id, tenant_id, start_date, end_date, rent_amount, charges_amount, deposit_amount, payment_day)
-    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
-  `).run(req.userId, property_id, tenant_id, start_date, end_date || null, rent_amount, charges_amount || 0, deposit_amount || 0, payment_day || 1);
+    INSERT INTO leases (user_id, property_id, tenant_id, owner_id, start_date, end_date, rent_amount, charges_amount, deposit_amount, payment_day)
+    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+  `).run(req.userId, property_id, tenant_id, owner_id || null, start_date, end_date || null, rent_amount, charges_amount || 0, deposit_amount || 0, payment_day || 1);
   res.status(201).json({ id: result.lastInsertRowid });
 });
 
 router.put('/:id', (req, res) => {
   const lease = db.prepare('SELECT id FROM leases WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
   if (!lease) return res.status(404).json({ error: 'Bail non trouvé' });
-  const { end_date, rent_amount, charges_amount, deposit_amount, payment_day, active } = req.body;
+  const { property_id, tenant_id, owner_id, start_date, end_date, rent_amount, charges_amount, deposit_amount, payment_day, active } = req.body;
   db.prepare(`
-    UPDATE leases SET end_date=?, rent_amount=?, charges_amount=?, deposit_amount=?, payment_day=?, active=? WHERE id=?
-  `).run(end_date || null, rent_amount, charges_amount || 0, deposit_amount || 0, payment_day || 1, active !== undefined ? active : 1, req.params.id);
+    UPDATE leases SET property_id=COALESCE(?,property_id), tenant_id=COALESCE(?,tenant_id),
+      owner_id=?, start_date=COALESCE(?,start_date), end_date=?,
+      rent_amount=?, charges_amount=?, deposit_amount=?, payment_day=?, active=?
+    WHERE id=?
+  `).run(property_id || null, tenant_id || null, owner_id || null,
+    start_date || null, end_date || null,
+    rent_amount, charges_amount || 0, deposit_amount || 0, payment_day || 1,
+    active !== undefined ? active : 1, req.params.id);
   res.json({ success: true });
 });
 

+ 56 - 0
backend/src/routes/owners.js

@@ -0,0 +1,56 @@
+const express = require('express');
+const db = require('../db');
+const { authMiddleware } = require('../auth');
+
+const router = express.Router();
+router.use(authMiddleware);
+
+router.get('/', (req, res) => {
+  const rows = db.prepare(`
+    SELECT * FROM owners WHERE user_id = ?
+    ORDER BY COALESCE(company_name, last_name), first_name
+  `).all(req.userId);
+  res.json(rows);
+});
+
+router.post('/', (req, res) => {
+  const { type, first_name, last_name, company_name, siret, address, zip_code, city, email, phone } = req.body;
+  if (type === 'physique' && (!first_name || !last_name))
+    return res.status(400).json({ error: 'Nom et prénom requis pour une personne physique' });
+  if (type === 'morale' && !company_name)
+    return res.status(400).json({ error: 'Raison sociale requise pour une personne morale' });
+
+  const result = db.prepare(`
+    INSERT INTO owners (user_id, type, first_name, last_name, company_name, siret, address, zip_code, city, email, phone)
+    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+  `).run(req.userId, type || 'physique', first_name || null, last_name || null,
+    company_name || null, siret || null, address || null, zip_code || null, city || null,
+    email || null, phone || null);
+  res.status(201).json({ id: result.lastInsertRowid });
+});
+
+router.put('/:id', (req, res) => {
+  const owner = db.prepare('SELECT id FROM owners WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+  if (!owner) return res.status(404).json({ error: 'Propriétaire non trouvé' });
+
+  const { type, first_name, last_name, company_name, siret, address, zip_code, city, email, phone } = req.body;
+  db.prepare(`
+    UPDATE owners SET type=?, first_name=?, last_name=?, company_name=?, siret=?,
+      address=?, zip_code=?, city=?, email=?, phone=?
+    WHERE id=?
+  `).run(type || 'physique', first_name || null, last_name || null, company_name || null,
+    siret || null, address || null, zip_code || null, city || null, email || null, phone || null,
+    req.params.id);
+  res.json({ success: true });
+});
+
+router.delete('/:id', (req, res) => {
+  const owner = db.prepare('SELECT id FROM owners WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+  if (!owner) return res.status(404).json({ error: 'Propriétaire non trouvé' });
+  const linked = db.prepare('SELECT id FROM leases WHERE owner_id = ? LIMIT 1').get(req.params.id);
+  if (linked) return res.status(409).json({ error: 'Ce propriétaire est lié à un ou plusieurs baux' });
+  db.prepare('DELETE FROM owners WHERE id = ?').run(req.params.id);
+  res.json({ success: true });
+});
+
+module.exports = router;

+ 1 - 0
backend/src/server.js

@@ -17,6 +17,7 @@ app.use(express.json());
 app.use('/api/auth', require('./routes/auth'));
 app.use('/api/properties', require('./routes/properties'));
 app.use('/api/tenants', require('./routes/tenants'));
+app.use('/api/owners', require('./routes/owners'));
 app.use('/api/leases', require('./routes/leases'));
 app.use('/api/payments', require('./routes/payments'));
 app.use('/api/receipts', require('./routes/receipts'));

+ 2 - 0
frontend/src/App.jsx

@@ -7,6 +7,7 @@ import Register from './pages/Register'
 import Dashboard from './pages/Dashboard'
 import Properties from './pages/Properties'
 import Tenants from './pages/Tenants'
+import Owners from './pages/Owners'
 import Leases from './pages/Leases'
 import LeaseDetail from './pages/LeaseDetail'
 import Payments from './pages/Payments'
@@ -37,6 +38,7 @@ export default function App() {
             <Route index element={<Dashboard />} />
             <Route path="properties" element={<Properties />} />
             <Route path="tenants" element={<Tenants />} />
+            <Route path="owners" element={<Owners />} />
             <Route path="leases" element={<Leases />} />
             <Route path="leases/:id" element={<LeaseDetail />} />
             <Route path="payments" element={<Payments />} />

+ 1 - 0
frontend/src/components/Layout.jsx

@@ -5,6 +5,7 @@ const navItems = [
   { to: '/', label: '📊 Tableau de bord', end: true },
   { to: '/properties', label: '🏠 Biens' },
   { to: '/tenants', label: '👤 Locataires' },
+  { to: '/owners', label: '🏢 Propriétaires' },
   { to: '/leases', label: '📋 Baux' },
   { to: '/payments', label: '💰 Paiements' },
   { to: '/charges', label: '🧾 Charges' },

+ 150 - 2
frontend/src/pages/LeaseDetail.jsx

@@ -34,10 +34,16 @@ export default function LeaseDetail() {
   const [revisions, setRevisions] = useState([])
   const [pendingReg, setPendingReg] = useState({ pending: [], total: 0 })
   const [showModal, setShowModal] = useState(false)
-  const [editPayment, setEditPayment] = useState(null) // null = création, sinon paiement existant
+  const [editPayment, setEditPayment] = useState(null)
   const [showRevisionModal, setShowRevisionModal] = useState(false)
+  const [showEditLeaseModal, setShowEditLeaseModal] = useState(false)
   const [loading, setLoading] = useState(false)
   const [revLoading, setRevLoading] = useState(false)
+  const [leaseEditLoading, setLeaseEditLoading] = useState(false)
+  const [properties, setProperties] = useState([])
+  const [tenants, setTenants] = useState([])
+  const [owners, setOwners] = useState([])
+  const [leaseForm, setLeaseForm] = useState({})
 
   const now = new Date()
   const [form, setForm] = useState({
@@ -62,16 +68,22 @@ export default function LeaseDetail() {
   })
 
   const load = async () => {
-    const [l, p, r, reg] = await Promise.all([
+    const [l, p, r, reg, props, ten, own] = await Promise.all([
       api.get(`/leases/${id}`),
       api.get(`/payments?lease_id=${id}`),
       api.get(`/leases/${id}/revisions`),
       api.get(`/payments/regularization/${id}`),
+      api.get('/properties'),
+      api.get('/tenants'),
+      api.get('/owners'),
     ])
     setLease(l.data)
     setPayments(p.data)
     setRevisions(r.data)
     setPendingReg(reg.data)
+    setProperties(props.data)
+    setTenants(ten.data)
+    setOwners(own.data)
     setForm(f => ({ ...f, rent_paid: l.data.rent_amount, charges_paid: l.data.charges_amount }))
   }
 
@@ -134,6 +146,42 @@ export default function LeaseDetail() {
     catch { toast.error('Erreur') }
   }
 
+  const openEditLease = () => {
+    setLeaseForm({
+      property_id: String(lease.property_id),
+      tenant_id: String(lease.tenant_id),
+      owner_id: lease.owner_id ? String(lease.owner_id) : '',
+      start_date: lease.start_date ? lease.start_date.slice(0, 10) : '',
+      end_date: lease.end_date ? lease.end_date.slice(0, 10) : '',
+      rent_amount: String(lease.rent_amount),
+      charges_amount: String(lease.charges_amount),
+      deposit_amount: String(lease.deposit_amount),
+      payment_day: String(lease.payment_day),
+    })
+    setShowEditLeaseModal(true)
+  }
+
+  const handleEditLease = async e => {
+    e.preventDefault(); setLeaseEditLoading(true)
+    try {
+      await api.put(`/leases/${id}`, {
+        ...leaseForm,
+        property_id: Number(leaseForm.property_id),
+        tenant_id: Number(leaseForm.tenant_id),
+        owner_id: leaseForm.owner_id ? Number(leaseForm.owner_id) : null,
+        rent_amount: parseFloat(leaseForm.rent_amount),
+        charges_amount: parseFloat(leaseForm.charges_amount || 0),
+        deposit_amount: parseFloat(leaseForm.deposit_amount || 0),
+        payment_day: parseInt(leaseForm.payment_day || 1),
+        active: lease.active,
+      })
+      toast.success('Bail mis à jour !')
+      setShowEditLeaseModal(false)
+      load()
+    } catch (err) { toast.error(err.response?.data?.error || 'Erreur') }
+    finally { setLeaseEditLoading(false) }
+  }
+
   const handleRevision = async e => {
     e.preventDefault(); setRevLoading(true)
     try {
@@ -198,6 +246,7 @@ export default function LeaseDetail() {
                 setShowRevisionModal(true)
               }} className="btn-secondary text-sm">📈 Réviser le loyer</button>
             )}
+            <button onClick={openEditLease} className="btn-secondary text-sm">✏️ Modifier le bail</button>
             <span className={lease.active ? 'badge-green' : 'badge-red'}>{lease.active ? 'Actif' : 'Clôturé'}</span>
           </div>
         </div>
@@ -232,6 +281,26 @@ export default function LeaseDetail() {
             <p className="font-semibold text-gray-900">Le {lease.payment_day} du mois</p>
           </div>
         </div>
+
+        {/* Propriétaire bailleur */}
+        {lease.owner_type && (
+          <div className="mt-4 pt-4 border-t">
+            <p className="text-sm text-gray-500 mb-1">🏢 Propriétaire bailleur</p>
+            <p className="font-medium text-gray-900">
+              {lease.owner_type === 'morale'
+                ? lease.owner_company_name
+                : `${lease.owner_first_name || ''} ${lease.owner_last_name || ''}`.trim()}
+              {lease.owner_siret && <span className="ml-2 text-xs text-gray-400 font-normal">SIRET {lease.owner_siret}</span>}
+            </p>
+            {lease.owner_address && (
+              <p className="text-sm text-gray-500">{lease.owner_address}{lease.owner_zip ? `, ${lease.owner_zip}` : ''}{lease.owner_city ? ` ${lease.owner_city}` : ''}</p>
+            )}
+            <div className="flex gap-4 mt-1 text-sm text-gray-500">
+              {lease.owner_email && <span>✉️ {lease.owner_email}</span>}
+              {lease.owner_phone && <span>📞 {lease.owner_phone}</span>}
+            </div>
+          </div>
+        )}
       </div>
 
       {/* Historique des révisions */}
@@ -540,6 +609,85 @@ export default function LeaseDetail() {
           </form>
         </Modal>
       )}
+
+      {/* Modal édition bail */}
+      {showEditLeaseModal && (
+        <Modal title="✏️ Modifier le bail" onClose={() => setShowEditLeaseModal(false)}>
+          <form onSubmit={handleEditLease} className="space-y-4 max-h-[70vh] overflow-y-auto pr-1">
+
+            <div>
+              <label className="label">Bien *</label>
+              <select className="input" required value={leaseForm.property_id}
+                onChange={e => setLeaseForm(f => ({ ...f, property_id: e.target.value }))}>
+                <option value="">Sélectionner un bien</option>
+                {properties.map(p => <option key={p.id} value={p.id}>{p.name} — {p.city}</option>)}
+              </select>
+            </div>
+
+            <div>
+              <label className="label">Locataire *</label>
+              <select className="input" required value={leaseForm.tenant_id}
+                onChange={e => setLeaseForm(f => ({ ...f, tenant_id: e.target.value }))}>
+                <option value="">Sélectionner un locataire</option>
+                {tenants.map(t => <option key={t.id} value={t.id}>{t.first_name} {t.last_name}</option>)}
+              </select>
+            </div>
+
+            <div>
+              <label className="label">Propriétaire bailleur</label>
+              <select className="input" value={leaseForm.owner_id}
+                onChange={e => setLeaseForm(f => ({ ...f, owner_id: e.target.value }))}>
+                <option value="">— Aucun / À renseigner —</option>
+                {owners.map(o => (
+                  <option key={o.id} value={o.id}>
+                    {o.type === 'morale' ? `🏢 ${o.company_name}` : `👤 ${o.first_name} ${o.last_name}`}
+                  </option>
+                ))}
+              </select>
+            </div>
+
+            <div className="grid grid-cols-2 gap-4">
+              <div>
+                <label className="label">Date de début *</label>
+                <input className="input" type="date" required value={leaseForm.start_date}
+                  onChange={e => setLeaseForm(f => ({ ...f, start_date: e.target.value }))} />
+              </div>
+              <div>
+                <label className="label">Date de fin</label>
+                <input className="input" type="date" value={leaseForm.end_date}
+                  onChange={e => setLeaseForm(f => ({ ...f, end_date: e.target.value }))} />
+              </div>
+              <div>
+                <label className="label">Loyer HC (€) *</label>
+                <input className="input" type="number" step="0.01" min="0" required value={leaseForm.rent_amount}
+                  onChange={e => setLeaseForm(f => ({ ...f, rent_amount: e.target.value }))} />
+              </div>
+              <div>
+                <label className="label">Charges (€)</label>
+                <input className="input" type="number" step="0.01" min="0" value={leaseForm.charges_amount}
+                  onChange={e => setLeaseForm(f => ({ ...f, charges_amount: e.target.value }))} />
+              </div>
+              <div>
+                <label className="label">Dépôt de garantie (€)</label>
+                <input className="input" type="number" step="0.01" min="0" value={leaseForm.deposit_amount}
+                  onChange={e => setLeaseForm(f => ({ ...f, deposit_amount: e.target.value }))} />
+              </div>
+              <div>
+                <label className="label">Jour de paiement</label>
+                <input className="input" type="number" min="1" max="28" value={leaseForm.payment_day}
+                  onChange={e => setLeaseForm(f => ({ ...f, payment_day: e.target.value }))} />
+              </div>
+            </div>
+
+            <div className="flex gap-3 pt-2">
+              <button type="button" onClick={() => setShowEditLeaseModal(false)} className="btn-secondary flex-1">Annuler</button>
+              <button type="submit" disabled={leaseEditLoading} className="btn-primary flex-1">
+                {leaseEditLoading ? '...' : 'Enregistrer'}
+              </button>
+            </div>
+          </form>
+        </Modal>
+      )}
     </div>
   )
 }

+ 21 - 2
frontend/src/pages/Leases.jsx

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
 import { Link } from 'react-router-dom'
 import api from '../api'
 import toast from 'react-hot-toast'
+import { ownerLabel } from './Owners'
 
 function Modal({ title, onClose, children }) {
   return (
@@ -17,12 +18,13 @@ function Modal({ title, onClose, children }) {
   )
 }
 
-const emptyForm = { property_id: '', tenant_id: '', start_date: '', end_date: '', rent_amount: '', charges_amount: '', deposit_amount: '', payment_day: '1' }
+const emptyForm = { property_id: '', tenant_id: '', owner_id: '', start_date: '', end_date: '', rent_amount: '', charges_amount: '', deposit_amount: '', payment_day: '1' }
 
 export default function Leases() {
   const [leases, setLeases] = useState([])
   const [properties, setProperties] = useState([])
   const [tenants, setTenants] = useState([])
+  const [owners, setOwners] = useState([])
   const [showModal, setShowModal] = useState(false)
   const [form, setForm] = useState(emptyForm)
   const [loading, setLoading] = useState(false)
@@ -31,13 +33,14 @@ export default function Leases() {
     api.get('/leases').then(r => setLeases(r.data))
     api.get('/properties').then(r => setProperties(r.data))
     api.get('/tenants').then(r => setTenants(r.data))
+    api.get('/owners').then(r => setOwners(r.data))
   }
   useEffect(() => { load() }, [])
 
   const handleSubmit = async e => {
     e.preventDefault(); setLoading(true)
     try {
-      await api.post('/leases', { ...form, property_id: Number(form.property_id), tenant_id: Number(form.tenant_id) })
+      await api.post('/leases', { ...form, property_id: Number(form.property_id), tenant_id: Number(form.tenant_id), owner_id: form.owner_id ? Number(form.owner_id) : null })
       toast.success('Bail créé !')
       setShowModal(false); load()
     } catch (err) { toast.error(err.response?.data?.error || 'Erreur') }
@@ -85,6 +88,11 @@ export default function Leases() {
                     </div>
                     <p className="text-sm text-gray-500">{l.property_name} · {l.property_address}, {l.property_city}</p>
                     <p className="text-sm text-gray-400">Du {fmtDate(l.start_date)}{l.end_date ? ` au ${fmtDate(l.end_date)}` : ''}</p>
+                    {l.owner_type && (
+                      <p className="text-xs text-gray-400 mt-0.5">
+                        🏢 Propriétaire : {l.owner_type === 'morale' ? l.owner_company_name : `${l.owner_first_name || ''} ${l.owner_last_name || ''}`.trim()}
+                      </p>
+                    )}
                   </div>
                 </div>
                 <div className="flex items-center gap-6">
@@ -122,6 +130,17 @@ export default function Leases() {
                 {tenants.map(t => <option key={t.id} value={t.id}>{t.first_name} {t.last_name}</option>)}
               </select>
             </div>
+            <div>
+              <label className="label">Propriétaire bailleur</label>
+              <select className="input" value={form.owner_id} onChange={e => setForm(f => ({ ...f, owner_id: e.target.value }))}>
+                <option value="">— Aucun / À renseigner —</option>
+                {owners.map(o => (
+                  <option key={o.id} value={o.id}>
+                    {o.type === 'morale' ? `🏢 ${o.company_name}` : `👤 ${o.first_name} ${o.last_name}`}
+                  </option>
+                ))}
+              </select>
+            </div>
             <div className="grid grid-cols-2 gap-4">
               <div>
                 <label className="label">Date de début *</label>

+ 205 - 0
frontend/src/pages/Owners.jsx

@@ -0,0 +1,205 @@
+import { useEffect, useState } from 'react'
+import api from '../api'
+import toast from 'react-hot-toast'
+
+function Modal({ title, onClose, children }) {
+  return (
+    <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 overflow-y-auto">
+      <div className="bg-white rounded-2xl shadow-xl w-full max-w-lg my-4">
+        <div className="flex items-center justify-between p-6 border-b">
+          <h2 className="text-lg font-semibold">{title}</h2>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-2xl leading-none">&times;</button>
+        </div>
+        <div className="p-6">{children}</div>
+      </div>
+    </div>
+  )
+}
+
+const emptyForm = {
+  type: 'physique',
+  first_name: '', last_name: '',
+  company_name: '', siret: '',
+  address: '', zip_code: '', city: '',
+  email: '', phone: '',
+}
+
+export function ownerLabel(o) {
+  if (!o) return '—'
+  return o.type === 'morale'
+    ? o.company_name || '—'
+    : `${o.first_name || ''} ${o.last_name || ''}`.trim() || '—'
+}
+
+export default function Owners() {
+  const [owners, setOwners] = useState([])
+  const [showModal, setShowModal] = useState(false)
+  const [form, setForm] = useState(emptyForm)
+  const [editing, setEditing] = useState(null)
+  const [loading, setLoading] = useState(false)
+
+  const load = () => api.get('/owners').then(r => setOwners(r.data))
+  useEffect(() => { load() }, [])
+
+  const openAdd = () => { setForm(emptyForm); setEditing(null); setShowModal(true) }
+  const openEdit = o => {
+    setForm({
+      type: o.type || 'physique',
+      first_name: o.first_name || '', last_name: o.last_name || '',
+      company_name: o.company_name || '', siret: o.siret || '',
+      address: o.address || '', zip_code: o.zip_code || '', city: o.city || '',
+      email: o.email || '', phone: o.phone || '',
+    })
+    setEditing(o.id); setShowModal(true)
+  }
+
+  const handleSubmit = async e => {
+    e.preventDefault(); setLoading(true)
+    try {
+      if (editing) await api.put(`/owners/${editing}`, form)
+      else await api.post('/owners', form)
+      toast.success(editing ? 'Propriétaire modifié !' : 'Propriétaire ajouté !')
+      setShowModal(false); load()
+    } catch (err) { toast.error(err.response?.data?.error || 'Erreur') }
+    finally { setLoading(false) }
+  }
+
+  const handleDelete = async id => {
+    if (!confirm('Supprimer ce propriétaire ?')) return
+    try { await api.delete(`/owners/${id}`); toast.success('Propriétaire supprimé'); load() }
+    catch (err) { toast.error(err.response?.data?.error || 'Erreur') }
+  }
+
+  const set = field => e => setForm(f => ({ ...f, [field]: e.target.value }))
+
+  return (
+    <div>
+      <div className="flex items-center justify-between mb-8">
+        <div>
+          <h1 className="text-2xl font-bold text-gray-900">🏢 Propriétaires</h1>
+          <p className="text-gray-500 mt-1">{owners.length} propriétaire(s) enregistré(s)</p>
+        </div>
+        <button onClick={openAdd} className="btn-primary">+ Ajouter un propriétaire</button>
+      </div>
+
+      {owners.length === 0 ? (
+        <div className="card text-center py-16">
+          <div className="text-5xl mb-4">🏢</div>
+          <p className="text-gray-500">Aucun propriétaire enregistré.</p>
+        </div>
+      ) : (
+        <div className="card overflow-hidden p-0">
+          <table className="w-full">
+            <thead className="bg-gray-50 border-b">
+              <tr>
+                <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Identité</th>
+                <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Type</th>
+                <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Email</th>
+                <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Téléphone</th>
+                <th className="text-left px-6 py-3 text-sm font-semibold text-gray-600">Adresse</th>
+                <th className="px-6 py-3"></th>
+              </tr>
+            </thead>
+            <tbody className="divide-y divide-gray-50">
+              {owners.map(o => (
+                <tr key={o.id} className="hover:bg-gray-50">
+                  <td className="px-6 py-4 font-medium text-gray-900">
+                    {ownerLabel(o)}
+                    {o.siret && <span className="ml-2 text-xs text-gray-400">SIRET {o.siret}</span>}
+                  </td>
+                  <td className="px-6 py-4">
+                    <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'}`}>
+                      {o.type === 'morale' ? 'Personne morale' : 'Personne physique'}
+                    </span>
+                  </td>
+                  <td className="px-6 py-4 text-gray-600">{o.email || '—'}</td>
+                  <td className="px-6 py-4 text-gray-600">{o.phone || '—'}</td>
+                  <td className="px-6 py-4 text-gray-600 text-sm">
+                    {o.address ? `${o.address}${o.zip_code ? ', ' + o.zip_code : ''}${o.city ? ' ' + o.city : ''}` : '—'}
+                  </td>
+                  <td className="px-6 py-4 text-right space-x-3">
+                    <button onClick={() => openEdit(o)} className="text-gray-400 hover:text-blue-600">✏️</button>
+                    <button onClick={() => handleDelete(o.id)} className="text-gray-400 hover:text-red-600">🗑️</button>
+                  </td>
+                </tr>
+              ))}
+            </tbody>
+          </table>
+        </div>
+      )}
+
+      {showModal && (
+        <Modal title={editing ? 'Modifier le propriétaire' : 'Ajouter un propriétaire'} onClose={() => setShowModal(false)}>
+          <form onSubmit={handleSubmit} className="space-y-4">
+
+            {/* Type */}
+            <div className="flex gap-3">
+              {['physique', 'morale'].map(t => (
+                <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'}`}>
+                  <input type="radio" className="hidden" value={t} checked={form.type === t}
+                    onChange={() => setForm(f => ({ ...f, type: t }))} />
+                  <span className="text-lg">{t === 'physique' ? '👤' : '🏢'}</span>
+                  <span className="text-sm font-medium">{t === 'physique' ? 'Personne physique' : 'Personne morale'}</span>
+                </label>
+              ))}
+            </div>
+
+            {/* Identité selon type */}
+            {form.type === 'physique' ? (
+              <div className="grid grid-cols-2 gap-4">
+                <div>
+                  <label className="label">Prénom *</label>
+                  <input className="input" required value={form.first_name} onChange={set('first_name')} />
+                </div>
+                <div>
+                  <label className="label">Nom *</label>
+                  <input className="input" required value={form.last_name} onChange={set('last_name')} />
+                </div>
+              </div>
+            ) : (
+              <div className="grid grid-cols-2 gap-4">
+                <div className="col-span-2">
+                  <label className="label">Raison sociale *</label>
+                  <input className="input" required value={form.company_name} onChange={set('company_name')} />
+                </div>
+                <div className="col-span-2">
+                  <label className="label">SIRET</label>
+                  <input className="input" value={form.siret} onChange={set('siret')} placeholder="XXX XXX XXX XXXXX" />
+                </div>
+              </div>
+            )}
+
+            {/* Coordonnées communes */}
+            <div className="grid grid-cols-2 gap-4">
+              <div className="col-span-2">
+                <label className="label">Adresse</label>
+                <input className="input" value={form.address} onChange={set('address')} />
+              </div>
+              <div>
+                <label className="label">Code postal</label>
+                <input className="input" value={form.zip_code} onChange={set('zip_code')} />
+              </div>
+              <div>
+                <label className="label">Ville</label>
+                <input className="input" value={form.city} onChange={set('city')} />
+              </div>
+              <div>
+                <label className="label">Email</label>
+                <input className="input" type="email" value={form.email} onChange={set('email')} />
+              </div>
+              <div>
+                <label className="label">Téléphone</label>
+                <input className="input" value={form.phone} onChange={set('phone')} />
+              </div>
+            </div>
+
+            <div className="flex gap-3 pt-2">
+              <button type="button" onClick={() => setShowModal(false)} className="btn-secondary flex-1">Annuler</button>
+              <button type="submit" disabled={loading} className="btn-primary flex-1">{loading ? '...' : 'Enregistrer'}</button>
+            </div>
+          </form>
+        </Modal>
+      )}
+    </div>
+  )
+}