Add Oudelaa dashboard API integration
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
هذا الالتزام موجود في:
463
oudelaa_dashboard/app/(dashboard)/dashboard/page.tsx
Normal file
463
oudelaa_dashboard/app/(dashboard)/dashboard/page.tsx
Normal file
@@ -0,0 +1,463 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Boxes, RefreshCcw, ShieldAlert, Store, Users2 } from "lucide-react";
|
||||
|
||||
import { NoPermissionState } from "@/components/auth/no-permission-state";
|
||||
import { PostPreviewCard } from "@/components/dashboard/post-preview-card";
|
||||
import { useSuperAdminSession } from "@/components/auth/session-context";
|
||||
import { PageHeader } from "@/components/dashboard/page-header";
|
||||
import { StatCard } from "@/components/dashboard/stat-card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
import { searchAdminUsers } from "@/lib/api/admin-users";
|
||||
import { getItems } from "@/lib/api/core";
|
||||
import { listModerationListings } from "@/lib/api/marketplace";
|
||||
import { listModerationPosts } from "@/lib/api/posts";
|
||||
import { getSuperAdminOverview, getSuperAdminRecentActivity } from "@/lib/api/superadmin";
|
||||
import { refreshSuperAdmin } from "@/lib/auth/client";
|
||||
import { formatCurrency, formatDateTime } from "@/lib/format";
|
||||
import { SUPERADMIN_PERMISSIONS, hasPermission } from "@/lib/permissions";
|
||||
import type {
|
||||
ApiPost,
|
||||
ApiUser,
|
||||
MarketplaceListing,
|
||||
MarketplaceResponse,
|
||||
PostsResponse,
|
||||
SuperAdminOverviewResponse,
|
||||
SuperAdminRecentActivityItem,
|
||||
SuperAdminRecentActivityResponse,
|
||||
UsersResponse,
|
||||
} from "@/types/api";
|
||||
import type { StatMetric } from "@/types";
|
||||
|
||||
type DashboardSnapshot = {
|
||||
overview: SuperAdminOverviewResponse | null;
|
||||
users: ApiUser[];
|
||||
latestPosts: ApiPost[];
|
||||
listings: MarketplaceListing[];
|
||||
recentActivity: SuperAdminRecentActivityItem[];
|
||||
};
|
||||
|
||||
const EMPTY_USERS: UsersResponse = { items: [], data: [] };
|
||||
const EMPTY_LISTINGS: MarketplaceResponse = { items: [], data: [] };
|
||||
const EMPTY_POSTS: PostsResponse = { items: [], data: [] };
|
||||
const EMPTY_ACTIVITY: SuperAdminRecentActivityResponse = { items: [] };
|
||||
|
||||
function ShortcutLink({
|
||||
href,
|
||||
icon,
|
||||
label,
|
||||
}: {
|
||||
href: string;
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="inline-flex w-full items-center justify-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold text-foreground transition hover:bg-secondary/60"
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { permissions } = useSuperAdminSession();
|
||||
const [snapshot, setSnapshot] = useState<DashboardSnapshot>({
|
||||
overview: null,
|
||||
users: [],
|
||||
latestPosts: [],
|
||||
listings: [],
|
||||
recentActivity: [],
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { toast } = useToast();
|
||||
|
||||
const canReadOverview = hasPermission(permissions, SUPERADMIN_PERMISSIONS.OVERVIEW_READ);
|
||||
const canReadAnalytics = hasPermission(permissions, SUPERADMIN_PERMISSIONS.ANALYTICS_READ);
|
||||
const canReadUsers = hasPermission(permissions, SUPERADMIN_PERMISSIONS.USERS_READ);
|
||||
const canModerateContent = hasPermission(
|
||||
permissions,
|
||||
SUPERADMIN_PERMISSIONS.CONTENT_MODERATE,
|
||||
);
|
||||
const canManageMarketplace = hasPermission(
|
||||
permissions,
|
||||
SUPERADMIN_PERMISSIONS.MARKETPLACE_MANAGE,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
const loadDashboard = async () => {
|
||||
if (!canReadOverview) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const [overview, recentActivity, usersResponse, postsResponse, listingsResponse] = await Promise.all([
|
||||
getSuperAdminOverview(),
|
||||
canReadAnalytics
|
||||
? getSuperAdminRecentActivity({ limit: 8 })
|
||||
: Promise.resolve(EMPTY_ACTIVITY),
|
||||
canReadUsers
|
||||
? searchAdminUsers({ page: 1, limit: 8, sortBy: "createdAt", sortOrder: "desc" })
|
||||
: Promise.resolve(EMPTY_USERS),
|
||||
canModerateContent
|
||||
? listModerationPosts({ page: 1, limit: 6, sortBy: "createdAt", sortOrder: "desc" })
|
||||
: Promise.resolve(EMPTY_POSTS),
|
||||
canManageMarketplace
|
||||
? listModerationListings({ page: 1, limit: 6, sortBy: "createdAt", sortOrder: "desc" })
|
||||
: Promise.resolve(EMPTY_LISTINGS),
|
||||
]);
|
||||
|
||||
if (!active) return;
|
||||
|
||||
setSnapshot({
|
||||
overview,
|
||||
recentActivity: recentActivity.items ?? [],
|
||||
users: getItems(usersResponse),
|
||||
latestPosts: getItems(postsResponse) as ApiPost[],
|
||||
listings: getItems(listingsResponse),
|
||||
});
|
||||
} catch (error) {
|
||||
if (!active) return;
|
||||
toast({
|
||||
title: "Failed to load dashboard",
|
||||
description: String(error),
|
||||
variant: "danger",
|
||||
});
|
||||
} finally {
|
||||
if (active) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
void loadDashboard();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [canManageMarketplace, canModerateContent, canReadAnalytics, canReadOverview, canReadUsers, toast]);
|
||||
|
||||
const metrics = snapshot.overview?.metrics;
|
||||
|
||||
const listingValue = useMemo(
|
||||
() => snapshot.listings.reduce((sum, item) => sum + (item.price ?? 0), 0),
|
||||
[snapshot.listings],
|
||||
);
|
||||
|
||||
const dashboardMetrics: StatMetric[] = [
|
||||
{
|
||||
id: "users",
|
||||
label: "Users",
|
||||
value: loading ? "..." : String(metrics?.usersCount ?? 0),
|
||||
delta: "Total user accounts on the platform",
|
||||
trend: "up",
|
||||
},
|
||||
{
|
||||
id: "admins",
|
||||
label: "Admins",
|
||||
value: loading ? "..." : String(metrics?.adminsCount ?? 0),
|
||||
delta: "Administrative accounts under platform control",
|
||||
trend: "up",
|
||||
},
|
||||
{
|
||||
id: "listings",
|
||||
label: "Marketplace",
|
||||
value: loading ? "..." : String(metrics?.marketplaceListingsCount ?? 0),
|
||||
delta: `Value of loaded listings ${formatCurrency(listingValue)}`,
|
||||
trend: "neutral",
|
||||
},
|
||||
{
|
||||
id: "alerts",
|
||||
label: "Unread alerts",
|
||||
value: loading ? "..." : String(metrics?.unreadNotificationsCount ?? 0),
|
||||
delta: `${metrics?.flaggedPostsCount ?? 0} flagged posts and ${metrics?.flaggedCommentsCount ?? 0} flagged comments`,
|
||||
trend: (metrics?.unreadNotificationsCount ?? 0) > 0 ? "down" : "neutral",
|
||||
},
|
||||
];
|
||||
|
||||
const shortcuts = [
|
||||
canReadUsers
|
||||
? {
|
||||
key: "users",
|
||||
href: "/users",
|
||||
icon: <Users2 className="h-4 w-4" />,
|
||||
label: "Manage users",
|
||||
}
|
||||
: null,
|
||||
canManageMarketplace
|
||||
? {
|
||||
key: "marketplace",
|
||||
href: "/marketplace",
|
||||
icon: <Store className="h-4 w-4" />,
|
||||
label: "Review marketplace",
|
||||
}
|
||||
: null,
|
||||
hasPermission(permissions, SUPERADMIN_PERMISSIONS.CONTENT_MODERATE)
|
||||
? {
|
||||
key: "content",
|
||||
href: "/content",
|
||||
icon: <Boxes className="h-4 w-4" />,
|
||||
label: "Moderate content",
|
||||
}
|
||||
: null,
|
||||
hasPermission(permissions, SUPERADMIN_PERMISSIONS.SESSIONS_MANAGE) ||
|
||||
hasPermission(permissions, SUPERADMIN_PERMISSIONS.AUDIT_READ) ||
|
||||
hasPermission(permissions, SUPERADMIN_PERMISSIONS.OPS_READ)
|
||||
? {
|
||||
key: "security",
|
||||
href: "/security",
|
||||
icon: <ShieldAlert className="h-4 w-4" />,
|
||||
label: "Security and sessions",
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean) as Array<{ key: string; href: string; icon: React.ReactNode; label: string }>;
|
||||
|
||||
if (!canReadOverview) {
|
||||
return (
|
||||
<div className="space-y-5 pb-8">
|
||||
<PageHeader
|
||||
title="SuperAdmin dashboard"
|
||||
subtitle="A high-level summary of the platform, moderation activity, and operator status."
|
||||
/>
|
||||
<NoPermissionState description="This page needs the overview.read permission." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5 pb-8">
|
||||
<PageHeader
|
||||
title="SuperAdmin dashboard"
|
||||
subtitle="A high-level summary of platform health, moderation, and operational activity."
|
||||
actions={
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await refreshSuperAdmin();
|
||||
toast({
|
||||
title: "Session refreshed",
|
||||
description: "SuperAdmin cookies were refreshed successfully.",
|
||||
variant: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Refresh failed",
|
||||
description: String(error),
|
||||
variant: "danger",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
Refresh session
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
{dashboardMetrics.map((metric) => (
|
||||
<StatCard key={metric.id} metric={metric} />
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 xl:grid-cols-12">
|
||||
{canReadUsers ? (
|
||||
<Card className="xl:col-span-7">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Latest users</CardTitle>
|
||||
<Link href="/users" className="text-sm text-primary">
|
||||
View all
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!snapshot.users.length ? (
|
||||
<EmptyState
|
||||
title="No user data"
|
||||
description="Recent users will appear here when the backend returns them."
|
||||
/>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{snapshot.users.slice(0, 6).map((user) => (
|
||||
<TableRow key={user._id}>
|
||||
<TableCell>{user.name ?? user.username ?? "-"}</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>{user.role ?? "user"}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={user.isDisabled ? "danger" : "success"}>
|
||||
{user.isDisabled ? "Disabled" : "Active"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<Card className={canReadUsers ? "xl:col-span-5" : "xl:col-span-12"}>
|
||||
<CardHeader>
|
||||
<CardTitle>Quick actions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{shortcuts.length ? (
|
||||
shortcuts.map((item) => (
|
||||
<ShortcutLink key={item.key} href={item.href} icon={item.icon} label={item.label} />
|
||||
))
|
||||
) : (
|
||||
<EmptyState
|
||||
title="No shortcuts available"
|
||||
description="The current session does not expose any additional dashboard areas."
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 xl:grid-cols-12">
|
||||
{canModerateContent ? (
|
||||
<Card className={(canManageMarketplace || canReadAnalytics) ? "xl:col-span-6" : "xl:col-span-12"}>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Latest posts</CardTitle>
|
||||
<Link href="/content" className="text-sm text-primary">
|
||||
Open moderation
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!snapshot.latestPosts.length ? (
|
||||
<EmptyState
|
||||
title="No posts available"
|
||||
description="Recent posts will appear here when the backend returns them."
|
||||
/>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{snapshot.latestPosts.slice(0, 4).map((post) => (
|
||||
<PostPreviewCard key={post._id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{canManageMarketplace ? (
|
||||
<Card
|
||||
className={
|
||||
canModerateContent
|
||||
? "xl:col-span-6"
|
||||
: canReadAnalytics
|
||||
? "xl:col-span-6"
|
||||
: "xl:col-span-12"
|
||||
}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Latest marketplace listings</CardTitle>
|
||||
<Link href="/marketplace" className="text-sm text-primary">
|
||||
Manage marketplace
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!snapshot.listings.length ? (
|
||||
<EmptyState
|
||||
title="No listings available"
|
||||
description="No marketplace listings matched the current backend response."
|
||||
/>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Store</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{snapshot.listings.slice(0, 5).map((listing) => (
|
||||
<TableRow key={listing._id}>
|
||||
<TableCell>{listing.title}</TableCell>
|
||||
<TableCell>{listing.listingCategory ?? "-"}</TableCell>
|
||||
<TableCell>{listing.storeName ?? "-"}</TableCell>
|
||||
<TableCell>{formatCurrency(listing.price, listing.currency ?? "SAR")}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{canReadAnalytics ? (
|
||||
<Card
|
||||
className={
|
||||
canManageMarketplace || canModerateContent ? "xl:col-span-6" : "xl:col-span-12"
|
||||
}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Recent activity</CardTitle>
|
||||
<Link href="/analytics" className="text-sm text-primary">
|
||||
Activity feeds
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!snapshot.recentActivity.length ? (
|
||||
<EmptyState
|
||||
title="No recent activity"
|
||||
description="Operational and moderation activity will appear here."
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{snapshot.recentActivity.map((item, index) => (
|
||||
<div
|
||||
key={`${item.type}-${index}`}
|
||||
className="rounded-xl border border-border/70 bg-secondary/20 p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-medium text-foreground">{item.title}</div>
|
||||
<Badge
|
||||
variant={
|
||||
item.status === "flagged" || item.status === "disabled"
|
||||
? "warning"
|
||||
: "muted"
|
||||
}
|
||||
>
|
||||
{item.type}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{item.subtitle} • {formatDateTime(item.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم