/* QuoteFlow Prototype · Customer Portal.
Where a customer accepts their proposal and pays the deposit. Reads/writes
the same window.QFStore the admin sends proposals into. */
function PortalApp({ onGoAdmin }) {
const { Card, Button, Badge, StatusBadge, Input, Avatar } = window.QuoteFlowDesignSystem_41788d;
const store = useStore();
const [leadId, setLeadId] = React.useState(null);
const [pay, setPay] = React.useState(false); // showing the payment form
const [card, setCard] = React.useState({ name: '', num: '', exp: '', cvc: '' });
const settings = store.getSettings();
const lead = leadId ? store.getLead(leadId) : null;
// ---------- account chooser (stand-in for a magic-link sign-in) ----------
if (!lead) {
const leads = store.getLeads();
return (
Greenline Energy · Customer portal
Demo sign-in — choose a customer account to open their portal. In production each customer gets a secure magic link by email.
{leads.map((l, i) => (
{ setLeadId(l.id); setPay(false); }}
style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '14px 18px', border: 'none', borderBottom: i < leads.length - 1 ? '1px solid var(--slate-100)' : 'none', background: 'transparent', cursor: 'pointer', textAlign: 'left' }}>
{l.name}
{l.system} · {l.proposal ? (l.proposal.payment ? 'Deposit paid' : l.proposal.status === 'accepted' ? 'Accepted' : 'Proposal ready') : 'Awaiting proposal'}
))}
);
}
const p = lead.proposal;
const steps = store.portalSteps(lead);
const paid = !!(p && p.payment);
const Header = () => (
Greenline Energy
setLeadId(null)} style={{ display: 'flex', alignItems: 'center', gap: 6, border: 'none', background: 'none', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 13, color: 'var(--text-muted)' }}>
{lead.name.split(' ')[0]} · Sign out
);
// ---------- status tracker ----------
const Tracker = () => (
{steps.map((s, i) => (
{s.done ? : {i + 1} }
{s.label}
{s.meta && {s.meta} }
{i < steps.length - 1 && }
))}
);
// ---------- action panel (varies by proposal state) ----------
const cardValid = card.name && card.num.replace(/\s/g, '').length >= 12 && card.exp && card.cvc.length >= 3;
const ActionPanel = () => {
if (!p) {
return (
Your proposal is being prepared
Greenline Energy is finalising your quote — you'll get an email when it's ready to review.
);
}
if (paid) {
return (
You're all set — deposit paid
We've received your £{p.payment.amount.toLocaleString('en-GB')} deposit (card ····{p.payment.last4}). Greenline will call to book your survey.
);
}
if (pay) {
return (
Pay your deposit
Secure · demo
15% deposit due now
{gbp(p.deposit)}
setCard(Object.assign({}, card, { name: e.target.value }))} />
setCard(Object.assign({}, card, { num: e.target.value }))} />
setCard(Object.assign({}, card, { exp: e.target.value }))} style={{ flex: 1 }} />
setCard(Object.assign({}, card, { cvc: e.target.value }))} style={{ flex: 1 }} />
setPay(false)}>Back
}
onClick={() => { store.payDeposit(lead.id, { last4: card.num.replace(/\s/g, '').slice(-4) || '0000' }); setPay(false); window.scrollTo(0, 0); }}>
Pay {gbp(p.deposit)} deposit
Demo only — no card is charged. Use any numbers.
);
}
// proposal sent or accepted (not yet paid)
const accepted = p.status === 'accepted';
return (
{accepted ? 'Proposal accepted' : 'Review your proposal'}
{accepted ? 'Thanks for accepting! Pay your deposit below to secure your install slot.' : 'Greenline Energy has prepared your proposal. Accept it to move forward — then pay a small deposit to book your survey.'}
{!accepted && } onClick={() => { store.acceptProposal(lead.id); if (!settings.takeDeposits) window.scrollTo(0, 0); }}>Accept proposal}
{(accepted || !settings.takeDeposits) && settings.takeDeposits && } onClick={() => setPay(true)}>Pay deposit ({gbp(p.deposit)})}
}>PDF
{!settings.takeDeposits && accepted && No deposit required — Greenline will be in touch to book your survey.
}
);
};
return (
Hello {lead.name.split(' ')[0]} 👋
Track your {lead.system} project and complete the next step below.
{/* booked appointments */}
{lead.project && (lead.project.surveyDate || lead.project.installDate) && (
Your appointments
{lead.project.surveyDate && (
Technical survey
{store.prettyISO(lead.project.surveyDate)}{lead.project.completedAt ? '' : ' · we\u2019ll confirm a time slot'}
)}
{lead.project.installDate && (
Installation
{lead.project.completedAt ? 'Completed — thank you!' : store.prettyISO(lead.project.installDate)}
)}
)}
{/* proposal / quote summary */}
{p ? 'Your proposal' : 'Your saved quote'}
{(p ? p.items : [{ label: lead.system, sub: 'Estimated', amount: lead.value }]).map((it, i, arr) => (
))}
{p ? 'Total' : 'Estimated range'}
{p ? gbp(p.total) : gbp(lead.low) + ' – ' + gbp(lead.high)}
{/* documents + contact */}
Documents
{[['Your quote summary', 'file-text'], ['Proposal' + (p ? '' : ' (pending)'), 'file-check'], ['Terms & conditions', 'scroll-text']].map((d, i) => (
-1 ? 'var(--text-faint)' : 'var(--text-body)') }}>
{d[0]}
{d[0].indexOf('pending') === -1 && }
))}
Your installer
{lead.owner === 'Unassigned' ? 'Greenline Energy' : lead.owner}
01305 123 456
}>Message your installer
);
}
window.PortalApp = PortalApp;