Add Oudelaa dashboard API integration
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
هذا الالتزام موجود في:
246
oudelaa_dashboard/app/(dashboard)/analytics/page.tsx
Normal file
246
oudelaa_dashboard/app/(dashboard)/analytics/page.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم