Files
apex/server/routes/engineeringRules.js
lejayandCursor 30921aade6 v2.1
Ship configurator updates with catalog DB included for server deploy.

Co-authored-by: Cursor <[email protected]>
2026-10-07 17:13:03 +02:00

399 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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' +
'AXILO 78 Press-fit réf. 637.76.710: centre avant à 59 mm du chant avant (plinthe finie 19 + 40), ' +
'centre arrière à 50 mm du chant arrière; 4 perçages borgnes Ø10 × 12 mm par platine, entraxe 64 mm.\n' +
'Répartition: 4 pieds aux extrémités de l\'armoire + 2 pieds (avant/arrière) tous les 600 mm.'
},
{
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 côté (Blum Legrabox)',
details:
'Hauteurs de côté autorisées: 66 (N), 90 (M), 128 (K), 177 (C), 240 (F) mm.\n' +
'Hauteurs de niche SMS correspondantes: 80, 106, 144, 193, 257 mm (à partir de 1/6, 1/6, 2/6, 2/6, 3/6 — 1/6 = 127 mm).'
},
{
sort_order: 215,
chapter: '4.C',
title: 'Tiroirs Legrabox - débit 16 mm et perçages CNC',
details:
'Fond 16 mm: L = NL − 10, l = X − 35, feuillure 38 × 8 mm G+D (X = largeur libre corps).\n' +
'Paroi arrière 16 mm: l = X − 38, h = 39/63/101/148/212 mm selon N/M/K/C/F.\n' +
'Coulisses (Système 32, depuis chant avant): 40 kg → 37, 69, 197, 229, 261, 293, 357 · 70 kg → 37, 69, 261, 293, 357.\n' +
'Façade INSERTA: cheville expansible Ø 10 mm, profondeur > 12 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 });
}
}