import pool, { run } from '../db/index.js'; const DEFAULT_ENGINEERING_RULES = [ { sort_order: 10, chapter: '1.A', title: "Hiérarchie d'assemblage (Côtés filants vs chapeau)", details: "Si hauteur >= 1200 mm: montage en Côtés filants (montants verticaux enferment fond et dessus).\n" + "Si hauteur < 1200 mm: montage en Chapeau (panneau dessus recouvre les montants)." }, { sort_order: 20, chapter: '1.B', title: 'Sens du fil du bois (variable sens_du_fil)', details: "Le matériau doit porter une variable booléenne sens_du_fil.\n" + 'Forcé vertical: portes, montants, joues.\n' + 'Forcé horizontal: tiroirs, tablettes/rayons, fonds/dessus.' }, { sort_order: 30, chapter: '2.A', title: 'Portée étagères anti-flexion', details: "Rayon libre mélaminé 19 mm <= 1200 mm.\n" + "Si > 1200 mm: forcer une séparation verticale OU imposer épaisseur 25 mm." }, { sort_order: 40, chapter: '2.A', title: 'Portes et gauchissement', details: 'Porte monobloc <= 2500 mm (hauteur) et <= 1000 mm (largeur).\n' + 'Au-delà: imposer automatiquement des tendeurs de redressement encastrés.' }, { sort_order: 50, chapter: '2.A', title: 'Socle et passage de pieds', details: "Recul face plinthe >= 50 mm par rapport à la façade.\n" + 'Calcul de référence: porte 19 + jeu 3 + retrait base 28 = 50 mm.\n' + "Avec plinthe 19 mm: centre pied Axilo à 69 mm des bords avant/arrière." }, { sort_order: 60, chapter: '2.B', title: "Tolérance jeu périphérique des façades", details: 'Façade (porte/tiroir) plus petite de 1.5 mm à 2 mm sur chaque bord.' }, { sort_order: 70, chapter: '2.B', title: "Tolérance jeu entre façades (fuge)", details: 'Jeu entre deux portes/tiroirs: 2 mm à 3 mm.' }, { sort_order: 80, chapter: '2.B', title: 'Jeu au sol porte descendante', details: 'Sans socle visible: déduire 10 mm à 15 mm en bas de la hauteur de porte.' }, { sort_order: 90, chapter: '2.C', title: 'Surcote de tablettage (doublage)', details: 'Prévoir du gras pour ajustement mur.\n' + 'Profondeur = ép. porte 19 + jeu butée 3 + profondeur caisse + surcote tablettage 20.' }, { sort_order: 100, chapter: '3', title: 'Placage chants - déduction CNC', details: 'La machine doit soustraire 2 mm par chant appliqué sur la coupe brute.' }, { sort_order: 110, chapter: '3', title: 'Placage chants - côté caisse / porte / face tiroir / plinthe', details: '4 chants. Ordre plaqueuse: 2 largeurs puis 2 longueurs.' }, { sort_order: 120, chapter: '3', title: 'Placage chants - fonds et dessus structure', details: '2 chants: longueurs avant et arrière.' }, { sort_order: 130, chapter: '3', title: 'Placage chants - rayons et séparations', details: '1 chant: bord avant.' }, { sort_order: 140, chapter: '3', title: 'Placage chants - joues de doublage', details: '3 chants: 2 largeurs puis chant avant sur toute la hauteur.' }, { sort_order: 150, chapter: '3', title: "Placage chants - dessus d'habillage (chapeau)", details: '3 chants: 2 largeurs puis chant avant sur la longueur.\n' + "Coupe à 45° avec petit plat 2x2 mm sur l'angle extérieur." }, { sort_order: 160, chapter: '3', title: 'Pièces sans chants', details: 'Dos 8 mm et fonds de tiroirs: 0 chant.' }, { sort_order: 170, chapter: '4.A', title: 'Mode accessibilité SIA 500', details: 'Option UI "Conception SIA 500".\n' + "Si activée: verrouiller la hauteur des éléments interactifs entre 800 mm et 1100 mm du sol." }, { sort_order: 180, chapter: '4.B', title: 'Penderie - profondeur cintres', details: 'Profondeur intérieure utile >= 600 mm (meuble total env. 647 mm).\n' + 'Si profondeur < 400 mm: désactiver le bouton "Ajouter penderie".' }, { sort_order: 190, chapter: '4.B', title: 'Penderie - hauteurs libres', details: 'Vestes courtes >= 1100 mm. Manteaux longs >= 1600 mm.' }, { sort_order: 200, chapter: '4.C', title: 'Tiroirs intérieurs - hauteur maximale', details: "Interdire un module tiroir au-delà de 1400 mm du sol (ligne d'épaule)." }, { sort_order: 210, chapter: '4.C', title: 'Tiroirs intérieurs - hauteurs de façades', details: 'Variantes autorisées: 60, 90, 128, 177, 240 mm.' }, { sort_order: 220, chapter: '4.C', title: 'Calepinage tiroirs', details: 'H façade = [H ouverture - (nb jeux * dimension jeu)] / nb tiroirs.' }, { sort_order: 230, chapter: '4.C', title: "Tiroirs à l'anglaise (cales de compensation)", details: "Si tiroir derrière porte à charnières: générer une cale (ép. 25 mm x prof. int., hauteur = hauteur bloc tiroirs).\n" + "La cale doit être dans la même couleur que l'intérieur." }, { sort_order: 240, chapter: '5.A', title: 'Mode cuisine - standards de largeur', details: 'Snapping des largeurs sur multiples de 60 cm (EN) ou 55 cm (SMS).\n' + 'Meubles bas: caisse 720/780 mm, plan fini 860-920 mm.\n' + 'Meubles hauts: profondeur 350 mm, crédence 500-600 mm (650 mm gaz), hauteurs 360/480/600/720/900 mm.\n' + 'Colonnes alignées aux meubles hauts: 2070/2150/2330 mm.' }, { sort_order: 250, chapter: '5.B', title: 'Meubles hauts suspendus - quincaillerie et usinages', details: 'Ferrure droite 290.54.900 et gauche 290.54.901.\n' + 'Entaille haute: 120x12 mm, prof. 25 mm, à 50 mm depuis le haut (bord arrière).\n' + 'Entaille basse: 120x12 mm, prof. 68 mm, à 50 mm du bord arrière.\n' + 'Perçages réglage face intérieure: axe 110 mm; +22 mm et -10 mm; profondeur 10 mm; à 60 mm du bord arrière.' }, { sort_order: 260, chapter: '5.C', title: 'Gestion thermique fours/frigos - règle absolue du dos', details: 'Ne jamais générer de dos 8 mm ni rainure derrière four/réfrigérateur.\n' + "Prévoir un vide arrière d'au moins 50 mm." }, { sort_order: 270, chapter: '5.C', title: 'Aération réfrigérateurs', details: 'Plinthe >= 100 mm pour la grille.\n' + 'Générer 1 grille basse (plinthe) + 1 grille haute (bandeau), découpe 457x92 mm.\n' + 'Références: Inox 959.10.000, Blanc 959.10.001, Pin 959.10.002, Acajou 959.10.003, Noyer 959.10.004.' }, { sort_order: 280, chapter: '5.C', title: 'Porte frigo - règle logistique', details: "Ne pas générer ni facturer les accessoires de liaison porte armoire/porte frigo (fournis avec l'électroménager)." }, { sort_order: 290, chapter: '5.D', title: 'Dimensions de niches appareils', details: 'Fours EN 600: standard 590/560/550, compact 450/560/550.\n' + 'Fours SMS 550: standard 762/512/550, compact 381 ou 508 / 512 / 550.\n' + 'Doubles fours: séparation fixe 19 mm obligatoire.\n' + 'Plaque cuisson: protéger le tiroir inférieur avec panneau horizontal + vide ventilé 20 mm.\n' + "Sous-évier: dos partiel (traverses 150 mm haut/bas) + option fond de protection aluminium." }, { sort_order: 300, chapter: '6.A', title: 'Système 32 - axe charnière à 103 mm', details: "Pour réversibilité: centre embase à 105 mm, déduction jeu façade 2 mm => axe perçage porte à 103 mm du haut/bas.\n" + 'Répartition intermédiaire: arrondir au multiple de 32 mm.' }, { sort_order: 310, chapter: '6.B', title: 'Matrice quantité charnières (épaisseur 19 mm)', details: '<=900 mm ou <=6 kg: 2 charnières.\n' + '901-1600 mm ou <=17 kg: 3.\n' + '1601-2000 mm ou <=20 kg: 4.\n' + '2001-2400 mm ou <=22 kg: 5.\n' + '2401-2600 mm: 6.' }, { sort_order: 320, chapter: '7.A', title: 'Sécurité LRFP - risque de basculement', details: "Si hauteur > 3 x profondeur: générer la ferrure murale obligatoire (Réf 988.98.010) et lever une alerte UI." }, { sort_order: 330, chapter: '7.A', title: 'Éclairage LED (NIN)', details: 'Le transformateur ne doit pas être enfermé derrière le dos.\n' + 'Usinage profilés LED: rainure 14x9 mm + perçage traversant Ø8 mm en extrémité.' }, { sort_order: 340, chapter: '7.A', title: 'Accès technique et pièces humides', details: 'Option "caisson amovible": découpe fenêtre dans le dos 8 mm selon cotes.\n' + 'Pièces humides: imposer colle PU étanche pour chants ABS et électricité IP44.' }, { sort_order: 350, chapter: '7.B', title: 'E-commerce Suisse - conformité légale', details: 'OChim: matériaux codés E1/CARB2 + badge "Swiss Quality / Faible émission E1".\n' + 'ODoB: stocker Nom commercial, Essence (nom latin), Pays, Label et afficher sur facture.\n' + 'OIP: affichage prix TTC 8.1% en direct.\n' + 'TAR: si électronique incluse, afficher "TVA et TAR incluses".\n' + 'LCD: afficher Impressum complet en pied de page.\n' + 'PCI-DSS: passerelle certifiée (Datatrans/Twint/Stripe), aucune donnée bancaire hébergée par APEX.' } ]; async function ensureEngineeringRulesTable() { await run(`CREATE TABLE IF NOT EXISTS engineering_rules ( id INTEGER PRIMARY KEY AUTOINCREMENT, sort_order INTEGER NOT NULL, chapter TEXT NOT NULL, title TEXT NOT NULL, details TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) )`); } export async function ensureEngineeringRulesSeed() { await ensureEngineeringRulesTable(); const { rows } = await pool.query('SELECT COUNT(*) AS total FROM engineering_rules'); const total = Number(rows?.[0]?.total || 0); if (total > 0) return; for (const rule of DEFAULT_ENGINEERING_RULES) { await run( 'INSERT INTO engineering_rules (sort_order, chapter, title, details) VALUES (?, ?, ?, ?)', [rule.sort_order, rule.chapter, rule.title, rule.details] ); } } export async function getEngineeringRules(req, res) { try { await ensureEngineeringRulesSeed(); const { rows } = await pool.query( 'SELECT id, sort_order, chapter, title, details, created_at, updated_at FROM engineering_rules ORDER BY sort_order ASC, id ASC' ); res.json(rows); } catch (err) { res.status(500).json({ error: err.message }); } } export async function createEngineeringRule(req, res) { try { await ensureEngineeringRulesTable(); const { sort_order, chapter, title, details } = req.body || {}; if (!chapter || !title || !details) { return res.status(400).json({ error: 'chapter, title et details sont requis' }); } const normalizedSort = Number.isFinite(Number(sort_order)) ? Number(sort_order) : 9999; await run( 'INSERT INTO engineering_rules (sort_order, chapter, title, details) VALUES (?, ?, ?, ?)', [normalizedSort, String(chapter).trim(), String(title).trim(), String(details).trim()] ); const { rows } = await pool.query( 'SELECT id, sort_order, chapter, title, details, created_at, updated_at FROM engineering_rules ORDER BY id DESC LIMIT 1' ); res.status(201).json(rows[0]); } catch (err) { res.status(500).json({ error: err.message }); } } export async function updateEngineeringRule(req, res) { try { await ensureEngineeringRulesTable(); const { id } = req.params; const { sort_order, chapter, title, details } = req.body || {}; const existing = await pool.query('SELECT id FROM engineering_rules WHERE id = ?', [id]); if (existing.rows.length === 0) { return res.status(404).json({ error: 'Règle non trouvée' }); } if (!chapter || !title || !details) { return res.status(400).json({ error: 'chapter, title et details sont requis' }); } const normalizedSort = Number.isFinite(Number(sort_order)) ? Number(sort_order) : 9999; await run( `UPDATE engineering_rules SET sort_order = ?, chapter = ?, title = ?, details = ?, updated_at = datetime('now') WHERE id = ?`, [normalizedSort, String(chapter).trim(), String(title).trim(), String(details).trim(), id] ); const { rows } = await pool.query( 'SELECT id, sort_order, chapter, title, details, created_at, updated_at FROM engineering_rules WHERE id = ?', [id] ); res.json(rows[0]); } catch (err) { res.status(500).json({ error: err.message }); } } export async function deleteEngineeringRule(req, res) { try { await ensureEngineeringRulesTable(); const { id } = req.params; const existing = await pool.query('SELECT id FROM engineering_rules WHERE id = ?', [id]); if (existing.rows.length === 0) { return res.status(404).json({ error: 'Règle non trouvée' }); } await run('DELETE FROM engineering_rules WHERE id = ?', [id]); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }