// ============================================================================ // public/screens/pricing.jsx — Plans & Billing (Phase 7D) // ============================================================================ function PricingScreen() { const [plans, setPlans] = React.useState(null); const [sub, setSub] = React.useState(null); const [usage, setUsage] = React.useState(null); const [invoices, setInv] = React.useState(null); const [tab, setTab] = React.useState("plan"); // plan | invoices | payment React.useEffect(() => { VoaisAPI.get("/api/billing/plans").then(r => { if (r.ok) setPlans(r.data.plans); }); VoaisAPI.get("/api/billing/subscription").then(r => { if (r.ok) setSub(r.data.subscription); }); VoaisAPI.get("/api/billing/usage").then(r => { if (r.ok) setUsage(r.data); }); VoaisAPI.get("/api/billing/invoices").then(r => { if (r.ok) setInv(r.data.invoices); }); }, []); const upgrade = async (code) => { if (!confirm("Switch to " + code + " plan?")) return; const r = await VoaisAPI.post("/api/billing/subscribe", { plan_code: code }); if (r.ok) { alert(r.data.msg); location.reload(); } else alert(r.data?.msg || "Failed."); }; if (!plans) return ; return (
{tab === "plan" && (<> {/* Current plan */} {sub && (
Current plan
{sub.plan_name}
{sub.status}
{sub.price_inr ? "₹" + Number(sub.price_inr).toLocaleString("en-IN") : "Custom"}
/month
{sub.trial_ends_at && sub.status === "trialing" && (
Trial ends: {new Date(sub.trial_ends_at).toLocaleDateString("en-IN")}
)}
)} {/* Usage meters */} {usage && sub && (
)} {/* Plan comparison */}
Available plans
{plans.map(p => { const isCurrent = sub?.plan_code === p.code; return ( {p.popular &&
Most popular
}
{p.name}
{p.tagline}
{p.price_inr ? "₹" + Number(p.price_inr).toLocaleString("en-IN") : "Custom"}
/month
{p.minutes_included.toLocaleString("en-IN")} minutes
{p.contacts_included.toLocaleString("en-IN")} contacts
{p.agents_included} agents · {p.concurrent_calls} concurrent calls
{(p.features || []).slice(0, 4).map((f, i) =>
{f}
)}
{isCurrent ? Current plan : upgrade(p.code)}> {sub && Number(p.price_inr) > Number(sub.price_inr) ? "Upgrade" : "Switch"}}
); })}
)} {tab === "invoices" && ( {(invoices || []).map(inv => ( ))} {(!invoices || invoices.length === 0) && }
InvoicePeriodAmountStatusDate
{inv.invoice_number} {inv.period_start?.slice(0,10)} — {inv.period_end?.slice(0,10)} ₹{Number(inv.total_inr).toLocaleString("en-IN")} {inv.status} {inv.issued_at ? new Date(inv.issued_at).toLocaleDateString("en-IN") : "—"}
No invoices yet.
)} {tab === "payment" && (
Payment method management will be available once Razorpay integration is configured. Currently billing is managed manually.
)}
); } function UsageMeter({ label, used, total, unit }) { const pct = total > 0 ? Math.min(100, Math.round((used / total) * 100)) : 0; const over = used > total && total > 0; return (
{label} {used}{unit ? " " + unit : ""} / {total || "∞"}
0 ? pct : 0) + "%", background: over ? "var(--err)" : pct > 80 ? "var(--warn)" : "var(--accent)", borderRadius: 3, transition: "width 0.5s" }}/>
); } window.PricingScreen = PricingScreen;