254 lines
9.0 KiB
JavaScript
254 lines
9.0 KiB
JavaScript
import { useReducer } from 'react';
|
|
import StepOperator from './StepOperator';
|
|
import StepGroundLoops from './StepGroundLoops';
|
|
import StepAccessControl from './StepAccessControl';
|
|
import StepCustomerInfo from './StepCustomerInfo';
|
|
import QuoteSummary from './QuoteSummary';
|
|
|
|
const initialState = {
|
|
step: 1,
|
|
operator: null,
|
|
armChoice: null,
|
|
optionalParts: [],
|
|
groundLoops: {
|
|
needed: false,
|
|
style: null,
|
|
types: [],
|
|
typeSizes: {},
|
|
},
|
|
accessControl: [],
|
|
remoteButtons: 4,
|
|
remoteQuantity: 1,
|
|
customerInfo: { name: '', project: '' },
|
|
};
|
|
|
|
function reducer(state, action) {
|
|
switch (action.type) {
|
|
case 'SELECT_OPERATOR':
|
|
return { ...state, operator: action.payload, armChoice: null, optionalParts: [], step: 2 };
|
|
case 'SET_ARM_CHOICE':
|
|
return { ...state, armChoice: action.payload };
|
|
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,
|
|
groundLoops: { needed: action.payload, style: null, types: [], typeSizes: {} },
|
|
step: action.payload ? state.step : state.step + 1,
|
|
};
|
|
case 'SET_GROUND_LOOP_STYLE':
|
|
return {
|
|
...state,
|
|
groundLoops: { ...state.groundLoops, style: action.payload },
|
|
};
|
|
case 'TOGGLE_GROUND_LOOP_TYPE': {
|
|
const id = action.payload;
|
|
const exists = state.groundLoops.types.includes(id);
|
|
const newTypeSizes = { ...state.groundLoops.typeSizes };
|
|
if (!exists) {
|
|
newTypeSizes[id] = '4x8';
|
|
} else {
|
|
delete newTypeSizes[id];
|
|
}
|
|
return {
|
|
...state,
|
|
groundLoops: {
|
|
...state.groundLoops,
|
|
types: exists
|
|
? state.groundLoops.types.filter((t) => t !== id)
|
|
: [...state.groundLoops.types, id],
|
|
typeSizes: newTypeSizes,
|
|
},
|
|
};
|
|
}
|
|
case 'TOGGLE_ACCESS_CONTROL': {
|
|
const id = action.payload;
|
|
const exists = state.accessControl.includes(id);
|
|
return {
|
|
...state,
|
|
accessControl: exists
|
|
? state.accessControl.filter((a) => a !== id)
|
|
: [...state.accessControl, id],
|
|
remoteButtons: id === 'remote-kit' && !exists ? 4 : state.remoteButtons,
|
|
};
|
|
}
|
|
case 'SET_REMOTE_BUTTONS':
|
|
return { ...state, remoteButtons: action.payload };
|
|
case 'SET_REMOTE_QUANTITY':
|
|
return { ...state, remoteQuantity: Math.max(1, action.payload) };
|
|
case 'SET_CUSTOMER_INFO':
|
|
return { ...state, customerInfo: action.payload };
|
|
case 'NEXT_STEP':
|
|
return { ...state, step: Math.min(state.step + 1, 5) };
|
|
case 'PREV_STEP':
|
|
return { ...state, step: Math.max(state.step - 1, 1) };
|
|
case 'SET_GROUND_LOOP_SIZE':
|
|
return {
|
|
...state,
|
|
groundLoops: {
|
|
...state.groundLoops,
|
|
typeSizes: {
|
|
...state.groundLoops.typeSizes,
|
|
[action.payload.typeId]: action.payload.sizeId,
|
|
},
|
|
},
|
|
};
|
|
case 'GO_TO_STEP':
|
|
return { ...state, step: Math.min(action.payload, 5) };
|
|
case 'RESET':
|
|
return { ...initialState };
|
|
default:
|
|
return state;
|
|
}
|
|
}
|
|
|
|
export default function Wizard({ pricing, onLogout }) {
|
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
const { step, operator, armChoice, optionalParts, groundLoops, accessControl, remoteButtons, remoteQuantity, customerInfo } = state;
|
|
|
|
const steps = [
|
|
{ num: 1, label: 'Operator' },
|
|
{ num: 2, label: 'Ground Loops' },
|
|
{ num: 3, label: 'Access Control' },
|
|
{ num: 4, label: 'Customer' },
|
|
{ num: 5, label: 'Quote' },
|
|
];
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto px-4 py-8">
|
|
<div className="text-center mb-8 relative">
|
|
<button
|
|
onClick={onLogout}
|
|
className="absolute right-0 top-0 text-xs text-gray-400 hover:text-gray-600 transition-colors flex items-center gap-1"
|
|
title="Logout"
|
|
>
|
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
|
</svg>
|
|
Logout
|
|
</button>
|
|
<h1 className="text-3xl font-bold text-gray-900">
|
|
Gate Operator Quotation System
|
|
</h1>
|
|
<p className="text-gray-500 mt-2">
|
|
Select your configuration to generate a detailed cost estimate
|
|
</p>
|
|
</div>
|
|
|
|
<nav className="mb-8">
|
|
<ol className="flex items-center justify-center gap-2">
|
|
{steps.map((s) => (
|
|
<li key={s.num} className="flex items-center gap-2">
|
|
{s.num > 1 && (
|
|
<div
|
|
className={`w-8 h-0.5 ${
|
|
step >= s.num ? 'bg-blue-600' : 'bg-gray-300'
|
|
}`}
|
|
/>
|
|
)}
|
|
<button
|
|
onClick={() => {
|
|
if (s.num < step) dispatch({ type: 'GO_TO_STEP', payload: s.num });
|
|
}}
|
|
disabled={s.num > step}
|
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium transition-colors ${
|
|
step === s.num
|
|
? 'bg-blue-600 text-white'
|
|
: step > s.num
|
|
? 'bg-blue-100 text-blue-700 cursor-pointer hover:bg-blue-200'
|
|
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`w-5 h-5 rounded-full flex items-center justify-center text-xs font-bold ${
|
|
step === s.num
|
|
? 'bg-white text-blue-600'
|
|
: step > s.num
|
|
? 'bg-blue-600 text-white'
|
|
: 'bg-gray-300 text-gray-500'
|
|
}`}
|
|
>
|
|
{step > s.num ? '\u2713' : s.num}
|
|
</span>
|
|
{s.label}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</nav>
|
|
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 md:p-8">
|
|
{step === 1 && (
|
|
<StepOperator
|
|
operators={pricing.operators}
|
|
selected={operator}
|
|
armChoice={armChoice}
|
|
optionalParts={optionalParts}
|
|
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 })}
|
|
onNext={() => dispatch({ type: 'NEXT_STEP' })}
|
|
/>
|
|
)}
|
|
|
|
{step === 2 && (
|
|
<StepGroundLoops
|
|
detectors={pricing.loopDetectors}
|
|
styles={pricing.groundLoopStyles}
|
|
sizes={pricing.groundLoopSizes}
|
|
types={pricing.groundLoopTypes}
|
|
value={groundLoops}
|
|
onSetNeeded={(needed) => dispatch({ type: 'SET_GROUND_LOOPS_NEEDED', payload: needed })}
|
|
onSetStyle={(id) => dispatch({ type: 'SET_GROUND_LOOP_STYLE', payload: id })}
|
|
onToggleType={(id) => dispatch({ type: 'TOGGLE_GROUND_LOOP_TYPE', payload: id })}
|
|
onSetSize={(typeId, sizeId) => dispatch({ type: 'SET_GROUND_LOOP_SIZE', payload: { typeId, sizeId } })}
|
|
onNext={() => dispatch({ type: 'NEXT_STEP' })}
|
|
onBack={() => dispatch({ type: 'PREV_STEP' })}
|
|
/>
|
|
)}
|
|
|
|
{step === 3 && (
|
|
<StepAccessControl
|
|
accessControl={pricing.accessControl}
|
|
remoteButtonOptions={pricing.remoteButtonOptions}
|
|
selected={accessControl}
|
|
remoteButtons={remoteButtons}
|
|
remoteQuantity={remoteQuantity}
|
|
onToggle={(id) => dispatch({ type: 'TOGGLE_ACCESS_CONTROL', payload: id })}
|
|
onSetRemoteButtons={(count) => dispatch({ type: 'SET_REMOTE_BUTTONS', payload: count })}
|
|
onSetRemoteQuantity={(qty) => dispatch({ type: 'SET_REMOTE_QUANTITY', payload: qty })}
|
|
onBack={() => dispatch({ type: 'PREV_STEP' })}
|
|
onNext={() => dispatch({ type: 'NEXT_STEP' })}
|
|
/>
|
|
)}
|
|
|
|
{step === 4 && (
|
|
<StepCustomerInfo
|
|
value={customerInfo}
|
|
onChange={(info) => dispatch({ type: 'SET_CUSTOMER_INFO', payload: info })}
|
|
onBack={() => dispatch({ type: 'PREV_STEP' })}
|
|
onNext={() => dispatch({ type: 'NEXT_STEP' })}
|
|
/>
|
|
)}
|
|
|
|
{step === 5 && (
|
|
<QuoteSummary
|
|
pricing={pricing}
|
|
selections={{ operator, armChoice, optionalParts, groundLoops, accessControl, remoteButtons, remoteQuantity, customerInfo }}
|
|
onBack={() => dispatch({ type: 'PREV_STEP' })}
|
|
onNew={() => dispatch({ type: 'RESET' })}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|