Files
stalendeuren/lib/pricing.ts
Ubuntu 3d788740cb feat: Latest production version with interior scene and glass
Includes room interior with floor, walls, glass you can see through,
and all uncommitted production changes that were running live.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 14:50:31 +00:00

176 lines
4.8 KiB
TypeScript

/**
* Pricing Engine for Proinn Configurator
* Based on Dutch market standard pricing
*/
import {
PROFILE_WIDTH,
RAIL_HEIGHT_SLIM,
RAIL_HEIGHT_ROBUST,
type GridLayout,
type DoorModel,
} from './door-models';
// Pricing constants (EUR)
const STEEL_PRICE_PER_METER = 45;
const GLASS_PRICE_PER_SQM = 140;
const BASE_FEE = 650;
const SIDE_PANEL_SURCHARGE = 250;
const DOUBLE_DOOR_SURCHARGE = 350;
const TAATS_MECHANISM_SURCHARGE = 450;
const HANDLE_PRICES: Record<string, number> = {
'beugelgreep': 85,
'hoekgreep': 75,
'maangreep': 95,
'ovaalgreep': 90,
'klink': 65,
'u-greep': 55,
'geen': 0,
};
// Premium finish surcharges
const FINISH_SURCHARGES: Record<string, number> = {
'zwart': 0,
'grijs': 0,
'brons': 0,
'goud': 150,
'beige': 75,
'ral': 200,
};
// Frame size multipliers (relative to standard 40mm)
const FRAME_SIZE_MULTIPLIERS: Record<number, number> = {
20: 0.7,
30: 0.85,
40: 1.0,
};
function getHorizontalDividerCount(gridLayout: GridLayout): number {
switch (gridLayout) {
case '2-vlak': return 1;
case '3-vlak': return 2;
case '4-vlak': return 3;
case '6-vlak': return 2;
case '8-vlak': return 3;
case 'kruis': return 1;
case 'ongelijk-3': return 2;
case 'boerderij': return 1;
case 'herenhuis': return 2;
default: return 0;
}
}
function hasVerticalDividers(gridLayout: GridLayout): boolean {
return ['6-vlak', '8-vlak', 'kruis', 'boerderij'].includes(gridLayout);
}
function calculateSteelLength(
doorWidth: number,
doorHeight: number,
gridLayout: GridLayout,
hasPaneelVerticalDivider: boolean
): number {
const innerWidth = doorWidth - PROFILE_WIDTH * 2;
// Perimeter
let totalLength = doorHeight * 2 + innerWidth * 2;
// Horizontal dividers
const hDividers = getHorizontalDividerCount(gridLayout);
totalLength += innerWidth * hDividers;
// Vertical dividers from grid pattern
if (hasVerticalDividers(gridLayout)) {
const innerHeight = doorHeight - RAIL_HEIGHT_ROBUST * 2;
totalLength += innerHeight;
}
// Paneel type adds center vertical divider
if (hasPaneelVerticalDivider) {
totalLength += doorHeight - RAIL_HEIGHT_ROBUST * 2;
}
return totalLength / 1000;
}
function calculateGlassArea(
doorWidth: number,
doorHeight: number,
gridLayout: GridLayout
): number {
const glassWidth = doorWidth - PROFILE_WIDTH * 2;
const glassHeight = doorHeight - RAIL_HEIGHT_ROBUST * 2;
const hDividers = getHorizontalDividerCount(gridLayout);
let dividerArea = glassWidth * RAIL_HEIGHT_SLIM * hDividers;
// Vertical divider area
if (hasVerticalDividers(gridLayout)) {
dividerArea += PROFILE_WIDTH * (glassHeight - RAIL_HEIGHT_SLIM * hDividers);
}
return (glassWidth * glassHeight - dividerArea) / 1_000_000;
}
export interface PriceBreakdown {
steelCost: number;
glassCost: number;
baseFee: number;
mechanismSurcharge: number;
sidePanelSurcharge: number;
handleCost: number;
finishSurcharge: number;
totalPrice: number;
steelLengthM: number;
glassAreaSqm: number;
}
export function calculatePrice(
doorWidth: number,
doorHeight: number,
doorType: DoorModel,
gridLayout: GridLayout,
doorConfig: 'enkele' | 'dubbele',
sidePanel: 'geen' | 'links' | 'rechts' | 'beide',
handle: string,
finish: string = 'zwart',
frameSize: number = 40
): PriceBreakdown {
const leafCount = doorConfig === 'dubbele' ? 2 : 1;
const hasPaneelVerticalDivider = doorType === 'paneel';
const frameSizeMultiplier = FRAME_SIZE_MULTIPLIERS[frameSize] ?? 1.0;
const steelLengthPerLeaf = calculateSteelLength(doorWidth, doorHeight, gridLayout, hasPaneelVerticalDivider);
const glassAreaPerLeaf = calculateGlassArea(doorWidth, doorHeight, gridLayout);
const totalSteelLength = steelLengthPerLeaf * leafCount;
const totalGlassArea = glassAreaPerLeaf * leafCount;
const sidePanelCount = sidePanel === 'beide' ? 2 : (sidePanel === 'geen' ? 0 : 1);
const steelCost = Math.round(totalSteelLength * STEEL_PRICE_PER_METER * frameSizeMultiplier);
const glassCost = Math.round(totalGlassArea * GLASS_PRICE_PER_SQM);
let mechanismSurcharge = 0;
if (doorType === 'taats') mechanismSurcharge += TAATS_MECHANISM_SURCHARGE;
if (doorConfig === 'dubbele') mechanismSurcharge += DOUBLE_DOOR_SURCHARGE;
const handleCost = HANDLE_PRICES[handle] || 0;
const sidePanelSurchrg = sidePanelCount * SIDE_PANEL_SURCHARGE;
const finishSurcharge = FINISH_SURCHARGES[finish] ?? 0;
const totalPrice = steelCost + glassCost + BASE_FEE + mechanismSurcharge + sidePanelSurchrg + handleCost + finishSurcharge;
return {
steelCost,
glassCost,
baseFee: BASE_FEE,
mechanismSurcharge,
sidePanelSurcharge: sidePanelSurchrg,
handleCost,
finishSurcharge,
totalPrice,
steelLengthM: Math.round(totalSteelLength * 100) / 100,
glassAreaSqm: Math.round(totalGlassArea * 100) / 100,
};
}