Mise en place des objectifs
This commit is contained in:
@@ -2222,4 +2222,23 @@ console.log('[DB] Migrations 2FA OK');
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Migration : table objectifs ──────────────────────────────────────────
|
||||
// Objectifs annuels par investisseur. `type` permet de réutiliser la table
|
||||
// pour d'autres natures d'objectifs plus tard (ex: 'rendement_annuel') —
|
||||
// pour l'instant seul 'versement_annuel' (objectif de dépôts nets) est utilisé.
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS objectifs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
investisseur_id INTEGER NOT NULL REFERENCES investisseurs(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL DEFAULT 'versement_annuel',
|
||||
annee INTEGER NOT NULL,
|
||||
montant REAL NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(investisseur_id, type, annee)
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_objectifs_investisseur ON objectifs(investisseur_id, type, annee)');
|
||||
|
||||
export default db;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Router } from 'express';
|
||||
import db from '../db/index.js';
|
||||
import { HttpError } from '../middleware/errorHandler.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const TYPE_DEFAUT = 'versement_annuel';
|
||||
|
||||
/** Valide et normalise le corps d'une requête POST */
|
||||
function parseBody(body) {
|
||||
const { investisseur_id, type = TYPE_DEFAUT, annee, montant, notes } = body;
|
||||
if (!investisseur_id) throw new HttpError(400, 'investisseur_id est requis');
|
||||
if (!type?.trim()) throw new HttpError(400, 'type est requis');
|
||||
if (!Number.isInteger(Number(annee)) || Number(annee) < 2000 || Number(annee) > 2100) {
|
||||
throw new HttpError(400, 'annee invalide');
|
||||
}
|
||||
if (montant === undefined || montant === null || Number.isNaN(Number(montant)) || Number(montant) < 0) {
|
||||
throw new HttpError(400, 'montant doit être un nombre positif');
|
||||
}
|
||||
return {
|
||||
investisseur_id: Number(investisseur_id),
|
||||
type: type.trim(),
|
||||
annee: Number(annee),
|
||||
montant: Number(montant),
|
||||
notes: notes?.trim() || null,
|
||||
};
|
||||
}
|
||||
|
||||
/* ── GET /api/objectifs?type=&annee= ─────────────────────────────────────
|
||||
Retourne tous les objectifs des investisseurs de l'utilisateur connecté
|
||||
(toutes plateformes / tout le portefeuille — pas de notion de scope=all
|
||||
vs single ici, un objectif appartient toujours à un investisseur donné). */
|
||||
router.get('/', (req, res) => {
|
||||
const { type, annee } = req.query;
|
||||
const conds = ['i.user_id = ?'];
|
||||
const args = [req.user.id];
|
||||
if (type) { conds.push('o.type = ?'); args.push(type); }
|
||||
if (annee) { conds.push('o.annee = ?'); args.push(Number(annee)); }
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT o.*, i.nom AS investisseur_nom
|
||||
FROM objectifs o
|
||||
JOIN investisseurs i ON i.id = o.investisseur_id
|
||||
WHERE ${conds.join(' AND ')}
|
||||
ORDER BY o.annee DESC, i.nom
|
||||
`).all(...args);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
/* ── POST /api/objectifs ── upsert (investisseur_id, type, annee) ───────── */
|
||||
router.post('/', (req, res, next) => {
|
||||
try {
|
||||
const data = parseBody(req.body);
|
||||
const inv = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?')
|
||||
.get(data.investisseur_id, req.user.id);
|
||||
if (!inv) throw new HttpError(404, 'Investisseur introuvable');
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO objectifs (investisseur_id, type, annee, montant, notes)
|
||||
VALUES (?,?,?,?,?)
|
||||
ON CONFLICT(investisseur_id, type, annee)
|
||||
DO UPDATE SET montant = excluded.montant, notes = excluded.notes, updated_at = datetime('now')
|
||||
`).run(data.investisseur_id, data.type, data.annee, data.montant, data.notes);
|
||||
|
||||
const saved = db.prepare(`
|
||||
SELECT o.*, i.nom AS investisseur_nom
|
||||
FROM objectifs o JOIN investisseurs i ON i.id = o.investisseur_id
|
||||
WHERE o.investisseur_id = ? AND o.type = ? AND o.annee = ?
|
||||
`).get(data.investisseur_id, data.type, data.annee);
|
||||
res.status(201).json(saved);
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
/* ── DELETE /api/objectifs/:id ────────────────────────────────────────── */
|
||||
router.delete('/:id', (req, res, next) => {
|
||||
try {
|
||||
const existing = db.prepare(`
|
||||
SELECT o.id FROM objectifs o
|
||||
JOIN investisseurs i ON i.id = o.investisseur_id
|
||||
WHERE o.id = ? AND i.user_id = ?
|
||||
`).get(req.params.id, req.user.id);
|
||||
if (!existing) throw new HttpError(404, 'Objectif introuvable');
|
||||
db.prepare('DELETE FROM objectifs WHERE id = ?').run(req.params.id);
|
||||
res.json({ deleted: true });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -24,6 +24,7 @@ import importsRouter from './routes/imports.js';
|
||||
import pfuRouter from './routes/pfu.js';
|
||||
import notationRouter from './routes/notation.js';
|
||||
import garantiesRouter from './routes/garanties.js';
|
||||
import objectifsRouter from './routes/objectifs.js';
|
||||
import reinvestissementsRouter from './routes/reinvestissements.js';
|
||||
import correctionsRouter from './routes/corrections.js';
|
||||
import comptesRouter from './routes/comptes.js';
|
||||
@@ -117,6 +118,7 @@ app.use('/api/imports', requireAuth, importsRouter);
|
||||
app.use('/api/pfu', requireAuth, pfuRouter);
|
||||
app.use('/api/notation', requireAuth, notationRouter);
|
||||
app.use('/api/garanties', requireAuth, garantiesRouter);
|
||||
app.use('/api/objectifs', requireAuth, objectifsRouter);
|
||||
app.use('/api/reinvestissements', requireAuth, reinvestissementsRouter);
|
||||
app.use('/api/corrections', requireAuth, correctionsRouter);
|
||||
app.use('/api/comptes', requireAuth, comptesRouter);
|
||||
|
||||
Reference in New Issue
Block a user