Ship configurator updates with catalog DB included for server deploy.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
lejay
2026-10-07 17:13:03 +02:00
co-authored by Cursor
parent 6c0d7e47b8
commit 30921aade6
75 changed files with 15618 additions and 1300 deletions
+368
View File
@@ -0,0 +1,368 @@
/**
* 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;
}