forked from UKSOURCE/ipv6
paypal
This commit is contained in:
@@ -52,6 +52,37 @@ export type AdminData = {
|
||||
footer: { legal: string[] };
|
||||
};
|
||||
|
||||
function paymentStatusLabel(status: string, lang: string): string {
|
||||
const labels: Record<string, { en: string; vi: string }> = {
|
||||
not_required: { en: "Not required", vi: "Chưa yêu cầu" },
|
||||
payment_pending: { en: "Pending", vi: "Chờ thanh toán" },
|
||||
paid: { en: "Paid", vi: "Đã thanh toán" },
|
||||
payment_failed: { en: "Failed", vi: "Thất bại" },
|
||||
payment_cancelled: { en: "Cancelled", vi: "Đã hủy" },
|
||||
};
|
||||
return lang === "vi" ? (labels[status]?.vi ?? status) : (labels[status]?.en ?? status);
|
||||
}
|
||||
|
||||
function paymentStatusStyle(status: string): string {
|
||||
switch (status) {
|
||||
case "paid": return "border-green-500/50 text-green-400 bg-green-500/10";
|
||||
case "payment_pending": return "border-yellow-500/50 text-yellow-400 bg-yellow-500/10";
|
||||
case "payment_failed": return "border-red-500/50 text-red-400 bg-red-500/10";
|
||||
case "payment_cancelled": return "border-gray-500/50 text-gray-400 bg-gray-500/10";
|
||||
default: return "border-outline/30 text-outline";
|
||||
}
|
||||
}
|
||||
|
||||
function paymentStatusDot(status: string): string {
|
||||
switch (status) {
|
||||
case "paid": return "bg-green-400";
|
||||
case "payment_pending": return "bg-yellow-400";
|
||||
case "payment_failed": return "bg-red-400";
|
||||
case "payment_cancelled": return "bg-gray-400";
|
||||
default: return "bg-outline";
|
||||
}
|
||||
}
|
||||
|
||||
function InfoItem({
|
||||
label,
|
||||
value,
|
||||
@@ -167,6 +198,8 @@ function buildLiveStats(data: AdminData, lang: string) {
|
||||
const approved = req.filter((r) => r.status === "approved").length;
|
||||
const rejected = req.filter((r) => r.status === "rejected").length;
|
||||
const conversionPct = total > 0 ? Math.round((approved / total) * 100) : 0;
|
||||
const paidCount = req.filter((r) => r.paymentStatus === "paid").length;
|
||||
const totalRevenue = paidCount * 120;
|
||||
|
||||
const isVi = lang === "vi";
|
||||
|
||||
@@ -209,6 +242,14 @@ function buildLiveStats(data: AdminData, lang: string) {
|
||||
: "",
|
||||
progressPercent: conversionPct,
|
||||
};
|
||||
case "revenue":
|
||||
return {
|
||||
...s,
|
||||
value: `$${formatCount(totalRevenue)}`,
|
||||
hint: isVi
|
||||
? `${formatCount(paidCount)} đăng ký đã thanh toán`
|
||||
: `${formatCount(paidCount)} paid registrations`,
|
||||
};
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
@@ -225,6 +266,7 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
|
||||
const [afterDate, setAfterDate] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [detail, setDetail] = useState<AdminRequest | null>(null);
|
||||
const [paymentFilter, setPaymentFilter] = useState("all");
|
||||
const { lang, setLang } = useLanguage();
|
||||
const { admin: t } = useTranslation();
|
||||
const [userDropdown, setUserDropdown] = useState(false);
|
||||
@@ -260,13 +302,13 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
|
||||
return data.requests.filter((r) => {
|
||||
if (statusFilter !== "all" && r.status !== statusFilter) return false;
|
||||
if (companyGroup !== "all" && r.segment !== companyGroup) return false;
|
||||
if (paymentFilter !== "all" && (r.paymentStatus ?? "not_required") !== paymentFilter) return false;
|
||||
if (afterDate && r.submittedAt < afterDate) return false;
|
||||
if (!q) return true;
|
||||
|
||||
const hay = `${r.id} ${r.fullName} ${r.company} ${r.email} ${r.phone} ${r.jobTitle} ${r.status}`.toLowerCase();
|
||||
return hay.includes(q);
|
||||
});
|
||||
}, [data.requests, search, statusFilter, companyGroup, afterDate]);
|
||||
}, [data.requests, search, statusFilter, companyGroup, paymentFilter, afterDate]);
|
||||
|
||||
async function commitStatus(publicId: string, next: SubmissionStatus) {
|
||||
try {
|
||||
@@ -477,6 +519,26 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 min-w-[160px]">
|
||||
<label className="font-[var(--font-label-caps)] text-[10px] text-outline uppercase tracking-wider">
|
||||
{lang === "vi" ? "Thanh toán" : "Payment"}
|
||||
</label>
|
||||
<select
|
||||
className="bg-surface-container-high border border-outline/30 text-on-surface text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
value={paymentFilter}
|
||||
onChange={(e) => {
|
||||
setPaymentFilter(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
<option value="all">{lang === "vi" ? "Tất cả" : "All"}</option>
|
||||
<option value="not_required">{lang === "vi" ? "Chưa yêu cầu" : "Not required"}</option>
|
||||
<option value="payment_pending">{lang === "vi" ? "Chờ thanh toán" : "Pending"}</option>
|
||||
<option value="paid">{lang === "vi" ? "Đã thanh toán" : "Paid"}</option>
|
||||
<option value="payment_failed">{lang === "vi" ? "Thất bại" : "Failed"}</option>
|
||||
<option value="payment_cancelled">{lang === "vi" ? "Đã hủy" : "Cancelled"}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 min-w-[160px]">
|
||||
<label className="font-[var(--font-label-caps)] text-[10px] text-outline uppercase tracking-wider">
|
||||
{lang === "vi" ? "Nộp sau ngày" : "Submitted after"}
|
||||
@@ -525,6 +587,9 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
|
||||
<th className="px-4 py-3 font-[var(--font-label-caps)] text-outline text-[10px] uppercase tracking-widest w-[110px]">
|
||||
{t.table.columns.status}
|
||||
</th>
|
||||
<th className="px-4 py-3 font-[var(--font-label-caps)] text-outline text-[10px] uppercase tracking-widest w-[120px]">
|
||||
{lang === "vi" ? "Thanh toán" : "Payment"}
|
||||
</th>
|
||||
<th className="px-4 py-3 font-[var(--font-label-caps)] text-outline text-[10px] uppercase tracking-widest text-right w-[160px]">
|
||||
{t.table.columns.actions}
|
||||
</th>
|
||||
@@ -542,8 +607,13 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
|
||||
{row.displayDate}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="font-[var(--font-body-base)] text-[13px] font-semibold text-on-surface truncate max-w-[200px]" title={row.fullName}>
|
||||
{row.fullName}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="font-[var(--font-body-base)] text-[13px] font-semibold text-on-surface truncate max-w-[200px]" title={row.fullName}>
|
||||
{row.fullName}
|
||||
</div>
|
||||
{row.paymentStatus === "paid" && (
|
||||
<span className="w-2 h-2 rounded-full bg-green-400 shrink-0" title={lang === "vi" ? "Đã thanh toán" : "Paid"} />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[11px] text-outline truncate max-w-[200px]" title={row.jobTitle}>
|
||||
{row.jobTitle}
|
||||
@@ -564,6 +634,16 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
|
||||
{sm.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{row.paymentStatus && row.paymentStatus !== "not_required" ? (
|
||||
<span className={`px-2 py-0.5 rounded-full text-[9px] font-[var(--font-label-caps)] border uppercase tracking-wider flex items-center gap-1.5 w-fit ${paymentStatusStyle(row.paymentStatus)}`}>
|
||||
<span className={`w-1 h-1 rounded-full status-led ${paymentStatusDot(row.paymentStatus)}`} />
|
||||
{paymentStatusLabel(row.paymentStatus, lang)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-outline/40 text-[11px]">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="flex items-center justify-end gap-0.5">
|
||||
<button
|
||||
@@ -838,6 +918,39 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment Info Section */}
|
||||
{detail.paymentStatus && detail.paymentStatus !== "not_required" && (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-xs font-[var(--font-label-caps)] text-primary uppercase tracking-[0.2em] border-b border-primary/20 pb-2">
|
||||
{lang === "vi" ? "Thông tin thanh toán" : "Payment Information"}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<InfoItem
|
||||
label={lang === "vi" ? "Trạng thái thanh toán" : "Payment Status"}
|
||||
value={paymentStatusLabel(detail.paymentStatus, lang)}
|
||||
/>
|
||||
{detail.paypalOrderId && (
|
||||
<InfoItem label="PayPal Order ID" value={detail.paypalOrderId} />
|
||||
)}
|
||||
{detail.paypalCaptureId && (
|
||||
<InfoItem label="PayPal Capture ID" value={detail.paypalCaptureId} />
|
||||
)}
|
||||
{detail.paymentAmount && (
|
||||
<InfoItem
|
||||
label={lang === "vi" ? "Số tiền" : "Amount"}
|
||||
value={`${detail.paymentAmount} ${detail.paymentCurrency ?? "USD"}`}
|
||||
/>
|
||||
)}
|
||||
{detail.paymentCompletedAt && (
|
||||
<InfoItem
|
||||
label={lang === "vi" ? "Thời gian thanh toán" : "Payment Date"}
|
||||
value={new Date(detail.paymentCompletedAt).toLocaleString(lang === "vi" ? "vi-VN" : "en-GB")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal Footer Actions */}
|
||||
|
||||
@@ -40,20 +40,26 @@ export default function RegistrationForm({
|
||||
setSubmitState("sending");
|
||||
const industryLabel = industries.find((o) => o.value === form.industry)?.label ?? "";
|
||||
try {
|
||||
const res = await fetch("/ipv6/api/submissions", {
|
||||
const res = await fetch("/ipv6/api/payments/create-order/", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
type: "registration",
|
||||
...form,
|
||||
fullName: form.fullName,
|
||||
phone: form.phone,
|
||||
email: form.email,
|
||||
company: form.company,
|
||||
jobTitle: form.jobTitle,
|
||||
industry: form.industry,
|
||||
industryLabel,
|
||||
notes: form.notes,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(typeof err?.error === "string" ? err.error : res.statusText);
|
||||
}
|
||||
setSubmitState("ok");
|
||||
const { approvalUrl } = await res.json();
|
||||
window.location.href = approvalUrl;
|
||||
} catch {
|
||||
setSubmitState("error");
|
||||
}
|
||||
@@ -69,6 +75,7 @@ export default function RegistrationForm({
|
||||
<input
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all"
|
||||
placeholder="John Doe"
|
||||
required
|
||||
value={form.fullName}
|
||||
onChange={(e) => setForm((s) => ({ ...s, fullName: e.target.value }))}
|
||||
/>
|
||||
@@ -88,6 +95,7 @@ export default function RegistrationForm({
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all"
|
||||
placeholder="john.doe@company.com"
|
||||
type="email"
|
||||
required
|
||||
value={form.email}
|
||||
onChange={(e) => setForm((s) => ({ ...s, email: e.target.value }))}
|
||||
/>
|
||||
@@ -177,17 +185,12 @@ export default function RegistrationForm({
|
||||
type="submit"
|
||||
disabled={submitState === "sending"}
|
||||
>
|
||||
{submitState === "sending" ? "Submitting…" : data.submit.label}
|
||||
{submitState === "sending" ? "Đang xử lý..." : data.submit.label}
|
||||
</button>
|
||||
|
||||
<SubmitToast
|
||||
state={submitState}
|
||||
successTitle="Registration received"
|
||||
successMessage="Thank you. Your registration was saved and will appear in the admin console under Pending."
|
||||
onDismiss={() => {
|
||||
setSubmitState("idle");
|
||||
setForm({ ...emptyRegistration });
|
||||
}}
|
||||
onDismiss={() => setSubmitState("idle")}
|
||||
/>
|
||||
|
||||
<p className="mt-[16px] text-center text-outline text-[11px] tracking-widest opacity-60 uppercase">
|
||||
|
||||
@@ -93,12 +93,12 @@ export default function SubmitToast({
|
||||
{/* Text */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-[var(--font-display-lg)] font-semibold text-sm leading-snug">
|
||||
{isSuccess ? successTitle : "Submission failed"}
|
||||
{isSuccess ? successTitle : "Gửi thông tin thất bại"}
|
||||
</p>
|
||||
<p className="mt-1 text-[13px] leading-relaxed opacity-80">
|
||||
{isSuccess
|
||||
? successMessage
|
||||
: "Please check your connection and try again."}
|
||||
: "Vui lòng kiểm tra kết nối và thử lại."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user