jeremy пре 4 месеци
родитељ
комит
36e14d6b5e

+ 6 - 0
.env.example

@@ -0,0 +1,6 @@
+PGHOST=localhost
+PGPORT=5432
+PGUSER=postgres
+PGPASSWORD=postgres
+PGDATABASE=gestion_locative
+JWT_SECRET=change_this_secret_in_production

+ 1 - 1
backend/package.json

@@ -8,7 +8,7 @@
   },
   "dependencies": {
     "bcryptjs": "^2.4.3",
-    "better-sqlite3": "^9.4.3",
+    "pg": "^8.11.3",
     "cors": "^2.8.5",
     "express": "^4.18.2",
     "jsonwebtoken": "^9.0.2",

+ 154 - 164
backend/src/db.js

@@ -1,167 +1,157 @@
-const Database = require('better-sqlite3');
-const path = require('path');
-
-const db = new Database(path.join(__dirname, '../data/gestion_locative.db'));
-
-db.pragma('journal_mode = WAL');
-db.pragma('foreign_keys = ON');
-
-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
-  );
-
-  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)
-  );
-
-  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)
-  );
-
-  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)
-  );
-
-  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)
-  );
-
-  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)
-  );
-`);
-
-// 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 {}
+const { Pool } = require('pg');
+
+const pool = new Pool({
+  host: process.env.PGHOST || 'localhost',
+  port: parseInt(process.env.PGPORT || '5432'),
+  user: process.env.PGUSER || 'postgres',
+  password: process.env.PGPASSWORD || 'postgres',
+  database: process.env.PGDATABASE || 'gestion_locative',
 });
 
-// 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 charge_regularization column to payments if missing
-try { db.exec('ALTER TABLE payments ADD COLUMN charge_regularization REAL DEFAULT 0'); } catch {}
-
-// Prorata columns on payments
-try { db.exec('ALTER TABLE payments ADD COLUMN is_prorata INTEGER DEFAULT 0'); } catch {}
-try { db.exec('ALTER TABLE payments ADD COLUMN prorata_days INTEGER'); } catch {}
-try { db.exec('ALTER TABLE payments ADD COLUMN prorata_total_days INTEGER'); } catch {}
-
-// Regularizations from annual exercise 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,
-    property_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)
-  )
-`);
+async function initDb() {
+  await pool.query(`
+    CREATE TABLE IF NOT EXISTS users (
+      id SERIAL PRIMARY KEY,
+      email TEXT UNIQUE NOT NULL,
+      password TEXT NOT NULL,
+      name TEXT NOT NULL,
+      first_name TEXT,
+      last_name TEXT,
+      address TEXT,
+      phone TEXT,
+      created_at TIMESTAMP DEFAULT NOW()
+    );
+
+    CREATE TABLE IF NOT EXISTS owners (
+      id SERIAL PRIMARY KEY,
+      user_id INTEGER NOT NULL REFERENCES users(id),
+      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 TIMESTAMP DEFAULT NOW()
+    );
+
+    CREATE TABLE IF NOT EXISTS properties (
+      id SERIAL PRIMARY KEY,
+      user_id INTEGER NOT NULL REFERENCES users(id),
+      owner_id INTEGER REFERENCES owners(id),
+      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 TIMESTAMP DEFAULT NOW()
+    );
+
+    CREATE TABLE IF NOT EXISTS tenants (
+      id SERIAL PRIMARY KEY,
+      user_id INTEGER NOT NULL REFERENCES users(id),
+      first_name TEXT NOT NULL,
+      last_name TEXT NOT NULL,
+      email TEXT,
+      phone TEXT,
+      created_at TIMESTAMP DEFAULT NOW()
+    );
+
+    CREATE TABLE IF NOT EXISTS leases (
+      id SERIAL PRIMARY KEY,
+      user_id INTEGER NOT NULL REFERENCES users(id),
+      property_id INTEGER NOT NULL REFERENCES properties(id),
+      tenant_id INTEGER NOT NULL REFERENCES tenants(id),
+      owner_id INTEGER REFERENCES owners(id),
+      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 TIMESTAMP DEFAULT NOW()
+    );
+
+    CREATE TABLE IF NOT EXISTS payments (
+      id SERIAL PRIMARY KEY,
+      lease_id INTEGER NOT NULL REFERENCES leases(id),
+      user_id INTEGER NOT NULL REFERENCES users(id),
+      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,
+      charge_regularization REAL DEFAULT 0,
+      is_prorata INTEGER DEFAULT 0,
+      prorata_days INTEGER,
+      prorata_total_days INTEGER,
+      created_at TIMESTAMP DEFAULT NOW()
+    );
+
+    CREATE TABLE IF NOT EXISTS charges (
+      id SERIAL PRIMARY KEY,
+      user_id INTEGER NOT NULL REFERENCES users(id),
+      property_id INTEGER NOT NULL REFERENCES properties(id),
+      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,
+      recoverable INTEGER NOT NULL DEFAULT 1,
+      created_at TIMESTAMP DEFAULT NOW()
+    );
+
+    CREATE TABLE IF NOT EXISTS lease_revisions (
+      id SERIAL PRIMARY KEY,
+      lease_id INTEGER NOT NULL REFERENCES leases(id),
+      user_id INTEGER NOT NULL REFERENCES users(id),
+      effective_date TEXT NOT NULL,
+      rent_amount REAL NOT NULL,
+      charges_amount REAL NOT NULL DEFAULT 0,
+      notes TEXT,
+      created_at TIMESTAMP DEFAULT NOW()
+    );
+
+    CREATE TABLE IF NOT EXISTS charge_regularizations (
+      id SERIAL PRIMARY KEY,
+      lease_id INTEGER NOT NULL REFERENCES leases(id),
+      user_id INTEGER NOT NULL REFERENCES users(id),
+      property_id INTEGER NOT NULL,
+      year INTEGER NOT NULL,
+      amount REAL NOT NULL,
+      notes TEXT,
+      applied_payment_id INTEGER,
+      created_at TIMESTAMP DEFAULT NOW()
+    );
+  `);
+
+  // Idempotent migrations for columns added after initial schema
+  await pool.query(`ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name TEXT`);
+  await pool.query(`ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name TEXT`);
+  await pool.query(`ALTER TABLE users ADD COLUMN IF NOT EXISTS address TEXT`);
+  await pool.query(`ALTER TABLE users ADD COLUMN IF NOT EXISTS phone TEXT`);
+  await pool.query(`ALTER TABLE leases ADD COLUMN IF NOT EXISTS owner_id INTEGER REFERENCES owners(id)`);
+  await pool.query(`ALTER TABLE charges ADD COLUMN IF NOT EXISTS recoverable INTEGER NOT NULL DEFAULT 1`);
+  await pool.query(`ALTER TABLE payments ADD COLUMN IF NOT EXISTS charge_regularization REAL DEFAULT 0`);
+  await pool.query(`ALTER TABLE payments ADD COLUMN IF NOT EXISTS is_prorata INTEGER DEFAULT 0`);
+  await pool.query(`ALTER TABLE payments ADD COLUMN IF NOT EXISTS prorata_days INTEGER`);
+  await pool.query(`ALTER TABLE payments ADD COLUMN IF NOT EXISTS prorata_total_days INTEGER`);
+}
+
+initDb().catch(err => {
+  console.error('Database initialization error:', err);
+  process.exit(1);
+});
 
-module.exports = db;
+module.exports = pool;

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

@@ -1,7 +1,7 @@
 const express = require('express');
 const bcrypt = require('bcryptjs');
 const jwt = require('jsonwebtoken');
-const db = require('../db');
+const pool = require('../db');
 const { JWT_SECRET } = require('../auth');
 
 const router = express.Router();
@@ -13,12 +13,15 @@ router.post('/register', async (req, res) => {
 
   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 pool.query(
+      'INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING id',
+      [email, hashed, name]
+    );
+    const id = result.rows[0].id;
+    const token = jwt.sign({ userId: id }, JWT_SECRET, { expiresIn: '7d' });
+    res.json({ token, user: { id, email, name } });
   } catch (err) {
-    if (err.message.includes('UNIQUE')) return res.status(409).json({ error: 'Email déjà utilisé' });
+    if (err.code === '23505') return res.status(409).json({ error: 'Email déjà utilisé' });
     res.status(500).json({ error: err.message });
   }
 });
@@ -28,7 +31,7 @@ router.post('/login', async (req, res) => {
   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 pool.query('SELECT * FROM users WHERE email = $1', [email])).rows[0];
   if (!user) return res.status(401).json({ error: 'Identifiants incorrects' });
 
   const valid = await bcrypt.compare(password, user.password);
@@ -38,18 +41,24 @@ router.post('/login', async (req, res) => {
   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', require('../auth').authMiddleware, async (req, res) => {
+  const user = (await pool.query(
+    'SELECT id, email, name, first_name, last_name, address, phone FROM users WHERE id = $1',
+    [req.userId]
+  )).rows[0];
   res.json(user);
 });
 
-router.put('/profile', require('../auth').authMiddleware, (req, res) => {
+router.put('/profile', require('../auth').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 pool.query(
+    `UPDATE users SET first_name=$1, last_name=$2, address=$3, phone=$4, email=$5, name=$6 WHERE id=$7`,
+    [first_name || null, last_name || null, address || null, phone || null, email, name, req.userId]
+  );
+  const user = (await pool.query(
+    'SELECT id, email, name, first_name, last_name, address, phone FROM users WHERE id = $1',
+    [req.userId]
+  )).rows[0];
   res.json(user);
 });
 

+ 104 - 96
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 pool = require('../db');
 const { authMiddleware } = require('../auth');
 
 const router = express.Router();
@@ -32,44 +32,44 @@ 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;
+  const params = [req.userId];
   let query = `
     SELECT c.*, p.name as property_name
     FROM charges c
     JOIN properties p ON c.property_id = p.id
-    WHERE c.user_id = ?
+    WHERE c.user_id = $1
   `;
-  const params = [req.userId];
-  if (property_id) { query += ' AND c.property_id = ?'; params.push(property_id); }
-  if (year) { query += ' AND c.year = ?'; params.push(year); }
+  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 pool.query(query, params)).rows);
 });
 
 // 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 pool.query('SELECT id FROM properties WHERE id = $1 AND user_id = $2', [property_id, req.userId])).rows[0];
   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 pool.query(`
     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);
+    VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id
+  `, [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.rows[0].id });
 });
 
 // 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 pool.query('SELECT * FROM charges WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   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(`
-    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);
+  await pool.query(`
+    UPDATE charges SET category=$1, label=$2, amount=$3, date=$4, year=$5, invoice_path=$6, invoice_name=$7, notes=$8, recoverable=$9
+    WHERE id=$10
+  `, [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 pool.query('SELECT * FROM charges WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   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 pool.query('DELETE FROM charges WHERE id = $1', [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 pool.query('SELECT * FROM charges WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   if (!charge || !charge.invoice_path) return res.status(404).json({ error: 'Facture non trouvée' });
 
   const filePath = path.join(uploadDir, charge.invoice_path);
@@ -122,58 +122,58 @@ 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 pool.query('SELECT * FROM properties WHERE id = $1 AND user_id = $2', [property_id, req.userId])).rows[0];
   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 pool.query(`
     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);
+    FROM charges WHERE property_id = $1 AND year = $2 AND user_id = $3 AND recoverable = 1
+  `, [property_id, year, req.userId])).rows[0];
 
   // Total non-recoverable (landlord's own charges, for info)
-  const totalNonRecovRow = db.prepare(`
+  const totalNonRecovRow = (await pool.query(`
     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);
+    FROM charges WHERE property_id = $1 AND year = $2 AND user_id = $3 AND recoverable = 0
+  `, [property_id, year, req.userId])).rows[0];
 
   const totalChargesReelles = totalChargesRow?.total || 0;
   const totalNonRecoverable = totalNonRecovRow?.total || 0;
 
   // Charges by category (recoverable only)
-  const chargesByCategory = db.prepare(`
+  const chargesByCategory = (await pool.query(`
     SELECT category, SUM(amount) as total
-    FROM charges WHERE property_id = ? AND year = ? AND user_id = ? AND recoverable = 1
+    FROM charges WHERE property_id = $1 AND year = $2 AND user_id = $3 AND recoverable = 1
     GROUP BY category ORDER BY total DESC
-  `).all(property_id, year, req.userId);
+  `, [property_id, year, req.userId])).rows;
 
   // Non-recoverable by category (for info)
-  const chargesNonRecovByCategory = db.prepare(`
+  const chargesNonRecovByCategory = (await pool.query(`
     SELECT category, SUM(amount) as total
-    FROM charges WHERE property_id = ? AND year = ? AND user_id = ? AND recoverable = 0
+    FROM charges WHERE property_id = $1 AND year = $2 AND user_id = $3 AND recoverable = 0
     GROUP BY category ORDER BY total DESC
-  `).all(property_id, year, req.userId);
+  `, [property_id, year, req.userId])).rows;
 
   // All leases on this property (active or that were active during the year)
-  const leases = db.prepare(`
+  const leases = (await pool.query(`
     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`);
+    WHERE l.property_id = $1 AND l.user_id = $2
+      AND (l.start_date <= $3 AND (l.end_date IS NULL OR l.end_date >= $4))
+  `, [property_id, req.userId, `${year}-12-31`, `${year}-01-01`])).rows;
 
   // For each lease, get provisions perçues for the year
-  const leaseBilans = leases.map(lease => {
-    const provisions = db.prepare(`
+  const leaseBilans = await Promise.all(leases.map(async lease => {
+    const provisions = (await pool.query(`
       SELECT SUM(charges_paid) as total
       FROM payments
-      WHERE lease_id = ? AND period_year = ?
-    `).get(lease.id, year);
+      WHERE lease_id = $1 AND period_year = $2
+    `, [lease.id, year])).rows[0];
 
     return {
       lease_id: lease.id,
@@ -182,7 +182,7 @@ router.get('/bilan/:property_id/:year', (req, res) => {
       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);
@@ -217,19 +217,19 @@ router.get('/bilan/:property_id/:year', (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 pool.query('SELECT * FROM properties WHERE id = $1 AND user_id = $2', [property_id, req.userId])).rows[0];
   if (!prop) return res.status(403).json({ error: 'Bien non autorisé' });
 
   // Chercher le propriétaire lié au bail actif sur ce bien, sinon fallback profil gestionnaire
-  const ownerRow = db.prepare(`
+  const ownerRow = (await pool.query(`
     SELECT o.type as o_type, o.first_name, o.last_name, o.company_name, o.siret,
            o.address, o.zip_code, o.city, o.email, o.phone
     FROM leases l
     JOIN owners o ON l.owner_id = o.id
-    WHERE l.property_id = ? AND l.user_id = ? AND o.id IS NOT NULL
+    WHERE l.property_id = $1 AND l.user_id = $2 AND o.id IS NOT NULL
     ORDER BY l.active DESC, l.start_date DESC
     LIMIT 1
-  `).get(property_id, req.userId);
+  `, [property_id, req.userId])).rows[0];
 
   let ownerFullName, ownerAddress, ownerEmail, ownerPhone, ownerSiret;
   if (ownerRow) {
@@ -241,7 +241,7 @@ router.get('/bilan/:property_id/:year/pdf', async (req, res) => {
     ownerEmail = ownerRow.email;
     ownerPhone = ownerRow.phone;
   } else {
-    const u = db.prepare('SELECT name, first_name, last_name, email, address, phone FROM users WHERE id = ?').get(req.userId);
+    const u = (await pool.query('SELECT name, first_name, last_name, email, address, phone FROM users WHERE id = $1', [req.userId])).rows[0];
     ownerFullName = (u.first_name && u.last_name) ? `${u.first_name} ${u.last_name}` : u.name;
     ownerSiret = null;
     ownerAddress = u.address;
@@ -249,39 +249,39 @@ router.get('/bilan/:property_id/:year/pdf', async (req, res) => {
     ownerPhone = u.phone;
   }
 
-  const totalChargesRow = db.prepare(`
+  const totalChargesRow = (await pool.query(`
     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);
+    WHERE property_id = $1 AND year = $2 AND user_id = $3 AND recoverable = 1
+  `, [property_id, year, req.userId])).rows[0];
   const totalChargesReelles = totalChargesRow?.total || 0;
 
-  const chargesList = db.prepare(`
-    SELECT * FROM charges WHERE property_id = ? AND year = ? AND user_id = ?
+  const chargesList = (await pool.query(`
+    SELECT * FROM charges WHERE property_id = $1 AND year = $2 AND user_id = $3
     ORDER BY date ASC
-  `).all(property_id, year, req.userId);
+  `, [property_id, year, req.userId])).rows;
 
-  const chargesByCategory = db.prepare(`
+  const chargesByCategory = (await pool.query(`
     SELECT category, SUM(amount) as total FROM charges
-    WHERE property_id = ? AND year = ? AND user_id = ? AND recoverable = 1
+    WHERE property_id = $1 AND year = $2 AND user_id = $3 AND recoverable = 1
     GROUP BY category ORDER BY total DESC
-  `).all(property_id, year, req.userId);
+  `, [property_id, year, req.userId])).rows;
 
-  const leases = db.prepare(`
+  const leases = (await pool.query(`
     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`);
+    WHERE l.property_id = $1 AND l.user_id = $2
+      AND (l.start_date <= $3 AND (l.end_date IS NULL OR l.end_date >= $4))
+  `, [property_id, req.userId, `${year}-12-31`, `${year}-01-01`])).rows;
 
-  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);
+  const leaseBilans = await Promise.all(leases.map(async lease => {
+    const provisions = (await pool.query(`SELECT SUM(charges_paid) as total FROM payments WHERE lease_id = $1 AND period_year = $2`, [lease.id, year])).rows[0];
     return {
       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);
@@ -474,45 +474,46 @@ router.get('/bilan/:property_id/:year/pdf', async (req, res) => {
 });
 
 // Close annual exercise — create charge_regularization 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 pool.query('SELECT * FROM properties WHERE id = $1 AND user_id = $2', [property_id, req.userId])).rows[0];
   if (!prop) return res.status(403).json({ error: 'Bien non autorisé' });
 
   // Check not already closed for this property/year
-  const alreadyClosed = db.prepare(
-    'SELECT id FROM charge_regularizations WHERE user_id = ? AND property_id = ? AND year = ?'
-  ).get(req.userId, property_id, parseInt(year));
+  const alreadyClosed = (await pool.query(
+    'SELECT id FROM charge_regularizations WHERE user_id = $1 AND property_id = $2 AND year = $3',
+    [req.userId, property_id, parseInt(year)]
+  )).rows[0];
   if (alreadyClosed) return res.status(409).json({ error: `L'exercice ${year} a déjà été clôturé pour ce bien.` });
 
   // Recompute bilan (recoverable only)
-  const totalRow = 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 totalRow = (await pool.query(
+    'SELECT SUM(amount) as total FROM charges WHERE property_id = $1 AND year = $2 AND user_id = $3 AND recoverable = 1',
+    [property_id, year, req.userId]
+  )).rows[0];
   const totalCharges = totalRow?.total || 0;
 
-  const leases = db.prepare(`
+  const leases = (await pool.query(`
     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`);
+    WHERE l.property_id = $1 AND l.user_id = $2
+      AND (l.start_date <= $3 AND (l.end_date IS NULL OR l.end_date >= $4))
+  `, [property_id, req.userId, `${year}-12-31`, `${year}-01-01`])).rows;
 
   if (leases.length === 0) return res.status(400).json({ error: 'Aucun locataire sur cette période.' });
 
-  const leaseBilans = leases.map(lease => {
-    const prov = db.prepare('SELECT SUM(charges_paid) as total FROM payments WHERE lease_id = ? AND period_year = ?').get(lease.id, year);
+  const leaseBilans = await Promise.all(leases.map(async lease => {
+    const prov = (await pool.query('SELECT SUM(charges_paid) as total FROM payments WHERE lease_id = $1 AND period_year = $2', [lease.id, year])).rows[0];
     return { lease_id: lease.id, tenant: `${lease.first_name} ${lease.last_name}`, provisions: prov?.total || 0 };
-  });
+  }));
   const totalProvisions = leaseBilans.reduce((s, l) => s + l.provisions, 0);
 
-  const insert = db.prepare(
-    'INSERT INTO charge_regularizations (lease_id, user_id, property_id, year, amount, notes) VALUES (?, ?, ?, ?, ?, ?)'
-  );
-
-  const closeAll = db.transaction(() => {
-    return leaseBilans.map(l => {
+  const client = await pool.connect();
+  try {
+    await client.query('BEGIN');
+    const results = [];
+    for (const l of leaseBilans) {
       const ratio = totalProvisions > 0 ? l.provisions / totalProvisions : 1 / leaseBilans.length;
       const quote_part = totalCharges * ratio;
       // Positive = trop-perçu (credit tenant), Negative = complément dû (debit tenant)
@@ -520,13 +521,20 @@ router.post('/bilan/:property_id/:year/close', (req, res) => {
       const label = trop_percu >= 0
         ? `Régularisation charges ${year} : trop-perçu de ${trop_percu.toFixed(2)} € à déduire`
         : `Régularisation charges ${year} : complément de ${Math.abs(trop_percu).toFixed(2)} € à appeler`;
-      insert.run(l.lease_id, req.userId, parseInt(property_id), parseInt(year), trop_percu, label);
-      return { lease_id: l.lease_id, tenant: l.tenant, trop_percu };
-    });
-  });
-
-  const results = closeAll();
-  res.json({ success: true, year: parseInt(year), property: prop.name, regularizations: results });
+      await client.query(
+        'INSERT INTO charge_regularizations (lease_id, user_id, property_id, year, amount, notes) VALUES ($1, $2, $3, $4, $5, $6)',
+        [l.lease_id, req.userId, parseInt(property_id), parseInt(year), trop_percu, label]
+      );
+      results.push({ lease_id: l.lease_id, tenant: l.tenant, trop_percu });
+    }
+    await client.query('COMMIT');
+    res.json({ success: true, year: parseInt(year), property: prop.name, regularizations: results });
+  } catch (e) {
+    await client.query('ROLLBACK');
+    throw e;
+  } finally {
+    client.release();
+  }
 });
 
 module.exports = router;

+ 44 - 43
backend/src/routes/leases.js

@@ -1,12 +1,12 @@
 const express = require('express');
-const db = require('../db');
+const pool = require('../db');
 const { authMiddleware } = require('../auth');
 
 const router = express.Router();
 router.use(authMiddleware);
 
-router.get('/', (req, res) => {
-  const rows = db.prepare(`
+router.get('/', async (req, res) => {
+  const rows = (await pool.query(`
     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,
@@ -18,14 +18,14 @@ router.get('/', (req, res) => {
     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 = ?
+    WHERE l.user_id = $1
     ORDER BY l.start_date DESC
-  `).all(req.userId);
+  `, [req.userId])).rows;
   res.json(rows);
 });
 
-router.get('/:id', (req, res) => {
-  const row = db.prepare(`
+router.get('/:id', async (req, res) => {
+  const row = (await pool.query(`
     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,
@@ -37,65 +37,66 @@ router.get('/:id', (req, res) => {
     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);
+    WHERE l.id = $1 AND l.user_id = $2
+  `, [req.params.id, req.userId])).rows[0];
   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, 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' });
 
-  const prop = db.prepare('SELECT id FROM properties WHERE id = ? AND user_id = ?').get(property_id, req.userId);
+  const prop = (await pool.query('SELECT id FROM properties WHERE id = $1 AND user_id = $2', [property_id, req.userId])).rows[0];
   if (!prop) return res.status(403).json({ error: 'Bien non autorisé' });
 
-  const result = db.prepare(`
+  const result = await pool.query(`
     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 });
+    VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id
+  `, [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.rows[0].id });
 });
 
-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 pool.query('SELECT id FROM leases WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   if (!lease) return res.status(404).json({ error: 'Bail non trouvé' });
   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 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,
+  await pool.query(`
+    UPDATE leases SET property_id=COALESCE($1,property_id), tenant_id=COALESCE($2,tenant_id),
+      owner_id=$3, start_date=COALESCE($4,start_date), end_date=$5,
+      rent_amount=$6, charges_amount=$7, deposit_amount=$8, payment_day=$9, active=$10
+    WHERE id=$11
+  `, [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);
+    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 pool.query('SELECT id FROM leases WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   if (!lease) return res.status(404).json({ error: 'Bail non trouvé' });
-  db.prepare('DELETE FROM leases WHERE id = ?').run(req.params.id);
+  await pool.query('DELETE FROM leases WHERE id = $1', [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 pool.query('SELECT id FROM leases WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   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 pool.query(
+    'SELECT * FROM lease_revisions WHERE lease_id = $1 ORDER BY effective_date DESC',
+    [req.params.id]
+  )).rows;
   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 pool.query('SELECT * FROM leases WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   if (!lease) return res.status(404).json({ error: 'Bail non trouvé' });
 
   const { effective_date, rent_amount, charges_amount, notes } = req.body;
@@ -103,26 +104,26 @@ router.post('/:id/revisions', (req, res) => {
     return res.status(400).json({ error: 'Date d\'effet et nouveau loyer requis' });
 
   // Save revision history entry
-  const result = db.prepare(`
+  const result = await pool.query(`
     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);
+    VALUES ($1, $2, $3, $4, $5, $6) RETURNING id
+  `, [req.params.id, req.userId, effective_date, parseFloat(rent_amount), parseFloat(charges_amount || 0), notes || null]);
 
   // If effective_date <= today, apply immediately to lease
   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 pool.query('UPDATE leases SET rent_amount=$1, charges_amount=$2 WHERE id=$3',
+      [parseFloat(rent_amount), parseFloat(charges_amount || 0), req.params.id]);
   }
 
-  res.status(201).json({ id: result.lastInsertRowid });
+  res.status(201).json({ id: result.rows[0].id });
 });
 
 // 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 pool.query('SELECT id FROM leases WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   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 pool.query('DELETE FROM lease_revisions WHERE id = $1 AND lease_id = $2', [req.params.revId, req.params.id]);
   res.json({ success: true });
 });
 

+ 23 - 23
backend/src/routes/owners.js

@@ -1,55 +1,55 @@
 const express = require('express');
-const db = require('../db');
+const pool = 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 = ?
+router.get('/', async (req, res) => {
+  const rows = (await pool.query(`
+    SELECT * FROM owners WHERE user_id = $1
     ORDER BY COALESCE(company_name, last_name), first_name
-  `).all(req.userId);
+  `, [req.userId])).rows;
   res.json(rows);
 });
 
-router.post('/', (req, res) => {
+router.post('/', async (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(`
+  const result = await pool.query(`
     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,
+    VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id
+  `, [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 });
+    email || null, phone || null]);
+  res.status(201).json({ id: result.rows[0].id });
 });
 
-router.put('/:id', (req, res) => {
-  const owner = db.prepare('SELECT id FROM owners WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+router.put('/:id', async (req, res) => {
+  const owner = (await pool.query('SELECT id FROM owners WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   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,
+  await pool.query(`
+    UPDATE owners SET type=$1, first_name=$2, last_name=$3, company_name=$4, siret=$5,
+      address=$6, zip_code=$7, city=$8, email=$9, phone=$10
+    WHERE id=$11
+  `, [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);
+    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);
+router.delete('/:id', async (req, res) => {
+  const owner = (await pool.query('SELECT id FROM owners WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   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);
+  const linked = (await pool.query('SELECT id FROM leases WHERE owner_id = $1 LIMIT 1', [req.params.id])).rows[0];
   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);
+  await pool.query('DELETE FROM owners WHERE id = $1', [req.params.id]);
   res.json({ success: true });
 });
 

+ 48 - 41
backend/src/routes/payments.js

@@ -1,11 +1,11 @@
 const express = require('express');
-const db = require('../db');
+const pool = 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,
@@ -15,93 +15,100 @@ router.get('/', (req, res) => {
     JOIN leases l ON p.lease_id = l.id
     JOIN properties pr ON l.property_id = pr.id
     JOIN tenants t ON l.tenant_id = t.id
-    WHERE p.user_id = ?
+    WHERE p.user_id = $1
   `;
   const params = [req.userId];
-  if (lease_id) { query += ' AND p.lease_id = ?'; params.push(lease_id); }
+  if (lease_id) { query += ' AND p.lease_id = $2'; params.push(lease_id); }
   query += ' ORDER BY p.period_year DESC, p.period_month DESC';
-  res.json(db.prepare(query).all(...params));
+  res.json((await pool.query(query, params)).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, charge_regularization,
           is_prorata, prorata_days, prorata_total_days } = 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 pool.query('SELECT id FROM leases WHERE id = $1 AND user_id = $2', [lease_id, req.userId])).rows[0];
   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 pool.query(
+    'SELECT id FROM payments WHERE lease_id = $1 AND period_month = $2 AND period_year = $3',
+    [lease_id, period_month, period_year]
+  )).rows[0];
   if (existing) return res.status(409).json({ error: 'Un paiement existe déjà pour cette période' });
 
   const reg = parseFloat(charge_regularization || 0);
 
-  const result = db.prepare(`
+  const result = await pool.query(`
     INSERT INTO payments (lease_id, user_id, period_month, period_year, rent_paid, charges_paid, payment_date, payment_method, notes, charge_regularization, is_prorata, prorata_days, prorata_total_days)
-    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
-  `).run(lease_id, req.userId, period_month, period_year, rent_paid, charges_paid || 0,
+    VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id
+  `, [lease_id, req.userId, period_month, period_year, rent_paid, charges_paid || 0,
     payment_date, payment_method || 'virement', notes || null, reg,
-    is_prorata ? 1 : 0, prorata_days || null, prorata_total_days || null);
+    is_prorata ? 1 : 0, prorata_days || null, prorata_total_days || null]);
+
+  const paymentId = result.rows[0].id;
 
   // Mark regularization as applied
   if (reg !== 0) {
-    db.prepare(`
-      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);
+    await pool.query(`
+      UPDATE charge_regularizations SET applied_payment_id = $1
+      WHERE id = (
+        SELECT id FROM charge_regularizations
+        WHERE lease_id = $2 AND applied_payment_id IS NULL
+        ORDER BY created_at ASC LIMIT 1
+      )
+    `, [paymentId, lease_id]);
   }
 
-  res.status(201).json({ id: result.lastInsertRowid });
+  res.status(201).json({ id: paymentId });
 });
 
-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 pool.query('SELECT id FROM payments WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   if (!payment) return res.status(404).json({ error: 'Paiement non trouvé' });
-  db.prepare('DELETE FROM payments WHERE id = ?').run(req.params.id);
+  await pool.query('DELETE FROM payments WHERE id = $1', [req.params.id]);
   res.json({ success: true });
 });
 
-router.put('/:id', (req, res) => {
-  const payment = db.prepare('SELECT id FROM payments WHERE id = ? AND user_id = ?').get(req.params.id, req.userId);
+router.put('/:id', async (req, res) => {
+  const payment = (await pool.query('SELECT id FROM payments WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   if (!payment) return res.status(404).json({ error: 'Paiement non trouvé' });
 
   const { rent_paid, charges_paid, payment_date, payment_method, notes, is_prorata, prorata_days, prorata_total_days } = req.body;
   if (rent_paid === undefined || !payment_date) return res.status(400).json({ error: 'Champs requis manquants' });
 
-  db.prepare(`
-    UPDATE payments SET rent_paid = ?, charges_paid = ?, payment_date = ?, payment_method = ?, notes = ?,
-      is_prorata = ?, prorata_days = ?, prorata_total_days = ?
-    WHERE id = ?
-  `).run(rent_paid, charges_paid || 0, payment_date, payment_method || 'virement', notes || null,
-    is_prorata ? 1 : 0, prorata_days || null, prorata_total_days || null, req.params.id);
+  await pool.query(`
+    UPDATE payments SET rent_paid = $1, charges_paid = $2, payment_date = $3, payment_method = $4, notes = $5,
+      is_prorata = $6, prorata_days = $7, prorata_total_days = $8
+    WHERE id = $9
+  `, [rent_paid, charges_paid || 0, payment_date, payment_method || 'virement', notes || null,
+    is_prorata ? 1 : 0, prorata_days || null, prorata_total_days || null, req.params.id]);
 
   res.json({ success: true });
 });
 
 // Pending 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 pool.query('SELECT id FROM leases WHERE id = $1 AND user_id = $2', [req.params.lease_id, req.userId])).rows[0];
   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 pool.query(
+    'SELECT * FROM charge_regularizations WHERE lease_id = $1 AND applied_payment_id IS NULL ORDER BY year DESC',
+    [req.params.lease_id]
+  )).rows;
   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(`
+router.get('/stats', async (req, res) => {
+  const stats = (await pool.query(`
     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
-    FROM leases l WHERE l.user_id = ? AND l.active = 1
-  `).get(req.userId, req.userId);
+      (SELECT SUM(rent_paid + charges_paid) FROM payments WHERE user_id = $1 AND period_year = EXTRACT(YEAR FROM NOW()) AND period_month = EXTRACT(MONTH FROM NOW())) as current_month_collected
+    FROM leases l WHERE l.user_id = $2 AND l.active = 1
+  `, [req.userId, req.userId])).rows[0];
   res.json(stats);
 });
 

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

@@ -1,39 +1,41 @@
 const express = require('express');
-const db = require('../db');
+const pool = 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 pool.query('SELECT * FROM properties WHERE user_id = $1 ORDER BY created_at DESC', [req.userId])).rows;
   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 pool.query(
+    'INSERT INTO properties (user_id, name, address, city, zip_code, type, rooms, area) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id',
+    [req.userId, name, address, city, zip_code, type, rooms || null, area || null]
+  );
+  res.status(201).json({ id: result.rows[0].id });
 });
 
-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 pool.query('SELECT id FROM properties WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   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 pool.query(
+    'UPDATE properties SET name=$1, address=$2, city=$3, zip_code=$4, type=$5, rooms=$6, area=$7 WHERE id=$8',
+    [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 pool.query('SELECT id FROM properties WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   if (!prop) return res.status(404).json({ error: 'Bien non trouvé' });
-  db.prepare('DELETE FROM properties WHERE id = ?').run(req.params.id);
+  await pool.query('DELETE FROM properties WHERE id = $1', [req.params.id]);
   res.json({ success: true });
 });
 

+ 5 - 5
backend/src/routes/receipts.js

@@ -1,6 +1,6 @@
 const express = require('express');
 const PDFDocument = require('pdfkit');
-const db = require('../db');
+const pool = 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 pool.query(`
     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,
@@ -30,8 +30,8 @@ router.get('/:paymentId', (req, res) => {
     JOIN tenants t ON l.tenant_id = t.id
     JOIN users u ON p.user_id = u.id
     LEFT JOIN owners o ON l.owner_id = o.id
-    WHERE p.id = ? AND p.user_id = ?
-  `).get(req.params.paymentId, req.userId);
+    WHERE p.id = $1 AND p.user_id = $2
+  `, [req.params.paymentId, req.userId])).rows[0];
 
   if (!payment) return res.status(404).json({ error: 'Paiement non trouvé' });
 

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

@@ -1,37 +1,38 @@
 const express = require('express');
-const db = require('../db');
+const pool = 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 pool.query('SELECT * FROM tenants WHERE user_id = $1 ORDER BY last_name, first_name', [req.userId])).rows;
   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 pool.query(
+    'INSERT INTO tenants (user_id, first_name, last_name, email, phone) VALUES ($1, $2, $3, $4, $5) RETURNING id',
+    [req.userId, first_name, last_name, email || null, phone || null]
+  );
+  res.status(201).json({ id: result.rows[0].id });
 });
 
-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 pool.query('SELECT id FROM tenants WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   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 pool.query('UPDATE tenants SET first_name=$1, last_name=$2, email=$3, phone=$4 WHERE id=$5',
+    [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 pool.query('SELECT id FROM tenants WHERE id = $1 AND user_id = $2', [req.params.id, req.userId])).rows[0];
   if (!tenant) return res.status(404).json({ error: 'Locataire non trouvé' });
-  db.prepare('DELETE FROM tenants WHERE id = ?').run(req.params.id);
+  await pool.query('DELETE FROM tenants WHERE id = $1', [req.params.id]);
   res.json({ success: true });
 });
 

+ 22 - 1
docker-compose.yml

@@ -1,6 +1,18 @@
 version: '3.8'
 
 services:
+  postgres:
+    image: postgres:16-alpine
+    environment:
+      POSTGRES_DB: gestion_locative
+      POSTGRES_USER: postgres
+      POSTGRES_PASSWORD: postgres
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
+    ports:
+      - "5432:5432"
+    restart: unless-stopped
+
   backend:
     build:
       context: ./backend
@@ -8,11 +20,17 @@ services:
     ports:
       - "3001:3001"
     volumes:
-      - ./backend/data:/app/data
       - ./backend/uploads:/app/uploads
     environment:
       - NODE_ENV=production
       - JWT_SECRET=change_this_secret_in_production
+      - PGHOST=postgres
+      - PGPORT=5432
+      - PGUSER=postgres
+      - PGPASSWORD=postgres
+      - PGDATABASE=gestion_locative
+    depends_on:
+      - postgres
     restart: unless-stopped
 
   frontend:
@@ -26,3 +44,6 @@ services:
     depends_on:
       - backend
     restart: unless-stopped
+
+volumes:
+  postgres_data: