feat: Add prop-types dependency, implement currency context, and enhance pricing display in components
- 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.
This commit is contained in:
761
website/src/utils/partUtils.js
Normal file
761
website/src/utils/partUtils.js
Normal file
@@ -0,0 +1,761 @@
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user