Files
verify/frontend/app/dashboard/billing/page.tsx
Mohamed Mathar Irfan ed6610d5d8 Initial project upload
2026-07-28 17:57:02 +05:30

200 lines
8.4 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { CheckCircle2, Download, CreditCard, RefreshCw, Tag } from "lucide-react";
import { api, apiBase } from "@/lib/api";
import { inr } from "@/lib/utils";
import { purchasePlan } from "@/lib/razorpay";
export default function Billing() {
const [sub, setSub] = useState<any>(null);
const [plans, setPlans] = useState<any[]>([]);
const [invoices, setInvoices] = useState<any[]>([]);
const [coupon, setCoupon] = useState("");
const [couponMsg, setCouponMsg] = useState("");
const [busy, setBusy] = useState("");
const [toast, setToast] = useState("");
async function load() {
api("/billing/subscription").then(setSub).catch(() => {});
api("/billing/plans").then(setPlans).catch(() => setPlans([]));
api("/billing/invoices").then(setInvoices).catch(() => {});
}
useEffect(() => { load(); }, []);
async function applyCoupon() {
setCouponMsg("");
try {
const r = await api("/billing/coupon/apply", { method: "POST", body: JSON.stringify({ code: coupon }) });
setCouponMsg(`${r.code}: ${r.percent_off}% off applied`);
} catch (e: any) {
setCouponMsg(typeof e.detail === "string" ? e.detail : "Invalid coupon");
}
}
async function choose(planId: string, planName: string) {
setBusy(planId);
try {
await purchasePlan(planId, coupon, (inv) => {
setToast(`${planName} activated${inv ? ` · Invoice ${inv}` : ""}`);
});
await load();
} catch (e: any) {
setToast(e?.message || "Payment failed");
} finally { setBusy(""); }
}
async function toggleAutoRenew() {
const next = !sub?.auto_renew;
await api("/billing/subscription/auto-renewal", { method: "PUT", body: JSON.stringify({ auto_renew: next }) });
load();
}
const u = sub?.usage;
return (
<div className="space-y-6">
{toast && (
<div className="card p-3 bg-emerald-50 border-emerald-200 text-emerald-800 text-sm flex items-center gap-2">
<CheckCircle2 className="h-4 w-4" /> {toast}
</div>
)}
<h2 className="text-xl font-bold">Billing & Subscription</h2>
{/* KPIs */}
<div className="grid sm:grid-cols-4 gap-4">
<Kpi label="Current Plan" value={sub?.plan?.name ?? "—"} />
<Kpi label="Status" value={(sub?.status ?? "—")} className="capitalize" />
<Kpi label="Days Remaining" value={sub?.days_remaining ?? "—"} />
<div className="card p-5">
<div className="text-sm text-slate-500">Auto-renewal</div>
<button onClick={toggleAutoRenew}
className={`mt-2 w-11 h-6 rounded-full relative transition ${sub?.auto_renew ? "bg-emerald-500" : "bg-slate-300"}`}>
<span className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition ${sub?.auto_renew ? "left-5" : "left-0.5"}`} />
</button>
</div>
</div>
{/* Usage bars */}
{u && (
<div className="card p-5">
<h3 className="font-bold mb-4">Usage</h3>
<div className="grid sm:grid-cols-3 gap-6">
<Usage label="Products" used={u.products} limit={u.product_limit} />
<Usage label="Users" used={u.users} limit={u.user_limit} />
<Usage label="Storage (GB)" used={u.storage_used_gb} limit={u.storage_total_gb} />
</div>
</div>
)}
{/* Coupon */}
<div className="card p-5">
<h3 className="font-bold mb-3 flex items-center gap-2"><Tag className="h-4 w-4" /> Have a coupon?</h3>
<div className="flex gap-2 max-w-md">
<input value={coupon} onChange={(e) => setCoupon(e.target.value)} placeholder="WELCOME10"
className="flex-1 px-3 py-2 rounded-lg border border-slate-200 text-sm" />
<button onClick={applyCoupon} className="px-4 py-2 rounded-lg bg-slate-100 text-sm font-medium">Apply</button>
</div>
{couponMsg && <p className="text-sm mt-2 text-slate-600">{couponMsg}</p>}
</div>
{/* Available plans */}
<div>
<h3 className="font-bold mb-3">Available Plans</h3>
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-4">
{plans.map((p) => {
const current = sub?.plan?.id === p.id;
return (
<div key={p.id} className={`card p-5 ${current ? "ring-2 ring-brand-500" : ""}`}>
<h4 className="font-bold">{p.name}</h4>
<div className="text-2xl font-extrabold mt-1">{p.price ? inr(p.price) : "Free"}
{p.price > 0 && <span className="text-xs font-normal text-slate-400">/mo</span>}</div>
<ul className="text-xs text-slate-500 mt-3 space-y-1">
<li>Products: {p.product_limit ?? "∞"}</li>
<li>Users: {p.user_limit ?? "∞"}</li>
<li>Storage: {p.storage_gb} GB</li>
</ul>
<button disabled={current || busy === p.id}
onClick={() => choose(p.id, p.name)}
className={`mt-4 w-full py-2 rounded-lg text-sm font-semibold ${current ? "bg-slate-100 text-slate-400" : "bg-brand-600 text-white"}`}>
{current ? "Current plan" : busy === p.id ? "Processing…" : "Choose"}
</button>
</div>
);
})}
{plans.length === 0 && <p className="text-sm text-slate-400">Plans load for billing admins.</p>}
</div>
</div>
{/* Payment history */}
<div>
<h3 className="font-bold mb-3">Payment History</h3>
<div className="card overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-50 text-slate-500 text-left">
<tr>
<th className="px-4 py-3 font-medium">Invoice</th>
<th className="px-4 py-3 font-medium">Plan</th>
<th className="px-4 py-3 font-medium">Subtotal</th>
<th className="px-4 py-3 font-medium">GST</th>
<th className="px-4 py-3 font-medium">Total</th>
<th className="px-4 py-3 font-medium">Status</th>
<th className="px-4 py-3"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{invoices.map((i) => (
<tr key={i.id} className="hover:bg-slate-50">
<td className="px-4 py-3 font-mono text-slate-700">{i.invoice_number}</td>
<td className="px-4 py-3">{i.plan_name}</td>
<td className="px-4 py-3">{inr(i.subtotal)}</td>
<td className="px-4 py-3">{inr(i.gst_amount)}</td>
<td className="px-4 py-3 font-semibold">{inr(i.total)}</td>
<td className="px-4 py-3">
<span className="text-xs font-semibold bg-emerald-50 text-emerald-700 px-2 py-0.5 rounded-full capitalize">{i.status}</span>
</td>
<td className="px-4 py-3 text-right">
<a href={`${apiBase}/billing/invoices/${i.id}/pdf`}
className="inline-flex items-center gap-1 text-brand-600 font-medium">
<Download className="h-4 w-4" /> PDF
</a>
</td>
</tr>
))}
{invoices.length === 0 && (
<tr><td colSpan={7} className="px-4 py-12 text-center text-slate-400">
<CreditCard className="h-8 w-8 mx-auto mb-2 opacity-50" /> No invoices yet.
</td></tr>
)}
</tbody>
</table>
</div>
</div>
</div>
);
}
function Kpi({ label, value, className }: any) {
return (
<div className="card p-5">
<div className="text-sm text-slate-500">{label}</div>
<div className={`text-2xl font-extrabold mt-1 ${className ?? ""}`}>{value}</div>
</div>
);
}
function Usage({ label, used, limit }: { label: string; used: number; limit: number | null }) {
const pct = limit == null ? 8 : Math.min(100, Math.round((used / limit) * 100));
return (
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-slate-600">{label}</span>
<span className="text-slate-500">{used} / {limit ?? "∞"}</span>
</div>
<div className="h-2 rounded-full bg-slate-100">
<div className="h-full rounded-full bg-brand-500" style={{ width: `${pct}%` }} />
</div>
</div>
);
}