sync: Auto-sync from ACG-M-L5090 at 2026-03-10 19:11:00
Synced files: - Quote wizard frontend (all components, hooks, types, config) - API updates (config, models, routers, schemas, services) - Client work (bg-builders, gurushow) - Scripts (BGB Lesley termination, CIPP, Datto, migration) - Temp files (Bardach contacts, VWP investigation, misc) - Credentials and session logs - Email service, PHP API, session logs Machine: ACG-M-L5090 Timestamp: 2026-03-10 19:11:00 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,46 +1,28 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
||||
import type { WizardStep } from '@/types/quote';
|
||||
|
||||
/**
|
||||
* Wizard steps configuration for the 7-step MSP Quote Wizard
|
||||
*/
|
||||
const WIZARD_STEPS: Omit<WizardStep, 'isComplete' | 'isActive'>[] = [
|
||||
{
|
||||
id: 'company',
|
||||
title: 'Company Profile',
|
||||
description: 'Tell us about your business',
|
||||
},
|
||||
{
|
||||
id: 'gps',
|
||||
title: 'GPS Monitoring',
|
||||
description: 'Select your monitoring tier',
|
||||
},
|
||||
{
|
||||
id: 'support',
|
||||
title: 'Support Plan',
|
||||
description: 'Choose your support level',
|
||||
},
|
||||
{
|
||||
id: 'voip',
|
||||
title: 'VoIP Phone System',
|
||||
description: 'Business phone options',
|
||||
},
|
||||
{
|
||||
id: 'web-email',
|
||||
title: 'Web & Email',
|
||||
description: 'Hosting and email services',
|
||||
},
|
||||
{
|
||||
id: 'summary',
|
||||
title: 'Review Quote',
|
||||
description: 'Review your selections',
|
||||
},
|
||||
{
|
||||
id: 'contact',
|
||||
title: 'Get Your Quote',
|
||||
description: 'Submit your information',
|
||||
},
|
||||
];
|
||||
export interface WizardStepDef {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/** Map step id from URL hash to step index */
|
||||
function stepIndexFromHash(steps: WizardStepDef[]): number {
|
||||
const hash = window.location.hash.replace('#', '');
|
||||
if (!hash) return 0;
|
||||
const idx = steps.findIndex((s) => s.id === hash);
|
||||
return idx >= 0 ? idx : 0;
|
||||
}
|
||||
|
||||
/** Determine which steps should be marked complete based on a restored index */
|
||||
function restoredCompletedSteps(upToIndex: number): Set<number> {
|
||||
const set = new Set<number>();
|
||||
for (let i = 0; i < upToIndex; i++) {
|
||||
set.add(i);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
export interface UseWizardReturn {
|
||||
currentStep: number;
|
||||
@@ -61,37 +43,103 @@ export interface UseWizardReturn {
|
||||
getStepByIndex: (index: number) => WizardStep | undefined;
|
||||
}
|
||||
|
||||
export function useWizard(): UseWizardReturn {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());
|
||||
/**
|
||||
* Dynamic wizard hook — accepts a step definition array that can change
|
||||
* as the user enables/disables services in the discovery step.
|
||||
*/
|
||||
export function useWizard(stepDefs: WizardStepDef[]): UseWizardReturn {
|
||||
const initialStep = stepIndexFromHash(stepDefs);
|
||||
const [currentStep, setCurrentStep] = useState(initialStep);
|
||||
const [completedSteps, setCompletedSteps] = useState<Set<number>>(
|
||||
() => restoredCompletedSteps(initialStep)
|
||||
);
|
||||
const [canProceed, setCanProceed] = useState(true);
|
||||
const isPopstateRef = useRef(false);
|
||||
const prevStepDefsRef = useRef(stepDefs);
|
||||
|
||||
const totalSteps = WIZARD_STEPS.length;
|
||||
const totalSteps = stepDefs.length;
|
||||
const isFirstStep = currentStep === 0;
|
||||
const isLastStep = currentStep === totalSteps - 1;
|
||||
|
||||
// When stepDefs change (services toggled), keep current position valid
|
||||
useEffect(() => {
|
||||
const prevDefs = prevStepDefsRef.current;
|
||||
prevStepDefsRef.current = stepDefs;
|
||||
|
||||
if (prevDefs.length === stepDefs.length) return;
|
||||
|
||||
// If current step is beyond new length, clamp it
|
||||
if (currentStep >= stepDefs.length) {
|
||||
setCurrentStep(Math.max(0, stepDefs.length - 1));
|
||||
}
|
||||
|
||||
// If a step was removed, try to stay on the same step id
|
||||
const currentId = prevDefs[currentStep]?.id;
|
||||
if (currentId) {
|
||||
const newIndex = stepDefs.findIndex((s) => s.id === currentId);
|
||||
if (newIndex >= 0 && newIndex !== currentStep) {
|
||||
setCurrentStep(newIndex);
|
||||
}
|
||||
}
|
||||
}, [stepDefs, currentStep]);
|
||||
|
||||
// Sync URL hash when currentStep changes
|
||||
useEffect(() => {
|
||||
if (isPopstateRef.current) {
|
||||
isPopstateRef.current = false;
|
||||
return;
|
||||
}
|
||||
const stepId = stepDefs[currentStep]?.id;
|
||||
if (stepId) {
|
||||
const newHash = `#${stepId}`;
|
||||
if (window.location.hash !== newHash) {
|
||||
window.history.pushState(null, '', newHash);
|
||||
}
|
||||
}
|
||||
}, [currentStep, stepDefs]);
|
||||
|
||||
// Listen for browser back/forward
|
||||
useEffect(() => {
|
||||
const handlePopState = () => {
|
||||
const idx = stepIndexFromHash(stepDefs);
|
||||
isPopstateRef.current = true;
|
||||
setCurrentStep(idx);
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
return () => window.removeEventListener('popstate', handlePopState);
|
||||
}, [stepDefs]);
|
||||
|
||||
// Set initial hash if none present
|
||||
useEffect(() => {
|
||||
if (!window.location.hash) {
|
||||
const stepId = stepDefs[0]?.id;
|
||||
if (stepId) {
|
||||
window.history.replaceState(null, '', `#${stepId}`);
|
||||
}
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const steps: WizardStep[] = useMemo(() => {
|
||||
return WIZARD_STEPS.map((step, index) => ({
|
||||
return stepDefs.map((step, index) => ({
|
||||
...step,
|
||||
isComplete: completedSteps.has(index),
|
||||
isActive: index === currentStep,
|
||||
}));
|
||||
}, [currentStep, completedSteps]);
|
||||
}, [stepDefs, currentStep, completedSteps]);
|
||||
|
||||
const currentStepId = useMemo(() => {
|
||||
return WIZARD_STEPS[currentStep]?.id || '';
|
||||
}, [currentStep]);
|
||||
return stepDefs[currentStep]?.id || '';
|
||||
}, [currentStep, stepDefs]);
|
||||
|
||||
const progress = useMemo(() => {
|
||||
// Progress based on current step position (0 to 100)
|
||||
if (totalSteps <= 1) return 100;
|
||||
return Math.round((currentStep / (totalSteps - 1)) * 100);
|
||||
}, [currentStep, totalSteps]);
|
||||
|
||||
const goToStep = useCallback(
|
||||
(step: number) => {
|
||||
if (step >= 0 && step < totalSteps) {
|
||||
// Allow going back to any previous step
|
||||
// Only allow going forward to completed steps or the next step
|
||||
if (step <= currentStep || completedSteps.has(step - 1) || step === currentStep + 1) {
|
||||
setCurrentStep(step);
|
||||
}
|
||||
@@ -102,7 +150,6 @@ export function useWizard(): UseWizardReturn {
|
||||
|
||||
const nextStep = useCallback(() => {
|
||||
if (!isLastStep && canProceed) {
|
||||
// Mark current step as complete when moving forward
|
||||
setCompletedSteps((prev) => new Set(prev).add(currentStep));
|
||||
setCurrentStep((prev) => prev + 1);
|
||||
}
|
||||
@@ -130,7 +177,8 @@ export function useWizard(): UseWizardReturn {
|
||||
setCurrentStep(0);
|
||||
setCompletedSteps(new Set());
|
||||
setCanProceed(true);
|
||||
}, []);
|
||||
window.history.replaceState(null, '', `#${stepDefs[0]?.id}`);
|
||||
}, [stepDefs]);
|
||||
|
||||
const getStepByIndex = useCallback(
|
||||
(index: number): WizardStep | undefined => {
|
||||
|
||||
Reference in New Issue
Block a user