247 أسطر
8.2 KiB
TypeScript
247 أسطر
8.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
|
import { NoPermissionState } from "@/components/auth/no-permission-state";
|
|
import { useSuperAdminSession } from "@/components/auth/session-context";
|
|
import { ChannelPieChart, InsightBarChart } from "@/components/dashboard/charts";
|
|
import { PageHeader } from "@/components/dashboard/page-header";
|
|
import { StatCard } from "@/components/dashboard/stat-card";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { EmptyState } from "@/components/ui/empty-state";
|
|
import { useToast } from "@/components/ui/toast";
|
|
import { getSuperAdminCharts, getSuperAdminOverview } from "@/lib/api/superadmin";
|
|
import { SUPERADMIN_PERMISSIONS, hasPermission } from "@/lib/permissions";
|
|
import type {
|
|
SuperAdminBreakdownItem,
|
|
SuperAdminChartsResponse,
|
|
SuperAdminChartPoint,
|
|
SuperAdminOverviewResponse,
|
|
} from "@/types/api";
|
|
import type { ChannelPoint, Insight, StatMetric } from "@/types";
|
|
|
|
type AnalyticsSnapshot = {
|
|
overview: SuperAdminOverviewResponse | null;
|
|
charts: SuperAdminChartsResponse | null;
|
|
};
|
|
|
|
function toInsight(points: SuperAdminChartPoint[]): Insight[] {
|
|
return points.map((point) => ({ label: point.label, value: point.count }));
|
|
}
|
|
|
|
function toInsightBreakdown(items: SuperAdminBreakdownItem[]): Insight[] {
|
|
return items.map((item) => ({ label: item.label, value: item.value }));
|
|
}
|
|
|
|
function toChannel(items: SuperAdminBreakdownItem[]): ChannelPoint[] {
|
|
return items.map((item) => ({ name: item.label, value: item.value }));
|
|
}
|
|
|
|
export default function AnalyticsPage() {
|
|
const { permissions } = useSuperAdminSession();
|
|
const [snapshot, setSnapshot] = useState<AnalyticsSnapshot>({
|
|
overview: null,
|
|
charts: null,
|
|
});
|
|
const [loading, setLoading] = useState(true);
|
|
const { toast } = useToast();
|
|
|
|
const canReadAnalytics = hasPermission(permissions, SUPERADMIN_PERMISSIONS.ANALYTICS_READ);
|
|
const canReadOverview = hasPermission(permissions, SUPERADMIN_PERMISSIONS.OVERVIEW_READ);
|
|
|
|
useEffect(() => {
|
|
let active = true;
|
|
|
|
const loadAnalytics = async () => {
|
|
if (!canReadAnalytics) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const [overview, charts] = await Promise.all([
|
|
canReadOverview ? getSuperAdminOverview() : Promise.resolve(null),
|
|
getSuperAdminCharts({ range: "30d" }),
|
|
]);
|
|
|
|
if (!active) return;
|
|
setSnapshot({ overview, charts });
|
|
} catch (error) {
|
|
if (!active) return;
|
|
toast({ title: "Failed to load analytics", description: String(error), variant: "danger" });
|
|
} finally {
|
|
if (active) setLoading(false);
|
|
}
|
|
};
|
|
|
|
void loadAnalytics();
|
|
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, [canReadAnalytics, canReadOverview, toast]);
|
|
|
|
const metrics = snapshot.overview?.metrics;
|
|
|
|
const dashboardMetrics: StatMetric[] = useMemo(
|
|
() => [
|
|
{
|
|
id: "users",
|
|
label: "Total users",
|
|
value: loading ? "..." : String(metrics?.usersCount ?? 0),
|
|
delta: `${metrics?.adminsCount ?? 0} admin accounts`,
|
|
trend: "up",
|
|
},
|
|
{
|
|
id: "posts",
|
|
label: "Published content",
|
|
value: loading ? "..." : String(metrics?.postsCount ?? 0),
|
|
delta: `${metrics?.commentsCount ?? 0} tracked comments`,
|
|
trend: "up",
|
|
},
|
|
{
|
|
id: "marketplace",
|
|
label: "Marketplace",
|
|
value: loading ? "..." : String(metrics?.marketplaceListingsCount ?? 0),
|
|
delta: `${metrics?.repairShopsCount ?? 0} repair shops`,
|
|
trend: "neutral",
|
|
},
|
|
{
|
|
id: "moderation",
|
|
label: "Moderation load",
|
|
value: loading
|
|
? "..."
|
|
: String((metrics?.flaggedPostsCount ?? 0) + (metrics?.flaggedCommentsCount ?? 0)),
|
|
delta: `${metrics?.hiddenPostsCount ?? 0} hidden posts and ${metrics?.hiddenCommentsCount ?? 0} hidden comments`,
|
|
trend:
|
|
(metrics?.flaggedPostsCount ?? 0) + (metrics?.flaggedCommentsCount ?? 0) > 0
|
|
? "down"
|
|
: "neutral",
|
|
},
|
|
],
|
|
[loading, metrics],
|
|
);
|
|
|
|
const charts = snapshot.charts;
|
|
const userSeries = toInsight(charts?.series.users ?? []);
|
|
const postSeries = toInsight(charts?.series.posts ?? []);
|
|
const listingSeries = toInsight(charts?.series.listings ?? []);
|
|
const roleBreakdown = toChannel(charts?.breakdowns.userRoles ?? []);
|
|
const listingBreakdown = toInsightBreakdown(charts?.breakdowns.listingCategories ?? []);
|
|
const moderationBreakdown = toInsightBreakdown(charts?.breakdowns.moderation ?? []);
|
|
|
|
if (!canReadAnalytics) {
|
|
return (
|
|
<div className="space-y-5 pb-8">
|
|
<PageHeader
|
|
title="Analytics"
|
|
subtitle="Charts and operational trends for users, content, moderation, and marketplace activity."
|
|
/>
|
|
<NoPermissionState description="This page needs the analytics.read permission." />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-5 pb-8">
|
|
<PageHeader
|
|
title="Analytics"
|
|
subtitle="Operational charts for growth, moderation, and marketplace activity."
|
|
/>
|
|
|
|
{canReadOverview ? (
|
|
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
{dashboardMetrics.map((metric) => (
|
|
<StatCard key={metric.id} metric={metric} />
|
|
))}
|
|
</section>
|
|
) : null}
|
|
|
|
<section className="grid gap-4 xl:grid-cols-12">
|
|
<Card className="xl:col-span-6">
|
|
<CardHeader>
|
|
<CardTitle>User signups - last 30 days</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{userSeries.length ? (
|
|
<InsightBarChart data={userSeries} />
|
|
) : (
|
|
<EmptyState title="No data" description="Not enough user activity to draw this chart." />
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="xl:col-span-6">
|
|
<CardHeader>
|
|
<CardTitle>Published content - last 30 days</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{postSeries.length ? (
|
|
<InsightBarChart data={postSeries} />
|
|
) : (
|
|
<EmptyState title="No data" description="Not enough content activity to draw this chart." />
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
|
|
<section className="grid gap-4 xl:grid-cols-12">
|
|
<Card className="xl:col-span-5">
|
|
<CardHeader>
|
|
<CardTitle>User role distribution</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{roleBreakdown.length ? (
|
|
<ChannelPieChart data={roleBreakdown} />
|
|
) : (
|
|
<EmptyState title="No role data" description="No role breakdown is currently available." />
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="xl:col-span-7">
|
|
<CardHeader>
|
|
<CardTitle>Marketplace categories</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{listingBreakdown.length ? (
|
|
<InsightBarChart data={listingBreakdown} />
|
|
) : (
|
|
<EmptyState title="No data" description="No category breakdown is available right now." />
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
|
|
<section className="grid gap-4 xl:grid-cols-12">
|
|
<Card className="xl:col-span-6">
|
|
<CardHeader>
|
|
<CardTitle>Marketplace creation - last 30 days</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{listingSeries.length ? (
|
|
<InsightBarChart data={listingSeries} />
|
|
) : (
|
|
<EmptyState title="No data" description="Not enough marketplace activity to draw this chart." />
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="xl:col-span-6">
|
|
<CardHeader>
|
|
<CardTitle>Current moderation states</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{moderationBreakdown.length ? (
|
|
<InsightBarChart data={moderationBreakdown} />
|
|
) : (
|
|
<EmptyState title="No data" description="No moderation breakdown is available right now." />
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|