385 lines
13 KiB
JavaScript
385 lines
13 KiB
JavaScript
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 });
|
|
}
|
|
}
|