136 lines
7.3 KiB
React
136 lines
7.3 KiB
React
/**
|
|
* ReportStep — Screen 9 "Report + next step" (spec §11, §13).
|
|
* Shows the report summary, lets the customer email/print/download/share, books
|
|
* the inspection, and fires the CRM + nurture handoff (once) with the immutable
|
|
* compliance record (spec §18 G1).
|
|
*/
|
|
import React, { useEffect, useState } from 'react';
|
|
import { Download, Printer, Mail, Share2, CalendarCheck, FileText, Loader2, Check } from 'lucide-react';
|
|
import { useEstimateWizard } from '../../EstimateWizardContext';
|
|
import { buildReportModel, downloadReportPdf, getReportBlobUrl } from '../../utils/wizardReportExport';
|
|
import { submitToCrm } from '../../api/crmHandoff';
|
|
import { formatUSD } from '../../engine/pricing';
|
|
|
|
const PKG_LABEL = { GOOD: 'Good', BETTER: 'Better', BEST: 'Best' };
|
|
|
|
const ActionButton = ({ icon: Icon, label, onClick, busy, primary }) => (
|
|
<button
|
|
type="button"
|
|
onClick={onClick}
|
|
disabled={busy}
|
|
className={`flex items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold transition-all disabled:opacity-50
|
|
${primary
|
|
? 'bg-gradient-to-r from-amber-400 to-orange-500 text-white shadow-lg shadow-amber-500/25'
|
|
: 'border border-zinc-200 dark:border-white/10 text-zinc-700 dark:text-zinc-200 hover:border-amber-400'}`}
|
|
>
|
|
{busy ? <Loader2 size={16} className="animate-spin" /> : Icon && <Icon size={16} />} {label}
|
|
</button>
|
|
);
|
|
|
|
const ReportStep = () => {
|
|
const { session, tenantConfig, patchSession, logEvent } = useEstimateWizard();
|
|
const model = buildReportModel(session, tenantConfig);
|
|
const [busy, setBusy] = useState(null);
|
|
const [booked, setBooked] = useState(!!session.report?.appointmentBooked);
|
|
|
|
// Fire CRM + nurture handoff once (spec §13 / §17 E2).
|
|
useEffect(() => {
|
|
if (session.report) return;
|
|
const reportMeta = { reportId: model.reportId, version: 1, generatedAt: model.generatedAt, deliveryStatus: 'created' };
|
|
patchSession({ report: reportMeta });
|
|
submitToCrm(session, tenantConfig, reportMeta);
|
|
logEvent('report_generated', { reportId: model.reportId });
|
|
logEvent('crm_handoff', { trigger: 'wizard_completed' });
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
const handleDownload = async () => {
|
|
setBusy('download');
|
|
try { await downloadReportPdf(session, tenantConfig); logEvent('report_downloaded', { reportId: model.reportId }); }
|
|
finally { setBusy(null); }
|
|
};
|
|
|
|
const handlePrint = async () => {
|
|
setBusy('print');
|
|
try {
|
|
const { url } = await getReportBlobUrl(session, tenantConfig);
|
|
const w = window.open(url, '_blank');
|
|
if (w) w.addEventListener('load', () => w.print());
|
|
patchSession({ report: { ...session.report, printedAt: new Date().toISOString() } });
|
|
logEvent('report_printed', { reportId: model.reportId });
|
|
} finally { setBusy(null); }
|
|
};
|
|
|
|
const handleEmail = () => {
|
|
const subject = encodeURIComponent(`Your Roof Options Report (${model.reportId})`);
|
|
const body = encodeURIComponent(
|
|
`Hi ${model.contact.name || ''},\n\nHere is a summary of your roof options report.\n` +
|
|
`Recommended: ${PKG_LABEL[model.recommended]}\nEstimated total: ~${formatUSD(model.totals.total)}\n\n` +
|
|
`${tenantConfig.branding.company} · ${tenantConfig.branding.phone}\n\n` +
|
|
`Note: ${tenantConfig.disclaimer}`
|
|
);
|
|
window.location.href = `mailto:${model.contact.email || ''}?subject=${subject}&body=${body}`;
|
|
patchSession({ report: { ...session.report, emailedAt: new Date().toISOString(), deliveryStatus: 'emailed' } });
|
|
logEvent('report_emailed', { reportId: model.reportId });
|
|
};
|
|
|
|
const handleShare = async () => {
|
|
const summary = `My roof options report (${PKG_LABEL[model.recommended]} recommended) from ${tenantConfig.branding.company}.`;
|
|
try {
|
|
if (navigator.share) await navigator.share({ title: 'Roof Options Report', text: summary });
|
|
else { await navigator.clipboard?.writeText(summary); }
|
|
logEvent('report_shared', { reportId: model.reportId });
|
|
} catch { /* user cancelled */ }
|
|
};
|
|
|
|
const handleBook = () => {
|
|
setBooked(true);
|
|
patchSession({ report: { ...session.report, appointmentBooked: true, bookedAt: new Date().toISOString() } });
|
|
logEvent('appointment_booked', { reportId: model.reportId });
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Report summary card */}
|
|
<div className="rounded-2xl border border-zinc-200 dark:border-white/10 bg-white dark:bg-zinc-900 p-5">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<FileText size={18} className="text-amber-500" />
|
|
<span className="font-bold text-zinc-900 dark:text-white">Your Roof Options Report</span>
|
|
<span className="ml-auto text-[11px] text-zinc-400">{model.reportId}</span>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
|
<div><div className="text-zinc-400 text-xs">Prepared for</div><div className="font-medium text-zinc-800 dark:text-zinc-100">{model.contact.name || '—'}</div></div>
|
|
<div><div className="text-zinc-400 text-xs">Recommended</div><div className="font-medium text-amber-600 dark:text-amber-400">{PKG_LABEL[model.recommended]}</div></div>
|
|
<div><div className="text-zinc-400 text-xs">Hail exposure</div><div className="font-medium text-zinc-800 dark:text-zinc-100 capitalize">{model.hail?.exposureTier || '—'}</div></div>
|
|
<div><div className="text-zinc-400 text-xs">Estimated total</div><div className="font-medium text-zinc-800 dark:text-zinc-100">~{formatUSD(model.totals.total)}</div></div>
|
|
</div>
|
|
<p className="text-[11px] text-zinc-400 mt-3 leading-relaxed border-t border-zinc-100 dark:border-white/5 pt-2.5">
|
|
{tenantConfig.disclaimer}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Delivery actions */}
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2.5">
|
|
<ActionButton icon={Download} label="Download" onClick={handleDownload} busy={busy === 'download'} />
|
|
<ActionButton icon={Printer} label="Print" onClick={handlePrint} busy={busy === 'print'} />
|
|
<ActionButton icon={Mail} label="Email" onClick={handleEmail} />
|
|
<ActionButton icon={Share2} label="Share" onClick={handleShare} />
|
|
</div>
|
|
|
|
{/* Next step CTA */}
|
|
{booked ? (
|
|
<div className="rounded-2xl border border-emerald-200 dark:border-emerald-500/20 bg-emerald-50 dark:bg-emerald-500/10 p-4 flex items-center gap-3">
|
|
<Check size={20} className="text-emerald-500" />
|
|
<div className="text-sm text-emerald-700 dark:text-emerald-300">
|
|
Inspection requested — your rep will reach out via {session.inputs.contactMethod}. Thank you!
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<ActionButton icon={CalendarCheck} label="Book my free inspection" onClick={handleBook} primary />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ReportStep;
|