Add Oudelaa dashboard API integration
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
هذا الالتزام موجود في:
276
oudelaa_dashboard/components/dashboard/topbar.tsx
Normal file
276
oudelaa_dashboard/components/dashboard/topbar.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Bell, MoonStar, RefreshCcw, ShieldCheck, SunMedium } from "lucide-react";
|
||||
|
||||
import { useSuperAdminSession } from "@/components/auth/session-context";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Drawer } from "@/components/ui/drawer";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
import { useTheme } from "@/components/theme/theme-provider";
|
||||
import { listSuperAdminSessions } from "@/lib/api/auth";
|
||||
import { listPlatformNotifications } from "@/lib/api/notifications";
|
||||
import { getSuperAdminOverview, getSuperAdminRecentActivity } from "@/lib/api/superadmin";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { SUPERADMIN_PERMISSIONS, hasPermission } from "@/lib/permissions";
|
||||
import type {
|
||||
NotificationItem,
|
||||
NotificationsResponse,
|
||||
SessionItem,
|
||||
SessionsResponse,
|
||||
SuperAdminOverviewResponse,
|
||||
SuperAdminRecentActivityItem,
|
||||
SuperAdminRecentActivityResponse,
|
||||
} from "@/types/api";
|
||||
|
||||
const EMPTY_NOTIFICATIONS: NotificationsResponse = { items: [], data: [], unreadCount: 0 };
|
||||
const EMPTY_SESSIONS: SessionsResponse = { items: [] };
|
||||
const EMPTY_ACTIVITY: SuperAdminRecentActivityResponse = { items: [] };
|
||||
|
||||
export function DashboardTopbar() {
|
||||
const { toast } = useToast();
|
||||
const { theme, toggle } = useTheme();
|
||||
const { permissions } = useSuperAdminSession();
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [overview, setOverview] = useState<SuperAdminOverviewResponse | null>(null);
|
||||
const [recentActivity, setRecentActivity] = useState<SuperAdminRecentActivityItem[]>([]);
|
||||
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
|
||||
const [sessions, setSessions] = useState<SessionItem[]>([]);
|
||||
|
||||
const canReadOverview = hasPermission(permissions, SUPERADMIN_PERMISSIONS.OVERVIEW_READ);
|
||||
const canReadAnalytics = hasPermission(permissions, SUPERADMIN_PERMISSIONS.ANALYTICS_READ);
|
||||
const canReadNotifications = hasPermission(permissions, SUPERADMIN_PERMISSIONS.NOTIFICATIONS_READ);
|
||||
const canManageSessions = hasPermission(permissions, SUPERADMIN_PERMISSIONS.SESSIONS_MANAGE);
|
||||
|
||||
const todayLabel = useMemo(() => {
|
||||
try {
|
||||
return new Intl.DateTimeFormat("ar-SA", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}).format(new Date());
|
||||
} catch {
|
||||
return "Today";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const unreadCount =
|
||||
overview?.metrics.unreadNotificationsCount ??
|
||||
notifications.filter((item) => item.read === false).length;
|
||||
const moderationAttentionCount =
|
||||
(overview?.metrics.flaggedPostsCount ?? 0) + (overview?.metrics.flaggedCommentsCount ?? 0);
|
||||
|
||||
const summaryCards = [
|
||||
canManageSessions
|
||||
? {
|
||||
key: "sessions",
|
||||
label: "SuperAdmin sessions",
|
||||
value: String(sessions.length),
|
||||
}
|
||||
: null,
|
||||
canReadNotifications
|
||||
? {
|
||||
key: "notifications",
|
||||
label: "Unread notifications",
|
||||
value: String(unreadCount),
|
||||
}
|
||||
: null,
|
||||
canReadOverview
|
||||
? {
|
||||
key: "moderation",
|
||||
label: "Needs review",
|
||||
value: String(moderationAttentionCount),
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean) as Array<{ key: string; label: string; value: string }>;
|
||||
const summaryGridClass =
|
||||
summaryCards.length >= 3
|
||||
? "grid gap-3 sm:grid-cols-3"
|
||||
: summaryCards.length === 2
|
||||
? "grid gap-3 sm:grid-cols-2"
|
||||
: "grid gap-3";
|
||||
|
||||
const loadOverview = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [overviewResponse, activityResponse, notificationsResponse, sessionsResponse] =
|
||||
await Promise.all([
|
||||
canReadOverview ? getSuperAdminOverview() : Promise.resolve(null),
|
||||
canReadAnalytics
|
||||
? getSuperAdminRecentActivity({ limit: 5 })
|
||||
: Promise.resolve(EMPTY_ACTIVITY),
|
||||
canReadNotifications
|
||||
? listPlatformNotifications({ limit: 5, sortOrder: "desc" })
|
||||
: Promise.resolve(EMPTY_NOTIFICATIONS),
|
||||
canManageSessions ? listSuperAdminSessions() : Promise.resolve(EMPTY_SESSIONS),
|
||||
]);
|
||||
setOverview(overviewResponse);
|
||||
setRecentActivity(activityResponse.items ?? []);
|
||||
setNotifications(notificationsResponse.items ?? notificationsResponse.data ?? []);
|
||||
setSessions(sessionsResponse.items ?? []);
|
||||
} catch (error) {
|
||||
toast({ title: "Failed to load command center", description: String(error), variant: "danger" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canManageSessions, canReadAnalytics, canReadNotifications, canReadOverview, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadOverview();
|
||||
}, [loadOverview]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-enter mb-5 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-border/70 bg-card/80 p-3 backdrop-blur">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="warning">SuperAdmin</Badge>
|
||||
<p className="text-sm text-muted-foreground">{todayLabel}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={() => void loadOverview()} disabled={loading}>
|
||||
<RefreshCcw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
toggle();
|
||||
toast({
|
||||
title: "Theme updated",
|
||||
description: theme === "dark" ? "Switched to light mode." : "Switched to dark mode.",
|
||||
});
|
||||
}}
|
||||
>
|
||||
{theme === "dark" ? <SunMedium className="h-4 w-4" /> : <MoonStar className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => setDrawerOpen(true)}>
|
||||
<Bell className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
title="Operations Center"
|
||||
description="A quick view of the latest notifications, activity, and active sessions."
|
||||
side="right"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{summaryCards.length ? (
|
||||
<div className={summaryGridClass}>
|
||||
{summaryCards.map((card) => (
|
||||
<div key={card.key} className="rounded-xl border border-border bg-secondary/40 p-4">
|
||||
<div className="text-xs text-muted-foreground">{card.label}</div>
|
||||
<div className="mt-2 text-2xl font-bold text-foreground">{card.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{canReadNotifications ? (
|
||||
<div className="rounded-xl border border-border bg-secondary/40 p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<p className="text-sm font-semibold text-foreground">Latest notifications</p>
|
||||
<Link href="/notifications" className="text-xs font-semibold text-primary">
|
||||
Open page
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{notifications.length ? (
|
||||
notifications.map((item) => (
|
||||
<div key={item._id} className="rounded-lg border border-border/60 bg-background/40 p-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-sm font-medium text-foreground">{item.title ?? item.type}</p>
|
||||
<Badge variant={item.read ? "muted" : "warning"}>
|
||||
{item.read ? "Read" : "New"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{item.previewText ?? item.deepLink ?? "-"}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No recent notifications.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{canReadAnalytics ? (
|
||||
<div className="rounded-xl border border-border bg-secondary/40 p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<p className="text-sm font-semibold text-foreground">Recent activity</p>
|
||||
<Link href="/dashboard" className="text-xs font-semibold text-primary">
|
||||
Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{recentActivity.length ? (
|
||||
recentActivity.map((item, index) => (
|
||||
<div
|
||||
key={`${item.type}-${item.createdAt ?? index}`}
|
||||
className="rounded-lg border border-border/60 bg-background/40 p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-sm font-medium text-foreground">{item.title}</p>
|
||||
<Badge
|
||||
variant={
|
||||
item.status === "flagged"
|
||||
? "warning"
|
||||
: item.status === "hidden"
|
||||
? "danger"
|
||||
: "muted"
|
||||
}
|
||||
>
|
||||
{item.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{item.subtitle}</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">{formatDateTime(item.createdAt)}</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No recent activity.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{canManageSessions ? (
|
||||
<div className="rounded-xl border border-border bg-secondary/40 p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<ShieldCheck className="h-4 w-4 text-primary" />
|
||||
<p className="text-sm font-semibold text-foreground">Active sessions</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{sessions.length ? (
|
||||
sessions.map((item) => (
|
||||
<div
|
||||
key={item.id ?? item.jti}
|
||||
className="rounded-lg border border-border/60 bg-background/40 p-3"
|
||||
>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{item.id ?? item.jti ?? "session"}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{formatDateTime(item.createdAt)} - {formatDateTime(item.expiresAt)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No active sessions to display.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم