مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت. تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
148 lines
5.2 KiB
TypeScript
148 lines
5.2 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import type { AuditEvent, ReadinessResponse } from "@agy/contracts";
|
|
import {
|
|
Badge,
|
|
Card,
|
|
CardHint,
|
|
CardTitle,
|
|
EmptyState,
|
|
ErrorState,
|
|
LoadingState,
|
|
Stat,
|
|
Timeline,
|
|
UsageBar,
|
|
} from "@agy/ui";
|
|
import { ApiError, api } from "@/lib/api";
|
|
import { useSession } from "@/lib/session";
|
|
|
|
export default function DashboardPage() {
|
|
const { activeOrganization, user } = useSession();
|
|
const [readiness, setReadiness] = React.useState<ReadinessResponse | null>(null);
|
|
const [audit, setAudit] = React.useState<AuditEvent[]>([]);
|
|
const [loading, setLoading] = React.useState(true);
|
|
const [error, setError] = React.useState<string | null>(null);
|
|
|
|
const load = React.useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const health = await api.readiness();
|
|
setReadiness(health);
|
|
if (activeOrganization) {
|
|
setAudit(await api.listAudit(activeOrganization.id).catch(() => []));
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof ApiError ? err.message : "دریافت وضعیت ناموفق بود.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [activeOrganization]);
|
|
|
|
React.useEffect(() => {
|
|
void load();
|
|
}, [load]);
|
|
|
|
if (loading) return <LoadingState label="در حال دریافت وضعیت سازمان…" />;
|
|
if (error) return <ErrorState message={error} onRetry={load} />;
|
|
|
|
if (!activeOrganization) {
|
|
return (
|
|
<EmptyState
|
|
title="هنوز سازمانی انتخاب نشده است"
|
|
description="برای دیدن داشبورد، ابتدا یک سازمان بسازید یا از منوی بالا یکی را انتخاب کنید."
|
|
action={
|
|
<a
|
|
href="/onboarding"
|
|
className="rounded-lg bg-accent px-4 py-2 text-sm text-white hover:bg-accent-strong"
|
|
>
|
|
ساخت سازمان
|
|
</a>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const adapters = readiness?.checks.adapters ?? {};
|
|
const databaseOk = readiness?.checks.database === "ok";
|
|
|
|
return (
|
|
<div className="flex flex-col gap-5">
|
|
<header className="flex flex-col gap-1">
|
|
<h1 className="text-lg font-medium text-slate-100">
|
|
{activeOrganization.name}
|
|
</h1>
|
|
<p className="text-sm text-slate-400">
|
|
خوش آمدید{user?.name ? `، ${user.name}` : ""}. وضعیت کلی سازمان در یک نگاه.
|
|
</p>
|
|
</header>
|
|
|
|
<section className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
|
<Stat label="اعتبار" value="—" hint="پس از فاز billing" />
|
|
<Stat label="وظایف فعال" value="0" hint="پس از فاز Task engine" />
|
|
<Stat label="کارمندهای فعال" value="0" hint="پس از فاز Workforce" />
|
|
<Stat
|
|
label="وضعیت سرویس"
|
|
value={databaseOk ? "سالم" : "ناپایدار"}
|
|
tone={databaseOk ? "neutral" : "danger"}
|
|
hint={`محیط: ${readiness?.checks.adapters ? "dev" : "—"}`}
|
|
/>
|
|
</section>
|
|
|
|
<section className="grid gap-4 lg:grid-cols-2">
|
|
<Card>
|
|
<CardTitle>مصرف این ماه</CardTitle>
|
|
<CardHint>سقف مصرف بر اساس پلن سازمان محاسبه میشود.</CardHint>
|
|
<div className="mt-4">
|
|
<UsageBar used={0} total={100} label="مصرف از سقف" />
|
|
</div>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardTitle>وضعیت اتصالها</CardTitle>
|
|
<CardHint>
|
|
مدل و provider از دید شما پنهان است؛ اینها فقط وضعیت فنی سرویساند.
|
|
</CardHint>
|
|
<div className="mt-3 flex flex-wrap gap-2">
|
|
<Badge tone={databaseOk ? "success" : "danger"}>
|
|
پایگاه داده: {databaseOk ? "متصل" : "خطا"}
|
|
</Badge>
|
|
{Object.entries(adapters).map(([key, value]) => (
|
|
<Badge key={key} tone={value === "mock" ? "warning" : "info"}>
|
|
{key}: {value}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
</section>
|
|
|
|
<section className="grid gap-4 lg:grid-cols-2">
|
|
<Card>
|
|
<CardTitle>رویدادهای اخیر سازمان</CardTitle>
|
|
<CardHint>هر عملیات حساس در audit log ثبت میشود.</CardHint>
|
|
<div className="mt-4">
|
|
{audit.length === 0 ? (
|
|
<p className="text-xs text-slate-500">رویدادی ثبت نشده است.</p>
|
|
) : (
|
|
<Timeline
|
|
items={audit.slice(0, 8).map((event) => ({
|
|
id: event.id,
|
|
title: event.action,
|
|
meta: new Date(event.created_at).toLocaleString("fa-IR"),
|
|
}))}
|
|
/>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardTitle>دامنههای پیشنمایش</CardTitle>
|
|
<CardHint>پس از فاز Domain Routing، دامنههای تست پروژهها اینجا دیده میشوند.</CardHint>
|
|
<p className="mt-4 text-xs text-slate-500">دامنهای ثبت نشده است.</p>
|
|
</Card>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|