jeremy 4 mesiacov pred
rodič
commit
13d6d568f6

+ 1 - 1
backend/package.json

@@ -8,11 +8,11 @@
   },
   "dependencies": {
     "bcryptjs": "^2.4.3",
-    "better-sqlite3": "^9.4.3",
     "cors": "^2.8.5",
     "express": "^4.18.2",
     "jsonwebtoken": "^9.0.2",
     "multer": "^1.4.5-lts.1",
+    "mysql2": "^3.9.2",
     "pdf-lib": "^1.17.1",
     "pdfkit": "^0.15.0"
   },

+ 58 - 136
backend/src/db.js

@@ -1,144 +1,66 @@
-const Database = require('better-sqlite3');
-const path = require('path');
+const mysql = require('mysql2/promise');
 
-const db = new Database(path.join(__dirname, '../data/gestion_locative.db'));
+let pool = null;
 
-db.pragma('journal_mode = WAL');
-db.pragma('foreign_keys = ON');
+function getPool() {
+  if (!pool) {
+    pool = mysql.createPool({
+      host: process.env.DB_HOST || 'localhost',
+      port: parseInt(process.env.DB_PORT || '3306'),
+      user: process.env.DB_USER || 'gluser',
+      password: process.env.DB_PASSWORD || 'glpass',
+      database: process.env.DB_NAME || 'gestion_locative',
+      waitForConnections: true,
+      connectionLimit: 10,
+      queueLimit: 0,
+      timezone: '+00:00',
+    });
+  }
+  return pool;
+}
 
-db.exec(`
-  CREATE TABLE IF NOT EXISTS users (
-    id INTEGER PRIMARY KEY AUTOINCREMENT,
-    email TEXT UNIQUE NOT NULL,
-    password TEXT NOT NULL,
-    name TEXT NOT NULL,
-    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
-  );
+// Helper: single row
+async function get(sql, params = []) {
+  const [rows] = await getPool().execute(sql, params);
+  return rows[0] || null;
+}
 
-  CREATE TABLE IF NOT EXISTS properties (
-    id INTEGER PRIMARY KEY AUTOINCREMENT,
-    user_id INTEGER NOT NULL,
-    name TEXT NOT NULL,
-    address TEXT NOT NULL,
-    city TEXT NOT NULL,
-    zip_code TEXT NOT NULL,
-    type TEXT NOT NULL,
-    rooms INTEGER,
-    area REAL,
-    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-    FOREIGN KEY (user_id) REFERENCES users(id)
-  );
+// Helper: multiple rows
+async function all(sql, params = []) {
+  const [rows] = await getPool().execute(sql, params);
+  return rows;
+}
 
-  CREATE TABLE IF NOT EXISTS tenants (
-    id INTEGER PRIMARY KEY AUTOINCREMENT,
-    user_id INTEGER NOT NULL,
-    first_name TEXT NOT NULL,
-    last_name TEXT NOT NULL,
-    email TEXT,
-    phone TEXT,
-    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-    FOREIGN KEY (user_id) REFERENCES users(id)
-  );
+// Helper: insert/update/delete
+async function run(sql, params = []) {
+  const [result] = await getPool().execute(sql, params);
+  return { insertId: result.insertId, affectedRows: result.affectedRows };
+}
 
-  CREATE TABLE IF NOT EXISTS leases (
-    id INTEGER PRIMARY KEY AUTOINCREMENT,
-    user_id INTEGER NOT NULL,
-    property_id INTEGER NOT NULL,
-    tenant_id INTEGER NOT NULL,
-    start_date TEXT NOT NULL,
-    end_date TEXT,
-    rent_amount REAL NOT NULL,
-    charges_amount REAL NOT NULL DEFAULT 0,
-    deposit_amount REAL NOT NULL DEFAULT 0,
-    payment_day INTEGER NOT NULL DEFAULT 1,
-    active INTEGER NOT NULL DEFAULT 1,
-    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-    FOREIGN KEY (user_id) REFERENCES users(id),
-    FOREIGN KEY (property_id) REFERENCES properties(id),
-    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
-  );
+// Helper: transaction
+async function transaction(fn) {
+  const conn = await getPool().getConnection();
+  await conn.beginTransaction();
+  try {
+    const result = await fn(conn);
+    await conn.commit();
+    return result;
+  } catch (e) {
+    await conn.rollback();
+    throw e;
+  } finally {
+    conn.release();
+  }
+}
 
-  CREATE TABLE IF NOT EXISTS payments (
-    id INTEGER PRIMARY KEY AUTOINCREMENT,
-    lease_id INTEGER NOT NULL,
-    user_id INTEGER NOT NULL,
-    period_month INTEGER NOT NULL,
-    period_year INTEGER NOT NULL,
-    rent_paid REAL NOT NULL,
-    charges_paid REAL NOT NULL DEFAULT 0,
-    payment_date TEXT NOT NULL,
-    payment_method TEXT NOT NULL DEFAULT 'virement',
-    notes TEXT,
-    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-    FOREIGN KEY (lease_id) REFERENCES leases(id),
-    FOREIGN KEY (user_id) REFERENCES users(id)
-  );
+// Test connection
+async function testConnection() {
+  try {
+    await getPool().execute('SELECT 1');
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
 
-  CREATE TABLE IF NOT EXISTS charges (
-    id INTEGER PRIMARY KEY AUTOINCREMENT,
-    user_id INTEGER NOT NULL,
-    property_id INTEGER NOT NULL,
-    category TEXT NOT NULL,
-    label TEXT NOT NULL,
-    amount REAL NOT NULL,
-    date TEXT NOT NULL,
-    year INTEGER NOT NULL,
-    invoice_path TEXT,
-    invoice_name TEXT,
-    notes TEXT,
-    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-    FOREIGN KEY (user_id) REFERENCES users(id),
-    FOREIGN KEY (property_id) REFERENCES properties(id)
-  );
-`);
-
-// 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 {}
-});
-
-// Lease revisions history
-db.exec(`
-  CREATE TABLE IF NOT EXISTS lease_revisions (
-    id INTEGER PRIMARY KEY AUTOINCREMENT,
-    lease_id INTEGER NOT NULL,
-    user_id INTEGER NOT NULL,
-    effective_date TEXT NOT NULL,
-    rent_amount REAL NOT NULL,
-    charges_amount REAL NOT NULL DEFAULT 0,
-    notes TEXT,
-    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-    FOREIGN KEY (lease_id) REFERENCES leases(id),
-    FOREIGN KEY (user_id) REFERENCES users(id)
-  )
-`);
-
-// Add recoverable column to charges if missing
-try { db.exec('ALTER TABLE charges ADD COLUMN recoverable INTEGER NOT NULL DEFAULT 1'); } catch {}
-
-// Add prorata + regularization columns to payments if missing
-['is_prorata INTEGER DEFAULT 0',
- 'prorata_days INTEGER',
- 'prorata_total_days INTEGER',
- 'charge_regularization REAL DEFAULT 0'
-].forEach(col => {
-  try { db.exec(`ALTER TABLE payments ADD COLUMN ${col}`); } catch {}
-});
-
-// Charge regularizations from annual bilan closure
-db.exec(`
-  CREATE TABLE IF NOT EXISTS charge_regularizations (
-    id INTEGER PRIMARY KEY AUTOINCREMENT,
-    lease_id INTEGER NOT NULL,
-    user_id INTEGER NOT NULL,
-    year INTEGER NOT NULL,
-    amount REAL NOT NULL,
-    notes TEXT,
-    applied_payment_id INTEGER,
-    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-    FOREIGN KEY (lease_id) REFERENCES leases(id),
-    FOREIGN KEY (user_id) REFERENCES users(id)
-  )
-`);
-
-module.exports = db;
+module.exports = { get, all, run, transaction, testConnection, getPool };

+ 15 - 20
backend/src/routes/auth.js

@@ -1,8 +1,8 @@
 const express = require('express');
 const bcrypt = require('bcryptjs');
 const jwt = require('jsonwebtoken');
-const db = require('../db');
-const { JWT_SECRET } = require('../auth');
+const { get, run } = require('../db');
+const { JWT_SECRET, authMiddleware } = require('../auth');
 
 const router = express.Router();
 
@@ -10,15 +10,13 @@ router.post('/register', async (req, res) => {
   const { email, password, name } = req.body;
   if (!email || !password || !name)
     return res.status(400).json({ error: 'Tous les champs sont requis' });
-
   try {
     const hashed = await bcrypt.hash(password, 10);
-    const stmt = db.prepare('INSERT INTO users (email, password, name) VALUES (?, ?, ?)');
-    const result = stmt.run(email, hashed, name);
-    const token = jwt.sign({ userId: result.lastInsertRowid }, JWT_SECRET, { expiresIn: '7d' });
-    res.json({ token, user: { id: result.lastInsertRowid, email, name } });
+    const result = await run('INSERT INTO users (email, password, name) VALUES (?, ?, ?)', [email, hashed, name]);
+    const token = jwt.sign({ userId: result.insertId }, JWT_SECRET, { expiresIn: '7d' });
+    res.json({ token, user: { id: result.insertId, email, name } });
   } catch (err) {
-    if (err.message.includes('UNIQUE')) return res.status(409).json({ error: 'Email déjà utilisé' });
+    if (err.code === 'ER_DUP_ENTRY') return res.status(409).json({ error: 'Email déjà utilisé' });
     res.status(500).json({ error: err.message });
   }
 });
@@ -27,29 +25,26 @@ router.post('/login', async (req, res) => {
   const { email, password } = req.body;
   if (!email || !password)
     return res.status(400).json({ error: 'Email et mot de passe requis' });
-
-  const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email);
+  const user = await get('SELECT * FROM users WHERE email = ?', [email]);
   if (!user) return res.status(401).json({ error: 'Identifiants incorrects' });
-
   const valid = await bcrypt.compare(password, user.password);
   if (!valid) return res.status(401).json({ error: 'Identifiants incorrects' });
-
   const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
   res.json({ token, user: { id: user.id, email: user.email, name: user.name } });
 });
 
-router.get('/me', require('../auth').authMiddleware, (req, res) => {
-  const user = db.prepare('SELECT id, email, name, first_name, last_name, address, phone FROM users WHERE id = ?').get(req.userId);
+router.get('/me', authMiddleware, async (req, res) => {
+  const user = await get('SELECT id, email, name, first_name, last_name, address, phone FROM users WHERE id = ?', [req.userId]);
   res.json(user);
 });
 
-router.put('/profile', require('../auth').authMiddleware, (req, res) => {
+router.put('/profile', authMiddleware, async (req, res) => {
   const { first_name, last_name, address, phone, email, name } = req.body;
-  db.prepare(`
-    UPDATE users SET first_name=?, last_name=?, address=?, phone=?, email=?, name=?
-    WHERE id=?
-  `).run(first_name || null, last_name || null, address || null, phone || null, email, name, req.userId);
-  const user = db.prepare('SELECT id, email, name, first_name, last_name, address, phone FROM users WHERE id = ?').get(req.userId);
+  await run(
+    'UPDATE users SET first_name=?, last_name=?, address=?, phone=?, email=?, name=? WHERE id=?',
+    [first_name || null, last_name || null, address || null, phone || null, email, name, req.userId]
+  );
+  const user = await get('SELECT id, email, name, first_name, last_name, address, phone FROM users WHERE id = ?', [req.userId]);
   res.json(user);
 });
 

+ 81 - 77
backend/src/routes/charges.js

@@ -4,7 +4,7 @@ const fs = require('fs');
 const multer = require('multer');
 const PDFDocument = require('pdfkit');
 const { PDFDocument: LibPDFDocument, rgb, StandardFonts } = require('pdf-lib');
-const db = require('../db');
+const { get, all, run, transaction } = require('../db');
 const { authMiddleware } = require('../auth');
 
 const router = express.Router();
@@ -32,7 +32,7 @@ const upload = multer({
 });
 
 // List charges (filter by property_id, year)
-router.get('/', (req, res) => {
+router.get('/', async (req, res) => {
   const { property_id, year } = req.query;
   let query = `
     SELECT c.*, p.name as property_name
@@ -44,32 +44,32 @@ router.get('/', (req, res) => {
   if (property_id) { query += ' AND c.property_id = ?'; params.push(property_id); }
   if (year) { query += ' AND c.year = ?'; params.push(year); }
   query += ' ORDER BY c.date DESC';
-  res.json(db.prepare(query).all(...params));
+  res.json(await all(query, params));
 });
 
 // Create charge (with optional invoice file)
-router.post('/', upload.single('invoice'), (req, res) => {
+router.post('/', upload.single('invoice'), async (req, res) => {
   const { property_id, category, label, amount, date, year, notes, recoverable } = req.body;
   if (!property_id || !category || !label || !amount || !date || !year)
     return res.status(400).json({ error: 'Champs requis manquants' });
 
-  const prop = db.prepare('SELECT id FROM properties WHERE id = ? AND user_id = ?').get(property_id, req.userId);
+  const prop = await get('SELECT id FROM properties WHERE id = ? AND user_id = ?', [property_id, req.userId]);
   if (!prop) return res.status(403).json({ error: 'Bien non autorisé' });
 
   const invoice_path = req.file ? req.file.filename : null;
   const invoice_name = req.file ? req.file.originalname : null;
 
-  const result = db.prepare(`
+  const result = await run(`
     INSERT INTO charges (user_id, property_id, category, label, amount, date, year, invoice_path, invoice_name, notes, recoverable)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
-  `).run(req.userId, property_id, category, label, parseFloat(amount), date, parseInt(year), invoice_path, invoice_name, notes || null, recoverable === 'false' || recoverable === '0' ? 0 : 1);
+  `, [req.userId, property_id, category, label, parseFloat(amount), date, parseInt(year), invoice_path, invoice_name, notes || null, recoverable === 'false' || recoverable === '0' ? 0 : 1]);
 
-  res.status(201).json({ id: result.lastInsertRowid });
+  res.status(201).json({ id: result.insertId });
 });
 
 // Update charge (with optional new invoice)
-router.put('/:id', upload.single('invoice'), (req, res) => {
-  const charge = db.prepare('SELECT * FROM charges WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+router.put('/:id', upload.single('invoice'), async (req, res) => {
+  const charge = await get('SELECT * FROM charges WHERE id = ? AND user_id = ?', [req.params.id, req.userId]);
   if (!charge) return res.status(404).json({ error: 'Charge non trouvée' });
 
   const { category, label, amount, date, year, notes, recoverable } = req.body;
@@ -87,17 +87,17 @@ router.put('/:id', upload.single('invoice'), (req, res) => {
     invoice_name = req.file.originalname;
   }
 
-  db.prepare(`
+  await run(`
     UPDATE charges SET category=?, label=?, amount=?, date=?, year=?, invoice_path=?, invoice_name=?, notes=?, recoverable=?
     WHERE id=?
-  `).run(category, label, parseFloat(amount), date, parseInt(year), invoice_path, invoice_name, notes || null, recoverable === 'false' || recoverable === '0' ? 0 : 1, req.params.id);
+  `, [category, label, parseFloat(amount), date, parseInt(year), invoice_path, invoice_name, notes || null, recoverable === 'false' || recoverable === '0' ? 0 : 1, req.params.id]);
 
   res.json({ success: true });
 });
 
 // Delete charge
-router.delete('/:id', (req, res) => {
-  const charge = db.prepare('SELECT * FROM charges WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+router.delete('/:id', async (req, res) => {
+  const charge = await get('SELECT * FROM charges WHERE id = ? AND user_id = ?', [req.params.id, req.userId]);
   if (!charge) return res.status(404).json({ error: 'Charge non trouvée' });
 
   if (charge.invoice_path) {
@@ -105,13 +105,13 @@ router.delete('/:id', (req, res) => {
     if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
   }
 
-  db.prepare('DELETE FROM charges WHERE id = ?').run(req.params.id);
+  await run('DELETE FROM charges WHERE id = ?', [req.params.id]);
   res.json({ success: true });
 });
 
 // Download invoice
-router.get('/:id/invoice', (req, res) => {
-  const charge = db.prepare('SELECT * FROM charges WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+router.get('/:id/invoice', async (req, res) => {
+  const charge = await get('SELECT * FROM charges WHERE id = ? AND user_id = ?', [req.params.id, req.userId]);
   if (!charge || !charge.invoice_path) return res.status(404).json({ error: 'Facture non trouvée' });
 
   const filePath = path.join(uploadDir, charge.invoice_path);
@@ -122,67 +122,68 @@ router.get('/:id/invoice', (req, res) => {
 });
 
 // Annual bilan for a property
-router.get('/bilan/:property_id/:year', (req, res) => {
+router.get('/bilan/:property_id/:year', async (req, res) => {
   const { property_id, year } = req.params;
 
   // Verify property ownership
-  const prop = db.prepare('SELECT * FROM properties WHERE id = ? AND user_id = ?').get(property_id, req.userId);
+  const prop = await get('SELECT * FROM properties WHERE id = ? AND user_id = ?', [property_id, req.userId]);
   if (!prop) return res.status(403).json({ error: 'Bien non autorisé' });
 
   // Total recoverable charges for this property/year (for tenant billing)
-  const totalChargesRow = db.prepare(`
+  const totalChargesRow = await get(`
     SELECT SUM(amount) as total, COUNT(*) as count
     FROM charges WHERE property_id = ? AND year = ? AND user_id = ? AND recoverable = 1
-  `).get(property_id, year, req.userId);
+  `, [property_id, year, req.userId]);
 
   // Total non-recoverable (landlord's own charges, for info)
-  const totalNonRecovRow = db.prepare(`
+  const totalNonRecovRow = await get(`
     SELECT SUM(amount) as total, COUNT(*) as count
     FROM charges WHERE property_id = ? AND year = ? AND user_id = ? AND recoverable = 0
-  `).get(property_id, year, req.userId);
+  `, [property_id, year, req.userId]);
 
   const totalChargesReelles = totalChargesRow?.total || 0;
   const totalNonRecoverable = totalNonRecovRow?.total || 0;
 
   // Charges by category (recoverable only)
-  const chargesByCategory = db.prepare(`
+  const chargesByCategory = await all(`
     SELECT category, SUM(amount) as total
     FROM charges WHERE property_id = ? AND year = ? AND user_id = ? AND recoverable = 1
     GROUP BY category ORDER BY total DESC
-  `).all(property_id, year, req.userId);
+  `, [property_id, year, req.userId]);
 
   // Non-recoverable by category (for info)
-  const chargesNonRecovByCategory = db.prepare(`
+  const chargesNonRecovByCategory = await all(`
     SELECT category, SUM(amount) as total
     FROM charges WHERE property_id = ? AND year = ? AND user_id = ? AND recoverable = 0
     GROUP BY category ORDER BY total DESC
-  `).all(property_id, year, req.userId);
+  `, [property_id, year, req.userId]);
 
   // All leases on this property (active or that were active during the year)
-  const leases = db.prepare(`
+  const leases = await all(`
     SELECT l.*, t.first_name, t.last_name, t.email
     FROM leases l
     JOIN tenants t ON l.tenant_id = t.id
     WHERE l.property_id = ? AND l.user_id = ?
       AND (l.start_date <= ? AND (l.end_date IS NULL OR l.end_date >= ?))
-  `).all(property_id, req.userId, `${year}-12-31`, `${year}-01-01`);
+  `, [property_id, req.userId, `${year}-12-31`, `${year}-01-01`]);
 
   // For each lease, get provisions perçues for the year
-  const leaseBilans = leases.map(lease => {
-    const provisions = db.prepare(`
+  const leaseBilans = [];
+  for (const lease of leases) {
+    const provisions = await get(`
       SELECT SUM(charges_paid) as total
       FROM payments
       WHERE lease_id = ? AND period_year = ?
-    `).get(lease.id, year);
+    `, [lease.id, year]);
 
-    return {
+    leaseBilans.push({
       lease_id: lease.id,
       tenant_name: `${lease.first_name} ${lease.last_name}`,
       tenant_email: lease.email,
       monthly_provision: lease.charges_amount,
       provisions_percues: provisions?.total || 0
-    };
-  });
+    });
+  }
 
   // Calculate total provisions across all tenants for proportional split
   const totalProvisions = leaseBilans.reduce((s, l) => s + l.provisions_percues, 0);
@@ -214,48 +215,48 @@ router.get('/bilan/:property_id/:year', (req, res) => {
 });
 
 // Close annual bilan: create charge_regularization records for each tenant
-router.post('/bilan/:property_id/:year/close', (req, res) => {
+router.post('/bilan/:property_id/:year/close', async (req, res) => {
   const { property_id, year } = req.params;
 
-  const prop = db.prepare('SELECT * FROM properties WHERE id = ? AND user_id = ?').get(property_id, req.userId);
+  const prop = await get('SELECT * FROM properties WHERE id = ? AND user_id = ?', [property_id, req.userId]);
   if (!prop) return res.status(403).json({ error: 'Bien non autorisé' });
 
   // Check not already closed
-  const alreadyClosed = db.prepare(
-    'SELECT id FROM charge_regularizations WHERE user_id = ? AND year = ? AND lease_id IN (SELECT id FROM leases WHERE property_id = ?)'
-  ).get(req.userId, year, property_id);
+  const alreadyClosed = await get(
+    'SELECT id FROM charge_regularizations WHERE user_id = ? AND year = ? AND lease_id IN (SELECT id FROM leases WHERE property_id = ?)',
+    [req.userId, year, property_id]
+  );
   if (alreadyClosed) return res.status(409).json({ error: `Le bilan ${year} a déjà été clôturé pour ce bien` });
 
   // Re-compute bilan
-  const totalChargesRow = db.prepare(
-    'SELECT SUM(amount) as total FROM charges WHERE property_id = ? AND year = ? AND user_id = ? AND recoverable = 1'
-  ).get(property_id, year, req.userId);
+  const totalChargesRow = await get(
+    'SELECT SUM(amount) as total FROM charges WHERE property_id = ? AND year = ? AND user_id = ? AND recoverable = 1',
+    [property_id, year, req.userId]
+  );
   const totalChargesReelles = totalChargesRow?.total || 0;
 
-  const leases = db.prepare(`
+  const leases = await all(`
     SELECT l.*, t.first_name, t.last_name FROM leases l
     JOIN tenants t ON l.tenant_id = t.id
     WHERE l.property_id = ? AND l.user_id = ?
       AND (l.start_date <= ? AND (l.end_date IS NULL OR l.end_date >= ?))
-  `).all(property_id, req.userId, `${year}-12-31`, `${year}-01-01`);
+  `, [property_id, req.userId, `${year}-12-31`, `${year}-01-01`]);
 
   if (leases.length === 0) return res.status(400).json({ error: 'Aucun locataire actif sur cette période' });
 
-  const leaseBilans = leases.map(lease => {
-    const provisions = db.prepare(
-      'SELECT SUM(charges_paid) as total FROM payments WHERE lease_id = ? AND period_year = ?'
-    ).get(lease.id, year);
-    return { lease_id: lease.id, tenant: `${lease.first_name} ${lease.last_name}`, provisions_percues: provisions?.total || 0 };
-  });
+  const leaseBilans = [];
+  for (const lease of leases) {
+    const provisions = await get(
+      'SELECT SUM(charges_paid) as total FROM payments WHERE lease_id = ? AND period_year = ?',
+      [lease.id, year]
+    );
+    leaseBilans.push({ lease_id: lease.id, tenant: `${lease.first_name} ${lease.last_name}`, provisions_percues: provisions?.total || 0 });
+  }
 
   const totalProvisions = leaseBilans.reduce((s, l) => s + l.provisions_percues, 0);
 
-  const insertReg = db.prepare(
-    'INSERT INTO charge_regularizations (lease_id, user_id, year, amount, notes) VALUES (?, ?, ?, ?, ?)'
-  );
-
-  const closeAll = db.transaction(() => {
-    const results = [];
+  const results = await transaction(async (conn) => {
+    const out = [];
     for (const l of leaseBilans) {
       const ratio = totalProvisions > 0 ? l.provisions_percues / totalProvisions : 1 / leaseBilans.length;
       const quote_part = totalChargesReelles * ratio;
@@ -263,14 +264,16 @@ router.post('/bilan/:property_id/:year/close', (req, res) => {
       // trop_percu < 0 = locataire doit payer en plus → débit
       const trop_percu = Math.round((l.provisions_percues - quote_part) * 100) / 100;
       // Stored as: positive = credit tenant (réduction loyer), negative = debit tenant (supplément)
-      insertReg.run(l.lease_id, req.userId, parseInt(year), trop_percu,
-        `Régularisation charges ${year} — quote-part ${Math.round(ratio * 10000) / 100}%`);
-      results.push({ lease_id: l.lease_id, tenant: l.tenant, trop_percu });
+      await conn.execute(
+        'INSERT INTO charge_regularizations (lease_id, user_id, year, amount, notes) VALUES (?, ?, ?, ?, ?)',
+        [l.lease_id, req.userId, parseInt(year), trop_percu,
+          `Régularisation charges ${year} — quote-part ${Math.round(ratio * 10000) / 100}%`]
+      );
+      out.push({ lease_id: l.lease_id, tenant: l.tenant, trop_percu });
     }
-    return results;
+    return out;
   });
 
-  const results = closeAll();
   res.json({ success: true, year: parseInt(year), regularizations: results });
 });
 
@@ -278,45 +281,46 @@ router.post('/bilan/:property_id/:year/close', (req, res) => {
 router.get('/bilan/:property_id/:year/pdf', async (req, res) => {
   const { property_id, year } = req.params;
 
-  const prop = db.prepare('SELECT * FROM properties WHERE id = ? AND user_id = ?').get(property_id, req.userId);
+  const prop = await get('SELECT * FROM properties WHERE id = ? AND user_id = ?', [property_id, req.userId]);
   if (!prop) return res.status(403).json({ error: 'Bien non autorisé' });
 
-  const owner = db.prepare('SELECT name, first_name, last_name, email, address, phone FROM users WHERE id = ?').get(req.userId);
+  const owner = await get('SELECT name, first_name, last_name, email, address, phone FROM users WHERE id = ?', [req.userId]);
   const ownerFullName = (owner.first_name && owner.last_name) ? `${owner.first_name} ${owner.last_name}` : owner.name;
 
-  const totalChargesRow = db.prepare(`
+  const totalChargesRow = await get(`
     SELECT SUM(amount) as total, COUNT(*) as count FROM charges
     WHERE property_id = ? AND year = ? AND user_id = ? AND recoverable = 1
-  `).get(property_id, year, req.userId);
+  `, [property_id, year, req.userId]);
   const totalChargesReelles = totalChargesRow?.total || 0;
 
-  const chargesList = db.prepare(`
+  const chargesList = await all(`
     SELECT * FROM charges WHERE property_id = ? AND year = ? AND user_id = ?
     ORDER BY date ASC
-  `).all(property_id, year, req.userId);
+  `, [property_id, year, req.userId]);
 
-  const chargesByCategory = db.prepare(`
+  const chargesByCategory = await all(`
     SELECT category, SUM(amount) as total FROM charges
     WHERE property_id = ? AND year = ? AND user_id = ? AND recoverable = 1
     GROUP BY category ORDER BY total DESC
-  `).all(property_id, year, req.userId);
+  `, [property_id, year, req.userId]);
 
-  const leases = db.prepare(`
+  const leases = await all(`
     SELECT l.*, t.first_name, t.last_name, t.email FROM leases l
     JOIN tenants t ON l.tenant_id = t.id
     WHERE l.property_id = ? AND l.user_id = ?
       AND (l.start_date <= ? AND (l.end_date IS NULL OR l.end_date >= ?))
-  `).all(property_id, req.userId, `${year}-12-31`, `${year}-01-01`);
+  `, [property_id, req.userId, `${year}-12-31`, `${year}-01-01`]);
 
-  const leaseBilans = leases.map(lease => {
-    const provisions = db.prepare(`SELECT SUM(charges_paid) as total FROM payments WHERE lease_id = ? AND period_year = ?`).get(lease.id, year);
-    return {
+  const leaseBilans = [];
+  for (const lease of leases) {
+    const provisions = await get(`SELECT SUM(charges_paid) as total FROM payments WHERE lease_id = ? AND period_year = ?`, [lease.id, year]);
+    leaseBilans.push({
       lease_id: lease.id,
       tenant_name: `${lease.first_name} ${lease.last_name}`,
       tenant_email: lease.email,
       provisions_percues: provisions?.total || 0
-    };
-  });
+    });
+  }
   const totalProvisions = leaseBilans.reduce((s, l) => s + l.provisions_percues, 0);
   const bilans = leaseBilans.map(l => {
     const ratio = totalProvisions > 0 ? l.provisions_percues / totalProvisions : (leaseBilans.length > 0 ? 1 / leaseBilans.length : 0);

+ 34 - 48
backend/src/routes/leases.js

@@ -1,13 +1,13 @@
 const express = require('express');
-const db = require('../db');
+const { get, all, run } = require('../db');
 const { authMiddleware } = require('../auth');
 
 const router = express.Router();
 router.use(authMiddleware);
 
-router.get('/', (req, res) => {
-  const rows = db.prepare(`
-    SELECT l.*, 
+router.get('/', async (req, res) => {
+  const rows = await all(`
+    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
     FROM leases l
@@ -15,12 +15,12 @@ router.get('/', (req, res) => {
     JOIN tenants t ON l.tenant_id = t.id
     WHERE l.user_id = ?
     ORDER BY l.start_date DESC
-  `).all(req.userId);
+  `, [req.userId]);
   res.json(rows);
 });
 
-router.get('/:id', (req, res) => {
-  const row = db.prepare(`
+router.get('/:id', async (req, res) => {
+  const row = await get(`
     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
@@ -28,85 +28,71 @@ router.get('/:id', (req, res) => {
     JOIN properties p ON l.property_id = p.id
     JOIN tenants t ON l.tenant_id = t.id
     WHERE l.id = ? AND l.user_id = ?
-  `).get(req.params.id, req.userId);
+  `, [req.params.id, req.userId]);
   if (!row) return res.status(404).json({ error: 'Bail non trouvé' });
   res.json(row);
 });
 
-router.post('/', (req, res) => {
+router.post('/', async (req, res) => {
   const { property_id, tenant_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' });
-
-  const prop = db.prepare('SELECT id FROM properties WHERE id = ? AND user_id = ?').get(property_id, req.userId);
+  const prop = await get('SELECT id FROM properties WHERE id = ? AND user_id = ?', [property_id, req.userId]);
   if (!prop) return res.status(403).json({ error: 'Bien non autorisé' });
-
-  const result = db.prepare(`
+  const result = await run(`
     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);
-  res.status(201).json({ id: result.lastInsertRowid });
+  `, [req.userId, property_id, tenant_id, start_date, end_date || null, rent_amount, charges_amount || 0, deposit_amount || 0, payment_day || 1]);
+  res.status(201).json({ id: result.insertId });
 });
 
-router.put('/:id', (req, res) => {
-  const lease = db.prepare('SELECT id FROM leases WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+router.put('/:id', async (req, res) => {
+  const lease = await get('SELECT id FROM leases WHERE id = ? AND user_id = ?', [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;
-  db.prepare(`
+  await run(`
     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);
+  `, [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 });
 });
 
-router.delete('/:id', (req, res) => {
-  const lease = db.prepare('SELECT id FROM leases WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+router.delete('/:id', async (req, res) => {
+  const lease = await get('SELECT id FROM leases WHERE id = ? AND user_id = ?', [req.params.id, req.userId]);
   if (!lease) return res.status(404).json({ error: 'Bail non trouvé' });
-  db.prepare('DELETE FROM leases WHERE id = ?').run(req.params.id);
+  await run('DELETE FROM leases WHERE id = ?', [req.params.id]);
   res.json({ success: true });
 });
 
 // --- Révisions de loyer ---
-
-// List revisions for a lease
-router.get('/:id/revisions', (req, res) => {
-  const lease = db.prepare('SELECT id FROM leases WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+router.get('/:id/revisions', async (req, res) => {
+  const lease = await get('SELECT id FROM leases WHERE id = ? AND user_id = ?', [req.params.id, req.userId]);
   if (!lease) return res.status(404).json({ error: 'Bail non trouvé' });
-  const rows = db.prepare(
-    'SELECT * FROM lease_revisions WHERE lease_id = ? ORDER BY effective_date DESC'
-  ).all(req.params.id);
+  const rows = await all('SELECT * FROM lease_revisions WHERE lease_id = ? ORDER BY effective_date DESC', [req.params.id]);
   res.json(rows);
 });
 
-// Create revision (and update lease current amounts)
-router.post('/:id/revisions', (req, res) => {
-  const lease = db.prepare('SELECT * FROM leases WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+router.post('/:id/revisions', async (req, res) => {
+  const lease = await get('SELECT * FROM leases WHERE id = ? AND user_id = ?', [req.params.id, req.userId]);
   if (!lease) return res.status(404).json({ error: 'Bail non trouvé' });
-
   const { effective_date, rent_amount, charges_amount, notes } = req.body;
   if (!effective_date || rent_amount === undefined)
-    return res.status(400).json({ error: 'Date d\'effet et nouveau loyer requis' });
-
-  // Save revision history entry
-  const result = db.prepare(`
+    return res.status(400).json({ error: "Date d'effet et nouveau loyer requis" });
+  const result = await run(`
     INSERT INTO lease_revisions (lease_id, user_id, effective_date, rent_amount, charges_amount, notes)
     VALUES (?, ?, ?, ?, ?, ?)
-  `).run(req.params.id, req.userId, effective_date, parseFloat(rent_amount), parseFloat(charges_amount || 0), notes || null);
-
-  // If effective_date <= today, apply immediately to lease
+  `, [req.params.id, req.userId, effective_date, parseFloat(rent_amount), parseFloat(charges_amount || 0), notes || null]);
   const today = new Date().toISOString().slice(0, 10);
   if (effective_date <= today) {
-    db.prepare('UPDATE leases SET rent_amount=?, charges_amount=? WHERE id=?')
-      .run(parseFloat(rent_amount), parseFloat(charges_amount || 0), req.params.id);
+    await run('UPDATE leases SET rent_amount=?, charges_amount=? WHERE id=?',
+      [parseFloat(rent_amount), parseFloat(charges_amount || 0), req.params.id]);
   }
-
-  res.status(201).json({ id: result.lastInsertRowid });
+  res.status(201).json({ id: result.insertId });
 });
 
-// Delete revision
-router.delete('/:id/revisions/:revId', (req, res) => {
-  const lease = db.prepare('SELECT id FROM leases WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+router.delete('/:id/revisions/:revId', async (req, res) => {
+  const lease = await get('SELECT id FROM leases WHERE id = ? AND user_id = ?', [req.params.id, req.userId]);
   if (!lease) return res.status(404).json({ error: 'Bail non trouvé' });
-  db.prepare('DELETE FROM lease_revisions WHERE id = ? AND lease_id = ?').run(req.params.revId, req.params.id);
+  await run('DELETE FROM lease_revisions WHERE id = ? AND lease_id = ?', [req.params.revId, req.params.id]);
   res.json({ success: true });
 });
 

+ 29 - 35
backend/src/routes/payments.js

@@ -1,11 +1,11 @@
 const express = require('express');
-const db = require('../db');
+const { get, all, run } = require('../db');
 const { authMiddleware } = require('../auth');
 
 const router = express.Router();
 router.use(authMiddleware);
 
-router.get('/', (req, res) => {
+router.get('/', async (req, res) => {
   const { lease_id } = req.query;
   let query = `
     SELECT p.*, l.rent_amount, l.charges_amount,
@@ -20,72 +20,66 @@ router.get('/', (req, res) => {
   const params = [req.userId];
   if (lease_id) { query += ' AND p.lease_id = ?'; params.push(lease_id); }
   query += ' ORDER BY p.period_year DESC, p.period_month DESC';
-  res.json(db.prepare(query).all(...params));
+  const rows = await all(query, params);
+  res.json(rows);
 });
 
-router.post('/', (req, res) => {
+router.post('/', async (req, res) => {
   const { lease_id, period_month, period_year, rent_paid, charges_paid, payment_date, payment_method, notes,
     is_prorata, prorata_days, prorata_total_days, charge_regularization } = req.body;
   if (!lease_id || !period_month || !period_year || rent_paid === undefined || !payment_date)
     return res.status(400).json({ error: 'Champs requis manquants' });
-
-  const lease = db.prepare('SELECT id FROM leases WHERE id = ? AND user_id = ?').get(lease_id, req.userId);
+  const lease = await get('SELECT id FROM leases WHERE id = ? AND user_id = ?', [lease_id, req.userId]);
   if (!lease) return res.status(403).json({ error: 'Bail non autorisé' });
-
-  const existing = db.prepare(
-    'SELECT id FROM payments WHERE lease_id = ? AND period_month = ? AND period_year = ?'
-  ).get(lease_id, period_month, period_year);
+  const existing = await get('SELECT id FROM payments WHERE lease_id = ? AND period_month = ? AND period_year = ?',
+    [lease_id, period_month, period_year]);
   if (existing) return res.status(409).json({ error: 'Un paiement existe déjà pour cette période' });
-
   const regularization = parseFloat(charge_regularization || 0);
-
-  const result = db.prepare(`
+  const result = await run(`
     INSERT INTO payments (lease_id, user_id, period_month, period_year, rent_paid, charges_paid, payment_date, payment_method, notes,
       is_prorata, prorata_days, prorata_total_days, charge_regularization)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
-  `).run(lease_id, req.userId, period_month, period_year, rent_paid, charges_paid || 0,
+  `, [lease_id, req.userId, period_month, period_year, rent_paid, charges_paid || 0,
     payment_date, payment_method || 'virement', notes || null,
-    is_prorata ? 1 : 0, prorata_days || null, prorata_total_days || null, regularization);
-
-  // If a regularization was applied, mark it as used
+    is_prorata ? 1 : 0, prorata_days || null, prorata_total_days || null, regularization]);
   if (regularization !== 0) {
-    db.prepare(`
+    await run(`
       UPDATE charge_regularizations SET applied_payment_id = ?
       WHERE lease_id = ? AND applied_payment_id IS NULL
       ORDER BY created_at ASC LIMIT 1
-    `).run(result.lastInsertRowid, lease_id);
+    `, [result.insertId, lease_id]);
   }
-
-  res.status(201).json({ id: result.lastInsertRowid });
+  res.status(201).json({ id: result.insertId });
 });
 
-router.delete('/:id', (req, res) => {
-  const payment = db.prepare('SELECT id FROM payments WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+router.delete('/:id', async (req, res) => {
+  const payment = await get('SELECT id FROM payments WHERE id = ? AND user_id = ?', [req.params.id, req.userId]);
   if (!payment) return res.status(404).json({ error: 'Paiement non trouvé' });
-  db.prepare('DELETE FROM payments WHERE id = ?').run(req.params.id);
+  await run('DELETE FROM payments WHERE id = ?', [req.params.id]);
   res.json({ success: true });
 });
 
-// Pending charge regularization for a lease
-router.get('/regularization/:lease_id', (req, res) => {
-  const lease = db.prepare('SELECT id FROM leases WHERE id = ? AND user_id = ?').get(req.params.lease_id, req.userId);
+router.get('/regularization/:lease_id', async (req, res) => {
+  const lease = await get('SELECT id FROM leases WHERE id = ? AND user_id = ?', [req.params.lease_id, req.userId]);
   if (!lease) return res.status(404).json({ error: 'Bail non trouvé' });
-  const rows = db.prepare(
-    'SELECT * FROM charge_regularizations WHERE lease_id = ? AND applied_payment_id IS NULL ORDER BY year DESC'
-  ).all(req.params.lease_id);
+  const rows = await all(
+    'SELECT * FROM charge_regularizations WHERE lease_id = ? AND applied_payment_id IS NULL ORDER BY year DESC',
+    [req.params.lease_id]
+  );
   const total = rows.reduce((s, r) => s + r.amount, 0);
   res.json({ pending: rows, total: Math.round(total * 100) / 100 });
 });
 
 // Dashboard stats
-router.get('/stats', (req, res) => {
-  const stats = db.prepare(`
-    SELECT 
+router.get('/stats', async (req, res) => {
+  const stats = await get(`
+    SELECT
       COUNT(DISTINCT l.id) as active_leases,
       SUM(l.rent_amount + l.charges_amount) as monthly_expected,
-      (SELECT SUM(rent_paid + charges_paid) FROM payments WHERE user_id = ? AND period_year = strftime('%Y', 'now') AND period_month = strftime('%m', 'now')) as current_month_collected
+      (SELECT SUM(rent_paid + charges_paid) FROM payments
+       WHERE user_id = ? AND period_year = YEAR(NOW()) AND period_month = MONTH(NOW())) as current_month_collected
     FROM leases l WHERE l.user_id = ? AND l.active = 1
-  `).get(req.userId, req.userId);
+  `, [req.userId, req.userId]);
   res.json(stats);
 });
 

+ 18 - 16
backend/src/routes/properties.js

@@ -1,39 +1,41 @@
 const express = require('express');
-const db = require('../db');
+const { get, all, run } = require('../db');
 const { authMiddleware } = require('../auth');
 
 const router = express.Router();
 router.use(authMiddleware);
 
-router.get('/', (req, res) => {
-  const rows = db.prepare('SELECT * FROM properties WHERE user_id = ? ORDER BY created_at DESC').all(req.userId);
+router.get('/', async (req, res) => {
+  const rows = await all('SELECT * FROM properties WHERE user_id = ? ORDER BY created_at DESC', [req.userId]);
   res.json(rows);
 });
 
-router.post('/', (req, res) => {
+router.post('/', async (req, res) => {
   const { name, address, city, zip_code, type, rooms, area } = req.body;
   if (!name || !address || !city || !zip_code || !type)
     return res.status(400).json({ error: 'Champs requis manquants' });
-  const result = db.prepare(
-    'INSERT INTO properties (user_id, name, address, city, zip_code, type, rooms, area) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
-  ).run(req.userId, name, address, city, zip_code, type, rooms || null, area || null);
-  res.status(201).json({ id: result.lastInsertRowid });
+  const result = await run(
+    'INSERT INTO properties (user_id, name, address, city, zip_code, type, rooms, area) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
+    [req.userId, name, address, city, zip_code, type, rooms || null, area || null]
+  );
+  res.status(201).json({ id: result.insertId });
 });
 
-router.put('/:id', (req, res) => {
+router.put('/:id', async (req, res) => {
   const { name, address, city, zip_code, type, rooms, area } = req.body;
-  const prop = db.prepare('SELECT id FROM properties WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+  const prop = await get('SELECT id FROM properties WHERE id = ? AND user_id = ?', [req.params.id, req.userId]);
   if (!prop) return res.status(404).json({ error: 'Bien non trouvé' });
-  db.prepare(
-    'UPDATE properties SET name=?, address=?, city=?, zip_code=?, type=?, rooms=?, area=? WHERE id=?'
-  ).run(name, address, city, zip_code, type, rooms || null, area || null, req.params.id);
+  await run(
+    'UPDATE properties SET name=?, address=?, city=?, zip_code=?, type=?, rooms=?, area=? WHERE id=?',
+    [name, address, city, zip_code, type, rooms || null, area || null, req.params.id]
+  );
   res.json({ success: true });
 });
 
-router.delete('/:id', (req, res) => {
-  const prop = db.prepare('SELECT id FROM properties WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+router.delete('/:id', async (req, res) => {
+  const prop = await get('SELECT id FROM properties WHERE id = ? AND user_id = ?', [req.params.id, req.userId]);
   if (!prop) return res.status(404).json({ error: 'Bien non trouvé' });
-  db.prepare('DELETE FROM properties WHERE id = ?').run(req.params.id);
+  await run('DELETE FROM properties WHERE id = ?', [req.params.id]);
   res.json({ success: true });
 });
 

+ 10 - 21
backend/src/routes/receipts.js

@@ -1,6 +1,6 @@
 const express = require('express');
 const PDFDocument = require('pdfkit');
-const db = require('../db');
+const { get } = require('../db');
 const { authMiddleware } = require('../auth');
 
 const router = express.Router();
@@ -8,8 +8,8 @@ router.use(authMiddleware);
 
 const MONTHS_FR = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'];
 
-router.get('/:paymentId', (req, res) => {
-  const payment = db.prepare(`
+router.get('/:paymentId', async (req, res) => {
+  const payment = await get(`
     SELECT p.*,
       l.rent_amount, l.charges_amount, l.deposit_amount,
       pr.name as property_name, pr.address as property_address, pr.city as property_city, pr.zip_code as property_zip,
@@ -23,18 +23,20 @@ router.get('/:paymentId', (req, res) => {
     JOIN tenants t ON l.tenant_id = t.id
     JOIN users u ON p.user_id = u.id
     WHERE p.id = ? AND p.user_id = ?
-  `).get(req.params.paymentId, req.userId);
+  `, [req.params.paymentId, req.userId]);
 
   if (!payment) return res.status(404).json({ error: 'Paiement non trouvé' });
 
   const doc = new PDFDocument({ size: 'A4', margin: 50 });
-
   res.setHeader('Content-Type', 'application/pdf');
   res.setHeader('Content-Disposition', `attachment; filename=quittance_${payment.period_year}_${String(payment.period_month).padStart(2,'0')}.pdf`);
   doc.pipe(res);
 
   const monthName = MONTHS_FR[payment.period_month - 1];
   const total = payment.rent_paid + payment.charges_paid;
+  const ownerFullName = (payment.owner_first_name && payment.owner_last_name)
+    ? `${payment.owner_first_name} ${payment.owner_last_name}`
+    : payment.owner_name;
 
   // Header
   doc.fontSize(22).font('Helvetica-Bold').text('QUITTANCE DE LOYER', { align: 'center' });
@@ -42,18 +44,14 @@ router.get('/:paymentId', (req, res) => {
   doc.fontSize(14).font('Helvetica').text(`Période : ${monthName} ${payment.period_year}`, { align: 'center' });
   doc.moveDown(1.5);
 
-  // Separator
   doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor('#2563eb').lineWidth(2).stroke();
   doc.moveDown(1);
 
   // Bailleur
-  const ownerFullName = (payment.owner_first_name && payment.owner_last_name)
-    ? `${payment.owner_first_name} ${payment.owner_last_name}`
-    : payment.owner_name;
   doc.fontSize(12).font('Helvetica-Bold').text('BAILLEUR');
   doc.font('Helvetica').fontSize(11).text(ownerFullName);
   if (payment.owner_address) doc.text(payment.owner_address);
-  doc.text(payment.owner_email);
+  doc.text(payment.owner_email || '');
   if (payment.owner_phone) doc.text(payment.owner_phone);
   doc.moveDown(1);
 
@@ -72,7 +70,6 @@ router.get('/:paymentId', (req, res) => {
     .text(`${payment.property_zip} ${payment.property_city}`);
   doc.moveDown(1);
 
-  // Separator
   doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor('#e5e7eb').lineWidth(1).stroke();
   doc.moveDown(1);
 
@@ -81,7 +78,6 @@ router.get('/:paymentId', (req, res) => {
   doc.moveDown(0.5);
 
   const col1 = 50, col2 = 400;
-
   const drawRow = (label, value, bold = false, color = '#000000') => {
     const y = doc.y;
     doc.font(bold ? 'Helvetica-Bold' : 'Helvetica').fontSize(11).fillColor(color);
@@ -102,9 +98,7 @@ router.get('/:paymentId', (req, res) => {
 
   const reg = payment.charge_regularization || 0;
   if (reg !== 0) {
-    const regLabel = reg > 0
-      ? `Régularisation de charges (trop-perçu déduit)`
-      : `Régularisation de charges (complément dû)`;
+    const regLabel = reg > 0 ? 'Régularisation de charges (trop-perçu déduit)' : 'Régularisation de charges (complément dû)';
     drawRow(regLabel, -reg, false, reg > 0 ? '#15803d' : '#c2410c');
   }
 
@@ -114,19 +108,14 @@ router.get('/:paymentId', (req, res) => {
   drawRow('TOTAL NET', netTotal, true);
   doc.moveDown(0.5);
 
-  // Mode et date de paiement
   const paymentMethods = { virement: 'Virement bancaire', cheque: 'Chèque', especes: 'Espèces', prelevement: 'Prélèvement automatique' };
   doc.font('Helvetica').fontSize(11)
     .text(`Mode de paiement : ${paymentMethods[payment.payment_method] || payment.payment_method}`)
     .text(`Date de paiement : ${new Date(payment.payment_date).toLocaleDateString('fr-FR')}`);
 
-  if (payment.notes) {
-    doc.moveDown(0.5).text(`Notes : ${payment.notes}`);
-  }
-
+  if (payment.notes) doc.moveDown(0.5).text(`Notes : ${payment.notes}`);
   doc.moveDown(2);
 
-  // Attestation
   doc.moveTo(50, doc.y).lineTo(545, doc.y).strokeColor('#e5e7eb').lineWidth(1).stroke();
   doc.moveDown(1);
   doc.font('Helvetica').fontSize(10).fillColor('#6b7280')

+ 41 - 0
backend/src/routes/setup.js

@@ -0,0 +1,41 @@
+const express = require('express');
+const bcrypt = require('bcryptjs');
+const jwt = require('jsonwebtoken');
+const router = express.Router();
+
+// Check if setup is needed (no users in DB)
+router.get('/status', async (req, res) => {
+  try {
+    const { get } = require('../db');
+    const user = await get('SELECT id FROM users LIMIT 1');
+    res.json({ needsSetup: !user, dbConnected: true });
+  } catch (e) {
+    res.json({ needsSetup: true, dbConnected: false, error: e.message });
+  }
+});
+
+// Complete setup: create first admin user
+router.post('/complete', async (req, res) => {
+  try {
+    const { get, run } = require('../db');
+    // Only allow if no users exist
+    const existing = await get('SELECT id FROM users LIMIT 1');
+    if (existing) return res.status(409).json({ error: 'Un compte existe déjà. Configuration déjà effectuée.' });
+
+    const { email, password, name, first_name, last_name } = req.body;
+    if (!email || !password || !name) return res.status(400).json({ error: 'Email, mot de passe et nom requis' });
+
+    const hashed = await bcrypt.hash(password, 10);
+    const JWT_SECRET = process.env.JWT_SECRET || 'change_this_secret_in_production';
+    const result = await run(
+      'INSERT INTO users (email, password, name, first_name, last_name) VALUES (?, ?, ?, ?, ?)',
+      [email, hashed, name, first_name || null, last_name || null]
+    );
+    const token = jwt.sign({ userId: result.insertId }, JWT_SECRET, { expiresIn: '7d' });
+    res.json({ token, user: { id: result.insertId, email, name, first_name, last_name } });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+module.exports = router;

+ 16 - 15
backend/src/routes/tenants.js

@@ -1,37 +1,38 @@
 const express = require('express');
-const db = require('../db');
+const { get, all, run } = require('../db');
 const { authMiddleware } = require('../auth');
 
 const router = express.Router();
 router.use(authMiddleware);
 
-router.get('/', (req, res) => {
-  const rows = db.prepare('SELECT * FROM tenants WHERE user_id = ? ORDER BY last_name, first_name').all(req.userId);
+router.get('/', async (req, res) => {
+  const rows = await all('SELECT * FROM tenants WHERE user_id = ? ORDER BY last_name, first_name', [req.userId]);
   res.json(rows);
 });
 
-router.post('/', (req, res) => {
+router.post('/', async (req, res) => {
   const { first_name, last_name, email, phone } = req.body;
   if (!first_name || !last_name) return res.status(400).json({ error: 'Nom et prénom requis' });
-  const result = db.prepare(
-    'INSERT INTO tenants (user_id, first_name, last_name, email, phone) VALUES (?, ?, ?, ?, ?)'
-  ).run(req.userId, first_name, last_name, email || null, phone || null);
-  res.status(201).json({ id: result.lastInsertRowid });
+  const result = await run(
+    'INSERT INTO tenants (user_id, first_name, last_name, email, phone) VALUES (?, ?, ?, ?, ?)',
+    [req.userId, first_name, last_name, email || null, phone || null]
+  );
+  res.status(201).json({ id: result.insertId });
 });
 
-router.put('/:id', (req, res) => {
+router.put('/:id', async (req, res) => {
   const { first_name, last_name, email, phone } = req.body;
-  const tenant = db.prepare('SELECT id FROM tenants WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+  const tenant = await get('SELECT id FROM tenants WHERE id = ? AND user_id = ?', [req.params.id, req.userId]);
   if (!tenant) return res.status(404).json({ error: 'Locataire non trouvé' });
-  db.prepare('UPDATE tenants SET first_name=?, last_name=?, email=?, phone=? WHERE id=?')
-    .run(first_name, last_name, email || null, phone || null, req.params.id);
+  await run('UPDATE tenants SET first_name=?, last_name=?, email=?, phone=? WHERE id=?',
+    [first_name, last_name, email || null, phone || null, req.params.id]);
   res.json({ success: true });
 });
 
-router.delete('/:id', (req, res) => {
-  const tenant = db.prepare('SELECT id FROM tenants WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+router.delete('/:id', async (req, res) => {
+  const tenant = await get('SELECT id FROM tenants WHERE id = ? AND user_id = ?', [req.params.id, req.userId]);
   if (!tenant) return res.status(404).json({ error: 'Locataire non trouvé' });
-  db.prepare('DELETE FROM tenants WHERE id = ?').run(req.params.id);
+  await run('DELETE FROM tenants WHERE id = ?', [req.params.id]);
   res.json({ success: true });
 });
 

+ 144 - 0
backend/src/schema.js

@@ -0,0 +1,144 @@
+const { getPool } = require('./db');
+
+async function createSchema() {
+  const pool = getPool();
+
+  await pool.execute(`
+    CREATE TABLE IF NOT EXISTS users (
+      id INT AUTO_INCREMENT PRIMARY KEY,
+      email VARCHAR(255) UNIQUE NOT NULL,
+      password VARCHAR(255) NOT NULL,
+      name VARCHAR(255) NOT NULL,
+      first_name VARCHAR(255),
+      last_name VARCHAR(255),
+      address TEXT,
+      phone VARCHAR(50),
+      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+    )
+  `);
+
+  await pool.execute(`
+    CREATE TABLE IF NOT EXISTS properties (
+      id INT AUTO_INCREMENT PRIMARY KEY,
+      user_id INT NOT NULL,
+      name VARCHAR(255) NOT NULL,
+      address VARCHAR(255) NOT NULL,
+      city VARCHAR(255) NOT NULL,
+      zip_code VARCHAR(20) NOT NULL,
+      type VARCHAR(50) NOT NULL,
+      rooms INT,
+      area DOUBLE,
+      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+      FOREIGN KEY (user_id) REFERENCES users(id)
+    )
+  `);
+
+  await pool.execute(`
+    CREATE TABLE IF NOT EXISTS tenants (
+      id INT AUTO_INCREMENT PRIMARY KEY,
+      user_id INT NOT NULL,
+      first_name VARCHAR(255) NOT NULL,
+      last_name VARCHAR(255) NOT NULL,
+      email VARCHAR(255),
+      phone VARCHAR(50),
+      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+      FOREIGN KEY (user_id) REFERENCES users(id)
+    )
+  `);
+
+  await pool.execute(`
+    CREATE TABLE IF NOT EXISTS leases (
+      id INT AUTO_INCREMENT PRIMARY KEY,
+      user_id INT NOT NULL,
+      property_id INT NOT NULL,
+      tenant_id INT NOT NULL,
+      start_date VARCHAR(20) NOT NULL,
+      end_date VARCHAR(20),
+      rent_amount DOUBLE NOT NULL,
+      charges_amount DOUBLE NOT NULL DEFAULT 0,
+      deposit_amount DOUBLE NOT NULL DEFAULT 0,
+      payment_day INT NOT NULL DEFAULT 1,
+      active TINYINT NOT NULL DEFAULT 1,
+      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+      FOREIGN KEY (user_id) REFERENCES users(id),
+      FOREIGN KEY (property_id) REFERENCES properties(id),
+      FOREIGN KEY (tenant_id) REFERENCES tenants(id)
+    )
+  `);
+
+  await pool.execute(`
+    CREATE TABLE IF NOT EXISTS payments (
+      id INT AUTO_INCREMENT PRIMARY KEY,
+      lease_id INT NOT NULL,
+      user_id INT NOT NULL,
+      period_month INT NOT NULL,
+      period_year INT NOT NULL,
+      rent_paid DOUBLE NOT NULL,
+      charges_paid DOUBLE NOT NULL DEFAULT 0,
+      payment_date VARCHAR(20) NOT NULL,
+      payment_method VARCHAR(50) NOT NULL DEFAULT 'virement',
+      notes TEXT,
+      is_prorata TINYINT DEFAULT 0,
+      prorata_days INT,
+      prorata_total_days INT,
+      charge_regularization DOUBLE DEFAULT 0,
+      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+      FOREIGN KEY (lease_id) REFERENCES leases(id),
+      FOREIGN KEY (user_id) REFERENCES users(id)
+    )
+  `);
+
+  await pool.execute(`
+    CREATE TABLE IF NOT EXISTS charges (
+      id INT AUTO_INCREMENT PRIMARY KEY,
+      user_id INT NOT NULL,
+      property_id INT NOT NULL,
+      category VARCHAR(50) NOT NULL,
+      label VARCHAR(255) NOT NULL,
+      amount DOUBLE NOT NULL,
+      date VARCHAR(20) NOT NULL,
+      year INT NOT NULL,
+      invoice_path VARCHAR(255),
+      invoice_name VARCHAR(255),
+      notes TEXT,
+      recoverable TINYINT NOT NULL DEFAULT 1,
+      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+      FOREIGN KEY (user_id) REFERENCES users(id),
+      FOREIGN KEY (property_id) REFERENCES properties(id)
+    )
+  `);
+
+  await pool.execute(`
+    CREATE TABLE IF NOT EXISTS lease_revisions (
+      id INT AUTO_INCREMENT PRIMARY KEY,
+      lease_id INT NOT NULL,
+      user_id INT NOT NULL,
+      effective_date VARCHAR(20) NOT NULL,
+      rent_amount DOUBLE NOT NULL,
+      charges_amount DOUBLE NOT NULL DEFAULT 0,
+      notes TEXT,
+      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+      FOREIGN KEY (lease_id) REFERENCES leases(id),
+      FOREIGN KEY (user_id) REFERENCES users(id)
+    )
+  `);
+
+  await pool.execute(`
+    CREATE TABLE IF NOT EXISTS charge_regularizations (
+      id INT AUTO_INCREMENT PRIMARY KEY,
+      lease_id INT NOT NULL,
+      user_id INT NOT NULL,
+      year INT NOT NULL,
+      amount DOUBLE NOT NULL,
+      notes TEXT,
+      applied_payment_id INT,
+      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+      FOREIGN KEY (lease_id) REFERENCES leases(id),
+      FOREIGN KEY (user_id) REFERENCES users(id)
+    )
+  `);
+
+  console.log('Schema created/verified.');
+}
+
+module.exports = { createSchema };

+ 39 - 5
backend/src/server.js

@@ -3,9 +3,6 @@ const cors = require('cors');
 const path = require('path');
 const fs = require('fs');
 
-// Ensure data and uploads directories exist
-const dataDir = path.join(__dirname, '../data');
-if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
 const uploadsDir = path.join(__dirname, '../uploads');
 if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });
 
@@ -13,6 +10,16 @@ const app = express();
 app.use(cors());
 app.use(express.json());
 
+// Setup status endpoint (always available)
+app.use('/api/setup', require('./routes/setup'));
+
+// Middleware: block all other routes until DB is ready
+let dbReady = false;
+app.use((req, res, next) => {
+  if (!dbReady) return res.status(503).json({ needsSetup: true });
+  next();
+});
+
 // Routes
 app.use('/api/auth', require('./routes/auth'));
 app.use('/api/properties', require('./routes/properties'));
@@ -24,5 +31,32 @@ app.use('/api/charges', require('./routes/charges'));
 
 app.get('/api/health', (_, res) => res.json({ ok: true }));
 
-const PORT = process.env.PORT || 3001;
-app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
+async function start() {
+  const { testConnection } = require('./db');
+  const { createSchema } = require('./schema');
+
+  const PORT = process.env.PORT || 3001;
+  app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
+
+  // Wait for DB with retries
+  let attempts = 0;
+  while (attempts < 30) {
+    const ok = await testConnection();
+    if (ok) {
+      try {
+        await createSchema();
+        dbReady = true;
+        console.log('Database ready.');
+      } catch (e) {
+        console.error('Schema creation failed:', e.message);
+      }
+      break;
+    }
+    attempts++;
+    console.log(`Waiting for database... (${attempts}/30)`);
+    await new Promise(r => setTimeout(r, 2000));
+  }
+  if (!dbReady) console.error('Could not connect to database after 30 attempts.');
+}
+
+start();

+ 30 - 2
docker-compose.yml

@@ -1,6 +1,24 @@
 version: '3.8'
 
 services:
+  mysql:
+    image: mysql:8.0
+    environment:
+      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpass}
+      MYSQL_DATABASE: ${MYSQL_DATABASE:-gestion_locative}
+      MYSQL_USER: ${MYSQL_USER:-gluser}
+      MYSQL_PASSWORD: ${MYSQL_PASSWORD:-glpass}
+    volumes:
+      - mysql_data:/var/lib/mysql
+    ports:
+      - "3306:3306"
+    healthcheck:
+      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-rootpass}"]
+      interval: 10s
+      timeout: 5s
+      retries: 10
+    restart: unless-stopped
+
   backend:
     build:
       context: ./backend
@@ -8,11 +26,18 @@ services:
     ports:
       - "3001:3001"
     volumes:
-      - ./backend/data:/app/data
       - ./backend/uploads:/app/uploads
     environment:
       - NODE_ENV=production
-      - JWT_SECRET=change_this_secret_in_production
+      - JWT_SECRET=${JWT_SECRET:-change_this_secret_in_production}
+      - DB_HOST=${DB_HOST:-mysql}
+      - DB_PORT=${DB_PORT:-3306}
+      - DB_USER=${DB_USER:-gluser}
+      - DB_PASSWORD=${DB_PASSWORD:-glpass}
+      - DB_NAME=${DB_NAME:-gestion_locative}
+    depends_on:
+      mysql:
+        condition: service_healthy
     restart: unless-stopped
 
   frontend:
@@ -26,3 +51,6 @@ services:
     depends_on:
       - backend
     restart: unless-stopped
+
+volumes:
+  mysql_data:

+ 2 - 0
frontend/src/App.jsx

@@ -12,6 +12,7 @@ import LeaseDetail from './pages/LeaseDetail'
 import Payments from './pages/Payments'
 import Charges from './pages/Charges'
 import Profile from './pages/Profile'
+import Setup from './pages/Setup'
 
 function PrivateRoute({ children }) {
   const { user, loading } = useAuth()
@@ -31,6 +32,7 @@ export default function App() {
       <BrowserRouter>
         <Toaster position="top-right" />
         <Routes>
+          <Route path="/setup" element={<Setup />} />
           <Route path="/login" element={<PublicRoute><Login /></PublicRoute>} />
           <Route path="/register" element={<PublicRoute><Register /></PublicRoute>} />
           <Route path="/" element={<PrivateRoute><Layout /></PrivateRoute>}>

+ 4 - 0
frontend/src/api.js

@@ -11,6 +11,10 @@ api.interceptors.request.use(config => {
 api.interceptors.response.use(
   res => res,
   err => {
+    if (err.response?.status === 503 && err.response?.data?.needsSetup) {
+      window.location.href = '/setup'
+      return Promise.reject(err)
+    }
     if (err.response?.status === 401 || err.response?.status === 403) {
       localStorage.removeItem('token')
       window.location.href = '/login'

+ 127 - 0
frontend/src/pages/Setup.jsx

@@ -0,0 +1,127 @@
+import { useState } from 'react'
+import api from '../api'
+import toast from 'react-hot-toast'
+import { useAuth } from '../context/AuthContext'
+import { useNavigate } from 'react-router-dom'
+
+export default function Setup() {
+  const { setUser } = useAuth()
+  const navigate = useNavigate()
+  const [step, setStep] = useState(1)
+  const [loading, setLoading] = useState(false)
+  const [form, setForm] = useState({
+    first_name: '',
+    last_name: '',
+    email: '',
+    password: '',
+    password2: '',
+    name: '',
+  })
+
+  const handleSubmit = async e => {
+    e.preventDefault()
+    if (form.password !== form.password2) {
+      toast.error('Les mots de passe ne correspondent pas')
+      return
+    }
+    setLoading(true)
+    try {
+      const name = form.name || `${form.first_name} ${form.last_name}`.trim()
+      const { data } = await api.post('/setup/complete', {
+        email: form.email,
+        password: form.password,
+        name,
+        first_name: form.first_name,
+        last_name: form.last_name,
+      })
+      localStorage.setItem('token', data.token)
+      setUser(data.user)
+      toast.success('Configuration terminée ! Bienvenue 🎉')
+      navigate('/')
+    } catch (err) {
+      toast.error(err.response?.data?.error || 'Erreur lors de la configuration')
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  return (
+    <div className="min-h-screen bg-gradient-to-br from-blue-50 to-blue-100 flex items-center justify-center p-4">
+      <div className="bg-white rounded-2xl shadow-xl w-full max-w-md p-8">
+        {/* Header */}
+        <div className="text-center mb-8">
+          <div className="text-5xl mb-3">🏠</div>
+          <h1 className="text-2xl font-bold text-gray-900">Gestion Locative</h1>
+          <p className="text-gray-500 mt-1">Configuration initiale</p>
+        </div>
+
+        {/* Steps */}
+        <div className="flex items-center justify-center gap-2 mb-8">
+          {[1, 2].map(s => (
+            <div key={s} className="flex items-center gap-2">
+              <div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold transition-colors ${step >= s ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-500'}`}>
+                {s}
+              </div>
+              {s < 2 && <div className={`w-12 h-1 rounded ${step > s ? 'bg-blue-600' : 'bg-gray-200'}`} />}
+            </div>
+          ))}
+        </div>
+
+        {step === 1 && (
+          <div className="space-y-4">
+            <div>
+              <h2 className="text-lg font-semibold text-gray-800 mb-1">Bienvenue !</h2>
+              <p className="text-sm text-gray-500">
+                La base de données MySQL est prête. Créez votre compte administrateur pour commencer.
+              </p>
+            </div>
+            <div className="bg-green-50 border border-green-200 rounded-xl p-4 flex items-center gap-3">
+              <span className="text-2xl">✅</span>
+              <div>
+                <p className="text-sm font-semibold text-green-800">Base de données connectée</p>
+                <p className="text-xs text-green-600">MySQL est opérationnel et le schéma a été initialisé.</p>
+              </div>
+            </div>
+            <button onClick={() => setStep(2)} className="btn-primary w-full">
+              Créer mon compte →
+            </button>
+          </div>
+        )}
+
+        {step === 2 && (
+          <form onSubmit={handleSubmit} className="space-y-4">
+            <h2 className="text-lg font-semibold text-gray-800">Compte administrateur</h2>
+            <div className="grid grid-cols-2 gap-3">
+              <div>
+                <label className="label">Prénom</label>
+                <input className="input" value={form.first_name} onChange={e => setForm(f => ({ ...f, first_name: e.target.value }))} placeholder="Jean" />
+              </div>
+              <div>
+                <label className="label">Nom</label>
+                <input className="input" value={form.last_name} onChange={e => setForm(f => ({ ...f, last_name: e.target.value }))} placeholder="Dupont" />
+              </div>
+            </div>
+            <div>
+              <label className="label">Email *</label>
+              <input className="input" type="email" required value={form.email} onChange={e => setForm(f => ({ ...f, email: e.target.value }))} placeholder="vous@exemple.fr" />
+            </div>
+            <div>
+              <label className="label">Mot de passe *</label>
+              <input className="input" type="password" required minLength={8} value={form.password} onChange={e => setForm(f => ({ ...f, password: e.target.value }))} placeholder="Minimum 8 caractères" />
+            </div>
+            <div>
+              <label className="label">Confirmer le mot de passe *</label>
+              <input className="input" type="password" required value={form.password2} onChange={e => setForm(f => ({ ...f, password2: e.target.value }))} placeholder="Répéter le mot de passe" />
+            </div>
+            <div className="flex gap-3 pt-2">
+              <button type="button" onClick={() => setStep(1)} className="btn-secondary flex-1">← Retour</button>
+              <button type="submit" disabled={loading} className="btn-primary flex-1">
+                {loading ? '⏳ Création…' : '✅ Terminer la configuration'}
+              </button>
+            </div>
+          </form>
+        )}
+      </div>
+    </div>
+  )
+}