502 lines
19 KiB
JavaScript
502 lines
19 KiB
JavaScript
/**
|
||
* Moteur de calcul de prix — configurateur armoires sur mesure.
|
||
* Règles : panneaux entiers (chute intégrée), blocs, kit, options.
|
||
* Specs APEX 4.0 importées de shared/manufacturing.js
|
||
*/
|
||
|
||
import { PANEL, DRAWERS, NICHE, getHingeCountForDoor } from '../../shared/manufacturing.js';
|
||
|
||
function numberOr(value, fallback) {
|
||
const n = typeof value === 'number' ? value : Number(value);
|
||
return Number.isFinite(n) ? n : fallback;
|
||
}
|
||
|
||
/**
|
||
* Calcule la surface utile des éléments visibles (portes, fileurs, bandeau, plinthe, côtés doublage).
|
||
* Épaisseur panneaux 19 mm (référence APEX 4.0).
|
||
* @param {Object} config - configuration (installation, width, height, depth, plinthHeightMm, blocks, doorType, slidingPanels)
|
||
* @param {number} panelThicknessMm - épaisseur panneaux (19)
|
||
*/
|
||
function calculateVisibleSurface(config, panelThicknessMm = PANEL.structure) {
|
||
const { installationType, widthMm, heightMm, depthMm, plinthHeightMm, blocks = [], doorType, slidingPanelsCount } = config;
|
||
const isNiche = installationType === 'niche';
|
||
let surface = 0;
|
||
|
||
// Portes (surface face visible)
|
||
if (doorType === 'sliding' && slidingPanelsCount) {
|
||
const panelWidth = widthMm / slidingPanelsCount;
|
||
surface += slidingPanelsCount * (panelWidth * (heightMm - plinthHeightMm)) * 2; // deux faces par vantail
|
||
} else {
|
||
const doorsCount = blocks.length <= 1 ? Math.min(2, Math.ceil((blocks[0]?.widthMm || widthMm) / 600)) : blocks.length * 2;
|
||
const doorWidth = widthMm / Math.max(1, doorsCount);
|
||
surface += doorsCount * (doorWidth * (heightMm - plinthHeightMm)) * 2;
|
||
}
|
||
|
||
// Plinthe (face avant)
|
||
surface += widthMm * plinthHeightMm;
|
||
|
||
// Bandeau et fileurs si niche
|
||
if (isNiche) {
|
||
surface += widthMm * plinthHeightMm; // bandeau haut
|
||
surface += 2 * plinthHeightMm * (heightMm - plinthHeightMm); // fileurs gauche/droite
|
||
}
|
||
|
||
// Côtés de doublage (coulissant)
|
||
if (doorType === 'sliding') {
|
||
surface += 2 * (depthMm * (heightMm - plinthHeightMm));
|
||
}
|
||
|
||
return surface / 1_000_000; // m²
|
||
}
|
||
|
||
/**
|
||
* Nombre de panneaux entiers nécessaires (arrondi supérieur).
|
||
* Coût = nombre_panneaux × prix_panneau.
|
||
*/
|
||
function panelCost(surfaceM2, panelWidthMm, panelHeightMm, pricePerPanelChf) {
|
||
const panelSurfaceM2 = (panelWidthMm * panelHeightMm) / 1_000_000;
|
||
const count = Math.ceil(surfaceM2 / panelSurfaceM2);
|
||
return { panels: Math.max(0, count), costChf: count * pricePerPanelChf };
|
||
}
|
||
|
||
/**
|
||
* Find the optimal dimension variant that minimizes cost (fewest panels needed).
|
||
* @param {number} surfaceM2 - Required surface in square meters
|
||
* @param {Array} dimensions - Array of dimension variants [{width_mm, height_mm, thickness_mm, id}]
|
||
* @param {number} pricePerPanel - Price per panel in CHF
|
||
* @returns {Object} - Optimal dimension with cost details and alternatives
|
||
*/
|
||
function findOptimalDimension(surfaceM2, dimensions, pricePerPanel) {
|
||
if (!dimensions || dimensions.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
const price = pricePerPanel || 0;
|
||
const results = [];
|
||
|
||
for (const dim of dimensions) {
|
||
if (!dim.width_mm || !dim.height_mm) continue;
|
||
|
||
const panelSurfaceM2 = (dim.width_mm * dim.height_mm) / 1_000_000;
|
||
const panelsNeeded = Math.ceil(surfaceM2 / panelSurfaceM2);
|
||
const totalCost = panelsNeeded * price;
|
||
const totalSurface = panelsNeeded * panelSurfaceM2;
|
||
const wasteM2 = totalSurface - surfaceM2;
|
||
const wastePercent = (wasteM2 / totalSurface) * 100;
|
||
|
||
results.push({
|
||
dimension: {
|
||
id: dim.id,
|
||
width_mm: dim.width_mm,
|
||
height_mm: dim.height_mm,
|
||
thickness_mm: dim.thickness_mm
|
||
},
|
||
panelsNeeded,
|
||
costChf: totalCost,
|
||
panelSurfaceM2,
|
||
wasteM2: Math.round(wasteM2 * 1000) / 1000,
|
||
wastePercent: Math.round(wastePercent * 10) / 10
|
||
});
|
||
}
|
||
|
||
// Sort by cost (lowest first), then by waste (lowest first)
|
||
results.sort((a, b) => {
|
||
if (a.costChf !== b.costChf) return a.costChf - b.costChf;
|
||
return a.wasteM2 - b.wasteM2;
|
||
});
|
||
|
||
if (results.length === 0) return null;
|
||
|
||
const optimal = results[0];
|
||
const alternatives = results.slice(1);
|
||
|
||
return {
|
||
selectedDimension: optimal.dimension,
|
||
panelsNeeded: optimal.panelsNeeded,
|
||
costChf: optimal.costChf,
|
||
wasteM2: optimal.wasteM2,
|
||
wastePercent: optimal.wastePercent,
|
||
alternatives: alternatives.map(alt => ({
|
||
dimension: alt.dimension,
|
||
panelsNeeded: alt.panelsNeeded,
|
||
costChf: alt.costChf,
|
||
wasteM2: alt.wasteM2,
|
||
wastePercent: alt.wastePercent
|
||
}))
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Calculate module costs (V2 mode)
|
||
* @param {Array} modules - Array of module configurations
|
||
* @param {Array} moduleCatalog - Available modules from database
|
||
* @returns {Object} - Module costs breakdown
|
||
*/
|
||
function calculateModuleCosts(modules, moduleCatalog) {
|
||
let totalChf = 0;
|
||
const breakdown = [];
|
||
|
||
for (const module of modules) {
|
||
const catalogModule = moduleCatalog.find(m => m.id === module.moduleTypeId || m.code === module.moduleCode);
|
||
const basePrice = catalogModule?.base_price_chf || 450;
|
||
|
||
// Adjust price based on customizations
|
||
let adjustedPrice = basePrice;
|
||
|
||
// Extra shelves cost (beyond default)
|
||
const defaultShelves = catalogModule?.shelves_count || 0;
|
||
const extraShelves = Math.max(0, (module.shelves || 0) - defaultShelves);
|
||
adjustedPrice += extraShelves * 25; // 25 CHF per extra shelf
|
||
|
||
// Extra drawers cost (beyond default)
|
||
const defaultDrawers = catalogModule?.drawers_count || 0;
|
||
const extraDrawers = Math.max(0, (module.drawers || 0) - defaultDrawers);
|
||
adjustedPrice += extraDrawers * 45; // 45 CHF per extra drawer
|
||
|
||
// Width adjustment (if different from default)
|
||
const defaultWidth = catalogModule?.width_mm || 600;
|
||
const widthDiff = (module.widthMm || defaultWidth) - defaultWidth;
|
||
if (widthDiff > 0) {
|
||
adjustedPrice += widthDiff * 0.15; // 0.15 CHF per extra mm
|
||
}
|
||
|
||
breakdown.push({
|
||
moduleCode: module.moduleCode || catalogModule?.code,
|
||
moduleName: catalogModule?.name || 'Module',
|
||
basePrice,
|
||
adjustedPrice: Math.round(adjustedPrice * 100) / 100,
|
||
adjustments: {
|
||
extraShelves,
|
||
extraDrawers,
|
||
widthDiff
|
||
}
|
||
});
|
||
|
||
totalChf += adjustedPrice;
|
||
}
|
||
|
||
return {
|
||
totalChf: Math.round(totalChf * 100) / 100,
|
||
breakdown
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Calculate hardware costs (V2)
|
||
* @param {Object} hardware - Hardware selection {handleVariantId, hingeVariantId, drawerSystemId}
|
||
* @param {Object} hardwareData - Hardware data from DB {handles, hinges, blum}
|
||
* @param {Object} config - Config with door and drawer counts
|
||
* @returns {Object} - Hardware costs breakdown
|
||
*/
|
||
function calculateHardwareCosts(hardware, hardwareData, config) {
|
||
const { handles = [], hinges = [], blum = [] } = hardwareData;
|
||
const breakdown = [];
|
||
let totalChf = 0;
|
||
|
||
// Handle costs - one per door
|
||
if (hardware.handleVariantId) {
|
||
const handle = handles.find(h => h.id === hardware.handleVariantId);
|
||
if (handle) {
|
||
const doorCount = config.doorType === 'sliding'
|
||
? (config.slidingPanelsCount || 2)
|
||
: (config.modules?.length || config.blocks?.length || 2);
|
||
const cost = handle.price_chf * doorCount;
|
||
totalChf += cost;
|
||
breakdown.push({
|
||
type: 'handle',
|
||
name: handle.name,
|
||
unitPrice: handle.price_chf,
|
||
quantity: doorCount,
|
||
totalChf: cost
|
||
});
|
||
}
|
||
}
|
||
|
||
// Hinge costs - dynamic quantity from APEX 4.0 matrix
|
||
if (hardware.hingeVariantId && config.doorType !== 'sliding') {
|
||
const hinge = hinges.find(h => h.id === hardware.hingeVariantId);
|
||
if (hinge) {
|
||
const doorCount = config.modules?.length || config.blocks?.length || 2;
|
||
const doorHeightMm = (config.heightMm || 2400) - (config.plinthHeightMm || 100) - 4;
|
||
const hingesPerDoor = getHingeCountForDoor({ heightMm: doorHeightMm });
|
||
const quantity = doorCount * hingesPerDoor;
|
||
const cost = hinge.price_chf * quantity;
|
||
totalChf += cost;
|
||
breakdown.push({
|
||
type: 'hinge',
|
||
name: hinge.name,
|
||
unitPrice: hinge.price_chf,
|
||
quantity,
|
||
totalChf: cost
|
||
});
|
||
}
|
||
}
|
||
|
||
// Drawer system costs - one per drawer
|
||
if (hardware.drawerSystemId) {
|
||
const system = blum.find(h => h.id === hardware.drawerSystemId && h.type === 'drawer_system');
|
||
if (system) {
|
||
const modules = config.modules || [];
|
||
const drawerCount = modules.reduce((sum, m) => sum + (m.drawers || 0), 0);
|
||
if (drawerCount > 0) {
|
||
const cost = system.price_chf * drawerCount;
|
||
totalChf += cost;
|
||
breakdown.push({
|
||
type: 'drawer_system',
|
||
name: system.name,
|
||
unitPrice: system.price_chf,
|
||
quantity: drawerCount,
|
||
totalChf: cost
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
totalChf: Math.round(totalChf * 100) / 100,
|
||
breakdown
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Valide la configuration et calcule le prix total.
|
||
* @param {Object} config - configuration client
|
||
* @param {Object} data - données DB (settings, blockTypes, finishes, serviceOptions, modules, hardware)
|
||
*/
|
||
export function calculatePrice(config, data) {
|
||
const errors = [];
|
||
const settingsArr = data.settings || [];
|
||
const settings = {};
|
||
for (const r of settingsArr) {
|
||
let v = r.value;
|
||
if (typeof v === 'string' && /^-?\d+(\.\d+)?$/.test(v)) v = Number(v);
|
||
settings[r.key] = v;
|
||
}
|
||
const kitFeeChf = numberOr(settings.kit_preparation_fee_chf, 350);
|
||
const plinthMin = numberOr(settings.plinth_height_min_mm, 60);
|
||
const plinthMax = numberOr(settings.plinth_height_max_mm, 160);
|
||
const slidingMinWidth = numberOr(settings.sliding_doors_min_width_mm, 1200);
|
||
const allowSlidingDoors = String(settings.allow_sliding_doors ?? '0') === '1';
|
||
const standardHandleAxisMm = numberOr(settings.standard_handle_axis_mm, 1000);
|
||
const standardHandleToleranceMm = numberOr(settings.standard_handle_tolerance_mm, 200);
|
||
const sanitaryGapMm = numberOr(settings.sanitary_gap_mm, NICHE.sanitaryGapAround);
|
||
|
||
const {
|
||
installationType, widthMm, heightMm, depthMm, plinthHeightMm,
|
||
blocks = [], modules = [], hardware = {},
|
||
doorType, slidingPanelsCount, finishId, serviceOptionIds = [],
|
||
columnMode: columnModeRaw
|
||
} = config;
|
||
const columnMode = columnModeRaw === 'block' ? 'block' : 'separations';
|
||
|
||
// Determine if using modules (V2) or blocks (legacy)
|
||
const useModules = modules && modules.length > 0;
|
||
|
||
// Plinthe 60–160 mm
|
||
if (plinthHeightMm < plinthMin || plinthHeightMm > plinthMax) {
|
||
errors.push({ field: 'plinthHeightMm', message: `La hauteur de plinthe doit être entre ${plinthMin} et ${plinthMax} mm.` });
|
||
}
|
||
|
||
// Portes coulissantes : largeur totale >= 1200 mm
|
||
if (doorType === 'sliding' && widthMm < slidingMinWidth) {
|
||
errors.push({ field: 'doorType', message: `Les portes coulissantes ne sont possibles qu'à partir de ${slidingMinWidth} mm de largeur.` });
|
||
}
|
||
if (doorType === 'sliding' && !allowSlidingDoors) {
|
||
errors.push({ field: 'doorType', message: 'Les portes coulissantes sont désactivées pour le moment.' });
|
||
}
|
||
|
||
if (doorType === 'sliding' && (slidingPanelsCount < 2 || slidingPanelsCount > 3)) {
|
||
errors.push({ field: 'slidingPanelsCount', message: 'Choisissez 2 ou 3 vantaux coulissants.' });
|
||
}
|
||
|
||
if (installationType === 'niche') {
|
||
const nm = config.nicheMeasurements || {};
|
||
const minW = Math.min(nm.widthTopMm || widthMm, nm.widthMidMm || widthMm, nm.widthBottomMm || widthMm);
|
||
const minD = Math.min(nm.depthTopMm || depthMm, nm.depthMidMm || depthMm, nm.depthBottomMm || depthMm);
|
||
const minH = Math.min(nm.heightLeftMm || heightMm, nm.heightRightMm || heightMm);
|
||
const expectedW = Math.max(400, minW - sanitaryGapMm * 2);
|
||
const expectedD = Math.max(400, minD - sanitaryGapMm * 2);
|
||
const expectedH = Math.max(1800, minH - sanitaryGapMm * 2);
|
||
if (Math.abs(widthMm - expectedW) > 2 || Math.abs(depthMm - expectedD) > 2 || Math.abs(heightMm - expectedH) > 2) {
|
||
errors.push({ field: 'nicheMeasurements', message: 'Les dimensions retenues ne correspondent pas aux plus petites mesures niche avec vide sanitaire de 3 cm.' });
|
||
}
|
||
}
|
||
|
||
const handleAxis = Number(config.hardware?.handleBoreHeightMm || standardHandleAxisMm);
|
||
const minStandard = standardHandleAxisMm - standardHandleToleranceMm;
|
||
const maxStandard = standardHandleAxisMm + standardHandleToleranceMm;
|
||
const isHandleAxisNonStandard = handleAxis < minStandard || handleAxis > maxStandard;
|
||
|
||
// Calculate block/module costs
|
||
let blocksChf = 0;
|
||
let modulesBreakdown = null;
|
||
|
||
if (useModules) {
|
||
// V2: Use module-based pricing
|
||
const moduleCatalog = data.modules || [];
|
||
const moduleCosts = calculateModuleCosts(modules, moduleCatalog);
|
||
blocksChf = moduleCosts.totalChf;
|
||
modulesBreakdown = moduleCosts.breakdown;
|
||
|
||
// Validate module widths
|
||
const totalModuleWidth = modules.reduce((sum, m) => sum + (m.widthMm || 0), 0);
|
||
if (Math.abs(totalModuleWidth - widthMm) > 10) {
|
||
errors.push({ field: 'modules', message: `La somme des largeurs des modules (${totalModuleWidth}mm) doit correspondre à la largeur totale (${widthMm}mm).` });
|
||
}
|
||
const hasDrawerColumnTooWide = modules.some((m) => ((m.drawers || 0) > 0 || m.type === 'tiroirs') && (m.widthMm || 0) > DRAWERS.maxColumnWidth);
|
||
if (hasDrawerColumnTooWide) {
|
||
errors.push({ field: 'modules', message: `Une colonne tiroirs dépasse ${DRAWERS.maxColumnWidth} mm (non autorisé).` });
|
||
}
|
||
if (modules.some((m) => m.hasRod) && heightMm < 1200) {
|
||
errors.push({ field: 'modules', message: 'Rayon avec tringle: hauteur minimale 1200 mm.' });
|
||
}
|
||
} else {
|
||
// Legacy: Use block-based pricing
|
||
const totalWidth = blocks.reduce((sum, b) => sum + (b.widthMm || 0), 0);
|
||
if (Math.abs(totalWidth - widthMm) > 5 && blocks.length > 0) {
|
||
errors.push({ field: 'blocks', message: 'La somme des largeurs des blocs doit correspondre à la largeur totale.' });
|
||
}
|
||
|
||
const blockTypes = data.blockTypes || [];
|
||
for (const block of blocks) {
|
||
const bt = blockTypes.find((b) => b.id === block.blockTypeId || b.code === block.blockTypeCode);
|
||
if (bt) blocksChf += Number(bt.base_price_chf) || 550;
|
||
else blocksChf += 550;
|
||
}
|
||
}
|
||
|
||
// Calculate hardware costs (V2)
|
||
let hardwareChf = 0;
|
||
let hardwareBreakdown = null;
|
||
|
||
if (hardware && (hardware.handleVariantId || hardware.hingeVariantId || hardware.drawerSystemId)) {
|
||
const hardwareData = {
|
||
handles: data.handles || [],
|
||
hinges: data.hinges || [],
|
||
blum: data.blumHardware || []
|
||
};
|
||
const hardwareCosts = calculateHardwareCosts(hardware, hardwareData, config);
|
||
hardwareChf = hardwareCosts.totalChf;
|
||
hardwareBreakdown = hardwareCosts.breakdown;
|
||
}
|
||
|
||
let panelsChf = 0;
|
||
let panelOptimization = null;
|
||
|
||
const surfaceM2 = calculateVisibleSurface({
|
||
...config,
|
||
widthMm: widthMm || blocks.reduce((sum, b) => sum + (b.widthMm || 0), 0),
|
||
plinthHeightMm: plinthHeightMm || 100,
|
||
blocks: useModules ? modules.map(m => ({ widthMm: m.widthMm })) : blocks,
|
||
doorType: doorType || 'hinged',
|
||
slidingPanelsCount: slidingPanelsCount || 2
|
||
});
|
||
|
||
// Find finish/material by ID or code
|
||
const finish = (data.finishes || []).find((f) => f.id === finishId || f.code === config.finishCode);
|
||
|
||
if (finish) {
|
||
const priceChf = finish.price_chf;
|
||
|
||
// Check if we have multiple dimension variants for optimization
|
||
if (finish.dimensions && finish.dimensions.length > 0 && priceChf != null) {
|
||
// Auto-optimize: find the best dimension that minimizes cost
|
||
panelOptimization = findOptimalDimension(surfaceM2, finish.dimensions, Number(priceChf));
|
||
|
||
if (panelOptimization) {
|
||
panelsChf = panelOptimization.costChf;
|
||
}
|
||
} else if (finish.width_mm && finish.height_mm && priceChf != null) {
|
||
// Legacy: single dimension from finish
|
||
const result = panelCost(surfaceM2, finish.width_mm, finish.height_mm, Number(priceChf));
|
||
panelsChf = result.costChf;
|
||
|
||
// Create optimization info for legacy finishes
|
||
const panelSurfaceM2 = (finish.width_mm * finish.height_mm) / 1_000_000;
|
||
const wasteM2 = (result.panels * panelSurfaceM2) - surfaceM2;
|
||
panelOptimization = {
|
||
selectedDimension: {
|
||
width_mm: finish.width_mm,
|
||
height_mm: finish.height_mm,
|
||
thickness_mm: null
|
||
},
|
||
panelsNeeded: result.panels,
|
||
costChf: result.costChf,
|
||
wasteM2: Math.round(wasteM2 * 1000) / 1000,
|
||
wastePercent: Math.round((wasteM2 / (result.panels * panelSurfaceM2)) * 1000) / 10,
|
||
alternatives: []
|
||
};
|
||
}
|
||
}
|
||
|
||
// Renforts Forteresse — one pair (haut+bas) per panneau vertical de structure
|
||
// Séparations: 2 côtés + (n−1) séparateurs = n+1 panneaux verticaux
|
||
// Block (x caisses): chaque caisse a 2 montants → 2n panneaux verticaux (plus de bois qu’en séparations)
|
||
const columnCount = useModules
|
||
? Math.max(1, modules.length)
|
||
: Math.max(1, blocks.length || 1);
|
||
const verticalStructurePanelCount = columnMode === 'block'
|
||
? 2 * columnCount
|
||
: 2 + Math.max(0, columnCount - 1);
|
||
const reinforcementCount = verticalStructurePanelCount * 2;
|
||
|
||
// Surcoût panneaux 19 mm (structure): en mode block, (n−1) montants de plus qu’en séparations
|
||
const bodyHeightMm = (heightMm || 0) - (plinthHeightMm || 100);
|
||
const extraBlockVerticalPanels = columnMode === 'block' && columnCount > 1 ? columnCount - 1 : 0;
|
||
const extraBlockStructureSurfaceM2 = extraBlockVerticalPanels > 0
|
||
? (extraBlockVerticalPanels * depthMm * bodyHeightMm) / 1_000_000
|
||
: 0;
|
||
const structureUnitPriceM2 = finish
|
||
? (Number(finish.price_chf) || 0) / ((finish.width_mm || 2800) * (finish.height_mm || 2070) / 1_000_000)
|
||
: 0;
|
||
const blockExtraStructureChf = Math.round(extraBlockStructureSurfaceM2 * structureUnitPriceM2 * 100) / 100;
|
||
|
||
// Each reinforcement is 100mm × innerDepth × 16mm — price as fraction of a panel
|
||
const reinforcementSurfaceM2 = (PANEL.reinforcementWidth * (depthMm - PANEL.structure - 20)) / 1_000_000 * reinforcementCount;
|
||
// Use 50% of panel price per m² for 16mm material (thinner, cheaper)
|
||
const reinforcementUnitPriceM2 = finish ? (Number(finish.price_chf) || 0) * 0.5 / ((finish.width_mm || 2800) * (finish.height_mm || 2070) / 1_000_000) : 0;
|
||
const reinforcementChf = Math.round(reinforcementSurfaceM2 * reinforcementUnitPriceM2 * 100) / 100;
|
||
|
||
let optionsChf = 0;
|
||
const services = data.serviceOptions || [];
|
||
for (const optId of serviceOptionIds) {
|
||
const opt = services.find((s) => s.id === optId || s.code === optId);
|
||
if (opt) optionsChf += Number(opt.price_chf) || 0;
|
||
}
|
||
|
||
const totalChf = blocksChf + kitFeeChf + panelsChf + optionsChf + hardwareChf + reinforcementChf + blockExtraStructureChf;
|
||
|
||
return {
|
||
totalChf: Math.round(totalChf * 100) / 100,
|
||
surfaceM2: Math.round(surfaceM2 * 1000) / 1000,
|
||
details: {
|
||
blocksChf,
|
||
modulesBreakdown,
|
||
kitChf: kitFeeChf,
|
||
panelsChf,
|
||
reinforcementChf,
|
||
reinforcementCount,
|
||
columnMode,
|
||
verticalStructurePanelCount,
|
||
blockExtraStructureChf,
|
||
optionsChf,
|
||
hardwareChf,
|
||
hardwareBreakdown,
|
||
panelOptimization
|
||
},
|
||
warnings: isHandleAxisNonStandard
|
||
? [{ field: 'handleBoreHeightMm', message: `Hauteur perçage poignée hors plage standard (${minStandard}-${maxStandard} mm).` }]
|
||
: undefined,
|
||
errors: errors.length ? errors : undefined
|
||
};
|
||
}
|
||
|
||
export default {
|
||
calculatePrice,
|
||
calculateVisibleSurface,
|
||
panelCost,
|
||
findOptimalDimension,
|
||
calculateModuleCosts,
|
||
calculateHardwareCosts
|
||
};
|