Add customer info step (name + project) between access control and quote
This commit is contained in:
@ -4,6 +4,7 @@ import { calculateQuote } from '../utils/quoteCalculator';
|
|||||||
export default function QuoteSummary({ pricing, selections, onBack, onNew }) {
|
export default function QuoteSummary({ pricing, selections, onBack, onNew }) {
|
||||||
const quoteRef = useRef(null);
|
const quoteRef = useRef(null);
|
||||||
const quote = calculateQuote(pricing, selections);
|
const quote = calculateQuote(pricing, selections);
|
||||||
|
const { customerInfo } = selections;
|
||||||
const operator =
|
const operator =
|
||||||
selections.operator && typeof selections.operator === 'object'
|
selections.operator && typeof selections.operator === 'object'
|
||||||
? selections.operator
|
? selections.operator
|
||||||
@ -78,6 +79,16 @@ export default function QuoteSummary({ pricing, selections, onBack, onNew }) {
|
|||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Quotation Date: {new Date().toLocaleDateString('en-CA')}
|
Quotation Date: {new Date().toLocaleDateString('en-CA')}
|
||||||
</p>
|
</p>
|
||||||
|
{customerInfo?.name && (
|
||||||
|
<p className="text-sm text-gray-700 mt-1.5 font-medium">
|
||||||
|
{customerInfo.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{customerInfo?.project && (
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{customerInfo.project}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-lg font-bold text-blue-600">
|
<div className="text-lg font-bold text-blue-600">
|
||||||
|
|||||||
81
client/src/components/StepCustomerInfo.jsx
Normal file
81
client/src/components/StepCustomerInfo.jsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function StepCustomerInfo({ value, onChange, onBack, onNext }) {
|
||||||
|
const [name, setName] = useState(value.name || '');
|
||||||
|
const [project, setProject] = useState(value.project || '');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setName(value.name || '');
|
||||||
|
setProject(value.project || '');
|
||||||
|
}, [value.name, value.project]);
|
||||||
|
|
||||||
|
const handleContinue = () => {
|
||||||
|
onChange({ name: name.trim(), project: project.trim() });
|
||||||
|
onNext();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-6">
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
className="p-1.5 rounded-lg hover:bg-gray-100 text-gray-500 transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900">
|
||||||
|
Step 4: Customer Information
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-500">
|
||||||
|
Enter the customer and project details for this quote
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-5 max-w-lg">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="customerName" className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
Customer Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="customerName"
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="e.g. John Smith or ACME Corp"
|
||||||
|
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all placeholder-gray-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="projectName" className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
Project / Site Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="projectName"
|
||||||
|
type="text"
|
||||||
|
value={project}
|
||||||
|
onChange={(e) => setProject(e.target.value)}
|
||||||
|
placeholder="e.g. Main Street Office Park"
|
||||||
|
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all placeholder-gray-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-6 border-t border-gray-200 mt-8">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{name || project ? 'Information entered' : 'Optional — you can leave these blank'}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleContinue}
|
||||||
|
className="px-6 py-2.5 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Generate Quote
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import { useReducer } from 'react';
|
|||||||
import StepOperator from './StepOperator';
|
import StepOperator from './StepOperator';
|
||||||
import StepGroundLoops from './StepGroundLoops';
|
import StepGroundLoops from './StepGroundLoops';
|
||||||
import StepAccessControl from './StepAccessControl';
|
import StepAccessControl from './StepAccessControl';
|
||||||
|
import StepCustomerInfo from './StepCustomerInfo';
|
||||||
import QuoteSummary from './QuoteSummary';
|
import QuoteSummary from './QuoteSummary';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
@ -18,6 +19,7 @@ const initialState = {
|
|||||||
accessControl: [],
|
accessControl: [],
|
||||||
remoteButtons: 4,
|
remoteButtons: 4,
|
||||||
remoteQuantity: 1,
|
remoteQuantity: 1,
|
||||||
|
customerInfo: { name: '', project: '' },
|
||||||
};
|
};
|
||||||
|
|
||||||
function reducer(state, action) {
|
function reducer(state, action) {
|
||||||
@ -82,8 +84,10 @@ function reducer(state, action) {
|
|||||||
return { ...state, remoteButtons: action.payload };
|
return { ...state, remoteButtons: action.payload };
|
||||||
case 'SET_REMOTE_QUANTITY':
|
case 'SET_REMOTE_QUANTITY':
|
||||||
return { ...state, remoteQuantity: Math.max(1, action.payload) };
|
return { ...state, remoteQuantity: Math.max(1, action.payload) };
|
||||||
|
case 'SET_CUSTOMER_INFO':
|
||||||
|
return { ...state, customerInfo: action.payload };
|
||||||
case 'NEXT_STEP':
|
case 'NEXT_STEP':
|
||||||
return { ...state, step: Math.min(state.step + 1, 4) };
|
return { ...state, step: Math.min(state.step + 1, 5) };
|
||||||
case 'PREV_STEP':
|
case 'PREV_STEP':
|
||||||
return { ...state, step: Math.max(state.step - 1, 1) };
|
return { ...state, step: Math.max(state.step - 1, 1) };
|
||||||
case 'SET_GROUND_LOOP_SIZE':
|
case 'SET_GROUND_LOOP_SIZE':
|
||||||
@ -98,7 +102,7 @@ function reducer(state, action) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
case 'GO_TO_STEP':
|
case 'GO_TO_STEP':
|
||||||
return { ...state, step: Math.min(action.payload, 4) };
|
return { ...state, step: Math.min(action.payload, 5) };
|
||||||
case 'RESET':
|
case 'RESET':
|
||||||
return { ...initialState };
|
return { ...initialState };
|
||||||
default:
|
default:
|
||||||
@ -108,13 +112,14 @@ 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, armChoice, optionalParts, groundLoops, accessControl, remoteButtons, remoteQuantity } = state;
|
const { step, operator, armChoice, optionalParts, groundLoops, accessControl, remoteButtons, remoteQuantity, customerInfo } = state;
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{ num: 1, label: 'Operator' },
|
{ num: 1, label: 'Operator' },
|
||||||
{ num: 2, label: 'Ground Loops' },
|
{ num: 2, label: 'Ground Loops' },
|
||||||
{ num: 3, label: 'Access Control' },
|
{ num: 3, label: 'Access Control' },
|
||||||
{ num: 4, label: 'Quote' },
|
{ num: 4, label: 'Customer' },
|
||||||
|
{ num: 5, label: 'Quote' },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -226,9 +231,18 @@ export default function Wizard({ pricing, onLogout }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{step === 4 && (
|
{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
|
<QuoteSummary
|
||||||
pricing={pricing}
|
pricing={pricing}
|
||||||
selections={{ operator, armChoice, optionalParts, groundLoops, accessControl, remoteButtons, remoteQuantity }}
|
selections={{ operator, armChoice, optionalParts, groundLoops, accessControl, remoteButtons, remoteQuantity, customerInfo }}
|
||||||
onBack={() => dispatch({ type: 'PREV_STEP' })}
|
onBack={() => dispatch({ type: 'PREV_STEP' })}
|
||||||
onNew={() => dispatch({ type: 'RESET' })}
|
onNew={() => dispatch({ type: 'RESET' })}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user