Initial commit

This commit is contained in:
Olivier CROGUENNEC
2026-06-13 14:57:15 +02:00
commit 48ed7fe65e
209 changed files with 49979 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
import { Router } from 'express';
import multer from 'multer';
import xlsx from 'xlsx';
import path from 'node:path';
import fs from 'node:fs';
import db from '../db/index.js';
import { HttpError } from '../middleware/errorHandler.js';
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.resolve('./uploads');
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
const upload = multer({ dest: UPLOAD_DIR, limits: { fileSize: 5 * 1024 * 1024 } });
const router = Router();
/* ── GET /api/garanties ──────────────────────────────────────── */
router.get('/', (req, res) => {
const rows = db.prepare(
'SELECT * FROM garantie_types WHERE user_id = ? ORDER BY ordre, id'
).all(req.user.id);
res.json(rows);
});
/* ── POST /api/garanties ─────────────────────────────────────── */
router.post('/', (req, res, next) => {
try {
const { libelle, description, ordre } = req.body || {};
if (!libelle?.trim()) throw new HttpError(400, 'libelle est requis');
const r = db.prepare(
'INSERT INTO garantie_types (user_id, libelle, description, ordre) VALUES (?,?,?,?)'
).run(req.user.id, libelle.trim(), description?.trim() || null, Number(ordre ?? 0));
res.status(201).json(db.prepare('SELECT * FROM garantie_types WHERE id = ?').get(r.lastInsertRowid));
} catch (e) { next(e); }
});
/* ── PUT /api/garanties/:id ──────────────────────────────────── */
router.put('/:id', (req, res, next) => {
try {
const row = db.prepare('SELECT id FROM garantie_types WHERE id = ? AND user_id = ?')
.get(req.params.id, req.user.id);
if (!row) throw new HttpError(404, 'Garantie introuvable');
const { libelle, description, ordre } = req.body || {};
if (!libelle?.trim()) throw new HttpError(400, 'libelle est requis');
db.prepare(
'UPDATE garantie_types SET libelle=?, description=?, ordre=? WHERE id=?'
).run(libelle.trim(), description?.trim() || null, Number(ordre ?? 0), req.params.id);
res.json(db.prepare('SELECT * FROM garantie_types WHERE id = ?').get(req.params.id));
} catch (e) { next(e); }
});
/* ── DELETE /api/garanties/:id ───────────────────────────────── */
router.delete('/:id', (req, res, next) => {
try {
const row = db.prepare('SELECT id FROM garantie_types WHERE id = ? AND user_id = ?')
.get(req.params.id, req.user.id);
if (!row) throw new HttpError(404, 'Garantie introuvable');
db.prepare('DELETE FROM garantie_types WHERE id = ?').run(req.params.id);
res.json({ deleted: true });
} catch (e) { next(e); }
});
/* ── POST /api/garanties/import ─────────────────────────────────
Accepte un fichier .xlsx / .csv / .json
Colonnes reconnues : libelle (obligatoire), description, ordre
Comportement : upsert sur libelle (insensible à la casse)
─────────────────────────────────────────────────────────────── */
router.post('/import', upload.single('file'), (req, res, next) => {
try {
if (!req.file) throw new HttpError(400, 'Aucun fichier reçu');
const ext = path.extname(req.file.originalname).toLowerCase();
let rows;
if (ext === '.json') {
const content = fs.readFileSync(req.file.path, 'utf8');
let parsed;
try { parsed = JSON.parse(content); } catch {
throw new HttpError(400, 'Fichier JSON invalide');
}
rows = Array.isArray(parsed) ? parsed
: Array.isArray(parsed?.garanties) ? parsed.garanties
: null;
if (!rows) throw new HttpError(400, 'Le JSON doit être un tableau ou contenir une clé "garanties"');
} else {
const wb = xlsx.readFile(req.file.path, { cellDates: true });
const ws = wb.Sheets[wb.SheetNames[0]];
rows = xlsx.utils.sheet_to_json(ws, { defval: null, raw: false });
}
try { fs.unlinkSync(req.file.path); } catch { /* noop */ }
if (!rows.length) return res.json({ inserted: 0, updated: 0, skipped: 0, errors: [] });
// Normalise les clés (insensible à la casse et aux espaces)
const norm = (obj) => {
const out = {};
for (const [k, v] of Object.entries(obj)) out[k.toLowerCase().trim()] = v;
return out;
};
let inserted = 0, updated = 0, skipped = 0;
const errors = [];
const tx = db.transaction(() => {
for (let i = 0; i < rows.length; i++) {
const r = norm(rows[i]);
const libelle = String(r.libelle ?? r['libellé'] ?? r.label ?? '').trim();
if (!libelle) { skipped++; errors.push({ row: i + 2, error: 'libelle vide — ligne ignorée' }); continue; }
const description = String(r.description ?? r.desc ?? '').trim() || null;
const ordre = parseInt(r.ordre ?? r.order ?? 0, 10) || 0;
const existing = db.prepare(
'SELECT id FROM garantie_types WHERE user_id = ? AND LOWER(libelle) = LOWER(?)'
).get(req.user.id, libelle);
if (existing) {
db.prepare(
'UPDATE garantie_types SET description=?, ordre=? WHERE id=?'
).run(description, ordre, existing.id);
updated++;
} else {
db.prepare(
'INSERT INTO garantie_types (user_id, libelle, description, ordre) VALUES (?,?,?,?)'
).run(req.user.id, libelle, description, ordre);
inserted++;
}
}
});
tx();
res.json({ inserted, updated, skipped, total: rows.length, errors: errors.slice(0, 20) });
} catch (e) { next(e); }
});
export default router;