Ship configurator updates with catalog DB included for server deploy. Co-authored-by: Cursor <[email protected]>
369 lines
17 KiB
JavaScript
369 lines
17 KiB
JavaScript
/**
|
||
* 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;
|
||
}
|