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:
2026-03-10 19:59:08 -07:00
parent a1a19f8c00
commit fa15b03180
169 changed files with 879909 additions and 1243 deletions

View File

@@ -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 => {