الملفات
back_end_oudelaa/oudelaa_dashboard/app/(dashboard)/dashboard/page.tsx
boutmoun123 8863f61d00
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
Add Oudelaa dashboard API integration
2026-05-25 20:36:52 +03:00

464 أسطر
16 KiB
TypeScript

"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>
);
}