- Added `prop-types` for better prop validation in components. - Introduced `CurrencyProvider` to manage currency context and preload exchange rates. - Updated pricing logic in various components to support new price structure and display currency. - Refactored BOMSummary, MotorStep, and PowerSupplyStep to utilize new pricing methods and improve user experience. - Enhanced export utilities to format prices correctly in markdown and Excel outputs. - Updated hardware and component data structures to include detailed pricing information.
762 lines
27 KiB
JavaScript
762 lines
27 KiB
JavaScript
import partsData from '../data/index.js';
|
|
import { evaluateCondition, shouldIncludeComponent, getPriceDisplayFromLinks } from './bomUtils';
|
|
import { formatPrice } from './priceFormat';
|
|
|
|
/**
|
|
* Categorize hardware by type
|
|
*/
|
|
export const getHardwareType = (hardware) => {
|
|
const id = hardware.id?.toLowerCase() || '';
|
|
const name = hardware.name?.toLowerCase() || '';
|
|
|
|
// Fasteners
|
|
if (id.includes('fastener') || id.includes('screw') || id.includes('nut') || id.includes('washer') ||
|
|
id.includes('bolt') || id.includes('handle') || name.includes('fastener') || name.includes('screw') ||
|
|
name.includes('nut') || name.includes('washer') || name.includes('bolt') || name.includes('handle')) {
|
|
return 'Fasteners';
|
|
}
|
|
|
|
// Motion components
|
|
if (id.includes('bearing') || id.includes('pulley') || id.includes('belt') || id.includes('gear') ||
|
|
id.includes('motor') || id.includes('rail') || id.includes('linear') ||
|
|
name.includes('bearing') || name.includes('pulley') || name.includes('belt') ||
|
|
name.includes('gear') || name.includes('motor') || name.includes('rail') || name.includes('linear')) {
|
|
return 'Motion Components';
|
|
}
|
|
|
|
// Aluminum extrusion / 3030
|
|
if (id.includes('3030') || id.includes('extrusion') || id.includes('aluminum') || id.includes('support') ||
|
|
name.includes('3030') || name.includes('extrusion') || name.includes('aluminum') || name.includes('90 degree support')) {
|
|
return 'Aluminum Extrusion';
|
|
}
|
|
|
|
// Electronics
|
|
if (id.includes('pcb') || id.includes('board') || id.includes('circuit') || id.includes('sensor') ||
|
|
id.includes('switch') || id.includes('led') || name.includes('pcb') || name.includes('board') ||
|
|
name.includes('circuit') || name.includes('sensor') || name.includes('switch') || name.includes('led')) {
|
|
return 'Electronics';
|
|
}
|
|
|
|
// Other / General Hardware
|
|
return 'Other Hardware';
|
|
};
|
|
|
|
/**
|
|
* Get required printed parts based on config
|
|
*/
|
|
export const getRequiredPrintedParts = (config) => {
|
|
const parts = [];
|
|
|
|
// Always include components that are marked as required and meet their conditions
|
|
Object.entries(partsData.components || {}).forEach(([componentKey, component]) => {
|
|
// Skip components that don't have selected options (except actuator)
|
|
if (!shouldIncludeComponent(componentKey, config)) {
|
|
return;
|
|
}
|
|
|
|
const category = component.category || componentKey;
|
|
|
|
// Handle standard printedParts array
|
|
if (component.printedParts) {
|
|
component.printedParts.forEach((part) => {
|
|
if (part.required && evaluateCondition(part.Condition, config)) {
|
|
parts.push({ ...part, category });
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handle systems (for hinges, remotes, etc.)
|
|
if (component.systems) {
|
|
// If it's a selected system, include its printed parts
|
|
const selectedSystemId = config[componentKey] || config.standHinge; // Fallback for naming mismatches
|
|
const system = component.systems[selectedSystemId?.id || selectedSystemId];
|
|
|
|
if (system) {
|
|
const systemParts = system.printedParts || system.bodyParts || [];
|
|
systemParts.forEach((part) => {
|
|
if (part.required && evaluateCondition(part.Condition, config)) {
|
|
parts.push({ ...part, category });
|
|
}
|
|
});
|
|
|
|
// Remote knobs are handled by looking up the knob in the system
|
|
if (componentKey === 'remotes' && config.remoteKnob) {
|
|
const knobPart = system.knobs?.find(k => k.id === config.remoteKnob.id);
|
|
if (knobPart) {
|
|
parts.push({ ...knobPart, category: 'Remote Knobs' });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle options that are not explicitly in the "components" top-level structure but represent printed parts
|
|
|
|
// Mount variations (if not already handled by required parts)
|
|
if (config.mount && partsData.components?.mounts?.printedParts) {
|
|
const mountPart = partsData.components.mounts.printedParts.find(p => p.id === config.mount.id);
|
|
if (mountPart) parts.push({ ...mountPart, category: 'Mount' });
|
|
}
|
|
|
|
// Custom Cover
|
|
const coverId = config.cover?.id;
|
|
const isStandardCover = coverId === 'standard-cover';
|
|
const isBlankCover = coverId === 'blank-cover';
|
|
const isCustomCover = config.cover !== null && !isStandardCover && !isBlankCover;
|
|
|
|
if (isCustomCover) {
|
|
const coverOption = config.cover;
|
|
parts.push({
|
|
id: coverOption.id,
|
|
name: coverOption.name,
|
|
description: coverOption.description || coverOption.name,
|
|
filamentEstimate: coverOption.filamentEstimate ? parseFloat(coverOption.filamentEstimate.replace('~', '').replace('g', '')) : 0,
|
|
filePath: `${coverOption.id}.3mf`,
|
|
category: 'Cover',
|
|
required: true,
|
|
colour: 'primary',
|
|
});
|
|
}
|
|
|
|
// Stand components (feet, supports)
|
|
if (config.standFeet && partsData.components?.feet?.printedParts) {
|
|
const feetPart = partsData.components.feet.printedParts.find(p => p.id === config.standFeet.id);
|
|
if (feetPart) parts.push({ ...feetPart, category: 'Stand Feet' });
|
|
}
|
|
|
|
if (config.standCrossbarSupports && partsData.components?.crossbarSupports?.printedParts) {
|
|
const selectedSupportIds = new Set(config.standCrossbarSupports.map(opt => opt.id));
|
|
partsData.components.crossbarSupports.printedParts.forEach((part) => {
|
|
if (selectedSupportIds.has(part.id) && !part.isHardwareOnly) {
|
|
parts.push({ ...part, category: 'Stand Crossbar Supports' });
|
|
}
|
|
});
|
|
}
|
|
|
|
// Toy Mounts
|
|
if (config.toyMountOptions && config.toyMountOptions.length > 0 && partsData.components?.toyMounts?.printedParts) {
|
|
const selectedToyMountIds = new Set(config.toyMountOptions.map(opt => opt.id));
|
|
partsData.components.toyMounts.printedParts.forEach((part) => {
|
|
if (selectedToyMountIds.has(part.id)) {
|
|
parts.push({ ...part, category: 'Toy Mounts' });
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handle 'replaces' logic
|
|
const replacedIds = new Set();
|
|
parts.forEach(part => {
|
|
if (part.replaces) {
|
|
part.replaces.forEach(id => replacedIds.add(id));
|
|
}
|
|
});
|
|
|
|
return parts.filter(part => !replacedIds.has(part.id));
|
|
};
|
|
|
|
/**
|
|
* Get required hardware parts based on config
|
|
*/
|
|
export const getRequiredHardwareParts = (config) => {
|
|
const printedParts = getRequiredPrintedParts(config);
|
|
const printedPartIds = new Set(printedParts.map(p => p.id));
|
|
const hardwareParts = [];
|
|
const hardwareMap = new Map(); // To aggregate quantities for same hardware part
|
|
|
|
// Collect all selected option IDs (including hardware-only options)
|
|
const selectedOptionIds = new Set();
|
|
|
|
// Add selected crossbar supports (may include hardware-only options)
|
|
if (config.standCrossbarSupports && config.standCrossbarSupports.length > 0) {
|
|
config.standCrossbarSupports.forEach(opt => selectedOptionIds.add(opt.id));
|
|
}
|
|
|
|
// Add other selected options that might be hardware-only
|
|
if (config.standHinge) selectedOptionIds.add(config.standHinge.id);
|
|
if (config.standFeet) selectedOptionIds.add(config.standFeet.id);
|
|
if (config.mount) selectedOptionIds.add(config.mount.id);
|
|
if (config.cover) selectedOptionIds.add(config.cover.id);
|
|
if (config.pcbMount) selectedOptionIds.add(config.pcbMount.id);
|
|
if (config.remoteKnob) selectedOptionIds.add(config.remoteKnob.id);
|
|
if (config.toyMountOptions && config.toyMountOptions.length > 0) {
|
|
config.toyMountOptions.forEach(opt => selectedOptionIds.add(opt.id));
|
|
}
|
|
|
|
// Handle hinges systems (new structure)
|
|
if (config.standHinge && partsData.components?.hinges?.systems) {
|
|
const hingeSystem = partsData.components.hinges.systems[config.standHinge.id];
|
|
if (hingeSystem?.hardwareParts) {
|
|
hingeSystem.hardwareParts.forEach((hardware) => {
|
|
if (!hardware.required) return;
|
|
|
|
// Evaluate condition for hardware
|
|
if (!evaluateCondition(hardware.Condition, config)) return;
|
|
|
|
const key = hardware.id;
|
|
if (hardwareMap.has(key)) {
|
|
const existing = hardwareMap.get(key);
|
|
existing.quantity = (existing.quantity || 1) + (hardware.quantity || 1);
|
|
} else {
|
|
hardwareMap.set(key, {
|
|
...hardware,
|
|
quantity: hardware.quantity || 1,
|
|
category: partsData.components.hinges.category || 'Hardware',
|
|
hardwareType: getHardwareType(hardware)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Handle remote systems (new structure)
|
|
if (config.remoteKnob && partsData.components?.remotes?.systems) {
|
|
// Find which system contains this knob
|
|
let remoteSystem = null;
|
|
Object.values(partsData.components.remotes.systems).forEach((system) => {
|
|
if (system.knobs && system.knobs.find(k => k.id === config.remoteKnob.id)) {
|
|
remoteSystem = system;
|
|
}
|
|
});
|
|
|
|
if (remoteSystem?.hardwareParts) {
|
|
remoteSystem.hardwareParts.forEach((hardware) => {
|
|
if (!hardware.required) return;
|
|
|
|
// Evaluate condition for hardware
|
|
if (!evaluateCondition(hardware.Condition, config)) return;
|
|
|
|
const key = hardware.id;
|
|
if (hardwareMap.has(key)) {
|
|
const existing = hardwareMap.get(key);
|
|
existing.quantity = (existing.quantity || 1) + (hardware.quantity || 1);
|
|
} else {
|
|
hardwareMap.set(key, {
|
|
...hardware,
|
|
quantity: hardware.quantity || 1,
|
|
category: partsData.components.remotes.category || 'Hardware'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Build a map of component keys to their printed part IDs for quick lookup
|
|
const componentPrintedPartIds = new Map();
|
|
Object.entries(partsData.components || {}).forEach(([componentKey, component]) => {
|
|
// Skip hinges and remotes as they're handled separately above
|
|
if (componentKey === 'hinges' || componentKey === 'remotes') return;
|
|
|
|
// Skip components that don't have selected options (except actuator)
|
|
if (!shouldIncludeComponent(componentKey, config)) {
|
|
return;
|
|
}
|
|
|
|
if (component.printedParts) {
|
|
const partIds = component.printedParts.map(p => p.id);
|
|
componentPrintedPartIds.set(componentKey, new Set(partIds));
|
|
}
|
|
});
|
|
|
|
// Iterate through all components to find hardware parts (excluding hinges and remotes)
|
|
Object.entries(partsData.components || {}).forEach(([componentKey, component]) => {
|
|
// Skip hinges and remotes as they're handled separately above
|
|
if (componentKey === 'hinges' || componentKey === 'remotes') return;
|
|
|
|
// Skip components that don't have selected options (except actuator)
|
|
if (!shouldIncludeComponent(componentKey, config)) {
|
|
return;
|
|
}
|
|
|
|
// Handle components with systems (like mounts)
|
|
if (component.systems) {
|
|
// Find the selected system based on config
|
|
let selectedSystemId = null;
|
|
if (componentKey === 'mounting' && config.mount) {
|
|
selectedSystemId = config.mount.id;
|
|
} else if (componentKey === 'toyMounts' && config.toyMountOptions && config.toyMountOptions.length > 0) {
|
|
// For toy mounts, process all selected options
|
|
config.toyMountOptions.forEach((toyMount) => {
|
|
const system = component.systems[toyMount.id];
|
|
if (system?.hardwareParts) {
|
|
system.hardwareParts.forEach((hardware) => {
|
|
if (!hardware.required) return;
|
|
if (!evaluateCondition(hardware.Condition, config)) return;
|
|
|
|
const key = hardware.id;
|
|
if (hardwareMap.has(key)) {
|
|
const existing = hardwareMap.get(key);
|
|
existing.quantity = (existing.quantity || 1) + (hardware.quantity || 1);
|
|
} else {
|
|
hardwareMap.set(key, {
|
|
...hardware,
|
|
quantity: hardware.quantity || 1,
|
|
category: component.category || 'Hardware',
|
|
hardwareType: getHardwareType(hardware)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
return; // Skip the rest for toy mounts
|
|
}
|
|
|
|
if (selectedSystemId) {
|
|
const system = component.systems[selectedSystemId];
|
|
if (system?.hardwareParts) {
|
|
system.hardwareParts.forEach((hardware) => {
|
|
if (!hardware.required) return;
|
|
if (!evaluateCondition(hardware.Condition, config)) return;
|
|
|
|
const key = hardware.id;
|
|
if (hardwareMap.has(key)) {
|
|
const existing = hardwareMap.get(key);
|
|
existing.quantity = (existing.quantity || 1) + (hardware.quantity || 1);
|
|
} else {
|
|
hardwareMap.set(key, {
|
|
...hardware,
|
|
quantity: hardware.quantity || 1,
|
|
category: component.category || 'Hardware',
|
|
hardwareType: getHardwareType(hardware)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return; // Skip direct hardwareParts check for components with systems
|
|
}
|
|
|
|
// Handle components with direct hardwareParts (old structure)
|
|
if (!component.hardwareParts) return;
|
|
|
|
// Check if this component has any selected printed parts OR selected options
|
|
const componentPartIds = componentPrintedPartIds.get(componentKey);
|
|
const hasSelectedParts = componentPartIds && Array.from(componentPartIds).some(id => printedPartIds.has(id) || selectedOptionIds.has(id));
|
|
|
|
component.hardwareParts.forEach((hardware) => {
|
|
if (!hardware.required) return;
|
|
|
|
// Evaluate condition for hardware
|
|
if (!evaluateCondition(hardware.Condition, config)) return;
|
|
|
|
// If component has selected parts, check if hardware should be included
|
|
let shouldInclude = false;
|
|
|
|
if (hasSelectedParts) {
|
|
const relatedParts = hardware.relatedParts || [];
|
|
// If no relatedParts specified, include if component has selected parts
|
|
// If relatedParts specified, include if any related part is selected (printed or option)
|
|
if (relatedParts.length === 0) {
|
|
shouldInclude = true;
|
|
} else {
|
|
shouldInclude = relatedParts.some(partId => printedPartIds.has(partId) || selectedOptionIds.has(partId));
|
|
}
|
|
}
|
|
|
|
if (shouldInclude) {
|
|
const key = hardware.id;
|
|
if (hardwareMap.has(key)) {
|
|
// Aggregate quantities if same hardware appears multiple times
|
|
const existing = hardwareMap.get(key);
|
|
existing.quantity = (existing.quantity || 1) + (hardware.quantity || 1);
|
|
} else {
|
|
hardwareMap.set(key, {
|
|
...hardware,
|
|
quantity: hardware.quantity || 1,
|
|
category: component.category || 'Hardware',
|
|
hardwareType: getHardwareType(hardware)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Add Motor, Power Supply, and PCB to hardware list
|
|
// Store original item reference for price conversion
|
|
if (config.motor) {
|
|
hardwareParts.push({
|
|
id: config.motor.id || 'motor',
|
|
name: config.motor.name || 'Motor',
|
|
description: config.motor.description || '',
|
|
quantity: 1,
|
|
category: 'Electronics',
|
|
hardwareType: 'Electronics',
|
|
price: config.motor, // Store original item for price conversion
|
|
_isItemReference: true // Flag to indicate this is an item reference
|
|
});
|
|
}
|
|
|
|
if (config.powerSupply) {
|
|
hardwareParts.push({
|
|
id: config.powerSupply.id || 'power-supply',
|
|
name: config.powerSupply.name || 'Power Supply',
|
|
description: config.powerSupply.description || '',
|
|
quantity: 1,
|
|
category: 'Electronics',
|
|
hardwareType: 'Electronics',
|
|
price: config.powerSupply, // Store original item for price conversion
|
|
_isItemReference: true
|
|
});
|
|
}
|
|
|
|
// Add PCB (always required, separate from PCB mount)
|
|
if (partsData.pcbs && partsData.pcbs.length > 0) {
|
|
const pcb = partsData.pcbs[0]; // Get the first/only PCB
|
|
hardwareParts.push({
|
|
id: pcb.id || 'pcb',
|
|
name: pcb.name || 'PCB',
|
|
description: pcb.description || '',
|
|
quantity: 1,
|
|
category: 'Electronics',
|
|
hardwareType: 'Electronics',
|
|
price: pcb, // Store original item for price conversion
|
|
_isItemReference: true
|
|
});
|
|
}
|
|
|
|
// Convert map to array
|
|
hardwareMap.forEach((hardware) => {
|
|
hardwareParts.push(hardware);
|
|
});
|
|
|
|
return hardwareParts;
|
|
};
|
|
|
|
/**
|
|
* Parse time string to minutes
|
|
*/
|
|
export const parseTimeToMinutes = (timeStr) => {
|
|
if (!timeStr || typeof timeStr !== 'string') return 0;
|
|
|
|
let totalMinutes = 0;
|
|
const hourMatch = timeStr.match(/(\d+)h/);
|
|
const minuteMatch = timeStr.match(/(\d+)m/);
|
|
const secondMatch = timeStr.match(/(\d+)s/);
|
|
|
|
if (hourMatch) totalMinutes += parseInt(hourMatch[1], 10) * 60;
|
|
if (minuteMatch) totalMinutes += parseInt(minuteMatch[1], 10);
|
|
if (secondMatch) totalMinutes += parseFloat(secondMatch[1]) / 60;
|
|
|
|
return totalMinutes;
|
|
};
|
|
|
|
/**
|
|
* Format minutes to readable string (e.g., "2h 14m")
|
|
*/
|
|
export const formatTimeFromMinutes = (minutes) => {
|
|
if (minutes === 0) return '0m';
|
|
|
|
const hours = Math.floor(minutes / 60);
|
|
const mins = Math.round(minutes % 60);
|
|
|
|
if (hours > 0 && mins > 0) {
|
|
return `${hours}h ${mins}m`;
|
|
} else if (hours > 0) {
|
|
return `${hours}h`;
|
|
} else {
|
|
return `${mins}m`;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Calculate total filament estimate
|
|
*/
|
|
export const getTotalFilamentEstimate = (printedParts) => {
|
|
const totals = {
|
|
primary: 0,
|
|
secondary: 0,
|
|
total: 0
|
|
};
|
|
|
|
printedParts.forEach((part) => {
|
|
let estimate = 0;
|
|
|
|
// Handle both numeric and string values (e.g., "~147g" or 147.19)
|
|
if (typeof part.filamentEstimate === 'number') {
|
|
estimate = part.filamentEstimate;
|
|
} else if (typeof part.filamentEstimate === 'string') {
|
|
// Parse string format like "~147g" or "147g"
|
|
const cleaned = part.filamentEstimate.replace(/[~g]/g, '').trim();
|
|
estimate = parseFloat(cleaned) || 0;
|
|
}
|
|
|
|
// Multiply by quantity if specified (default to 1)
|
|
const quantity = part.quantity || 1;
|
|
estimate = estimate * quantity;
|
|
|
|
const colour = part.colour || 'primary';
|
|
|
|
if (colour === 'primary') {
|
|
totals.primary += estimate;
|
|
} else if (colour === 'secondary') {
|
|
totals.secondary += estimate;
|
|
}
|
|
totals.total += estimate;
|
|
});
|
|
|
|
return totals;
|
|
};
|
|
|
|
/**
|
|
* Calculate total time estimate
|
|
*/
|
|
export const getTotalTimeEstimate = (printedParts) => {
|
|
let totalMinutes = 0;
|
|
|
|
printedParts.forEach((part) => {
|
|
if (part.timeEstimate) {
|
|
const timeMinutes = parseTimeToMinutes(part.timeEstimate);
|
|
// Multiply by quantity if specified (default to 1)
|
|
const quantity = part.quantity || 1;
|
|
totalMinutes += timeMinutes * quantity;
|
|
}
|
|
});
|
|
|
|
return formatTimeFromMinutes(totalMinutes);
|
|
};
|
|
|
|
/**
|
|
* Get color name from color ID
|
|
*/
|
|
export const getColorName = (colorId, type = 'primary') => {
|
|
const colors = type === 'primary' ? partsData.colors.primary : partsData.colors.accent;
|
|
const color = colors.find((c) => c.id === colorId);
|
|
return color ? color.name : colorId;
|
|
};
|
|
|
|
/**
|
|
* Get color hex from color ID
|
|
*/
|
|
export const getColorHex = (colorId, type = 'primary') => {
|
|
const colors = type === 'primary' ? partsData.colors.primary : partsData.colors.accent;
|
|
const color = colors.find((c) => c.id === colorId);
|
|
return color ? color.hex : '#000000';
|
|
};
|
|
|
|
/**
|
|
* Get expanded hardware parts grouped by component
|
|
*/
|
|
export const getExpandedHardwareParts = (config) => {
|
|
const printedParts = getRequiredPrintedParts(config);
|
|
const printedPartIds = new Set(printedParts.map(p => p.id));
|
|
const expandedHardware = [];
|
|
const selectedOptionIds = new Set();
|
|
|
|
// Add Electronics section with Motor, Power Supply, and PCB
|
|
const electronicsHardware = [];
|
|
if (config.motor) {
|
|
electronicsHardware.push({
|
|
id: config.motor.id || 'motor',
|
|
name: config.motor.name || 'Motor',
|
|
description: config.motor.description || '',
|
|
quantity: 1,
|
|
hardwareType: 'Electronics',
|
|
price: config.motor, // Store original item for price conversion
|
|
_isItemReference: true
|
|
});
|
|
}
|
|
if (config.powerSupply) {
|
|
electronicsHardware.push({
|
|
id: config.powerSupply.id || 'power-supply',
|
|
name: config.powerSupply.name || 'Power Supply',
|
|
description: config.powerSupply.description || '',
|
|
quantity: 1,
|
|
hardwareType: 'Electronics',
|
|
price: config.powerSupply, // Store original item for price conversion
|
|
_isItemReference: true
|
|
});
|
|
}
|
|
// Add PCB (always required, separate from PCB mount)
|
|
if (partsData.pcbs && partsData.pcbs.length > 0) {
|
|
const pcb = partsData.pcbs[0]; // Get the first/only PCB
|
|
electronicsHardware.push({
|
|
id: pcb.id || 'pcb',
|
|
name: pcb.name || 'PCB',
|
|
description: pcb.description || '',
|
|
quantity: 1,
|
|
hardwareType: 'Electronics',
|
|
price: pcb, // Store original item for price conversion
|
|
_isItemReference: true
|
|
});
|
|
}
|
|
if (electronicsHardware.length > 0) {
|
|
expandedHardware.push({
|
|
component: 'Electronics',
|
|
parts: electronicsHardware
|
|
});
|
|
}
|
|
|
|
if (config.standCrossbarSupports && config.standCrossbarSupports.length > 0) {
|
|
config.standCrossbarSupports.forEach(opt => selectedOptionIds.add(opt.id));
|
|
}
|
|
if (config.standHinge) selectedOptionIds.add(config.standHinge.id);
|
|
if (config.standFeet) selectedOptionIds.add(config.standFeet.id);
|
|
if (config.mount) selectedOptionIds.add(config.mount.id);
|
|
if (config.cover) selectedOptionIds.add(config.cover.id);
|
|
if (config.pcbMount) selectedOptionIds.add(config.pcbMount.id);
|
|
if (config.remoteKnob) selectedOptionIds.add(config.remoteKnob.id);
|
|
if (config.toyMountOptions && config.toyMountOptions.length > 0) {
|
|
config.toyMountOptions.forEach(opt => selectedOptionIds.add(opt.id));
|
|
}
|
|
|
|
// Handle hinges systems
|
|
if (config.standHinge && partsData.components?.hinges?.systems) {
|
|
const hingeSystem = partsData.components.hinges.systems[config.standHinge.id];
|
|
if (hingeSystem?.hardwareParts) {
|
|
const componentHardware = [];
|
|
hingeSystem.hardwareParts.forEach((hardware) => {
|
|
if (hardware.required) {
|
|
componentHardware.push({
|
|
...hardware,
|
|
quantity: hardware.quantity || 1,
|
|
hardwareType: getHardwareType(hardware)
|
|
});
|
|
}
|
|
});
|
|
if (componentHardware.length > 0) {
|
|
expandedHardware.push({
|
|
component: partsData.components.hinges.category || 'Hinges',
|
|
parts: componentHardware,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle remote systems
|
|
if (config.remoteKnob && partsData.components?.remotes?.systems) {
|
|
let remoteSystem = null;
|
|
Object.values(partsData.components.remotes.systems).forEach((system) => {
|
|
if (system.knobs && system.knobs.find(k => k.id === config.remoteKnob.id)) {
|
|
remoteSystem = system;
|
|
}
|
|
});
|
|
if (remoteSystem?.hardwareParts) {
|
|
const componentHardware = [];
|
|
remoteSystem.hardwareParts.forEach((hardware) => {
|
|
if (hardware.required) {
|
|
componentHardware.push({
|
|
...hardware,
|
|
quantity: hardware.quantity || 1,
|
|
hardwareType: getHardwareType(hardware)
|
|
});
|
|
}
|
|
});
|
|
if (componentHardware.length > 0) {
|
|
expandedHardware.push({
|
|
component: partsData.components.remotes.category || 'Remote',
|
|
parts: componentHardware,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle mount systems
|
|
if (config.mount && partsData.components?.mounting?.systems) {
|
|
const mountSystem = partsData.components.mounting.systems[config.mount.id];
|
|
if (mountSystem?.hardwareParts) {
|
|
const componentHardware = [];
|
|
mountSystem.hardwareParts.forEach((hardware) => {
|
|
if (hardware.required && evaluateCondition(hardware.Condition, config)) {
|
|
componentHardware.push({
|
|
...hardware,
|
|
quantity: hardware.quantity || 1,
|
|
hardwareType: getHardwareType(hardware)
|
|
});
|
|
}
|
|
});
|
|
if (componentHardware.length > 0) {
|
|
expandedHardware.push({
|
|
component: partsData.components.mounting.category || 'Mounting',
|
|
parts: componentHardware,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle toy mount systems
|
|
if (config.toyMountOptions && config.toyMountOptions.length > 0 && partsData.components?.toyMounts?.systems) {
|
|
config.toyMountOptions.forEach((toyMount) => {
|
|
const toyMountSystem = partsData.components.toyMounts.systems[toyMount.id];
|
|
if (toyMountSystem?.hardwareParts) {
|
|
const componentHardware = [];
|
|
toyMountSystem.hardwareParts.forEach((hardware) => {
|
|
if (hardware.required && evaluateCondition(hardware.Condition, config)) {
|
|
componentHardware.push({
|
|
...hardware,
|
|
quantity: hardware.quantity || 1,
|
|
hardwareType: getHardwareType(hardware)
|
|
});
|
|
}
|
|
});
|
|
if (componentHardware.length > 0) {
|
|
expandedHardware.push({
|
|
component: partsData.components.toyMounts.category || 'Toy Mounts',
|
|
parts: componentHardware,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handle other components
|
|
const componentPrintedPartIds = new Map();
|
|
Object.entries(partsData.components || {}).forEach(([componentKey, component]) => {
|
|
if (componentKey === 'hinges' || componentKey === 'remotes' || componentKey === 'mounting' || componentKey === 'toyMounts') return;
|
|
|
|
// Skip components that don't have selected options (except actuator)
|
|
if (!shouldIncludeComponent(componentKey, config)) {
|
|
return;
|
|
}
|
|
|
|
if (component.printedParts) {
|
|
const partIds = component.printedParts.map(p => p.id);
|
|
componentPrintedPartIds.set(componentKey, new Set(partIds));
|
|
}
|
|
});
|
|
|
|
Object.entries(partsData.components || {}).forEach(([componentKey, component]) => {
|
|
if (componentKey === 'hinges' || componentKey === 'remotes' || componentKey === 'mounting' || componentKey === 'toyMounts') return;
|
|
|
|
// Skip components that don't have selected options (except actuator)
|
|
if (!shouldIncludeComponent(componentKey, config)) {
|
|
return;
|
|
}
|
|
|
|
if (!component.hardwareParts) return;
|
|
|
|
const componentPartIds = componentPrintedPartIds.get(componentKey);
|
|
const hasSelectedParts = componentPartIds && Array.from(componentPartIds).some(id => printedPartIds.has(id) || selectedOptionIds.has(id));
|
|
|
|
const componentHardware = [];
|
|
component.hardwareParts.forEach((hardware) => {
|
|
if (!hardware.required) return;
|
|
|
|
let shouldInclude = false;
|
|
if (hasSelectedParts) {
|
|
const relatedParts = hardware.relatedParts || [];
|
|
if (relatedParts.length === 0) {
|
|
shouldInclude = true;
|
|
} else {
|
|
shouldInclude = relatedParts.some(partId => printedPartIds.has(partId) || selectedOptionIds.has(partId));
|
|
}
|
|
}
|
|
|
|
if (shouldInclude) {
|
|
componentHardware.push({
|
|
...hardware,
|
|
quantity: hardware.quantity || 1,
|
|
hardwareType: getHardwareType(hardware)
|
|
});
|
|
}
|
|
});
|
|
|
|
if (componentHardware.length > 0) {
|
|
expandedHardware.push({
|
|
component: component.category || componentKey,
|
|
parts: componentHardware,
|
|
});
|
|
}
|
|
});
|
|
|
|
return expandedHardware;
|
|
};
|