sync: Auto-sync from Mikes-MacBook-Air.local at 2026-03-09 08:14:13
Synced files: - Session logs updated - Latest context and credentials - Command/directive updates Machine: Mikes-MacBook-Air.local Timestamp: 2026-03-09 08:14:13 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
612
projects/msp-tools/quote-wizard/frontend/src/hooks/useQuote.ts
Normal file
612
projects/msp-tools/quote-wizard/frontend/src/hooks/useQuote.ts
Normal file
@@ -0,0 +1,612 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import type {
|
||||
QuoteData,
|
||||
QuoteResult,
|
||||
QuoteBreakdown,
|
||||
CompanyInfo,
|
||||
GPSSelection,
|
||||
SupportSelection,
|
||||
VoIPSelection,
|
||||
WebHostingSelection,
|
||||
EmailSelection,
|
||||
ContactInfo,
|
||||
GPSTierId,
|
||||
SupportPlanId,
|
||||
BlockTimeId,
|
||||
VoIPTierId,
|
||||
WebHostingTierId,
|
||||
EmailTierId,
|
||||
EmailProvider,
|
||||
Industry,
|
||||
ContactPreference,
|
||||
} from '@/types/quote';
|
||||
import {
|
||||
gpsTiers,
|
||||
equipmentMonitoring,
|
||||
supportPlans,
|
||||
blockTimeOptions,
|
||||
voipTiers,
|
||||
voipHardware,
|
||||
webHostingTiers,
|
||||
emailTiers,
|
||||
} from '@/lib/pricing-data';
|
||||
|
||||
/**
|
||||
* Initial state values
|
||||
*/
|
||||
const initialCompanyInfo: CompanyInfo = {
|
||||
name: '',
|
||||
endpointCount: 10,
|
||||
industry: '',
|
||||
notes: '',
|
||||
};
|
||||
|
||||
const initialGPSSelection: GPSSelection = {
|
||||
tierId: 'pro',
|
||||
endpointCount: 10,
|
||||
includeEquipment: false,
|
||||
equipmentDeviceCount: 0,
|
||||
};
|
||||
|
||||
const initialSupportSelection: SupportSelection = {
|
||||
planId: 'standard',
|
||||
useBlockTime: false,
|
||||
blockTimeId: null,
|
||||
};
|
||||
|
||||
const initialVoIPSelection: VoIPSelection = {
|
||||
enabled: false,
|
||||
tierId: 'voip-standard',
|
||||
userCount: 0,
|
||||
hardware: [],
|
||||
};
|
||||
|
||||
const initialWebHostingSelection: WebHostingSelection = {
|
||||
enabled: false,
|
||||
tierId: 'hosting-business',
|
||||
};
|
||||
|
||||
const initialEmailSelection: EmailSelection = {
|
||||
enabled: false,
|
||||
provider: 'm365',
|
||||
tierId: 'm365-standard',
|
||||
mailboxCount: 0,
|
||||
};
|
||||
|
||||
const initialContactInfo: ContactInfo = {
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
companyName: '',
|
||||
currentITSituation: '',
|
||||
contactPreference: 'email',
|
||||
agreedToTerms: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook return type
|
||||
*/
|
||||
export interface UseQuoteReturn {
|
||||
quoteData: QuoteData;
|
||||
quoteResult: QuoteResult | null;
|
||||
|
||||
// Company updates
|
||||
updateCompany: (data: Partial<CompanyInfo>) => void;
|
||||
setEndpointCount: (count: number) => void;
|
||||
setIndustry: (industry: Industry | '') => void;
|
||||
|
||||
// GPS updates
|
||||
updateGPS: (data: Partial<GPSSelection>) => void;
|
||||
setGPSTier: (tierId: GPSTierId) => void;
|
||||
setEquipmentEnabled: (enabled: boolean) => void;
|
||||
setEquipmentCount: (count: number) => void;
|
||||
|
||||
// Support updates
|
||||
updateSupport: (data: Partial<SupportSelection>) => void;
|
||||
setSupportPlan: (planId: SupportPlanId) => void;
|
||||
setBlockTimeEnabled: (enabled: boolean) => void;
|
||||
setBlockTime: (blockTimeId: BlockTimeId) => void;
|
||||
|
||||
// VoIP updates
|
||||
updateVoIP: (data: Partial<VoIPSelection>) => void;
|
||||
setVoIPEnabled: (enabled: boolean) => void;
|
||||
setVoIPTier: (tierId: VoIPTierId) => void;
|
||||
setVoIPUserCount: (count: number) => void;
|
||||
addHardware: (hardwareId: string, quantity: number, isRental: boolean) => void;
|
||||
removeHardware: (hardwareId: string) => void;
|
||||
updateHardwareQuantity: (hardwareId: string, quantity: number) => void;
|
||||
|
||||
// Web Hosting updates
|
||||
updateWebHosting: (data: Partial<WebHostingSelection>) => void;
|
||||
setWebHostingEnabled: (enabled: boolean) => void;
|
||||
setWebHostingTier: (tierId: WebHostingTierId) => void;
|
||||
|
||||
// Email updates
|
||||
updateEmail: (data: Partial<EmailSelection>) => void;
|
||||
setEmailEnabled: (enabled: boolean) => void;
|
||||
setEmailProvider: (provider: EmailProvider) => void;
|
||||
setEmailTier: (tierId: EmailTierId) => void;
|
||||
setMailboxCount: (count: number) => void;
|
||||
|
||||
// Contact updates
|
||||
updateContact: (data: Partial<ContactInfo>) => void;
|
||||
setContactPreference: (preference: ContactPreference) => void;
|
||||
setAgreedToTerms: (agreed: boolean) => void;
|
||||
|
||||
// Calculations
|
||||
calculateQuote: () => QuoteResult;
|
||||
getGPSMonthly: () => number;
|
||||
getSupportMonthly: () => number;
|
||||
getVoIPMonthly: () => number;
|
||||
getWebHostingMonthly: () => number;
|
||||
getEmailMonthly: () => number;
|
||||
getVoIPOneTime: () => number;
|
||||
|
||||
// Reset
|
||||
resetQuote: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote calculation and state management hook
|
||||
*/
|
||||
export function useQuote(): UseQuoteReturn {
|
||||
const [company, setCompany] = useState<CompanyInfo>(initialCompanyInfo);
|
||||
const [gps, setGPS] = useState<GPSSelection>(initialGPSSelection);
|
||||
const [support, setSupport] = useState<SupportSelection>(initialSupportSelection);
|
||||
const [voip, setVoIP] = useState<VoIPSelection>(initialVoIPSelection);
|
||||
const [webHosting, setWebHosting] = useState<WebHostingSelection>(initialWebHostingSelection);
|
||||
const [email, setEmail] = useState<EmailSelection>(initialEmailSelection);
|
||||
const [contact, setContact] = useState<ContactInfo>(initialContactInfo);
|
||||
const [quoteResult, setQuoteResult] = useState<QuoteResult | null>(null);
|
||||
|
||||
// Combined quote data
|
||||
const quoteData: QuoteData = useMemo(
|
||||
() => ({
|
||||
company,
|
||||
gps,
|
||||
support,
|
||||
voip,
|
||||
webHosting,
|
||||
email,
|
||||
contact,
|
||||
}),
|
||||
[company, gps, support, voip, webHosting, email, contact]
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Company Updates
|
||||
// ============================================================================
|
||||
|
||||
const updateCompany = useCallback((data: Partial<CompanyInfo>) => {
|
||||
setCompany((prev) => {
|
||||
const updated = { ...prev, ...data };
|
||||
// Sync endpoint count with GPS selection
|
||||
if (data.endpointCount !== undefined) {
|
||||
setGPS((gpsState) => ({ ...gpsState, endpointCount: data.endpointCount as number }));
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setEndpointCount = useCallback((count: number) => {
|
||||
const validCount = Math.max(1, count);
|
||||
setCompany((prev) => ({ ...prev, endpointCount: validCount }));
|
||||
setGPS((prev) => ({ ...prev, endpointCount: validCount }));
|
||||
}, []);
|
||||
|
||||
const setIndustry = useCallback((industry: Industry | '') => {
|
||||
setCompany((prev) => ({ ...prev, industry }));
|
||||
}, []);
|
||||
|
||||
// ============================================================================
|
||||
// GPS Updates
|
||||
// ============================================================================
|
||||
|
||||
const updateGPS = useCallback((data: Partial<GPSSelection>) => {
|
||||
setGPS((prev) => ({ ...prev, ...data }));
|
||||
}, []);
|
||||
|
||||
const setGPSTier = useCallback((tierId: GPSTierId) => {
|
||||
setGPS((prev) => ({ ...prev, tierId }));
|
||||
}, []);
|
||||
|
||||
const setEquipmentEnabled = useCallback((enabled: boolean) => {
|
||||
setGPS((prev) => ({
|
||||
...prev,
|
||||
includeEquipment: enabled,
|
||||
equipmentDeviceCount: enabled ? Math.max(prev.equipmentDeviceCount, 1) : 0,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setEquipmentCount = useCallback((count: number) => {
|
||||
setGPS((prev) => ({ ...prev, equipmentDeviceCount: Math.max(0, count) }));
|
||||
}, []);
|
||||
|
||||
// ============================================================================
|
||||
// Support Updates
|
||||
// ============================================================================
|
||||
|
||||
const updateSupport = useCallback((data: Partial<SupportSelection>) => {
|
||||
setSupport((prev) => ({ ...prev, ...data }));
|
||||
}, []);
|
||||
|
||||
const setSupportPlan = useCallback((planId: SupportPlanId) => {
|
||||
setSupport((prev) => ({ ...prev, planId }));
|
||||
}, []);
|
||||
|
||||
const setBlockTimeEnabled = useCallback((enabled: boolean) => {
|
||||
setSupport((prev) => ({
|
||||
...prev,
|
||||
useBlockTime: enabled,
|
||||
blockTimeId: enabled ? (prev.blockTimeId || 'block-10') : null,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setBlockTime = useCallback((blockTimeId: BlockTimeId) => {
|
||||
setSupport((prev) => ({ ...prev, blockTimeId, useBlockTime: true }));
|
||||
}, []);
|
||||
|
||||
// ============================================================================
|
||||
// VoIP Updates
|
||||
// ============================================================================
|
||||
|
||||
const updateVoIP = useCallback((data: Partial<VoIPSelection>) => {
|
||||
setVoIP((prev) => ({ ...prev, ...data }));
|
||||
}, []);
|
||||
|
||||
const setVoIPEnabled = useCallback((enabled: boolean) => {
|
||||
setVoIP((prev) => ({
|
||||
...prev,
|
||||
enabled,
|
||||
userCount: enabled ? Math.max(prev.userCount, 1) : 0,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setVoIPTier = useCallback((tierId: VoIPTierId) => {
|
||||
setVoIP((prev) => ({ ...prev, tierId }));
|
||||
}, []);
|
||||
|
||||
const setVoIPUserCount = useCallback((count: number) => {
|
||||
setVoIP((prev) => ({ ...prev, userCount: Math.max(0, count) }));
|
||||
}, []);
|
||||
|
||||
const addHardware = useCallback((hardwareId: string, quantity: number, isRental: boolean) => {
|
||||
setVoIP((prev) => {
|
||||
const existing = prev.hardware.find((h) => h.hardwareId === hardwareId);
|
||||
if (existing) {
|
||||
return {
|
||||
...prev,
|
||||
hardware: prev.hardware.map((h) =>
|
||||
h.hardwareId === hardwareId ? { ...h, quantity, isRental } : h
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
hardware: [...prev.hardware, { hardwareId, quantity, isRental }],
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const removeHardware = useCallback((hardwareId: string) => {
|
||||
setVoIP((prev) => ({
|
||||
...prev,
|
||||
hardware: prev.hardware.filter((h) => h.hardwareId !== hardwareId),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const updateHardwareQuantity = useCallback((hardwareId: string, quantity: number) => {
|
||||
setVoIP((prev) => ({
|
||||
...prev,
|
||||
hardware: prev.hardware.map((h) =>
|
||||
h.hardwareId === hardwareId ? { ...h, quantity: Math.max(0, quantity) } : h
|
||||
),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// ============================================================================
|
||||
// Web Hosting Updates
|
||||
// ============================================================================
|
||||
|
||||
const updateWebHosting = useCallback((data: Partial<WebHostingSelection>) => {
|
||||
setWebHosting((prev) => ({ ...prev, ...data }));
|
||||
}, []);
|
||||
|
||||
const setWebHostingEnabled = useCallback((enabled: boolean) => {
|
||||
setWebHosting((prev) => ({ ...prev, enabled }));
|
||||
}, []);
|
||||
|
||||
const setWebHostingTier = useCallback((tierId: WebHostingTierId) => {
|
||||
setWebHosting((prev) => ({ ...prev, tierId }));
|
||||
}, []);
|
||||
|
||||
// ============================================================================
|
||||
// Email Updates
|
||||
// ============================================================================
|
||||
|
||||
const updateEmail = useCallback((data: Partial<EmailSelection>) => {
|
||||
setEmail((prev) => ({ ...prev, ...data }));
|
||||
}, []);
|
||||
|
||||
const setEmailEnabled = useCallback((enabled: boolean) => {
|
||||
setEmail((prev) => ({
|
||||
...prev,
|
||||
enabled,
|
||||
mailboxCount: enabled ? Math.max(prev.mailboxCount, 1) : 0,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setEmailProvider = useCallback((provider: EmailProvider) => {
|
||||
setEmail((prev) => {
|
||||
// Set default tier for provider
|
||||
const defaultTier = provider === 'm365' ? 'm365-standard' : 'whm-standard';
|
||||
return { ...prev, provider, tierId: defaultTier as EmailTierId };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setEmailTier = useCallback((tierId: EmailTierId) => {
|
||||
setEmail((prev) => ({ ...prev, tierId }));
|
||||
}, []);
|
||||
|
||||
const setMailboxCount = useCallback((count: number) => {
|
||||
setEmail((prev) => ({ ...prev, mailboxCount: Math.max(0, count) }));
|
||||
}, []);
|
||||
|
||||
// ============================================================================
|
||||
// Contact Updates
|
||||
// ============================================================================
|
||||
|
||||
const updateContact = useCallback((data: Partial<ContactInfo>) => {
|
||||
setContact((prev) => ({ ...prev, ...data }));
|
||||
}, []);
|
||||
|
||||
const setContactPreference = useCallback((preference: ContactPreference) => {
|
||||
setContact((prev) => ({ ...prev, contactPreference: preference }));
|
||||
}, []);
|
||||
|
||||
const setAgreedToTerms = useCallback((agreed: boolean) => {
|
||||
setContact((prev) => ({ ...prev, agreedToTerms: agreed }));
|
||||
}, []);
|
||||
|
||||
// ============================================================================
|
||||
// Calculation Functions
|
||||
// ============================================================================
|
||||
|
||||
const getGPSMonthly = useCallback((): number => {
|
||||
const tier = gpsTiers.find((t) => t.id === gps.tierId);
|
||||
if (!tier) return 0;
|
||||
|
||||
let total = tier.pricePerEndpoint * gps.endpointCount;
|
||||
|
||||
if (gps.includeEquipment && gps.equipmentDeviceCount > 0) {
|
||||
const additionalDevices = Math.max(0, gps.equipmentDeviceCount - equipmentMonitoring.baseDevices);
|
||||
total += equipmentMonitoring.basePrice + (additionalDevices * equipmentMonitoring.additionalDevicePrice);
|
||||
}
|
||||
|
||||
return total;
|
||||
}, [gps]);
|
||||
|
||||
const getSupportMonthly = useCallback((): number => {
|
||||
const plan = supportPlans.find((p) => p.id === support.planId);
|
||||
if (!plan) return 0;
|
||||
|
||||
let total = plan.monthlyPrice;
|
||||
|
||||
if (support.useBlockTime && support.blockTimeId) {
|
||||
const blockTime = blockTimeOptions.find((b) => b.id === support.blockTimeId);
|
||||
if (blockTime) {
|
||||
total += blockTime.price;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}, [support]);
|
||||
|
||||
const getVoIPMonthly = useCallback((): number => {
|
||||
if (!voip.enabled) return 0;
|
||||
|
||||
const tier = voipTiers.find((t) => t.id === voip.tierId);
|
||||
if (!tier) return 0;
|
||||
|
||||
let total = tier.pricePerUser * voip.userCount;
|
||||
|
||||
// Add rental hardware costs
|
||||
voip.hardware.forEach((hw) => {
|
||||
if (hw.isRental) {
|
||||
const hardware = voipHardware.find((h) => h.id === hw.hardwareId);
|
||||
if (hardware) {
|
||||
total += hardware.monthlyRental * hw.quantity;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return total;
|
||||
}, [voip]);
|
||||
|
||||
const getVoIPOneTime = useCallback((): number => {
|
||||
if (!voip.enabled) return 0;
|
||||
|
||||
let total = 0;
|
||||
|
||||
// Add purchased hardware costs
|
||||
voip.hardware.forEach((hw) => {
|
||||
if (!hw.isRental) {
|
||||
const hardware = voipHardware.find((h) => h.id === hw.hardwareId);
|
||||
if (hardware) {
|
||||
total += hardware.oneTimePrice * hw.quantity;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return total;
|
||||
}, [voip]);
|
||||
|
||||
const getWebHostingMonthly = useCallback((): number => {
|
||||
if (!webHosting.enabled) return 0;
|
||||
|
||||
const tier = webHostingTiers.find((t) => t.id === webHosting.tierId);
|
||||
return tier ? tier.monthlyPrice : 0;
|
||||
}, [webHosting]);
|
||||
|
||||
const getEmailMonthly = useCallback((): number => {
|
||||
if (!email.enabled) return 0;
|
||||
|
||||
const tier = emailTiers.find((t) => t.id === email.tierId);
|
||||
return tier ? tier.pricePerMailbox * email.mailboxCount : 0;
|
||||
}, [email]);
|
||||
|
||||
const calculateQuote = useCallback((): QuoteResult => {
|
||||
const gpsMonthly = getGPSMonthly();
|
||||
const supportMonthly = getSupportMonthly();
|
||||
const voipMonthly = getVoIPMonthly();
|
||||
const voipOneTime = getVoIPOneTime();
|
||||
const webHostingMonthly = getWebHostingMonthly();
|
||||
const emailMonthly = getEmailMonthly();
|
||||
|
||||
// Calculate GPS breakdown
|
||||
const gpsTier = gpsTiers.find((t) => t.id === gps.tierId);
|
||||
const gpsMonitoring = gpsTier ? gpsTier.pricePerEndpoint * gps.endpointCount : 0;
|
||||
let gpsEquipment = 0;
|
||||
if (gps.includeEquipment && gps.equipmentDeviceCount > 0) {
|
||||
const additionalDevices = Math.max(0, gps.equipmentDeviceCount - equipmentMonitoring.baseDevices);
|
||||
gpsEquipment = equipmentMonitoring.basePrice + (additionalDevices * equipmentMonitoring.additionalDevicePrice);
|
||||
}
|
||||
|
||||
// Calculate support breakdown
|
||||
const supportPlan = supportPlans.find((p) => p.id === support.planId);
|
||||
const supportPlanCost = supportPlan ? supportPlan.monthlyPrice : 0;
|
||||
let supportBlockTime = 0;
|
||||
if (support.useBlockTime && support.blockTimeId) {
|
||||
const blockTime = blockTimeOptions.find((b) => b.id === support.blockTimeId);
|
||||
if (blockTime) {
|
||||
supportBlockTime = blockTime.price;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate VoIP breakdown
|
||||
const voipTier = voipTiers.find((t) => t.id === voip.tierId);
|
||||
const voipService = voip.enabled && voipTier ? voipTier.pricePerUser * voip.userCount : 0;
|
||||
let voipHardwareMonthly = 0;
|
||||
if (voip.enabled) {
|
||||
voip.hardware.forEach((hw) => {
|
||||
if (hw.isRental) {
|
||||
const hardware = voipHardware.find((h) => h.id === hw.hardwareId);
|
||||
if (hardware) {
|
||||
voipHardwareMonthly += hardware.monthlyRental * hw.quantity;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const breakdown: QuoteBreakdown = {
|
||||
gps: {
|
||||
monitoring: gpsMonitoring,
|
||||
equipment: gpsEquipment,
|
||||
total: gpsMonthly,
|
||||
},
|
||||
support: {
|
||||
plan: supportPlanCost,
|
||||
blockTime: supportBlockTime,
|
||||
total: supportMonthly,
|
||||
},
|
||||
voip: {
|
||||
service: voipService,
|
||||
hardware: voipHardwareMonthly,
|
||||
total: voipMonthly,
|
||||
},
|
||||
webHosting: webHostingMonthly,
|
||||
email: emailMonthly,
|
||||
};
|
||||
|
||||
const monthlyTotal = gpsMonthly + supportMonthly + voipMonthly + webHostingMonthly + emailMonthly;
|
||||
|
||||
const result: QuoteResult = {
|
||||
monthlyTotal,
|
||||
oneTimeTotal: voipOneTime,
|
||||
breakdown,
|
||||
gpsMonthly,
|
||||
supportMonthly,
|
||||
voipMonthly,
|
||||
webHostingMonthly,
|
||||
emailMonthly,
|
||||
};
|
||||
|
||||
setQuoteResult(result);
|
||||
return result;
|
||||
}, [gps, support, voip, webHosting, email, getGPSMonthly, getSupportMonthly, getVoIPMonthly, getVoIPOneTime, getWebHostingMonthly, getEmailMonthly]);
|
||||
|
||||
// ============================================================================
|
||||
// Reset
|
||||
// ============================================================================
|
||||
|
||||
const resetQuote = useCallback(() => {
|
||||
setCompany(initialCompanyInfo);
|
||||
setGPS(initialGPSSelection);
|
||||
setSupport(initialSupportSelection);
|
||||
setVoIP(initialVoIPSelection);
|
||||
setWebHosting(initialWebHostingSelection);
|
||||
setEmail(initialEmailSelection);
|
||||
setContact(initialContactInfo);
|
||||
setQuoteResult(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
quoteData,
|
||||
quoteResult,
|
||||
|
||||
// Company updates
|
||||
updateCompany,
|
||||
setEndpointCount,
|
||||
setIndustry,
|
||||
|
||||
// GPS updates
|
||||
updateGPS,
|
||||
setGPSTier,
|
||||
setEquipmentEnabled,
|
||||
setEquipmentCount,
|
||||
|
||||
// Support updates
|
||||
updateSupport,
|
||||
setSupportPlan,
|
||||
setBlockTimeEnabled,
|
||||
setBlockTime,
|
||||
|
||||
// VoIP updates
|
||||
updateVoIP,
|
||||
setVoIPEnabled,
|
||||
setVoIPTier,
|
||||
setVoIPUserCount,
|
||||
addHardware,
|
||||
removeHardware,
|
||||
updateHardwareQuantity,
|
||||
|
||||
// Web Hosting updates
|
||||
updateWebHosting,
|
||||
setWebHostingEnabled,
|
||||
setWebHostingTier,
|
||||
|
||||
// Email updates
|
||||
updateEmail,
|
||||
setEmailEnabled,
|
||||
setEmailProvider,
|
||||
setEmailTier,
|
||||
setMailboxCount,
|
||||
|
||||
// Contact updates
|
||||
updateContact,
|
||||
setContactPreference,
|
||||
setAgreedToTerms,
|
||||
|
||||
// Calculations
|
||||
calculateQuote,
|
||||
getGPSMonthly,
|
||||
getSupportMonthly,
|
||||
getVoIPMonthly,
|
||||
getWebHostingMonthly,
|
||||
getEmailMonthly,
|
||||
getVoIPOneTime,
|
||||
|
||||
// Reset
|
||||
resetQuote,
|
||||
};
|
||||
}
|
||||
160
projects/msp-tools/quote-wizard/frontend/src/hooks/useWizard.ts
Normal file
160
projects/msp-tools/quote-wizard/frontend/src/hooks/useWizard.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { useState, useCallback, useMemo } 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 UseWizardReturn {
|
||||
currentStep: number;
|
||||
steps: WizardStep[];
|
||||
totalSteps: number;
|
||||
isFirstStep: boolean;
|
||||
isLastStep: boolean;
|
||||
goToStep: (step: number) => void;
|
||||
nextStep: () => void;
|
||||
prevStep: () => void;
|
||||
markStepComplete: (stepIndex: number) => void;
|
||||
markStepIncomplete: (stepIndex: number) => void;
|
||||
resetWizard: () => void;
|
||||
progress: number;
|
||||
canProceed: boolean;
|
||||
setCanProceed: (canProceed: boolean) => void;
|
||||
currentStepId: string;
|
||||
getStepByIndex: (index: number) => WizardStep | undefined;
|
||||
}
|
||||
|
||||
export function useWizard(): UseWizardReturn {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());
|
||||
const [canProceed, setCanProceed] = useState(true);
|
||||
|
||||
const totalSteps = WIZARD_STEPS.length;
|
||||
const isFirstStep = currentStep === 0;
|
||||
const isLastStep = currentStep === totalSteps - 1;
|
||||
|
||||
const steps: WizardStep[] = useMemo(() => {
|
||||
return WIZARD_STEPS.map((step, index) => ({
|
||||
...step,
|
||||
isComplete: completedSteps.has(index),
|
||||
isActive: index === currentStep,
|
||||
}));
|
||||
}, [currentStep, completedSteps]);
|
||||
|
||||
const currentStepId = useMemo(() => {
|
||||
return WIZARD_STEPS[currentStep]?.id || '';
|
||||
}, [currentStep]);
|
||||
|
||||
const progress = useMemo(() => {
|
||||
// Progress based on current step position (0 to 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
[totalSteps, currentStep, completedSteps]
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}, [currentStep, isLastStep, canProceed]);
|
||||
|
||||
const prevStep = useCallback(() => {
|
||||
if (!isFirstStep) {
|
||||
setCurrentStep((prev) => prev - 1);
|
||||
}
|
||||
}, [isFirstStep]);
|
||||
|
||||
const markStepComplete = useCallback((stepIndex: number) => {
|
||||
setCompletedSteps((prev) => new Set(prev).add(stepIndex));
|
||||
}, []);
|
||||
|
||||
const markStepIncomplete = useCallback((stepIndex: number) => {
|
||||
setCompletedSteps((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(stepIndex);
|
||||
return newSet;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const resetWizard = useCallback(() => {
|
||||
setCurrentStep(0);
|
||||
setCompletedSteps(new Set());
|
||||
setCanProceed(true);
|
||||
}, []);
|
||||
|
||||
const getStepByIndex = useCallback(
|
||||
(index: number): WizardStep | undefined => {
|
||||
return steps[index];
|
||||
},
|
||||
[steps]
|
||||
);
|
||||
|
||||
return {
|
||||
currentStep,
|
||||
steps,
|
||||
totalSteps,
|
||||
isFirstStep,
|
||||
isLastStep,
|
||||
goToStep,
|
||||
nextStep,
|
||||
prevStep,
|
||||
markStepComplete,
|
||||
markStepIncomplete,
|
||||
resetWizard,
|
||||
progress,
|
||||
canProceed,
|
||||
setCanProceed,
|
||||
currentStepId,
|
||||
getStepByIndex,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user