افزودن پالت فرمان (⌘K) و صفحهٔ مصرف سراسری پنل مالک
دو شکاف رسیدگیشده از بازبینی PLAN.md و TODO.md: - پالت فرمان: جستوجو و رفتن سریع بین بخشها با ⌘K/Ctrl+K، پیمایش با کلیدهای جهتدار، Enter برای رفتن، و Escape برای بستن. گروه اختصاصی «کنسول مالک سامانه» فقط برای مالک سامانه نمایش داده میشود. - صفحهٔ /admin/usage: گزارش مصرف سراسری به تفکیک سازمان. اندپوینت `GET /admin/usage` و کلاینت `adminUsage()` از قبل وجود داشتند و توسط صفحات نمای کلی و سازمانها مصرف میشدند؛ این صفحهٔ اختصاصی آن است. همچنین `NAV_ITEMS` به `lib/nav.ts` منتقل شد تا sidebar و پالت فرمان هرگز از هم واگرا نشوند. تست: ۱۰ تست جدید برای پالت فرمان. مجموع تستهای فرانتاند از ۲۵ به ۳۵ رسید. tsc، eslint و ruff تمیز.
This commit is contained in:
@@ -20,6 +20,7 @@ const SECTIONS = [
|
||||
{ href: "/admin/models", label: "مدلها" },
|
||||
{ href: "/admin/routing", label: "مسیریابی" },
|
||||
{ href: "/admin/rate-cards", label: "تعرفهها" },
|
||||
{ href: "/admin/usage", label: "مصرف سراسری" },
|
||||
{ href: "/admin/organizations", label: "سازمانها" },
|
||||
{ href: "/admin/errors", label: "کنسول خطا" },
|
||||
] as const;
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import type { AdminOrganization, AdminUsageRow } from "@agy/contracts";
|
||||
import {
|
||||
Card,
|
||||
CardTitle,
|
||||
EmptyState,
|
||||
ErrorState,
|
||||
LoadingState,
|
||||
Stat,
|
||||
Table,
|
||||
type TableColumn,
|
||||
} from "@agy/ui";
|
||||
import { ApiError, api } from "@/lib/api";
|
||||
|
||||
/**
|
||||
* Platform-wide usage ledger.
|
||||
*
|
||||
* TODO §6.6 asks the console for a usage view. The endpoint and the API client
|
||||
* already existed and were consumed by the overview and organizations pages;
|
||||
* this is the dedicated view, with the per-organization rows the ledger
|
||||
* aggregates by tenant.
|
||||
*/
|
||||
export default function AdminUsagePage() {
|
||||
const [usage, setUsage] = React.useState<AdminUsageRow[]>([]);
|
||||
const [organizations, setOrganizations] = React.useState<AdminOrganization[]>([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const load = React.useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
// Both calls are needed: the ledger keys rows by organization id, and a
|
||||
// bare id is meaningless to whoever is reading this page.
|
||||
const [nextUsage, nextOrganizations] = await Promise.all([
|
||||
api.adminUsage(),
|
||||
api.adminOrganizations(),
|
||||
]);
|
||||
setUsage(nextUsage);
|
||||
setOrganizations(nextOrganizations);
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : "دریافت مصرف سراسری ناموفق بود.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
const orgName = React.useCallback(
|
||||
(id: string) => organizations.find((org) => org.id === id)?.name ?? id,
|
||||
[organizations],
|
||||
);
|
||||
|
||||
const totalEvents = usage.reduce((sum, row) => sum + row.quantity, 0);
|
||||
const totalBilled = usage.reduce((sum, row) => sum + row.total_price, 0);
|
||||
|
||||
const columns: Array<TableColumn<AdminUsageRow>> = [
|
||||
{
|
||||
key: "organization",
|
||||
header: "سازمان",
|
||||
render: (row) => <span className="text-slate-200">{orgName(row.organization_id)}</span>,
|
||||
},
|
||||
{
|
||||
key: "events",
|
||||
header: "رویداد مصرف",
|
||||
align: "end",
|
||||
render: (row) => row.quantity,
|
||||
},
|
||||
{
|
||||
key: "billed",
|
||||
header: "مبلغ",
|
||||
align: "end",
|
||||
render: (row) => (
|
||||
<span className="ltr-inline text-slate-200">{row.total_price.toFixed(2)}</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<header className="flex flex-col gap-1">
|
||||
<h1 className="text-lg font-medium text-slate-100">مصرف سراسری</h1>
|
||||
<p className="text-sm text-slate-400">
|
||||
مصرف همهٔ مستأجرها بر اساس دفتر ثبت استفاده. این صفحه برای مشتریان قابل دسترسی نیست.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<Stat label="تعداد سازمانها" value={organizations.length} />
|
||||
<Stat label="رویدادهای مصرف" value={totalEvents} />
|
||||
<Stat label="مجموع مبلغ" value={totalBilled.toFixed(2)} />
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardTitle>مصرف به تفکیک سازمان</CardTitle>
|
||||
<div className="mt-3">
|
||||
<Table
|
||||
columns={columns}
|
||||
rows={usage}
|
||||
rowKey={(row) => row.organization_id}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onRetry={load}
|
||||
caption="مصرف سازمانها"
|
||||
empty={
|
||||
<EmptyState
|
||||
title="مصرفی ثبت نشده است"
|
||||
description="هنوز هیچ رویداد مصرفی در سامانه ثبت نشده است."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user