128 lines
7.2 KiB
TypeScript
128 lines
7.2 KiB
TypeScript
"use client";
|
|
import { useEffect, useState } from "react";
|
|
import { Plus, Pencil, X, CheckCircle2 } from "lucide-react";
|
|
import { api } from "@/lib/api";
|
|
import { inr } from "@/lib/utils";
|
|
|
|
type PlanForm = {
|
|
_id?: string; name: string; price: number;
|
|
product_limit: number | null; user_limit: number | null;
|
|
storage_gb: number; grace_days: number; archive_years: number;
|
|
features: string; enabled: boolean;
|
|
};
|
|
|
|
const empty: PlanForm = {
|
|
name: "", price: 0, product_limit: 100, user_limit: 3, storage_gb: 10,
|
|
grace_days: 30, archive_years: 1, features: "", enabled: true,
|
|
};
|
|
|
|
export default function Plans() {
|
|
const [plans, setPlans] = useState<any[]>([]);
|
|
const [form, setForm] = useState<PlanForm | null>(null);
|
|
const [toast, setToast] = useState("");
|
|
|
|
function load() { api("/admin/plans").then(setPlans).catch(() => {}); }
|
|
useEffect(() => { load(); }, []);
|
|
|
|
function openEdit(p: any) {
|
|
setForm({ _id: p.id, name: p.name, price: p.price,
|
|
product_limit: p.product_limit, user_limit: p.user_limit,
|
|
storage_gb: p.storage_gb, grace_days: p.grace_days ?? 30,
|
|
archive_years: p.archive_years ?? 1,
|
|
features: (p.features ?? []).join("\n"), enabled: p.enabled });
|
|
}
|
|
async function save() {
|
|
if (!form) return;
|
|
const { _id, features, ...rest } = form;
|
|
const payload = { ...rest, features: features.split("\n").map((s) => s.trim()).filter(Boolean) };
|
|
if (_id) await api(`/admin/plans/${_id}`, { method: "PUT", body: JSON.stringify(payload) });
|
|
else await api("/admin/plans", { method: "POST", body: JSON.stringify(payload) });
|
|
setForm(null); setToast("Plan saved"); setTimeout(() => setToast(""), 2000); load();
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
{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>}
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-xl font-bold">Plans & Pricing</h2>
|
|
<button onClick={() => setForm({ ...empty })} className="flex items-center gap-2 bg-brand-600 text-white px-4 py-2 rounded-lg text-sm font-semibold"><Plus className="h-4 w-4" /> Add Plan</button>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-4">
|
|
{plans.map((p) => (
|
|
<div key={p.id} className={`card p-5 relative ${!p.enabled ? "opacity-60" : ""}`}>
|
|
<button onClick={() => openEdit(p)} className="absolute top-3 right-3 h-8 w-8 grid place-items-center rounded-lg border border-slate-200 text-slate-500 hover:bg-slate-50"><Pencil className="h-4 w-4" /></button>
|
|
<h3 className="font-bold">{p.name}</h3>
|
|
<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>
|
|
{!p.enabled && <span className="text-[10px] font-semibold text-rose-500 mt-2 block">DISABLED</span>}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="card p-5">
|
|
<h3 className="font-bold mb-1">System Configuration</h3>
|
|
<p className="text-sm text-slate-500">Grace period and archive/preservation windows are configured per plan above. Changes to plan prices do not affect active subscriptions until renewal.</p>
|
|
</div>
|
|
|
|
{form && (
|
|
<div className="fixed inset-0 bg-black/40 z-50 grid place-items-center p-4 overflow-y-auto" onClick={() => setForm(null)}>
|
|
<div className="bg-white rounded-2xl w-full max-w-lg my-4" onClick={(e) => e.stopPropagation()}>
|
|
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
|
|
<h3 className="font-bold">{form._id ? "Edit Plan" : "Add Plan"}</h3>
|
|
<button onClick={() => setForm(null)}><X className="h-5 w-5 text-slate-400" /></button>
|
|
</div>
|
|
<div className="p-5 grid grid-cols-2 gap-3">
|
|
<Inp label="Name" v={form.name} on={(x) => setForm({ ...form, name: x })} full />
|
|
<Inp label="Price (₹/mo)" type="number" v={form.price} on={(x) => setForm({ ...form, price: Number(x) })} />
|
|
<Inp label="Storage (GB)" type="number" v={form.storage_gb} on={(x) => setForm({ ...form, storage_gb: Number(x) })} />
|
|
<LimitInp label="Product limit" v={form.product_limit} on={(x) => setForm({ ...form, product_limit: x })} />
|
|
<LimitInp label="User limit" v={form.user_limit} on={(x) => setForm({ ...form, user_limit: x })} />
|
|
<Inp label="Grace days" type="number" v={form.grace_days} on={(x) => setForm({ ...form, grace_days: Number(x) })} />
|
|
<Inp label="Archive years" type="number" v={form.archive_years} on={(x) => setForm({ ...form, archive_years: Number(x) })} />
|
|
<label className="block col-span-2">
|
|
<span className="text-sm font-medium">Features (one per line)</span>
|
|
<textarea value={form.features} onChange={(e) => setForm({ ...form, features: e.target.value })} rows={4} className="mt-1 w-full px-3 py-2 rounded-lg border border-slate-200 text-sm" />
|
|
</label>
|
|
<label className="flex items-center gap-2 col-span-2 text-sm">
|
|
<input type="checkbox" checked={form.enabled} onChange={(e) => setForm({ ...form, enabled: e.target.checked })} /> Enabled (new signups can select this plan)
|
|
</label>
|
|
</div>
|
|
<div className="flex justify-end gap-3 px-5 py-4 border-t border-slate-100">
|
|
<button onClick={() => setForm(null)} className="px-5 py-2 rounded-lg border border-slate-200 text-sm font-medium">Cancel</button>
|
|
<button onClick={save} className="px-6 py-2 rounded-lg bg-brand-600 text-white text-sm font-semibold">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Inp({ label, v, on, type = "text", full }: { label: string; v: any; on: (x: string) => void; type?: string; full?: boolean }) {
|
|
return (
|
|
<label className={`block ${full ? "col-span-2" : ""}`}>
|
|
<span className="text-sm font-medium">{label}</span>
|
|
<input type={type} value={v ?? ""} onChange={(e) => on(e.target.value)} className="mt-1 w-full px-3 py-2 rounded-lg border border-slate-200 text-sm" />
|
|
</label>
|
|
);
|
|
}
|
|
function LimitInp({ label, v, on }: { label: string; v: number | null; on: (x: number | null) => void }) {
|
|
return (
|
|
<label className="block">
|
|
<span className="text-sm font-medium">{label}</span>
|
|
<div className="mt-1 flex gap-2 items-center">
|
|
<input type="number" value={v ?? ""} disabled={v === null} onChange={(e) => on(Number(e.target.value))}
|
|
className="flex-1 px-3 py-2 rounded-lg border border-slate-200 text-sm disabled:bg-slate-50" />
|
|
<label className="flex items-center gap-1 text-xs text-slate-500">
|
|
<input type="checkbox" checked={v === null} onChange={(e) => on(e.target.checked ? null : 100)} /> ∞
|
|
</label>
|
|
</div>
|
|
</label>
|
|
);
|
|
}
|