Add arm length options for MAT (12ft/17ft) and Techna (12ft/14ft)

This commit is contained in:
Todd
2026-05-25 22:06:16 -04:00
parent 5ce5f6ddee
commit b696c5054d
4 changed files with 90 additions and 26 deletions

View File

@ -10,8 +10,10 @@ const categoryLabels = {
export default function StepOperator({ export default function StepOperator({
operators, operators,
selected, selected,
armChoice,
optionalParts, optionalParts,
onSelect, onSelect,
onSetArmChoice,
onToggleOptionalPart, onToggleOptionalPart,
onNext, onNext,
}) { }) {
@ -92,6 +94,44 @@ export default function StepOperator({
</div> </div>
))} ))}
{selectedOp?.armOptions && selectedOp.armOptions.length > 0 && (
<div className="mt-8 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-3">
Select Arm Length
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{selectedOp.armOptions.map((opt) => {
const isSelected = armChoice === opt.id;
return (
<button
key={opt.id}
onClick={() => onSetArmChoice(opt.id)}
className={`text-left p-4 rounded-xl border-2 transition-all ${
isSelected
? 'border-blue-600 bg-blue-50 shadow-md'
: 'border-gray-200 bg-white hover:border-blue-300 hover:shadow-sm'
}`}
>
<div className="flex items-start gap-3">
<div className={`shrink-0 w-5 h-5 rounded-full border-2 flex items-center justify-center mt-0.5 ${
isSelected ? 'border-blue-600' : 'border-gray-300'
}`}>
{isSelected && <div className="w-2.5 h-2.5 rounded-full bg-blue-600" />}
</div>
<div>
<div className="font-semibold text-gray-900">{opt.name}</div>
<div className="mt-1 text-lg font-bold text-blue-600">
C${opt.price.toLocaleString('en-CA')}
</div>
</div>
</div>
</button>
);
})}
</div>
</div>
)}
{selectedOp?.optionalParts && selectedOp.optionalParts.length > 0 && ( {selectedOp?.optionalParts && selectedOp.optionalParts.length > 0 && (
<div className="mt-8 pt-6 border-t border-gray-200"> <div className="mt-8 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-3"> <h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-3">

View File

@ -7,6 +7,7 @@ import QuoteSummary from './QuoteSummary';
const initialState = { const initialState = {
step: 1, step: 1,
operator: null, operator: null,
armChoice: null,
optionalParts: [], optionalParts: [],
groundLoops: { groundLoops: {
needed: false, needed: false,
@ -22,7 +23,9 @@ const initialState = {
function reducer(state, action) { function reducer(state, action) {
switch (action.type) { switch (action.type) {
case 'SELECT_OPERATOR': case 'SELECT_OPERATOR':
return { ...state, operator: action.payload, optionalParts: [], step: 2 }; return { ...state, operator: action.payload, armChoice: null, optionalParts: [], step: 2 };
case 'SET_ARM_CHOICE':
return { ...state, armChoice: action.payload };
case 'TOGGLE_OPTIONAL_PART': { case 'TOGGLE_OPTIONAL_PART': {
const id = action.payload; const id = action.payload;
const exists = state.optionalParts.includes(id); const exists = state.optionalParts.includes(id);
@ -105,7 +108,7 @@ function reducer(state, action) {
export default function Wizard({ pricing, onLogout }) { export default function Wizard({ pricing, onLogout }) {
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
const { step, operator, optionalParts, groundLoops, accessControl, remoteButtons, remoteQuantity } = state; const { step, operator, armChoice, optionalParts, groundLoops, accessControl, remoteButtons, remoteQuantity } = state;
const steps = [ const steps = [
{ num: 1, label: 'Operator' }, { num: 1, label: 'Operator' },
@ -182,8 +185,10 @@ export default function Wizard({ pricing, onLogout }) {
<StepOperator <StepOperator
operators={pricing.operators} operators={pricing.operators}
selected={operator} selected={operator}
armChoice={armChoice}
optionalParts={optionalParts} optionalParts={optionalParts}
onSelect={(id) => dispatch({ type: 'SELECT_OPERATOR', payload: id })} onSelect={(id) => dispatch({ type: 'SELECT_OPERATOR', payload: id })}
onSetArmChoice={(id) => dispatch({ type: 'SET_ARM_CHOICE', payload: id })}
onToggleOptionalPart={(id) => dispatch({ type: 'TOGGLE_OPTIONAL_PART', payload: id })} onToggleOptionalPart={(id) => dispatch({ type: 'TOGGLE_OPTIONAL_PART', payload: id })}
onNext={() => dispatch({ type: 'NEXT_STEP' })} onNext={() => dispatch({ type: 'NEXT_STEP' })}
/> />
@ -223,7 +228,7 @@ export default function Wizard({ pricing, onLogout }) {
{step === 4 && ( {step === 4 && (
<QuoteSummary <QuoteSummary
pricing={pricing} pricing={pricing}
selections={{ operator, optionalParts, groundLoops, accessControl, remoteButtons, remoteQuantity }} selections={{ operator, armChoice, optionalParts, groundLoops, accessControl, remoteButtons, remoteQuantity }}
onBack={() => dispatch({ type: 'PREV_STEP' })} onBack={() => dispatch({ type: 'PREV_STEP' })}
onNew={() => dispatch({ type: 'RESET' })} onNew={() => dispatch({ type: 'RESET' })}
/> />

View File

@ -1,5 +1,5 @@
export function calculateQuote(pricing, selections) { export function calculateQuote(pricing, selections) {
const { operator, optionalParts, groundLoops, accessControl } = selections; const { operator, armChoice, optionalParts, groundLoops, accessControl } = selections;
const items = []; const items = [];
let subtotal = 0; let subtotal = 0;
@ -32,13 +32,25 @@ export function calculateQuote(pricing, selections) {
}); });
subtotal += lineTotal; subtotal += lineTotal;
}); });
// Selected arm option (barrier operators)
if (armChoice && op.armOptions) {
const arm = op.armOptions.find((a) => a.id === armChoice);
if (arm) {
items.push({
type: 'armOption',
category: 'Required Equipment',
name: arm.name,
qty: 1,
unitPrice: arm.price,
lineTotal: arm.price,
});
subtotal += arm.price;
} }
} }
// Optional add-on parts for selected operator // Optional add-on parts
if (optionalParts && optionalParts.length > 0) { if (optionalParts && optionalParts.length > 0 && op.optionalParts) {
const op = typeof operator === 'string' && pricing.operators.find((o) => o.id === operator);
if (op?.optionalParts) {
optionalParts.forEach((partId) => { optionalParts.forEach((partId) => {
const part = op.optionalParts.find((p) => p.id === partId); const part = op.optionalParts.find((p) => p.id === partId);
if (part) { if (part) {
@ -57,6 +69,7 @@ export function calculateQuote(pricing, selections) {
} }
} }
} }
}
if (groundLoops?.needed) { if (groundLoops?.needed) {
if (groundLoops.style) { if (groundLoops.style) {

View File

@ -85,8 +85,11 @@
"image": "barrier", "image": "barrier",
"imageFile": "Mat.jpeg", "imageFile": "Mat.jpeg",
"requiredParts": [ "requiredParts": [
{ "id": "op-unit-barrier", "name": "Mega Arm Tower Barrier Gate Operator(MAT)", "qty": 1, "unitPrice": 3195 }, { "id": "op-unit-barrier", "name": "Mega Arm Tower Barrier Gate Operator (MAT)", "qty": 1, "unitPrice": 3195 }
{ "id": "barrier-arm", "name": "Barrier Arm (8ft Aluminum)", "qty": 1, "unitPrice": 395 } ],
"armOptions": [
{ "id": "arm-mat-12", "name": "Barrier Arm (12ft Aluminum)", "price": 495 },
{ "id": "arm-mat-17", "name": "Barrier Arm (17ft Aluminum)", "price": 695 }
] ]
}, },
{ {
@ -99,8 +102,11 @@
"image": "barrier", "image": "barrier",
"imageFile": "techno.jpeg", "imageFile": "techno.jpeg",
"requiredParts": [ "requiredParts": [
{ "id": "op-unit-barrier-hd", "name": "Techna Barrier Gate Operator (CBG24DC)", "qty": 1, "unitPrice": 5495 }, { "id": "op-unit-barrier-hd", "name": "Techna Barrier Gate Operator (CBG24DC)", "qty": 1, "unitPrice": 5495 }
{ "id": "barrier-arm-hd", "name": "Barrier Arm (16ft Steel)", "qty": 1, "unitPrice": 695 } ],
"armOptions": [
{ "id": "arm-techna-12", "name": "Barrier Arm (12ft Steel)", "price": 595 },
{ "id": "arm-techna-14", "name": "Barrier Arm (14ft Steel)", "price": 695 }
] ]
}, },
{ {