Files
apex/shared/blumLegrabox.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

369 lines
17 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.
/**
* APEX 4.0 — Blum LEGRABOX pure
* Données d'intégration : débit panneaux 16 mm, perçage CNC caisson (coulisses),
* perçage façades (INSERTA/EXPANDO), norme suisse SMS, génération de commande (BOM)
* et accessoires intérieurs.
*
* Source : dossier « INTÉGRATION DATA - APEX 4.0 » (modules usinage CNC / SMS / BOM /
* accessoires) + catalogue technique Blum. Toutes les cotes en mm.
*/
/** Épaisseur de référence stricte des fonds et parois arrière de tiroir. */
export const LEGRABOX_PANEL_THICKNESS_MM = 16;
/**
* Systèmes de hauteur Legrabox (N/M/K/C/F).
* - sideHeightMm : hauteur de côté (profil métallique) — « hauteur_cote_mm »
* - nicheHeightMm : hauteur de niche / encombrement façade min — « hauteur_des_niches_mm »
* - backHeightMm : hauteur C de la paroi arrière 16 mm — « hauteur_C_mm_selon_systeme »
* - hch : code hauteur commercial (S/M/L/XL/XXL)
* - smsFromSixths : norme suisse SMS — disponible à partir de n/6 (1/6 = 127 mm)
*/
export const LEGRABOX_SYSTEMS = Object.freeze({
N: Object.freeze({ key: 'N', hch: 'S', sideHeightMm: 66, nicheHeightMm: 80, backHeightMm: 39, smsFromSixths: 1 }),
M: Object.freeze({ key: 'M', hch: 'M', sideHeightMm: 90, nicheHeightMm: 106, backHeightMm: 63, smsFromSixths: 1 }),
K: Object.freeze({ key: 'K', hch: 'L', sideHeightMm: 128, nicheHeightMm: 144, backHeightMm: 101, smsFromSixths: 2 }),
C: Object.freeze({ key: 'C', hch: 'XL', sideHeightMm: 177, nicheHeightMm: 193, backHeightMm: 148, smsFromSixths: 2 }),
F: Object.freeze({ key: 'F', hch: 'XXL', sideHeightMm: 240, nicheHeightMm: 257, backHeightMm: 212, smsFromSixths: 3 })
});
/** Hauteurs de côté sélectionnables (remplace l'ancien 60 mm par 66 mm — système N). */
export const LEGRABOX_SIDE_HEIGHTS_MM = Object.freeze([66, 90, 128, 177, 240]);
/** Norme suisse SMS — division en sixièmes. */
export const SMS = Object.freeze({
sixthMm: 127,
description: 'Système de mesure suisse (Schweizer Mass-System) — division en sixièmes (1/6 = 127 mm)'
});
/** @returns {object|null} système Legrabox pour une hauteur de côté (66/90/128/177/240). */
export function getLegraboxSystemForSideHeight(sideHeightMm) {
const h = Number(sideHeightMm);
for (const sys of Object.values(LEGRABOX_SYSTEMS)) {
if (sys.sideHeightMm === h) return sys;
}
return null;
}
/**
* Hauteur de niche (encombrement façade min) pour une hauteur de côté.
* Hauteur inconnue → valeur inchangée (compat anciennes configs).
*/
export function legraboxNicheHeightMm(sideHeightMm) {
const sys = getLegraboxSystemForSideHeight(sideHeightMm);
return sys ? sys.nicheHeightMm : Math.max(0, Number(sideHeightMm) || 0);
}
// ── 1. Débit panneaux — épaisseur 16 mm ─────────────────────────────────
/**
* Fond de tiroir 16 mm.
* longueur A = NL − 10 · largeur B = X − 35 (X = largeur libre du corps).
* Usinage spécial : feuillure/renfort 38 × 8 mm à gauche et à droite.
* @param {number} nominalLengthMm — longueur nominale NL de la coulisse
* @param {number} internalWidthMm — largeur libre du corps (X)
*/
export function legraboxBottomPanelMm(nominalLengthMm, internalWidthMm) {
const nl = Math.max(0, Number(nominalLengthMm) || 0);
const x = Math.max(0, Number(internalWidthMm) || 0);
if (nl <= 10 || x <= 35) return null;
return {
lengthMm: nl - 10,
widthMm: x - 35,
thicknessMm: LEGRABOX_PANEL_THICKNESS_MM,
rebate: {
widthMm: 38,
depthMm: 8,
sides: ['left', 'right'],
note: 'Fond feuilluré/renforcé à gauche et à droite : 38 × 8 mm'
}
};
}
/**
* Paroi arrière (dos de tiroir) 16 mm.
* largeur B = X − 38 · hauteur C selon système (N 39 / M 63 / K 101 / C 148 / F 212).
* @param {number} internalWidthMm — largeur libre du corps (X)
* @param {string} systemKey — 'N' | 'M' | 'K' | 'C' | 'F'
*/
export function legraboxBackPanelMm(internalWidthMm, systemKey) {
const x = Math.max(0, Number(internalWidthMm) || 0);
const sys = LEGRABOX_SYSTEMS[systemKey];
if (!sys || x <= 38) return null;
return {
widthMm: x - 38,
heightMm: sys.backHeightMm,
thicknessMm: LEGRABOX_PANEL_THICKNESS_MM
};
}
// ── 2. Perçage CNC — coulisses sur caisson (Système 32) ────────────────
/**
* Positions de perçage cumulées (axe X) en mm depuis le bord AVANT du montant,
* pour le vissage des coulisses, selon capacité de charge.
* Perçage pilote Système 32 : Ø5 × 13 mm (vis à tête plate recommandées par Blum).
*/
export const LEGRABOX_RAIL_DRILLING = Object.freeze({
compatibleSideHeightsMm: LEGRABOX_SIDE_HEIGHTS_MM,
holeDiameterMm: 5,
holeDepthMm: 13,
/**
* Axe vertical de la ligne de vissage du profil de corps, au-dessus du bas
* de la niche tiroir. Hypothèse atelier (catalogue Blum) — à confirmer au gabarit ZML.3710.
*/
screwLineAboveNicheBottomMm: 9.5,
byLoadKg: Object.freeze({
40: Object.freeze([37, 69, 197, 229, 261, 293, 357]),
70: Object.freeze([37, 69, 261, 293, 357])
})
});
/**
* Positions X (depuis le chant avant du panneau vertical) pour une capacité donnée,
* bornées à la profondeur utile du panneau.
* @param {40|70} loadKg
* @param {number} [maxXMm] — profondeur maximum perçable (ex. profondeur panneau − retrait)
*/
export function getLegraboxRailHolePositionsMm(loadKg = 40, maxXMm = Infinity) {
const base = LEGRABOX_RAIL_DRILLING.byLoadKg[Number(loadKg) === 70 ? 70 : 40];
const max = Number.isFinite(Number(maxXMm)) ? Number(maxXMm) : Infinity;
return base.filter((x) => x <= max);
}
// ── 3. Perçage CNC — façades (INSERTA / cheville expansible) ────────────
/**
* Matrice de perçage façade — fixation INSERTA (cheville expansible Ø10, prof. > 12 mm).
* Cotes catalogue conservées telles quelles (validation programmeur CNC) :
* - minFrontHeightMm (A) : encombrement min de la façade
* - coteC / coteD : positions verticales des 2 perçages du support, depuis le bas façade
* - coteB : rangées supplémentaires (façades hautes C / F), depuis le bas façade
* - coteE : retrait horizontal de l'axe depuis la référence côté tiroir
* - coteG : cote catalogue complémentaire (référence gabarit)
*/
export const LEGRABOX_FACADE_INSERTA = Object.freeze({
toolNote: 'Cheville expansible Ø 10 mm, profondeur de perçage > 12 mm',
holeDiameterMm: 10,
holeDepthMm: 13,
bySystem: Object.freeze({
N: Object.freeze({ minFrontHeightMm: 80, coteC: 45.5, coteD: 16, coteE: 23.4, coteG: 14 }),
M: Object.freeze({ minFrontHeightMm: 106, coteC: 51, coteD: 32, coteE: 23.4, coteG: 14 }),
K: Object.freeze({ minFrontHeightMm: 144, coteC: 51, coteD: 32, coteE: 23.4, coteG: 14 }),
C: Object.freeze({ minFrontHeightMm: 193, coteB: Object.freeze([147, 179]), coteC: 51, coteD: 32, coteE: 24.4, coteG: 14 }),
F: Object.freeze({ minFrontHeightMm: 257, coteB: Object.freeze([179, 211, 243]), coteC: 51, coteD: 32, coteE: 24.4, coteG: 14 })
})
});
/**
* Positions XY des perçages INSERTA sur une façade (repère : origine bas-gauche façade).
* Interprétation atelier : 2 perçages verticaux par côté (y = coteD bas, y = coteC haut,
* depuis le bas façade) + rangées coteB pour C/F. Axe horizontal à coteE de la
* référence côté de tiroir (bord de la largeur libre X), soit
* x = (largeur façade − X)/2 + coteE depuis chaque bord de façade.
* @param {string} systemKey — 'N'…'F'
* @param {number} facadeWidthMm
* @param {number} facadeHeightMm
* @param {number} internalWidthMm — largeur libre du corps X
* @returns {{ xMm:number, yMm:number, role:string }[]}
*/
export function buildLegraboxFacadeInsertaHolePositionsMm(systemKey, facadeWidthMm, facadeHeightMm, internalWidthMm) {
const spec = LEGRABOX_FACADE_INSERTA.bySystem[systemKey];
const W = Math.max(0, Number(facadeWidthMm) || 0);
const H = Math.max(0, Number(facadeHeightMm) || 0);
const X = Math.max(0, Number(internalWidthMm) || 0);
if (!spec || W <= 0 || H <= 0 || X <= 0 || W <= X - 2 * spec.coteE) return [];
const inset = Math.max(spec.coteG, (W - X) / 2 + spec.coteE);
const xs = [
{ xMm: Math.round(inset * 10) / 10, side: 'gauche' },
{ xMm: Math.round((W - inset) * 10) / 10, side: 'droite' }
];
const ys = [
{ yMm: spec.coteD, role: 'bas' },
{ yMm: spec.coteC, role: 'haut' },
...(Array.isArray(spec.coteB) ? spec.coteB.map((b, i) => ({ yMm: b, role: `rangee_B${i + 1}` })) : [])
].filter((p) => p.yMm > 0 && p.yMm < H);
const out = [];
for (const { xMm, side } of xs) {
for (const { yMm, role } of ys) {
out.push({ xMm, yMm, role: `${side}_${role}` });
}
}
return out;
}
// ── 4. Montage / ajustement ──────────────────────────────────────────────
export const LEGRABOX_ASSEMBLY = Object.freeze({
montageFacade: 'Sans outils',
demontageFacade: 'Tournevis',
reglageHauteur: "Depuis l'intérieur (devant)",
reglageInclinaison: 'Depuis le bas (devant)',
technologiesOuvertureFermeture: Object.freeze([
'Blumotion (Fermeture automatique avec amortissement)',
'Tip-on Blumotion (Ouverture automatique avec amortissement intégré)',
'Tip-on (Système d\u2019ouverture mécanique)'
])
});
// ── 5. Génération de commande (BOM) ─────────────────────────────────────
export const LEGRABOX_ORDER_OPTIONS = Object.freeze({
couleursChassis: Object.freeze([
'blanc soie',
'argent polaire',
'gris orion',
'noir terra',
'acier inoxydable (anti-empreintes digitales)'
]),
typesDeTiroir: Object.freeze([
'Type de façade du tiroir (Standard)',
'Type de tiroir intérieur (Anglais)'
]),
variantesOuvertureFermeture: Object.freeze(['Blumotion', 'Tip-On Blumotion', 'Tip-On']),
fixationsFacadeDisponibles: Object.freeze(['Vis (Douille d\u2019écartement)', 'Expan-do T']),
etatDeLivraison: Object.freeze(['F', 'C', 'S'])
});
/** Règles métier strictes (regles_automatiques_requises). */
export const LEGRABOX_BOM_RULES = Object.freeze({
/** Ajouter « Stabilisation de façade » si largeur_corps_mm >= 600. */
frontStabiliserMinBodyWidthMm: 600,
/** Ajouter « Unité de synchronisation » si variante Tip-On Blumotion. */
syncUnitVariant: 'tip_on_blumotion'
});
/**
* Longueurs nominales disponibles (mm) par système et capacité de charge.
* N absent du dossier BOM — longueurs issues du catalogue Blum (40 kg uniquement).
* Exception : K (hauteur L) indisponible en acier inoxydable.
*/
export const LEGRABOX_NOMINAL_LENGTHS_MM = Object.freeze({
N: Object.freeze({ 40: Object.freeze([400, 450, 500, 550]), 70: Object.freeze([]) }),
M: Object.freeze({ 40: Object.freeze([270, 300, 350, 400, 450, 500, 550, 600]), 70: Object.freeze([450, 500, 550, 600, 650]) }),
K: Object.freeze({ 40: Object.freeze([300, 350, 400, 450, 500, 550, 600]), 70: Object.freeze([450, 500, 550, 600]) }),
C: Object.freeze({ 40: Object.freeze([270, 300, 350, 400, 450, 500, 550, 600]), 70: Object.freeze([450, 500, 550, 600, 650]) }),
F: Object.freeze({ 40: Object.freeze([400, 450, 500, 550, 600]), 70: Object.freeze([450, 500, 550, 600, 650]) })
});
/** Couleur châssis indisponible : hauteur K (L) en acier inoxydable. */
export function isLegraboxChassisColorAvailable(systemKey, chassisColor) {
const isInox = String(chassisColor || '').toLowerCase().includes('inox');
return !(systemKey === 'K' && isInox);
}
/**
* Plus grande longueur nominale disponible pour un système / capacité, bornée par la
* profondeur utile (profondeur intérieure min = NL + 3 mm gérée par l'appelant).
* @returns {number|null}
*/
export function getLegraboxNominalLengthMm(systemKey, availableDepthMm, loadKg = 40) {
const table = LEGRABOX_NOMINAL_LENGTHS_MM[systemKey];
if (!table) return null;
const list = table[Number(loadKg) === 70 ? 70 : 40] || [];
const depth = Math.max(0, Number(availableDepthMm) || 0);
let best = null;
for (const nl of list) {
if (nl <= depth && (best == null || nl > best)) best = nl;
}
return best;
}
// ── 6. Accessoires et subdivisions intérieures (up-sell) ────────────────
export const LEGRABOX_INTERIOR_ACCESSORIES = Object.freeze({
garnituresUniversellesPlastique: Object.freeze({
label: 'Garniture universelle plastique',
nominalLengthCompatibleMm: 500,
couleurs: Object.freeze(['Anthracite pearl', 'Blanc grainé']),
modelesParHauteur: Object.freeze({
h40: Object.freeze({
hauteurMm: 40,
largeursCorpsCompatiblesMm: Object.freeze([600, 800, 900]),
note: 'Particulièrement adapté sous les champs d\u2019induction'
}),
h52: Object.freeze({
hauteurMm: 52,
largeursCorpsCompatiblesMm: Object.freeze([500, 600, 800, 900])
})
})
}),
assortimentsPorteCouverts: Object.freeze({
label: 'Assortiment porte-couverts modulaire',
typeFixation: 'Autocollant avec insert en feutre',
couleurs: Object.freeze(['Lie de vin', 'Anthracite']),
configurations: Object.freeze([
Object.freeze({ pieces: 50, composition: '12 couteaux, 12 fourchettes, 12 cuillères, 12 cuillères à café, 2 grandes pièces' }),
Object.freeze({ pieces: 70, composition: '12 couteaux, 12 fourchettes, 12 cuillères, 12 cuillères à café, 12 fourchettes de cuisine, 6 petites pièces, 4 grandes pièces' }),
Object.freeze({ pieces: 90, composition: '24 couteaux, 24 fourchettes, 12 cuillères, 12 cuillères à café, 12 fourchettes de cuisine, 6 grandes pièces' })
])
}),
garnituresReglablesBoisInox: Object.freeze({
label: 'Garniture réglable hêtre / acier inox',
finition: 'Hêtre / acier inox',
nominalLengthCompatibleMm: 500,
profondeurReelleMm: 478,
modeles: Object.freeze({
bacEtroit: Object.freeze({ largeurCorpsCibleMm: [300, 400], largeurReelleAjustableMm: [200, 300] }),
bacLarge: Object.freeze({ largeurCorpsCibleMm: [450, 700], largeurReelleAjustableMm: [350, 600] })
})
}),
rangeCouvertsBoisHetre: Object.freeze({
label: 'Range-couverts réglable hêtre (laqué)',
finition: 'Hêtre (laqué)',
nominalLengthsCompatiblesMm: Object.freeze([500, 550, 600, 650]),
/** Profondeur réelle = longueur nominale − 28 mm */
profondeurReelleDeductionMm: 28,
modeles: Object.freeze({
bacEtroit: Object.freeze({ largeurCorpsCibleMm: [300, 400], largeurReelleAjustableMm: [200, 300] }),
bacLarge: Object.freeze({ largeurCorpsCibleMm: [450, 700], largeurReelleAjustableMm: [350, 600] })
})
})
});
/**
* Suggestions d'accessoires intérieurs compatibles avec la géométrie du tiroir.
* @param {{ bodyWidthMm:number, nominalLengthMm:number }} params — largeur de corps (KB) et NL
* @returns {{ key:string, label:string, detail:string }[]}
*/
export function getLegraboxAccessorySuggestions({ bodyWidthMm, nominalLengthMm }) {
const kb = Math.max(0, Number(bodyWidthMm) || 0);
const nl = Math.max(0, Number(nominalLengthMm) || 0);
const out = [];
const A = LEGRABOX_INTERIOR_ACCESSORIES;
if (nl === A.garnituresUniversellesPlastique.nominalLengthCompatibleMm) {
for (const [k, m] of Object.entries(A.garnituresUniversellesPlastique.modelesParHauteur)) {
if (m.largeursCorpsCompatiblesMm.includes(kb)) {
out.push({
key: `garniture_plastique_${k}`,
label: `${A.garnituresUniversellesPlastique.label} h${m.hauteurMm} mm`,
detail: m.note || `Largeur corps ${kb} mm, NL ${nl} mm`
});
}
}
}
const inRange = ([min, max]) => kb >= min && kb <= max;
if (nl === A.garnituresReglablesBoisInox.nominalLengthCompatibleMm) {
if (inRange(A.garnituresReglablesBoisInox.modeles.bacEtroit.largeurCorpsCibleMm)) {
out.push({ key: 'garniture_bois_inox_etroit', label: `${A.garnituresReglablesBoisInox.label} — bac étroit`, detail: `Largeur réelle ajustable 200–300 mm` });
}
if (inRange(A.garnituresReglablesBoisInox.modeles.bacLarge.largeurCorpsCibleMm)) {
out.push({ key: 'garniture_bois_inox_large', label: `${A.garnituresReglablesBoisInox.label} — bac large`, detail: `Largeur réelle ajustable 350–600 mm` });
}
}
if (A.rangeCouvertsBoisHetre.nominalLengthsCompatiblesMm.includes(nl)) {
const prof = nl - A.rangeCouvertsBoisHetre.profondeurReelleDeductionMm;
if (inRange(A.rangeCouvertsBoisHetre.modeles.bacEtroit.largeurCorpsCibleMm)) {
out.push({ key: 'range_couverts_hetre_etroit', label: `${A.rangeCouvertsBoisHetre.label} — bac étroit`, detail: `Profondeur réelle ${prof} mm` });
}
if (inRange(A.rangeCouvertsBoisHetre.modeles.bacLarge.largeurCorpsCibleMm)) {
out.push({ key: 'range_couverts_hetre_large', label: `${A.rangeCouvertsBoisHetre.label} — bac large`, detail: `Profondeur réelle ${prof} mm` });
}
}
return out;
}