Add Oudelaa dashboard API integration
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled

هذا الالتزام موجود في:
boutmoun123
2026-05-25 20:36:52 +03:00
الأصل 367fce6557
التزام 8863f61d00
90 ملفات معدلة مع 16694 إضافات و1 حذوفات

عرض الملف

@@ -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>
</>
);
}