diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 9e26fe5..5920293 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -3,6 +3,8 @@ export const dynamic = "force-dynamic"; import InviteVendorCard from "@/components/admin/InviteVendorCard"; import BanUserCard from "@/components/admin/BanUserCard"; import RecentOrdersCard from "@/components/admin/RecentOrdersCard"; +import SystemStatusCard from "@/components/admin/SystemStatusCard"; +import InvitationsListCard from "@/components/admin/InvitationsListCard"; export default function AdminPage() { return ( @@ -12,18 +14,10 @@ export default function AdminPage() {

Restricted area. Only admin1 can access.

-
- -
-
-

System status

-

Uptime, versions, environment

-
- OK -
-
+
+ - +

Logs

@@ -36,6 +30,7 @@ export default function AdminPage() { +
@@ -47,7 +42,7 @@ export default function AdminPage() {
- +

Config

@@ -57,7 +52,7 @@ export default function AdminPage() {
- +

Payments

diff --git a/components/admin/BanUserCard.tsx b/components/admin/BanUserCard.tsx index cc04ed8..9ddcf0e 100644 --- a/components/admin/BanUserCard.tsx +++ b/components/admin/BanUserCard.tsx @@ -28,7 +28,7 @@ export default function BanUserCard() { } return ( -
+

Ban User

Block abusive users by Telegram ID

diff --git a/components/admin/InvitationsListCard.tsx b/components/admin/InvitationsListCard.tsx new file mode 100644 index 0000000..111b1a3 --- /dev/null +++ b/components/admin/InvitationsListCard.tsx @@ -0,0 +1,71 @@ +"use client"; +import { useEffect, useState } from "react"; +import { fetchClient } from "@/lib/api-client"; + +interface Invitation { + _id: string; + code: string; + isUsed: boolean; + createdAt: string; + expiresAt: string; + usedAt?: string | null; +} + +export default function InvitationsListCard() { + const [invites, setInvites] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let mounted = true; + (async () => { + try { + const data = await fetchClient("/admin/invitations"); + if (mounted) setInvites(data); + } catch (e: any) { + if (mounted) setError(e?.message || "Failed to load invitations"); + } finally { + if (mounted) setLoading(false); + } + })(); + return () => { mounted = false; }; + }, []); + + return ( +
+

Invitations

+

Active and recent invitations

+ + {loading ? ( +

Loading...

+ ) : error ? ( +

{error}

+ ) : invites.length === 0 ? ( +

No invitations found

+ ) : ( +
+ {invites.map((inv) => { + const expired = new Date(inv.expiresAt).getTime() < Date.now(); + return ( +
+
+
+ Code: {inv.code} +
+
+ Created: {new Date(inv.createdAt).toLocaleString()} ยท Expires: {new Date(inv.expiresAt).toLocaleString()} +
+
+ + {inv.isUsed ? 'Used' : expired ? 'Expired' : 'Active'} + +
+ ); + })} +
+ )} +
+ ); +} + + diff --git a/components/admin/InviteVendorCard.tsx b/components/admin/InviteVendorCard.tsx index 0a26e4c..b7295b6 100644 --- a/components/admin/InviteVendorCard.tsx +++ b/components/admin/InviteVendorCard.tsx @@ -3,23 +3,18 @@ import { useState } from "react"; import { fetchClient } from "@/lib/api-client"; export default function InviteVendorCard() { - const [username, setUsername] = useState(""); - const [email, setEmail] = useState(""); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(null); + const [code, setCode] = useState(null); - async function handleInvite(e: React.FormEvent) { - e.preventDefault(); + async function handleInvite() { setLoading(true); setMessage(null); + setCode(null); try { - await fetchClient("/admin/invitations", { - method: "POST", - body: { username, email } - }); - setMessage("Invitation sent"); - setUsername(""); - setEmail(""); + const res = await fetchClient<{ code: string }>("/admin/invitations", { method: "POST" }); + setMessage("Invitation created"); + setCode(res.code); } catch (e: any) { setMessage(e?.message || "Failed to send invitation"); } finally { @@ -28,33 +23,24 @@ export default function InviteVendorCard() { } return ( -
+

Invite Vendor

-

Generate and send an invite

- - setUsername(e.target.value)} - required - /> - setEmail(e.target.value)} - type="email" - /> +

Generate a new invitation code

+
{message &&

{message}

} - + {code && ( +
+ Code: {code} +
+ )} +
); } diff --git a/components/admin/RecentOrdersCard.tsx b/components/admin/RecentOrdersCard.tsx index 7d4c3b5..9c6fe05 100644 --- a/components/admin/RecentOrdersCard.tsx +++ b/components/admin/RecentOrdersCard.tsx @@ -36,7 +36,7 @@ export default function RecentOrdersCard() { }, []); return ( -
+

Recent Orders

Last 10 orders across stores

{loading ? ( diff --git a/components/admin/SystemStatusCard.tsx b/components/admin/SystemStatusCard.tsx new file mode 100644 index 0000000..01a583b --- /dev/null +++ b/components/admin/SystemStatusCard.tsx @@ -0,0 +1,72 @@ +"use client"; +import { useEffect, useState } from "react"; +import { fetchClient } from "@/lib/api-client"; + +interface Status { + uptimeSeconds: number; + memory: Record; + versions: Record; + counts: { vendors: number; orders: number; products: number; chats: number }; +} + +function formatDuration(seconds: number) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + return `${h}h ${m}m ${s}s`; +} + +export default function SystemStatusCard() { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + let mounted = true; + (async () => { + try { + const res = await fetchClient("/admin/system-status"); + if (mounted) setData(res); + } catch (e: any) { + if (mounted) setError(e?.message || "Failed to load status"); + } + })(); + return () => { mounted = false; }; + }, []); + + return ( +
+
+
+

System status

+

Uptime, versions, environment

+
+ OK +
+ + {error &&

{error}

} + + {data && ( +
+
+
Uptime
+
{formatDuration(data.uptimeSeconds)}
+
+
+
Node
+
{data.versions?.node}
+
+
+
Vendors
+
{data.counts?.vendors}
+
+
+
Orders
+
{data.counts?.orders}
+
+
+ )} +
+ ); +} + +