import partsData from '../data/index.js'; import { getNumericPrice, extractNumericPrice, formatPrice, formatPriceWithConversion } from './priceFormat'; import { convertPrice } from './currencyService'; /** * Evaluate a condition object against the config */ export const evaluateCondition = (condition, config) => { if (!condition) return true; return Object.entries(condition).every(([key, value]) => { // Handle dot notation for nested config (e.g., motor.id) const keys = key.split('.'); let current = config; for (const k of keys) { if (current === null || current === undefined) return false; current = current[k]; } return current === value; }); }; /** * Check if a component should be included based on config selections */ export const shouldIncludeComponent = (componentKey, config) => { // Actuator is always included (it's the base component) if (componentKey === 'actuator') { return true; } // Mounting: only if mount is selected if (componentKey === 'mounting' || componentKey === 'mounts') { return !!config.mount; } // Stand components: only if stand options are selected if (componentKey === 'stand') { return !!(config.standFeet || config.standHinge || (config.standCrossbarSupports && config.standCrossbarSupports.length > 0)); } // Feet: only if standFeet is selected if (componentKey === 'feet') { return !!config.standFeet; } // Hinges: only if standHinge is selected if (componentKey === 'hinges') { return !!config.standHinge; } // Crossbar supports: only if standCrossbarSupports are selected if (componentKey === 'crossbarSupports') { return !!(config.standCrossbarSupports && config.standCrossbarSupports.length > 0); } // Remotes: only if remote is selected if (componentKey === 'remotes') { return !!(config.remoteKnob || config.remoteType || config.remote?.id); } // Toy mounts: only if toy mount options are selected if (componentKey === 'toyMounts') { return !!(config.toyMountOptions && config.toyMountOptions.length > 0); } // PCB: only if pcbMount is selected if (componentKey === 'pcb' || componentKey === 'pcbMount') { return !!config.pcbMount; } // By default, don't include other components unless explicitly selected return false; }; /** * Get minimum price from links or fallback to price field */ export const getPriceFromLinks = (item) => { if (!item) return 0; if (item.links && item.links.length > 0) { const prices = item.links.map(link => extractNumericPrice(link.price)).filter(p => p != null && p > 0); if (prices.length > 0) { return Math.min(...prices); } } // Fallback to old price field if links don't have prices return getNumericPrice(item.price); }; /** * Get price range or single price from links for display (synchronous version, no conversion) */ export const getPriceDisplayFromLinks = (item, targetCurrency = null) => { if (!item) return 'C$0.00'; if (item.links && item.links.length > 0) { // Get price objects (with currency) from links, filtering out null/invalid prices const priceObjects = item.links .map(link => link.price) .filter(price => price && (price.amount || (typeof price === 'object' && 'amount' in price))); if (priceObjects.length === 0) return 'C$0.00'; // If all prices have the same currency, show range with that currency const currencies = priceObjects .map(p => p?.currency || 'CAD') .filter((v, i, a) => a.indexOf(v) === i); const isSingleCurrency = currencies.length === 1; // Extract numeric values for min/max calculation // For overall min: use the minimum values from each price (min from ranges, single prices, etc.) // For overall max: use the maximum values from each price (max from ranges, single prices, etc.) const minValues = priceObjects.map(p => { if (typeof p === 'object' && 'amount' in p) { const amount = p.amount; // For ranges, use the min value; for single prices, use the amount if (typeof amount === 'object' && 'min' in amount) { return amount.min; } if (typeof amount === 'number') { return amount; } } const extracted = extractNumericPrice(p); return extracted != null ? extracted : 0; }).filter(p => p != null && p > 0); const maxValues = priceObjects.map(p => { if (typeof p === 'object' && 'amount' in p) { const amount = p.amount; // For ranges, use the max value; for single prices, use the amount if (typeof amount === 'object' && 'max' in amount) { return amount.max; } if (typeof amount === 'object' && 'min' in amount) { return amount.min; // If no max, use min (single value range) } if (typeof amount === 'number') { return amount; } } const extracted = extractNumericPrice(p); return extracted != null ? extracted : 0; }).filter(p => p != null && p > 0); if (minValues.length === 0 || maxValues.length === 0) return 'C$0.00'; const minPrice = Math.min(...minValues); const maxPrice = Math.max(...maxValues); if (minPrice === maxPrice) { // Single price - format with currency from the first link return formatPrice(priceObjects[0], targetCurrency || 'CAD'); } // Price range - format both with their respective currencies if different, or same currency if same if (isSingleCurrency) { const currency = targetCurrency || currencies[0]; const currencySymbol = currency === 'CAD' ? 'C$' : currency === 'USD' ? '$' : currency === 'EUR' ? '€' : currency === 'GBP' ? '£' : currency; return `${currencySymbol}${minPrice.toFixed(2)} - ${currencySymbol}${maxPrice.toFixed(2)}`; } else { // Multiple currencies - find the price objects that contain the overall min and max const minPriceObj = priceObjects.find(p => { if (typeof p === 'object' && 'amount' in p) { const amount = p.amount; if (typeof amount === 'object' && 'min' in amount) { return amount.min === minPrice; } if (typeof amount === 'number') { return amount === minPrice; } } return extractNumericPrice(p) === minPrice; }) || priceObjects[0]; // Fallback to first if not found const maxPriceObj = priceObjects.find(p => { if (typeof p === 'object' && 'amount' in p) { const amount = p.amount; if (typeof amount === 'object' && 'max' in amount) { return amount.max === maxPrice; } if (typeof amount === 'object' && 'min' in amount) { return amount.min === maxPrice; // Single value range } if (typeof amount === 'number') { return amount === maxPrice; } } return extractNumericPrice(p) === maxPrice; }) || priceObjects[priceObjects.length - 1]; // Fallback to last if not found return `${formatPrice(minPriceObj, targetCurrency || 'CAD')} - ${formatPrice(maxPriceObj, targetCurrency || 'CAD')}`; } } // Fallback to old price field if links don't exist return formatPrice(item.price || 0, targetCurrency || 'CAD'); }; /** * Async version with currency conversion */ export const getPriceDisplayFromLinksAsync = async (item, targetCurrency = 'CAD', exchangeRates = null) => { if (!item) return 'C$0.00'; if (item.links && item.links.length > 0) { // Convert all prices to target currency first const convertedPrices = await Promise.all( item.links .map(link => link.price) .filter(price => price && (price.amount || (typeof price === 'object' && 'amount' in price))) .map(async (price) => { if (exchangeRates) { return await convertPrice(price, targetCurrency, exchangeRates); } return price; }) ); if (convertedPrices.length === 0) return 'C$0.00'; // Extract numeric values for min/max calculation // For overall min: use the minimum values from each price (min from ranges, single prices, etc.) // For overall max: use the maximum values from each price (max from ranges, single prices, etc.) const minValues = convertedPrices.map(p => { if (typeof p === 'object' && 'amount' in p) { const amount = p.amount; // For ranges, use the min value; for single prices, use the amount if (typeof amount === 'object' && 'min' in amount) { return amount.min; } if (typeof amount === 'number') { return amount; } } const extracted = extractNumericPrice(p); return extracted != null ? extracted : 0; }).filter(p => p != null && p > 0); const maxValues = convertedPrices.map(p => { if (typeof p === 'object' && 'amount' in p) { const amount = p.amount; // For ranges, use the max value; for single prices, use the amount if (typeof amount === 'object' && 'max' in amount) { return amount.max; } if (typeof amount === 'object' && 'min' in amount) { return amount.min; // If no max, use min (single value range) } if (typeof amount === 'number') { return amount; } } const extracted = extractNumericPrice(p); return extracted != null ? extracted : 0; }).filter(p => p != null && p > 0); if (minValues.length === 0 || maxValues.length === 0) return 'C$0.00'; const minPrice = Math.min(...minValues); const maxPrice = Math.max(...maxValues); if (minPrice === maxPrice) { return await formatPriceWithConversion(convertedPrices[0], targetCurrency, exchangeRates); } // Price range const currencySymbol = targetCurrency === 'CAD' ? 'C$' : targetCurrency === 'USD' ? '$' : targetCurrency === 'EUR' ? '€' : targetCurrency === 'GBP' ? '£' : targetCurrency; return `${currencySymbol}${minPrice.toFixed(2)} - ${currencySymbol}${maxPrice.toFixed(2)}`; } // Fallback to old price field if (item.price) { return await formatPriceWithConversion(item.price, targetCurrency, exchangeRates); } return 'C$0.00'; }; /** * Calculate total hardware cost */ export const calculateTotal = (config) => { let total = 0; if (config.motor) total += getPriceFromLinks(config.motor); if (config.powerSupply) total += getPriceFromLinks(config.powerSupply); if (config.mount) { const mountOption = partsData.options?.mounts?.find(m => m.id === config.mount.id); if (mountOption?.hardwareCost) total += getNumericPrice(mountOption.hardwareCost); } if (config.standHinge) { // Check new structure (systems) first, then fall back to options const hingeSystem = partsData.components?.hinges?.systems?.[config.standHinge.id]; if (hingeSystem?.hardwareCost) { total += getNumericPrice(hingeSystem.hardwareCost); } else { const hingeOption = partsData.options?.standHinges?.find(h => h.id === config.standHinge.id); if (hingeOption?.hardwareCost) total += getNumericPrice(hingeOption.hardwareCost); } } if (config.standFeet) { const feetOption = partsData.options?.standFeet?.find(f => f.id === config.standFeet.id); if (feetOption?.hardwareCost) total += getNumericPrice(feetOption.hardwareCost); } if (config.standCrossbarSupports) { config.standCrossbarSupports.forEach((support) => { const supportOption = partsData.options?.standCrossbarSupports?.find(s => s.id === support.id); if (supportOption?.hardwareCost) total += getNumericPrice(supportOption.hardwareCost); }); } return total; };