fix: Quote wizard - correct total calculation and email sender

- Fix calculateQuote() to respect serviceInterests flags
- Only include GPS/Support costs when user has enabled them
- Update Step6Summary to conditionally render service sections
- Add sender display name (Arizona Computer Guru) to emails
- Add reply-to address (admin@azcomputerguru.com)
- Fixes phantom $380 support charge appearing in totals

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 20:42:40 -07:00
parent fa15b03180
commit c629890e32
4 changed files with 104 additions and 71 deletions

View File

@@ -88,54 +88,58 @@ export function Step6Summary({
</div> </div>
)} )}
{/* GPS Monitoring Section */} {/* GPS Monitoring Section - only show if enabled */}
<SummarySection {quoteData.serviceInterests.gps && (
icon={<Monitor className="w-5 h-5" />} <SummarySection
title="GPS Monitoring" icon={<Monitor className="w-5 h-5" />}
monthlyTotal={result.gpsMonthly} title="GPS Monitoring"
onEdit={() => onGoToStep(1)} monthlyTotal={result.gpsMonthly}
> onEdit={() => onGoToStep(1)}
<div className="space-y-2"> >
<SummaryLine <div className="space-y-2">
label={`${gpsTier?.name} Plan (${quoteData.gps.endpointCount} endpoints)`}
value={formatCurrency(result.breakdown.gps.monitoring)}
/>
{quoteData.gps.includeEquipment && quoteData.gps.equipmentDeviceCount > 0 && (
<SummaryLine <SummaryLine
label={`Equipment Pack (${quoteData.gps.equipmentDeviceCount} devices)`} label={`${gpsTier?.name} Plan (${quoteData.gps.endpointCount} endpoints)`}
value={formatCurrency(result.breakdown.gps.equipment)} value={formatCurrency(result.breakdown.gps.monitoring)}
/> />
)} {quoteData.gps.includeEquipment && quoteData.gps.equipmentDeviceCount > 0 && (
</div> <SummaryLine
</SummarySection> label={`Equipment Pack (${quoteData.gps.equipmentDeviceCount} devices)`}
value={formatCurrency(result.breakdown.gps.equipment)}
/>
)}
</div>
</SummarySection>
)}
{/* Support Plan Section */} {/* Support Plan Section - only show if enabled */}
<SummarySection {quoteData.serviceInterests.support && (
icon={<Headphones className="w-5 h-5" />} <SummarySection
title="Support Plan" icon={<Headphones className="w-5 h-5" />}
monthlyTotal={result.supportMonthly} title="Support Plan"
onEdit={() => onGoToStep(2)} monthlyTotal={result.supportMonthly}
> onEdit={() => onGoToStep(2)}
<div className="space-y-2"> >
{quoteData.support.planId === 'none' ? ( <div className="space-y-2">
<SummaryLine {quoteData.support.planId === 'none' ? (
label="No Monthly Plan (pay-as-you-go)" <SummaryLine
value="$0" label="No Monthly Plan (pay-as-you-go)"
/> value="$0"
) : ( />
<SummaryLine ) : (
label={`${supportPlan?.name} Plan (${supportPlan?.includedHours} hrs/mo)`} <SummaryLine
value={formatCurrency(result.breakdown.support.plan)} label={`${supportPlan?.name} Plan (${supportPlan?.includedHours} hrs/mo)`}
/> value={formatCurrency(result.breakdown.support.plan)}
)} />
{blockTime && ( )}
<SummaryLine {blockTime && (
label={`Block Time (${blockTime.hours} hours) — one-time`} <SummaryLine
value={formatCurrency(result.breakdown.support.blockTime)} label={`Block Time (${blockTime.hours} hours) — one-time`}
/> value={formatCurrency(result.breakdown.support.blockTime)}
)} />
</div> )}
</SummarySection> </div>
</SummarySection>
)}
{/* VoIP Section */} {/* VoIP Section */}
{quoteData.voip.enabled && ( {quoteData.voip.enabled && (
@@ -230,15 +234,19 @@ export function Step6Summary({
Monthly Breakdown Monthly Breakdown
</h4> </h4>
<div className="space-y-3"> <div className="space-y-3">
<BreakdownRow label="GPS Monitoring" value={result.gpsMonthly} /> {quoteData.serviceInterests.gps && (
<BreakdownRow label="Support Plan" value={result.supportMonthly} /> <BreakdownRow label="GPS Monitoring" value={result.gpsMonthly} />
{quoteData.voip.enabled && ( )}
{quoteData.serviceInterests.support && (
<BreakdownRow label="Support Plan" value={result.supportMonthly} />
)}
{quoteData.serviceInterests.voip && quoteData.voip.enabled && (
<BreakdownRow label="VoIP Phone System" value={result.voipMonthly} /> <BreakdownRow label="VoIP Phone System" value={result.voipMonthly} />
)} )}
{quoteData.webHosting.enabled && ( {quoteData.serviceInterests.webHosting && quoteData.webHosting.enabled && (
<BreakdownRow label="Web Hosting" value={result.webHostingMonthly} /> <BreakdownRow label="Web Hosting" value={result.webHostingMonthly} />
)} )}
{quoteData.email.enabled && ( {quoteData.serviceInterests.email && quoteData.email.enabled && (
<BreakdownRow label="Email Service" value={result.emailMonthly} /> <BreakdownRow label="Email Service" value={result.emailMonthly} />
)} )}
<div className="pt-4 mt-1 border-t-2 border-[#fe7400]/20 flex justify-between items-center"> <div className="pt-4 mt-1 border-t-2 border-[#fe7400]/20 flex justify-between items-center">

View File

@@ -568,32 +568,40 @@ export function useQuote(): UseQuoteReturn {
}, [email]); }, [email]);
const calculateQuote = useCallback((): QuoteResult => { const calculateQuote = useCallback((): QuoteResult => {
const gpsMonthly = getGPSMonthly(); // Only include services that are enabled in serviceInterests
const supportMonthly = getSupportMonthly(); const gpsMonthly = serviceInterests.gps ? getGPSMonthly() : 0;
const voipMonthly = getVoIPMonthly(); const supportMonthly = serviceInterests.support ? getSupportMonthly() : 0;
const voipOneTime = getVoIPOneTime(); const supportBlockTimeOneTime = serviceInterests.support ? getSupportBlockTimeOneTime() : 0;
const supportBlockTimeOneTime = getSupportBlockTimeOneTime(); const voipMonthly = serviceInterests.voip ? getVoIPMonthly() : 0;
const webHostingMonthly = getWebHostingMonthly(); const voipOneTime = serviceInterests.voip ? getVoIPOneTime() : 0;
const emailMonthly = getEmailMonthly(); const webHostingMonthly = serviceInterests.webHosting ? getWebHostingMonthly() : 0;
const emailMonthly = serviceInterests.email ? getEmailMonthly() : 0;
// Calculate GPS breakdown // Calculate GPS breakdown (only if enabled)
const gpsTier = gpsTiers.find((t) => t.id === gps.tierId); let gpsMonitoring = 0;
const gpsMonitoring = gpsTier ? gpsTier.pricePerEndpoint * gps.endpointCount : 0;
let gpsEquipment = 0; let gpsEquipment = 0;
if (gps.includeEquipment && gps.equipmentDeviceCount > 0) { if (serviceInterests.gps) {
const additionalDevices = Math.max(0, gps.equipmentDeviceCount - equipmentMonitoring.baseDevices); const gpsTier = gpsTiers.find((t) => t.id === gps.tierId);
gpsEquipment = equipmentMonitoring.basePrice + (additionalDevices * equipmentMonitoring.additionalDevicePrice); 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 // Calculate support breakdown (only if enabled)
const supportPlan = support.planId !== 'none' ? supportPlans.find((p) => p.id === support.planId) : null; let supportPlanCost = 0;
const supportPlanCost = supportPlan ? supportPlan.monthlyPrice : 0; if (serviceInterests.support && support.planId !== 'none') {
const supportPlan = supportPlans.find((p) => p.id === support.planId);
supportPlanCost = supportPlan ? supportPlan.monthlyPrice : 0;
}
// Calculate VoIP breakdown // Calculate VoIP breakdown (only if enabled)
const voipTier = voipTiers.find((t) => t.id === voip.tierId); let voipService = 0;
const voipService = voip.enabled && voipTier ? voipTier.pricePerUser * voip.userCount : 0;
let voipHardwareMonthly = 0; let voipHardwareMonthly = 0;
if (voip.enabled) { 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) => { voip.hardware.forEach((hw) => {
if (hw.isRental) { if (hw.isRental) {
const hardware = voipHardware.find((h) => h.id === hw.hardwareId); const hardware = voipHardware.find((h) => h.id === hw.hardwareId);
@@ -639,7 +647,7 @@ export function useQuote(): UseQuoteReturn {
setQuoteResult(result); setQuoteResult(result);
return result; return result;
}, [gps, support, voip, webHosting, email, getGPSMonthly, getSupportMonthly, getSupportBlockTimeOneTime, getVoIPMonthly, getVoIPOneTime, getWebHostingMonthly, getEmailMonthly]); }, [serviceInterests, gps, support, voip, webHosting, email, getGPSMonthly, getSupportMonthly, getSupportBlockTimeOneTime, getVoIPMonthly, getVoIPOneTime, getWebHostingMonthly, getEmailMonthly]);
// ============================================================================ // ============================================================================
// Reset // Reset

View File

@@ -29,6 +29,9 @@ define('GRAPH_TENANT_ID', 'ce61461e-81a0-4c84-bb4a-7b354a9a356d');
define('GRAPH_CLIENT_ID', '15b0fafb-ab51-4cc9-adc7-f6334c805c22'); define('GRAPH_CLIENT_ID', '15b0fafb-ab51-4cc9-adc7-f6334c805c22');
define('GRAPH_CLIENT_SECRET', 'rRN8Q~FPfSL8O24iZthi_LVJTjGOCZG.DnxGHaSk'); define('GRAPH_CLIENT_SECRET', 'rRN8Q~FPfSL8O24iZthi_LVJTjGOCZG.DnxGHaSk');
define('GRAPH_SENDER_EMAIL', 'noreply@azcomputerguru.com'); define('GRAPH_SENDER_EMAIL', 'noreply@azcomputerguru.com');
define('GRAPH_SENDER_NAME', 'Arizona Computer Guru');
define('GRAPH_REPLY_TO_EMAIL', 'admin@azcomputerguru.com');
define('GRAPH_REPLY_TO_NAME', 'Arizona Computer Guru');
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
// Admin / Auth // Admin / Auth

View File

@@ -124,9 +124,23 @@ function send_email(string $to_email, string $subject, string $body_html, ?strin
'contentType' => 'HTML', 'contentType' => 'HTML',
'content' => $body_html, 'content' => $body_html,
], ],
'from' => [
'emailAddress' => [
'name' => defined('GRAPH_SENDER_NAME') ? GRAPH_SENDER_NAME : 'Arizona Computer Guru',
'address' => GRAPH_SENDER_EMAIL,
],
],
'toRecipients' => [ 'toRecipients' => [
['emailAddress' => ['address' => $to_email]], ['emailAddress' => ['address' => $to_email]],
], ],
'replyTo' => [
[
'emailAddress' => [
'name' => defined('GRAPH_REPLY_TO_NAME') ? GRAPH_REPLY_TO_NAME : 'Arizona Computer Guru',
'address' => defined('GRAPH_REPLY_TO_EMAIL') ? GRAPH_REPLY_TO_EMAIL : GRAPH_SENDER_EMAIL,
],
],
],
], ],
'saveToSentItems' => 'true', 'saveToSentItems' => 'true',
]; ];