From 5ce5f6ddee130ee4f38fea038b03838e085b3166 Mon Sep 17 00:00:00 2001 From: Todd Date: Mon, 25 May 2026 21:59:15 -0400 Subject: [PATCH] Swing gate changes: LA500 1 op + secondary arm, CSW24UL optional second operator --- client/src/components/StepOperator.jsx | 75 +++++++++++++++++++++++++- client/src/components/Wizard.jsx | 20 +++++-- client/src/utils/quoteCalculator.js | 24 ++++++++- server/data/pricing.json | 12 +++-- 4 files changed, 122 insertions(+), 9 deletions(-) diff --git a/client/src/components/StepOperator.jsx b/client/src/components/StepOperator.jsx index 8bc0246..e789515 100644 --- a/client/src/components/StepOperator.jsx +++ b/client/src/components/StepOperator.jsx @@ -7,8 +7,16 @@ const categoryLabels = { tilt: 'Tilt Gate Operators', }; -export default function StepOperator({ operators, selected, onSelect }) { +export default function StepOperator({ + operators, + selected, + optionalParts, + onSelect, + onToggleOptionalPart, + onNext, +}) { const categories = [...new Set(operators.map((o) => o.category))]; + const selectedOp = operators.find((o) => o.id === selected); return (
@@ -83,6 +91,71 @@ export default function StepOperator({ operators, selected, onSelect }) {
))} + + {selectedOp?.optionalParts && selectedOp.optionalParts.length > 0 && ( +
+

+ Optional Add-Ons for {selectedOp.model} +

+
+ {selectedOp.optionalParts.map((part) => { + const isSelected = optionalParts.includes(part.id); + return ( + + ); + })} +
+
+ )} + + {selected && ( +
+
+ {selectedOp?.optionalParts?.length > 0 + ? `${optionalParts.length} optional add-on${optionalParts.length !== 1 ? 's' : ''} selected` + : 'Operator selected'} +
+ +
+ )} ); } diff --git a/client/src/components/Wizard.jsx b/client/src/components/Wizard.jsx index 8a2fea0..9b9511f 100644 --- a/client/src/components/Wizard.jsx +++ b/client/src/components/Wizard.jsx @@ -7,6 +7,7 @@ import QuoteSummary from './QuoteSummary'; const initialState = { step: 1, operator: null, + optionalParts: [], groundLoops: { needed: false, style: null, @@ -21,7 +22,17 @@ const initialState = { function reducer(state, action) { switch (action.type) { case 'SELECT_OPERATOR': - return { ...state, operator: action.payload, step: 2 }; + return { ...state, operator: action.payload, optionalParts: [], step: 2 }; + case 'TOGGLE_OPTIONAL_PART': { + const id = action.payload; + const exists = state.optionalParts.includes(id); + return { + ...state, + optionalParts: exists + ? state.optionalParts.filter((p) => p !== id) + : [...state.optionalParts, id], + }; + } case 'SET_GROUND_LOOPS_NEEDED': return { ...state, @@ -94,7 +105,7 @@ function reducer(state, action) { export default function Wizard({ pricing, onLogout }) { const [state, dispatch] = useReducer(reducer, initialState); - const { step, operator, groundLoops, accessControl, remoteButtons, remoteQuantity } = state; + const { step, operator, optionalParts, groundLoops, accessControl, remoteButtons, remoteQuantity } = state; const steps = [ { num: 1, label: 'Operator' }, @@ -171,7 +182,10 @@ export default function Wizard({ pricing, onLogout }) { dispatch({ type: 'SELECT_OPERATOR', payload: id })} + onToggleOptionalPart={(id) => dispatch({ type: 'TOGGLE_OPTIONAL_PART', payload: id })} + onNext={() => dispatch({ type: 'NEXT_STEP' })} /> )} @@ -209,7 +223,7 @@ export default function Wizard({ pricing, onLogout }) { {step === 4 && ( dispatch({ type: 'PREV_STEP' })} onNew={() => dispatch({ type: 'RESET' })} /> diff --git a/client/src/utils/quoteCalculator.js b/client/src/utils/quoteCalculator.js index 071737c..94cc91d 100644 --- a/client/src/utils/quoteCalculator.js +++ b/client/src/utils/quoteCalculator.js @@ -1,5 +1,5 @@ export function calculateQuote(pricing, selections) { - const { operator, groundLoops, accessControl } = selections; + const { operator, optionalParts, groundLoops, accessControl } = selections; const items = []; let subtotal = 0; @@ -34,6 +34,28 @@ export function calculateQuote(pricing, selections) { }); } } + + // Optional add-on parts for selected operator + if (optionalParts && optionalParts.length > 0) { + const op = typeof operator === 'string' && pricing.operators.find((o) => o.id === operator); + if (op?.optionalParts) { + optionalParts.forEach((partId) => { + const part = op.optionalParts.find((p) => p.id === partId); + if (part) { + const lineTotal = part.qty * part.unitPrice; + items.push({ + type: 'optionalPart', + category: 'Optional Add-Ons', + name: part.name, + qty: part.qty, + unitPrice: part.unitPrice, + lineTotal, + }); + subtotal += lineTotal; + } + }); + } + } } if (groundLoops?.needed) { diff --git a/server/data/pricing.json b/server/data/pricing.json index 339b9ea..6879862 100644 --- a/server/data/pricing.json +++ b/server/data/pricing.json @@ -11,11 +11,12 @@ "model": "LA500", "category": "swing", "description": "24V DC Swing Gate Linear Operator ", - "basePrice": 2495, + "basePrice": 1732.50, "image": "swing", "imageFile": "liftmaster-la500-bundle.jpg", "requiredParts": [ - { "id": "op-unit-swing", "name": "Operator Unit (LA500)", "qty": 2, "unitPrice": 1247.50 }, + { "id": "op-unit-swing", "name": "Operator Unit (LA500)", "qty": 1, "unitPrice": 1247.50 }, + { "id": "secondary-arm", "name": "Secondary Arm Kit (LA500)", "qty": 1, "unitPrice": 485 }, { "id": "mounting-post", "name": "Mounting Post", "qty": 1, "unitPrice": 85 }, { "id": "prep-cost", "name": "In House Prep Cost", "qty": 1, "unitPrice": 150 }, { "id": "shipping", "name": "Shipping Cost", "qty": 1, "unitPrice": 200 } @@ -27,15 +28,18 @@ "model": "CSW24UL", "category": "swing", "description": "24V DC Heavy Duty Swing Gate Operator", - "basePrice": 3995, + "basePrice": 2442.50, "image": "swing", "imageFile": "liftmaster-csw24.jpg", "requiredParts": [ - { "id": "op-unit-swing-hd", "name": "Heavy Duty Operator Unit (CSW24UL)", "qty": 2, "unitPrice": 1997.50 }, + { "id": "op-unit-swing-hd", "name": "Heavy Duty Operator Unit (CSW24UL)", "qty": 1, "unitPrice": 1997.50 }, { "id": "mount-pad", "name": "Mounting Pad", "qty": 2, "unitPrice": 145 }, { "id": "prep-cost", "name": "In House Prep Cost", "qty": 1, "unitPrice": 150 }, { "id": "shipping", "name": "Shipping Cost", "qty": 1, "unitPrice": 200 }, { "id": "mounting-post", "name": "Mounting Post", "qty": 2, "unitPrice": 85 } + ], + "optionalParts": [ + { "id": "second-op-csw", "name": "Second Heavy Duty Operator Unit (CSW24UL)", "qty": 1, "unitPrice": 1997.50 } ] }, {