Files
iddaai-fe/src/components/admin/admin-content.tsx
T
fahricansecer 2695cfffb4
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 2m27s
ch
2026-06-02 03:37:07 +03:00

570 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import {
Box,
Flex,
Heading,
Text,
SimpleGrid,
Card,
VStack,
HStack,
Badge,
Spinner,
Button,
Separator,
Input,
} from "@chakra-ui/react";
import {
NativeSelectRoot,
NativeSelectField,
} from "@/components/ui/forms/native-select";
import { useTranslations, useFormatter } from "next-intl";
import { useColorModeValue } from "@/components/ui/color-mode";
import {
SlideUp,
StaggerContainer,
StaggerItem,
AnimatedCounter,
} from "@/components/motion";
import { useAdminAnalytics, useAdminUsers } from "@/lib/api/admin/use-hooks";
import type { AdminUserDto, AnalyticsOverviewDto } from "@/lib/api/admin/types";
import { formatRoleLabel, isAdminRole } from "@/lib/auth/roles";
import {
LuUsers,
LuChartBar,
LuActivity,
LuShield,
LuPencil,
} from "react-icons/lu";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { EditUserModal } from "./edit-user-modal";
import LeagueTiersContent from "./league-tiers-content";
import ModelPerformanceContent from "./model-performance-content";
type AdminTab = "overview" | "users" | "league-tiers" | "model-performance";
// ========================
// Admin Stat Card
// ========================
interface AdminStatProps {
label: string;
value: number;
icon: React.ReactNode;
colorPalette: string;
}
function AdminStat({ label, value, icon, colorPalette }: AdminStatProps) {
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.100", "gray.700");
return (
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
<Card.Body>
<HStack gap={4}>
<Flex
boxSize="48px"
bg={`${colorPalette}.subtle`}
borderRadius="xl"
align="center"
justify="center"
color={`${colorPalette}.fg`}
fontSize="xl"
>
{icon}
</Flex>
<VStack gap={0} align="flex-start">
<Text fontSize="2xl" fontWeight="900" lineHeight="1">
<AnimatedCounter value={value} />
</Text>
<Text fontSize="xs" color="fg.muted" fontWeight="medium">
{label}
</Text>
</VStack>
</HStack>
</Card.Body>
</Card.Root>
);
}
// ========================
// Admin Content
// ========================
export default function AdminContent() {
const t = useTranslations("admin");
const tCommon = useTranslations("common");
const format = useFormatter();
const [activeTab, setActiveTab] = useState<AdminTab>("overview");
const [editingUser, setEditingUser] = useState<AdminUserDto | null>(null);
const [searchParams, setSearchParams] = useState({
search: "",
role: "",
subscriptionStatus: "",
page: 1,
limit: 10,
});
const [debouncedSearch, setDebouncedSearch] = useState("");
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearch(searchParams.search);
setSearchParams((prev) => ({ ...prev, page: 1 }));
}, 500);
return () => clearTimeout(handler);
}, [searchParams.search]);
const { data: session, status } = useSession();
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.100", "gray.700");
const canAccessAdmin = isAdminRole(session?.user?.roles);
const { data: analyticsData, isLoading: analyticsLoading } =
useAdminAnalytics(canAccessAdmin);
const { data: usersData, isLoading: usersLoading } = useAdminUsers(
{
search: debouncedSearch,
role: searchParams.role,
subscriptionStatus: searchParams.subscriptionStatus,
page: searchParams.page,
limit: searchParams.limit,
},
canAccessAdmin,
);
const analytics = analyticsData?.data as AnalyticsOverviewDto | undefined;
const users = usersData?.data?.items ?? [];
const meta = usersData?.data?.meta;
const tabs: { key: AdminTab; label: string }[] = [
{ key: "overview", label: t("overview") },
{ key: "users", label: t("user-management") },
{ key: "league-tiers", label: "Lig Tier" },
{ key: "model-performance", label: "Model Performansı" },
];
const getUserDisplayName = (user: AdminUserDto) => {
if (user.firstName && user.lastName)
return `${user.firstName} ${user.lastName}`;
if (user.firstName) return user.firstName;
return user.email.split("@")[0];
};
if (status === "loading") {
return (
<Flex justify="center" py={16}>
<Spinner size="lg" color="primary.500" />
</Flex>
);
}
if (!canAccessAdmin) {
return (
<SlideUp>
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
<Card.Body py={10}>
<VStack gap={3}>
<Badge colorPalette="red" variant="subtle" borderRadius="full">
<LuShield />
{t("restricted")}
</Badge>
<Heading as="h2" size="md">
{t("admin-access-required")}
</Heading>
<Text color="fg.muted" textAlign="center" maxW="md">
{t("admin-access-description")}
</Text>
</VStack>
</Card.Body>
</Card.Root>
</SlideUp>
);
}
return (
<SlideUp>
<Box>
<Flex justify="space-between" align="center" mb={6}>
<VStack gap={1} align="flex-start">
<Heading as="h1" size="xl" fontWeight="bold">
{t("title")}
</Heading>
<Text color="fg.muted" fontSize="sm">
{t("subtitle")}
</Text>
</VStack>
<Badge
colorPalette="red"
variant="solid"
px={3}
py={1}
borderRadius="full"
>
<LuShield />
{t("admin-badge")}
</Badge>
</Flex>
{/* Tabs */}
<HStack gap={2} mb={6}>
{tabs.map((tab) => (
<Button
key={tab.key}
variant={activeTab === tab.key ? "solid" : "outline"}
colorPalette={activeTab === tab.key ? "primary" : "gray"}
size="sm"
borderRadius="full"
onClick={() => setActiveTab(tab.key)}
>
{tab.label}
</Button>
))}
</HStack>
{/* Overview Tab */}
{activeTab === "overview" &&
(analyticsLoading ? (
<Flex justify="center" py={16}>
<Spinner size="lg" color="primary.500" />
</Flex>
) : (
<StaggerContainer>
<SimpleGrid columns={{ base: 2, md: 4 }} gap={4} mb={8}>
<StaggerItem>
<AdminStat
label={t("total-users")}
value={
analytics?.totalUsers ?? analytics?.users?.total ?? 0
}
icon={<LuUsers />}
colorPalette="primary"
/>
</StaggerItem>
<StaggerItem>
<AdminStat
label={t("total-predictions")}
value={
analytics?.totalPredictions ?? analytics?.predictions ?? 0
}
icon={<LuChartBar />}
colorPalette="green"
/>
</StaggerItem>
<StaggerItem>
<AdminStat
label={t("premium-users")}
value={analytics?.users?.premium ?? 0}
icon={<LuShield />}
colorPalette="purple"
/>
</StaggerItem>
<StaggerItem>
<AdminStat
label={t("active-users")}
value={
analytics?.activeUsers ?? analytics?.users?.active ?? 0
}
icon={<LuActivity />}
colorPalette="orange"
/>
</StaggerItem>
<StaggerItem>
<AdminStat
label={t("total-coupons")}
value={analytics?.totalCoupons ?? 0}
icon={<LuShield />}
colorPalette="purple"
/>
</StaggerItem>
</SimpleGrid>
</StaggerContainer>
))}
{/* Users Tab */}
{activeTab === "users" && (
<VStack gap={4} align="stretch">
{/* Filters */}
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
<Card.Body py={4}>
<SimpleGrid columns={{ base: 1, md: 3 }} gap={4}>
<Input
placeholder={t("search-users-placeholder")}
value={searchParams.search}
onChange={(e) =>
setSearchParams({
...searchParams,
search: e.target.value,
})
}
/>
<NativeSelectRoot>
<NativeSelectField
placeholder={t("all-roles")}
value={searchParams.role}
onChange={(e) =>
setSearchParams({
...searchParams,
role: e.target.value,
page: 1,
})
}
items={[
{ label: t("standard-user"), value: "user" },
{ label: t("superadmin"), value: "superadmin" },
]}
/>
</NativeSelectRoot>
<NativeSelectRoot>
<NativeSelectField
placeholder={t("all-plans")}
value={searchParams.subscriptionStatus}
onChange={(e) =>
setSearchParams({
...searchParams,
subscriptionStatus: e.target.value,
page: 1,
})
}
items={[
{ label: t("plan-free"), value: "free" },
{ label: "Plus", value: "plus" },
{ label: "Premium", value: "premium" },
{ label: t("plan-past-due"), value: "past_due" },
{ label: t("plan-cancelled"), value: "cancelled" },
]}
/>
</NativeSelectRoot>
</SimpleGrid>
</Card.Body>
</Card.Root>
{usersLoading ? (
<Flex justify="center" py={16}>
<Spinner size="lg" color="primary.500" />
</Flex>
) : users.length > 0 ? (
<Card.Root
bg={cardBg}
borderColor={borderColor}
borderRadius="xl"
>
<Card.Body>
<VStack gap={0} align="stretch">
{/* Table Header */}
<Flex
px={4}
py={2}
bg="bg.muted"
borderRadius="lg"
mb={2}
fontWeight="semibold"
fontSize="xs"
color="fg.muted"
>
<Text flex={2}>{t("user-name")}</Text>
<Text flex={2}>{t("user-email")}</Text>
<Text flex={1} textAlign="center">
{t("user-role")}
</Text>
<Text flex={1} textAlign="center">
{t("subscription")}
</Text>
<Text flex={1} textAlign="center">
{t("user-status")}
</Text>
<Text width="40px" textAlign="center"></Text>
</Flex>
{/* User Rows */}
{users.map((user: AdminUserDto, idx: number) => (
<Box key={user.id ?? idx}>
{idx > 0 && <Separator />}
<Flex
px={4}
py={3}
align="center"
_hover={{ bg: "bg.muted" }}
borderRadius="lg"
>
<Text
flex={2}
fontSize="sm"
fontWeight="medium"
truncate
>
{getUserDisplayName(user)}
</Text>
<Text
flex={2}
fontSize="sm"
color="fg.muted"
truncate
>
{user.email}
</Text>
<Flex flex={1} justify="center">
<Badge
colorPalette={
isAdminRole([user.role]) ? "red" : "gray"
}
variant="subtle"
fontSize="2xs"
borderRadius="full"
>
{formatRoleLabel(user.role)}
</Badge>
</Flex>
<Flex
flex={1}
justify="center"
direction="column"
align="center"
gap={1}
>
<Badge
colorPalette={
user.subscriptionStatus === "premium" ||
user.subscriptionStatus === "plus"
? "purple"
: "gray"
}
variant="subtle"
fontSize="2xs"
borderRadius="full"
textTransform="capitalize"
>
{user.subscriptionStatus || "free"}
</Badge>
{user.subscriptionExpiresAt ? (
<Text fontSize="2xs" color="fg.muted">
{format.dateTime(
new Date(user.subscriptionExpiresAt),
{
year: "numeric",
month: "2-digit",
day: "2-digit",
},
)}
</Text>
) : (
<Text fontSize="2xs" color="fg.muted">
-
</Text>
)}
</Flex>
<Flex flex={1} justify="center">
<Badge
colorPalette={
user.subscriptionStatus === "premium"
? "purple"
: user.subscriptionStatus === "plus"
? "blue"
: "gray"
}
variant="subtle"
fontSize="2xs"
borderRadius="full"
textTransform="capitalize"
>
{user.subscriptionStatus || "free"}
</Badge>
</Flex>
<Flex flex={1} justify="center">
<Badge
colorPalette={user.isActive ? "green" : "gray"}
variant="subtle"
fontSize="2xs"
borderRadius="full"
>
{user.isActive
? tCommon("active")
: tCommon("inactive")}
</Badge>
</Flex>
<Flex width="40px" justify="center">
<Button
size="sm"
variant="ghost"
onClick={() => setEditingUser(user)}
>
<LuPencil />
</Button>
</Flex>
</Flex>
</Box>
))}
{/* Pagination */}
{meta && meta.totalPages > 1 && (
<Flex
justify="center"
pt={4}
pb={2}
gap={2}
borderTopWidth="1px"
borderColor={borderColor}
mt={2}
>
<Button
size="sm"
variant="outline"
disabled={!meta.hasPreviousPage}
onClick={() =>
setSearchParams({
...searchParams,
page: meta.page - 1,
})
}
>
{tCommon("previous")}
</Button>
<Flex align="center" gap={2} fontSize="sm">
<Text>
{tCommon("page")} {meta.page} / {meta.totalPages}
</Text>
</Flex>
<Button
size="sm"
variant="outline"
disabled={!meta.hasNextPage}
onClick={() =>
setSearchParams({
...searchParams,
page: meta.page + 1,
})
}
>
{tCommon("next")}
</Button>
</Flex>
)}
</VStack>
</Card.Body>
</Card.Root>
) : (
<Flex justify="center" py={16}>
<Text color="fg.muted">{t("no-users")}</Text>
</Flex>
)}
</VStack>
)}
{/* League Tiers Tab */}
{activeTab === "league-tiers" && <LeagueTiersContent />}
{/* Model Performance Tab */}
{activeTab === "model-performance" && <ModelPerformanceContent />}
<EditUserModal
user={editingUser}
isOpen={!!editingUser}
onClose={() => setEditingUser(null)}
/>
</Box>
</SlideUp>
);
}