Adapt + improve all admin pages to the new design system

Swept the whole housekeeping panel (34 pages) so it's a consistent,
improved adaptation of the old Filament admin rather than bare tables:
- Live status tiles (StatusCard) summarising each page's data — counts
  and stateful metrics (users online, active/permanent bans, pending
  applications, settings, radio shouts, etc.).
- Status pills (admin-badge ok/danger) replacing ad-hoc text: online/
  offline, account/ip ban types, permanent vs timed, command OK/FAIL,
  alert severity & read/new, enabled/disabled, open/closed.
- Sections wrapped in .admin-section with h2 headers; redundant inline
  table styling removed (the global .admin layer handles it).
- alerts/logs/emulator hand-built as the reference pattern; the rest
  via parallel agents that read the schema and preserved every server
  action, query and field name.

Behaviour unchanged (same actions, queries, field names). Verified in a
real authed admin session (prod, amx_test): pages render with live data
(Users tiles show 7 real accounts + online/offline pills), no errors.
tsc 0, vitest 49/49, next build 0.
This commit is contained in:
Simo committed 2026-06-28 19:07:48 +02:00
1 parent d419731566
commit 0de0ac6a3c
34 files changed
+2357 -1396

No files matched your search

+123 -66
View File
@@ -1,8 +1,20 @@
import Link from "next/link";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
const PAID_STATUS = new Set(["completed", "paid", "success", "approved"]);
function statusState(status: string | null | undefined): "ok" | "danger" | undefined {
if (!status) return undefined;
const s = status.toLowerCase();
if (PAID_STATUS.has(s)) return "ok";
if (s === "failed" || s === "denied" || s === "refunded" || s === "cancelled" || s === "canceled")
return "danger";
return undefined;
}
export default async function AdminShop() {
let packages: Awaited<ReturnType<typeof prisma.websiteShopArticles.findMany>> = [];
try {
@@ -24,82 +36,127 @@ export default async function AdminShop() {
orders = [];
}
const paidOrders = orders.filter((o) => statusState(o.status) === "ok").length;
return (
<main>
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
<h1 style={{ flex: 1 }}>Shop</h1>
<Link href="/admin/shop/new">+ New package</Link>
<Link href="/admin/shop/new" className="btn btn-primary">
+ New package
</Link>
</div>
<p className="muted">
<p className="muted" style={{ marginTop: "-0.4rem" }}>
Website store packages (<code>website_shop_articles</code>). <code>costs</code> is the price
shown in the store; credits/duckets/diamonds/badges are what the buyer receives.
</p>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr style={{ textAlign: "left", color: "var(--muted)" }}>
<th>#</th>
<th>Name</th>
<th>Costs</th>
<th>Credits</th>
<th>Duckets</th>
<th>Diamonds</th>
<th>Rank</th>
<th>Position</th>
</tr>
</thead>
<tbody>
{packages.map((p) => (
<tr key={String(p.id)} style={{ borderTop: "1px solid var(--border)" }}>
<td className="muted">{String(p.id)}</td>
<td>
<Link href={`/admin/shop/${p.id}`}>{p.name}</Link>
</td>
<td>{p.costs}</td>
<td className="muted">{p.credits ?? ""}</td>
<td className="muted">{p.duckets ?? ""}</td>
<td className="muted">{p.diamonds ?? ""}</td>
<td className="muted">{p.giveRank ?? ""}</td>
<td className="muted">{p.position}</td>
</tr>
))}
</tbody>
</table>
{packages.length === 0 ? <p className="muted">No shop packages yet.</p> : null}
<div className="admin-stats">
<StatusCard label="Packages" value={packages.length} icon="🛍️" />
<StatusCard label="Recent orders" value={orders.length} icon="🧾" />
<StatusCard
label="Paid orders"
value={paidOrders}
state={paidOrders > 0 ? "ok" : "neutral"}
icon="💳"
/>
</div>
<h2 style={{ marginTop: "2rem" }}>Recent orders</h2>
<p className="muted">
Read-only PayPal transaction log (<code>website_paypal_transactions</code>).
</p>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr style={{ textAlign: "left", color: "var(--muted)" }}>
<th>Transaction</th>
<th>User</th>
<th>Description</th>
<th>Amount</th>
<th>Status</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{orders.map((o) => (
<tr key={String(o.id)} style={{ borderTop: "1px solid var(--border)" }}>
<td>
<code>{o.transactionId}</code>
</td>
<td className="muted">{o.userId}</td>
<td className="muted">{o.description ?? ""}</td>
<td>
{o.amount} {o.currency}
</td>
<td className="muted">{o.status ?? ""}</td>
<td className="muted">{o.createdAt ? o.createdAt.toISOString().slice(0, 10) : ""}</td>
</tr>
))}
</tbody>
</table>
{orders.length === 0 ? <p className="muted">No orders recorded.</p> : null}
<section className="admin-section">
<h2>Packages ({packages.length})</h2>
{packages.length === 0 ? (
<p className="muted">No shop packages yet.</p>
) : (
<div style={{ overflowX: "auto" }}>
<table>
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Costs</th>
<th>Credits</th>
<th>Duckets</th>
<th>Diamonds</th>
<th>Rank</th>
<th>Position</th>
</tr>
</thead>
<tbody>
{packages.map((p) => (
<tr key={String(p.id)}>
<td className="muted">{String(p.id)}</td>
<td>
<Link href={`/admin/shop/${p.id}`}>{p.name}</Link>
</td>
<td>{p.costs}</td>
<td className="muted">{p.credits ?? "—"}</td>
<td className="muted">{p.duckets ?? "—"}</td>
<td className="muted">{p.diamonds ?? "—"}</td>
<td className="muted">{p.giveRank ?? "—"}</td>
<td className="muted">{p.position}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
<section className="admin-section">
<h2>Recent orders ({orders.length})</h2>
<p className="muted" style={{ marginTop: "-0.4rem" }}>
Read-only PayPal transaction log (<code>website_paypal_transactions</code>).
</p>
{orders.length === 0 ? (
<p className="muted">No orders recorded.</p>
) : (
<div style={{ overflowX: "auto" }}>
<table>
<thead>
<tr>
<th>Transaction</th>
<th>User</th>
<th>Description</th>
<th>Amount</th>
<th>Status</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{orders.map((o) => {
const state = statusState(o.status);
return (
<tr key={String(o.id)}>
<td>
<code>{o.transactionId}</code>
</td>
<td className="muted">{o.userId}</td>
<td className="muted">{o.description ?? "—"}</td>
<td>
{o.amount} {o.currency}
</td>
<td>
{o.status ? (
<span
className={`admin-badge ${state === "ok" ? "ok" : state === "danger" ? "danger" : ""}`}
>
{o.status}
</span>
) : (
<span className="muted">—</span>
)}
</td>
<td className="muted">
{o.createdAt ? o.createdAt.toISOString().slice(0, 10) : "—"}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</section>
</main>
);
}