import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; import type { QuoteData, QuoteResult, QuoteBreakdown, CompanyInfo, GPSSelection, SupportSelection, VoIPSelection, WebHostingSelection, EmailSelection, ContactInfo, GPSTierId, SupportPlanId, BlockTimeId, VoIPTierId, WebHostingTierId, EmailTierId, EmailProvider, Industry, ContactPreference, ClientType, ServiceInterests, } from '@/types/quote'; import { gpsTiers, equipmentMonitoring, supportPlans, blockTimeOptions, voipTiers, voipHardware, webHostingTiers, emailTiers, } from '@/lib/pricing-data'; const DRAFT_STORAGE_KEY = 'quote-wizard-draft'; /** * Load saved draft from localStorage if available. * Returns partial state keyed by section, or null if nothing saved. */ function loadDraft(): { clientType?: ClientType; serviceInterests?: ServiceInterests; company?: CompanyInfo; gps?: GPSSelection; support?: SupportSelection; voip?: VoIPSelection; webHosting?: WebHostingSelection; email?: EmailSelection; contact?: ContactInfo; accessToken?: string; } | null { try { const raw = localStorage.getItem(DRAFT_STORAGE_KEY); if (!raw) return null; return JSON.parse(raw); } catch { return null; } } /** * Initial state values */ const initialClientType: ClientType = 'company'; const initialServiceInterests: ServiceInterests = { gps: true, support: true, voip: false, webHosting: false, email: false, }; 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; // Client type & service interests setClientType: (type: ClientType) => void; setServiceInterest: (service: keyof ServiceInterests, enabled: boolean) => void; // Company updates updateCompany: (data: Partial) => void; setEndpointCount: (count: number) => void; setIndustry: (industry: Industry | '') => void; // GPS updates updateGPS: (data: Partial) => void; setGPSTier: (tierId: GPSTierId) => void; setEquipmentEnabled: (enabled: boolean) => void; setEquipmentCount: (count: number) => void; // Support updates updateSupport: (data: Partial) => void; setSupportPlan: (planId: SupportPlanId) => void; setBlockTimeEnabled: (enabled: boolean) => void; setBlockTime: (blockTimeId: BlockTimeId) => void; // VoIP updates updateVoIP: (data: Partial) => 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) => void; setWebHostingEnabled: (enabled: boolean) => void; setWebHostingTier: (tierId: WebHostingTierId) => void; // Email updates updateEmail: (data: Partial) => void; setEmailEnabled: (enabled: boolean) => void; setEmailProvider: (provider: EmailProvider) => void; setEmailTier: (tierId: EmailTierId) => void; setMailboxCount: (count: number) => void; // Contact updates updateContact: (data: Partial) => void; setContactPreference: (preference: ContactPreference) => void; setAgreedToTerms: (agreed: boolean) => void; // Calculations calculateQuote: () => QuoteResult; getGPSMonthly: () => number; getSupportMonthly: () => number; getVoIPMonthly: () => number; getWebHostingMonthly: () => number; getEmailMonthly: () => number; getSupportBlockTimeOneTime: () => number; getVoIPOneTime: () => number; // Reset resetQuote: () => void; } /** * Quote calculation and state management hook */ export function useQuote(): UseQuoteReturn { const draft = useRef(loadDraft()); const [clientType, setClientType] = useState(draft.current?.clientType ?? initialClientType); const [serviceInterests, setServiceInterests] = useState(draft.current?.serviceInterests ?? initialServiceInterests); const [company, setCompany] = useState(draft.current?.company ?? initialCompanyInfo); const [gps, setGPS] = useState(draft.current?.gps ?? initialGPSSelection); const [support, setSupport] = useState(draft.current?.support ?? initialSupportSelection); const [voip, setVoIP] = useState(draft.current?.voip ?? initialVoIPSelection); const [webHosting, setWebHosting] = useState(draft.current?.webHosting ?? initialWebHostingSelection); const [email, setEmail] = useState(draft.current?.email ?? initialEmailSelection); const [contact, setContact] = useState(draft.current?.contact ?? initialContactInfo); const [quoteResult, setQuoteResult] = useState(null); // Persist draft to localStorage when any section changes (debounced) const saveTimerRef = useRef | null>(null); useEffect(() => { if (saveTimerRef.current) { clearTimeout(saveTimerRef.current); } saveTimerRef.current = setTimeout(() => { try { // Preserve the accessToken that WizardContainer may have written const existing = localStorage.getItem(DRAFT_STORAGE_KEY); let accessToken: string | undefined; if (existing) { try { accessToken = JSON.parse(existing).accessToken; } catch { // ignore } } const payload = { clientType, serviceInterests, company, gps, support, voip, webHosting, email, contact, ...(accessToken ? { accessToken } : {}), }; localStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(payload)); } catch { // localStorage write failures are non-critical } }, 500); return () => { if (saveTimerRef.current) { clearTimeout(saveTimerRef.current); } }; }, [clientType, serviceInterests, company, gps, support, voip, webHosting, email, contact]); // Combined quote data const quoteData: QuoteData = useMemo( () => ({ clientType, serviceInterests, company, gps, support, voip, webHosting, email, contact, }), [clientType, serviceInterests, company, gps, support, voip, webHosting, email, contact] ); // ============================================================================ // Client Type & Service Interests // ============================================================================ const setClientTypeValue = useCallback((type: ClientType) => { setClientType(type); }, []); const setServiceInterest = useCallback((service: keyof ServiceInterests, enabled: boolean) => { setServiceInterests((prev) => ({ ...prev, [service]: enabled })); // Sync the enabled flags on the corresponding selections if (service === 'voip') { setVoIP((prev) => ({ ...prev, enabled, userCount: enabled ? Math.max(prev.userCount, 1) : 0 })); } if (service === 'webHosting') { setWebHosting((prev) => ({ ...prev, enabled })); } if (service === 'email') { setEmail((prev) => ({ ...prev, enabled, mailboxCount: enabled ? Math.max(prev.mailboxCount, 1) : 0 })); } }, []); // ============================================================================ // Company Updates // ============================================================================ const updateCompany = useCallback((data: Partial) => { 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) => { 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) => { 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) => { 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) => { 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) => { 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) => { 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 => { if (support.planId === 'none') return 0; const plan = supportPlans.find((p) => p.id === support.planId); return plan ? plan.monthlyPrice : 0; }, [support]); const getSupportBlockTimeOneTime = useCallback((): number => { if (!support.useBlockTime || !support.blockTimeId) return 0; const blockTime = blockTimeOptions.find((b) => b.id === support.blockTimeId); return blockTime ? blockTime.price : 0; }, [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 => { // Only include services that are enabled in serviceInterests const gpsMonthly = serviceInterests.gps ? getGPSMonthly() : 0; const supportMonthly = serviceInterests.support ? getSupportMonthly() : 0; const supportBlockTimeOneTime = serviceInterests.support ? getSupportBlockTimeOneTime() : 0; const voipMonthly = serviceInterests.voip ? getVoIPMonthly() : 0; const voipOneTime = serviceInterests.voip ? getVoIPOneTime() : 0; const webHostingMonthly = serviceInterests.webHosting ? getWebHostingMonthly() : 0; const emailMonthly = serviceInterests.email ? getEmailMonthly() : 0; // Calculate GPS breakdown (only if enabled) let gpsMonitoring = 0; let gpsEquipment = 0; if (serviceInterests.gps) { const gpsTier = gpsTiers.find((t) => t.id === gps.tierId); gpsMonitoring = gpsTier ? gpsTier.pricePerEndpoint * gps.endpointCount : 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 (only if enabled) let supportPlanCost = 0; if (serviceInterests.support && support.planId !== 'none') { const supportPlan = supportPlans.find((p) => p.id === support.planId); supportPlanCost = supportPlan ? supportPlan.monthlyPrice : 0; } // Calculate VoIP breakdown (only if enabled) let voipService = 0; let voipHardwareMonthly = 0; if (serviceInterests.voip && voip.enabled) { const voipTier = voipTiers.find((t) => t.id === voip.tierId); voipService = voipTier ? voipTier.pricePerUser * voip.userCount : 0; 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: supportBlockTimeOneTime, 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 + supportBlockTimeOneTime, breakdown, gpsMonthly, supportMonthly, voipMonthly, webHostingMonthly, emailMonthly, }; setQuoteResult(result); return result; }, [serviceInterests, gps, support, voip, webHosting, email, getGPSMonthly, getSupportMonthly, getSupportBlockTimeOneTime, getVoIPMonthly, getVoIPOneTime, getWebHostingMonthly, getEmailMonthly]); // ============================================================================ // Reset // ============================================================================ const resetQuote = useCallback(() => { setClientType(initialClientType); setServiceInterests(initialServiceInterests); setCompany(initialCompanyInfo); setGPS(initialGPSSelection); setSupport(initialSupportSelection); setVoIP(initialVoIPSelection); setWebHosting(initialWebHostingSelection); setEmail(initialEmailSelection); setContact(initialContactInfo); setQuoteResult(null); localStorage.removeItem(DRAFT_STORAGE_KEY); }, []); return { quoteData, quoteResult, // Client type & service interests setClientType: setClientTypeValue, setServiceInterest, // 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, getSupportBlockTimeOneTime, getVoIPOneTime, // Reset resetQuote, }; }