This commit is contained in:
@@ -40,8 +40,9 @@ import {
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { EditUserModal } from "./edit-user-modal";
|
import { EditUserModal } from "./edit-user-modal";
|
||||||
|
import LeagueTiersContent from "./league-tiers-content";
|
||||||
|
|
||||||
type AdminTab = "overview" | "users";
|
type AdminTab = "overview" | "users" | "league-tiers";
|
||||||
|
|
||||||
// ========================
|
// ========================
|
||||||
// Admin Stat Card
|
// Admin Stat Card
|
||||||
@@ -140,6 +141,7 @@ export default function AdminContent() {
|
|||||||
const tabs: { key: AdminTab; label: string }[] = [
|
const tabs: { key: AdminTab; label: string }[] = [
|
||||||
{ key: "overview", label: t("overview") },
|
{ key: "overview", label: t("overview") },
|
||||||
{ key: "users", label: t("user-management") },
|
{ key: "users", label: t("user-management") },
|
||||||
|
{ key: "league-tiers", label: "Lig Tier" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const getUserDisplayName = (user: AdminUserDto) => {
|
const getUserDisplayName = (user: AdminUserDto) => {
|
||||||
@@ -548,6 +550,9 @@ export default function AdminContent() {
|
|||||||
</VStack>
|
</VStack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* League Tiers Tab */}
|
||||||
|
{activeTab === "league-tiers" && <LeagueTiersContent />}
|
||||||
|
|
||||||
<EditUserModal
|
<EditUserModal
|
||||||
user={editingUser}
|
user={editingUser}
|
||||||
isOpen={!!editingUser}
|
isOpen={!!editingUser}
|
||||||
|
|||||||
@@ -0,0 +1,535 @@
|
|||||||
|
"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 { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
|
import { AnimatedCounter } from "@/components/motion";
|
||||||
|
import {
|
||||||
|
useLeagueTiers,
|
||||||
|
useLeagueTierStats,
|
||||||
|
useAddLeagueTier,
|
||||||
|
useUpdateLeagueTier,
|
||||||
|
useDeactivateLeagueTier,
|
||||||
|
useDeleteLeagueTier,
|
||||||
|
useSyncLeagueTiers,
|
||||||
|
useTriggerRetrain,
|
||||||
|
} from "@/lib/api/admin/use-hooks";
|
||||||
|
import { useLeagues } from "@/lib/api/leagues/use-hooks";
|
||||||
|
import type { LeagueTierDto } from "@/lib/api/admin/types";
|
||||||
|
import {
|
||||||
|
LuDiamond,
|
||||||
|
LuMedal,
|
||||||
|
LuShield,
|
||||||
|
LuPlus,
|
||||||
|
LuTrash2,
|
||||||
|
LuRefreshCw,
|
||||||
|
LuBrain,
|
||||||
|
LuSearch,
|
||||||
|
LuX,
|
||||||
|
LuChevronDown,
|
||||||
|
LuChevronUp,
|
||||||
|
} from "react-icons/lu";
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
|
||||||
|
const TIER_CONFIG: Record<
|
||||||
|
number,
|
||||||
|
{ label: string; color: string; icon: React.ReactNode }
|
||||||
|
> = {
|
||||||
|
1: { label: "Elmas", color: "cyan", icon: <LuDiamond /> },
|
||||||
|
2: { label: "Altin", color: "yellow", icon: <LuMedal /> },
|
||||||
|
3: { label: "Gumus", color: "gray", icon: <LuShield /> },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LeagueTiersContent() {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [filterTier, setFilterTier] = useState<string>("");
|
||||||
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
|
const [addLeagueId, setAddLeagueId] = useState("");
|
||||||
|
const [addTier, setAddTier] = useState("2");
|
||||||
|
const [addNotes, setAddNotes] = useState("");
|
||||||
|
const [expandedTier, setExpandedTier] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
|
const borderColor = useColorModeValue("gray.100", "gray.700");
|
||||||
|
|
||||||
|
const { data: tiersData, isLoading: tiersLoading } = useLeagueTiers();
|
||||||
|
const { data: statsData, isLoading: statsLoading } = useLeagueTierStats();
|
||||||
|
const { data: leaguesData } = useLeagues();
|
||||||
|
|
||||||
|
const addMutation = useAddLeagueTier();
|
||||||
|
const updateMutation = useUpdateLeagueTier();
|
||||||
|
const deactivateMutation = useDeactivateLeagueTier();
|
||||||
|
const deleteMutation = useDeleteLeagueTier();
|
||||||
|
const syncMutation = useSyncLeagueTiers();
|
||||||
|
const retrainMutation = useTriggerRetrain();
|
||||||
|
|
||||||
|
const tiers = (tiersData?.data as LeagueTierDto[] | undefined) ?? [];
|
||||||
|
const stats = statsData?.data;
|
||||||
|
const leagues = leaguesData?.data ?? [];
|
||||||
|
|
||||||
|
// Group tiers by tier level
|
||||||
|
const tierGroups = useMemo(() => {
|
||||||
|
const groups: Record<number, LeagueTierDto[]> = { 1: [], 2: [], 3: [] };
|
||||||
|
tiers.forEach((t) => {
|
||||||
|
if (!groups[t.tier]) groups[t.tier] = [];
|
||||||
|
groups[t.tier].push(t);
|
||||||
|
});
|
||||||
|
return groups;
|
||||||
|
}, [tiers]);
|
||||||
|
|
||||||
|
// Filter tiers
|
||||||
|
const filteredTiers = useMemo(() => {
|
||||||
|
let result = tiers;
|
||||||
|
if (filterTier) {
|
||||||
|
result = result.filter((t) => t.tier === parseInt(filterTier));
|
||||||
|
}
|
||||||
|
if (searchQuery) {
|
||||||
|
const q = searchQuery.toLowerCase();
|
||||||
|
result = result.filter(
|
||||||
|
(t) =>
|
||||||
|
t.league?.name?.toLowerCase().includes(q) ||
|
||||||
|
t.league?.country?.name?.toLowerCase().includes(q) ||
|
||||||
|
t.leagueId.toLowerCase().includes(q),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [tiers, filterTier, searchQuery]);
|
||||||
|
|
||||||
|
// Leagues not yet in tiers (for add form)
|
||||||
|
const availableLeagues = useMemo(() => {
|
||||||
|
const tierLeagueIds = new Set(tiers.map((t) => t.leagueId));
|
||||||
|
return (leagues as Array<{ id: string; name: string }>).filter(
|
||||||
|
(l) => !tierLeagueIds.has(l.id),
|
||||||
|
);
|
||||||
|
}, [leagues, tiers]);
|
||||||
|
|
||||||
|
const handleAdd = async () => {
|
||||||
|
if (!addLeagueId) return;
|
||||||
|
await addMutation.mutateAsync({
|
||||||
|
leagueId: addLeagueId,
|
||||||
|
tier: parseInt(addTier),
|
||||||
|
notes: addNotes || undefined,
|
||||||
|
addedBy: "admin",
|
||||||
|
});
|
||||||
|
setAddLeagueId("");
|
||||||
|
setAddNotes("");
|
||||||
|
setShowAddForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTierChange = async (leagueId: string, newTier: number) => {
|
||||||
|
await updateMutation.mutateAsync({
|
||||||
|
leagueId,
|
||||||
|
dto: { tier: newTier },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeactivate = async (leagueId: string) => {
|
||||||
|
await deactivateMutation.mutateAsync(leagueId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (leagueId: string) => {
|
||||||
|
await deleteMutation.mutateAsync(leagueId);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tiersLoading) {
|
||||||
|
return (
|
||||||
|
<Flex justify="center" py={16}>
|
||||||
|
<Spinner size="lg" color="primary.500" />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack gap={6} align="stretch">
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<SimpleGrid columns={{ base: 2, md: 4 }} gap={4}>
|
||||||
|
{[1, 2, 3].map((tier) => {
|
||||||
|
const config = TIER_CONFIG[tier];
|
||||||
|
const count =
|
||||||
|
stats?.tiers?.find((t: { tier: number }) => t.tier === tier)
|
||||||
|
?.count ?? 0;
|
||||||
|
return (
|
||||||
|
<Card.Root
|
||||||
|
key={tier}
|
||||||
|
bg={cardBg}
|
||||||
|
borderColor={borderColor}
|
||||||
|
borderRadius="xl"
|
||||||
|
>
|
||||||
|
<Card.Body>
|
||||||
|
<HStack gap={3}>
|
||||||
|
<Flex
|
||||||
|
boxSize="40px"
|
||||||
|
bg={`${config.color}.subtle`}
|
||||||
|
borderRadius="lg"
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
color={`${config.color}.fg`}
|
||||||
|
fontSize="lg"
|
||||||
|
>
|
||||||
|
{config.icon}
|
||||||
|
</Flex>
|
||||||
|
<VStack gap={0} align="flex-start">
|
||||||
|
<Text fontSize="xl" fontWeight="900" lineHeight="1">
|
||||||
|
<AnimatedCounter value={count} />
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color="fg.muted">
|
||||||
|
{config.label}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
||||||
|
<Card.Body>
|
||||||
|
<HStack gap={3}>
|
||||||
|
<Flex
|
||||||
|
boxSize="40px"
|
||||||
|
bg="green.subtle"
|
||||||
|
borderRadius="lg"
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
color="green.fg"
|
||||||
|
fontSize="lg"
|
||||||
|
>
|
||||||
|
<LuBrain />
|
||||||
|
</Flex>
|
||||||
|
<VStack gap={0} align="flex-start">
|
||||||
|
<Text fontSize="xl" fontWeight="900" lineHeight="1">
|
||||||
|
<AnimatedCounter
|
||||||
|
value={stats?.totalQualifiedMatches ?? 0}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color="fg.muted">
|
||||||
|
Toplam Mac
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<HStack gap={2} flexWrap="wrap">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
colorPalette="primary"
|
||||||
|
borderRadius="full"
|
||||||
|
onClick={() => setShowAddForm(!showAddForm)}
|
||||||
|
>
|
||||||
|
<LuPlus />
|
||||||
|
Lig Ekle
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
borderRadius="full"
|
||||||
|
onClick={() => syncMutation.mutate()}
|
||||||
|
disabled={syncMutation.isPending}
|
||||||
|
>
|
||||||
|
<LuRefreshCw />
|
||||||
|
{syncMutation.isPending ? "Senkronize ediliyor..." : "Sync JSON"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorPalette="purple"
|
||||||
|
borderRadius="full"
|
||||||
|
onClick={() => retrainMutation.mutate()}
|
||||||
|
disabled={retrainMutation.isPending}
|
||||||
|
>
|
||||||
|
<LuBrain />
|
||||||
|
{retrainMutation.isPending ? "Baslatiliyor..." : "Model Egit"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{(syncMutation.isSuccess || retrainMutation.isSuccess) && (
|
||||||
|
<Badge colorPalette="green" variant="subtle" borderRadius="full">
|
||||||
|
Basarili
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* Add League Form */}
|
||||||
|
{showAddForm && (
|
||||||
|
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
||||||
|
<Card.Body>
|
||||||
|
<VStack gap={3} align="stretch">
|
||||||
|
<Heading size="sm">Yeni Lig Ekle</Heading>
|
||||||
|
<SimpleGrid columns={{ base: 1, md: 3 }} gap={3}>
|
||||||
|
<Input
|
||||||
|
placeholder="League ID gir..."
|
||||||
|
value={addLeagueId}
|
||||||
|
onChange={(e) => setAddLeagueId(e.target.value)}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<NativeSelectRoot size="sm">
|
||||||
|
<NativeSelectField
|
||||||
|
value={addTier}
|
||||||
|
onChange={(e) => setAddTier(e.target.value)}
|
||||||
|
items={[
|
||||||
|
{ label: "Tier 1 - Elmas", value: "1" },
|
||||||
|
{ label: "Tier 2 - Altin", value: "2" },
|
||||||
|
{ label: "Tier 3 - Gumus", value: "3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</NativeSelectRoot>
|
||||||
|
<Input
|
||||||
|
placeholder="Not (opsiyonel)"
|
||||||
|
value={addNotes}
|
||||||
|
onChange={(e) => setAddNotes(e.target.value)}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
<HStack>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
colorPalette="primary"
|
||||||
|
onClick={handleAdd}
|
||||||
|
disabled={!addLeagueId || addMutation.isPending}
|
||||||
|
>
|
||||||
|
{addMutation.isPending ? "Ekleniyor..." : "Ekle"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setShowAddForm(false)}
|
||||||
|
>
|
||||||
|
Iptal
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
||||||
|
<Card.Body py={3}>
|
||||||
|
<SimpleGrid columns={{ base: 1, md: 2 }} gap={3}>
|
||||||
|
<HStack>
|
||||||
|
<LuSearch />
|
||||||
|
<Input
|
||||||
|
placeholder="Lig veya ulke ara..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setSearchQuery("")}
|
||||||
|
>
|
||||||
|
<LuX />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
<NativeSelectRoot size="sm">
|
||||||
|
<NativeSelectField
|
||||||
|
placeholder="Tum Tierler"
|
||||||
|
value={filterTier}
|
||||||
|
onChange={(e) => setFilterTier(e.target.value)}
|
||||||
|
items={[
|
||||||
|
{ label: "Tier 1 - Elmas", value: "1" },
|
||||||
|
{ label: "Tier 2 - Altin", value: "2" },
|
||||||
|
{ label: "Tier 3 - Gumus", value: "3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</NativeSelectRoot>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
|
{/* Tier Groups */}
|
||||||
|
{[1, 2, 3].map((tier) => {
|
||||||
|
const config = TIER_CONFIG[tier];
|
||||||
|
const tierItems = filterTier
|
||||||
|
? filteredTiers.filter((t) => t.tier === tier)
|
||||||
|
: tierGroups[tier]?.filter((t) => {
|
||||||
|
if (!searchQuery) return true;
|
||||||
|
const q = searchQuery.toLowerCase();
|
||||||
|
return (
|
||||||
|
t.league?.name?.toLowerCase().includes(q) ||
|
||||||
|
t.league?.country?.name?.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}) ?? [];
|
||||||
|
|
||||||
|
if (filterTier && parseInt(filterTier) !== tier) return null;
|
||||||
|
|
||||||
|
const isExpanded = expandedTier === tier || expandedTier === null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card.Root
|
||||||
|
key={tier}
|
||||||
|
bg={cardBg}
|
||||||
|
borderColor={borderColor}
|
||||||
|
borderRadius="xl"
|
||||||
|
>
|
||||||
|
<Card.Body>
|
||||||
|
<Flex
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
mb={isExpanded ? 3 : 0}
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setExpandedTier(expandedTier === tier ? null : tier)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<HStack gap={2}>
|
||||||
|
<Badge
|
||||||
|
colorPalette={config.color}
|
||||||
|
variant="subtle"
|
||||||
|
borderRadius="full"
|
||||||
|
px={3}
|
||||||
|
py={1}
|
||||||
|
>
|
||||||
|
{config.icon}
|
||||||
|
Tier {tier} - {config.label}
|
||||||
|
</Badge>
|
||||||
|
<Text fontSize="sm" color="fg.muted">
|
||||||
|
({tierItems.length} lig)
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
{isExpanded ? <LuChevronUp /> : <LuChevronDown />}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<VStack gap={0} align="stretch">
|
||||||
|
{/* Header */}
|
||||||
|
<Flex
|
||||||
|
px={4}
|
||||||
|
py={2}
|
||||||
|
bg="bg.muted"
|
||||||
|
borderRadius="lg"
|
||||||
|
mb={1}
|
||||||
|
fontWeight="semibold"
|
||||||
|
fontSize="xs"
|
||||||
|
color="fg.muted"
|
||||||
|
>
|
||||||
|
<Text flex={3}>Lig</Text>
|
||||||
|
<Text flex={2}>Ulke</Text>
|
||||||
|
<Text flex={1} textAlign="center">
|
||||||
|
Tier
|
||||||
|
</Text>
|
||||||
|
<Text flex={1} textAlign="center">
|
||||||
|
Durum
|
||||||
|
</Text>
|
||||||
|
<Text width="80px" textAlign="center">
|
||||||
|
Islem
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{tierItems.length === 0 ? (
|
||||||
|
<Text
|
||||||
|
py={4}
|
||||||
|
textAlign="center"
|
||||||
|
color="fg.muted"
|
||||||
|
fontSize="sm"
|
||||||
|
>
|
||||||
|
Bu tier'da lig yok
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
tierItems.map((item, idx) => (
|
||||||
|
<Box key={item.id}>
|
||||||
|
{idx > 0 && <Separator />}
|
||||||
|
<Flex
|
||||||
|
px={4}
|
||||||
|
py={2}
|
||||||
|
align="center"
|
||||||
|
_hover={{ bg: "bg.muted" }}
|
||||||
|
borderRadius="lg"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
flex={3}
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="medium"
|
||||||
|
truncate
|
||||||
|
>
|
||||||
|
{item.league?.name ?? item.leagueId}
|
||||||
|
</Text>
|
||||||
|
<Text flex={2} fontSize="sm" color="fg.muted" truncate>
|
||||||
|
{item.league?.country?.name ?? "-"}
|
||||||
|
</Text>
|
||||||
|
<Flex flex={1} justify="center">
|
||||||
|
<NativeSelectRoot size="xs">
|
||||||
|
<NativeSelectField
|
||||||
|
value={String(item.tier)}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleTierChange(
|
||||||
|
item.leagueId,
|
||||||
|
parseInt(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items={[
|
||||||
|
{ label: "1", value: "1" },
|
||||||
|
{ label: "2", value: "2" },
|
||||||
|
{ label: "3", value: "3" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</NativeSelectRoot>
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1} justify="center">
|
||||||
|
<Badge
|
||||||
|
colorPalette={item.isActive ? "green" : "gray"}
|
||||||
|
variant="subtle"
|
||||||
|
fontSize="2xs"
|
||||||
|
borderRadius="full"
|
||||||
|
>
|
||||||
|
{item.isActive ? "Aktif" : "Pasif"}
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
<Flex width="80px" justify="center" gap={1}>
|
||||||
|
{item.isActive && (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
colorPalette="orange"
|
||||||
|
onClick={() =>
|
||||||
|
handleDeactivate(item.leagueId)
|
||||||
|
}
|
||||||
|
title="Pasif yap"
|
||||||
|
>
|
||||||
|
<LuX />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
colorPalette="red"
|
||||||
|
onClick={() => handleDelete(item.leagueId)}
|
||||||
|
title="Sil"
|
||||||
|
>
|
||||||
|
<LuTrash2 />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
)}
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,8 +3,13 @@ import { ApiResponse, PaginatedData } from "@/types/api-response";
|
|||||||
import type {
|
import type {
|
||||||
AdminPaginationParams,
|
AdminPaginationParams,
|
||||||
AdminUserDto,
|
AdminUserDto,
|
||||||
|
AddLeagueTierDto,
|
||||||
AnalyticsOverviewDto,
|
AnalyticsOverviewDto,
|
||||||
|
LeagueTierDto,
|
||||||
|
LeagueTierStatsDto,
|
||||||
|
RetrainStatusDto,
|
||||||
SettingDto,
|
SettingDto,
|
||||||
|
UpdateLeagueTierDto,
|
||||||
UpdateSettingDto,
|
UpdateSettingDto,
|
||||||
UpdateUserRoleDto,
|
UpdateUserRoleDto,
|
||||||
UpdateUserSubscriptionDto,
|
UpdateUserSubscriptionDto,
|
||||||
@@ -121,6 +126,82 @@ const toggleUserActive = (id: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// League Tiers
|
||||||
|
const getLeagueTiers = (active?: boolean) => {
|
||||||
|
return apiRequest<ApiResponse<LeagueTierDto[]>>({
|
||||||
|
url: "/admin/league-tiers",
|
||||||
|
client: "admin",
|
||||||
|
method: "get",
|
||||||
|
params: active !== undefined ? { active: String(active) } : undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLeagueTierStats = () => {
|
||||||
|
return apiRequest<ApiResponse<LeagueTierStatsDto>>({
|
||||||
|
url: "/admin/league-tiers/stats",
|
||||||
|
client: "admin",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addLeagueTier = (dto: AddLeagueTierDto) => {
|
||||||
|
return apiRequest<ApiResponse<LeagueTierDto>>({
|
||||||
|
url: "/admin/league-tiers",
|
||||||
|
client: "admin",
|
||||||
|
method: "post",
|
||||||
|
data: dto,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLeagueTier = (leagueId: string, dto: UpdateLeagueTierDto) => {
|
||||||
|
return apiRequest<ApiResponse<LeagueTierDto>>({
|
||||||
|
url: `/admin/league-tiers/${leagueId}/tier`,
|
||||||
|
client: "admin",
|
||||||
|
method: "put",
|
||||||
|
data: dto,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deactivateLeagueTier = (leagueId: string) => {
|
||||||
|
return apiRequest<ApiResponse<LeagueTierDto>>({
|
||||||
|
url: `/admin/league-tiers/${leagueId}/deactivate`,
|
||||||
|
client: "admin",
|
||||||
|
method: "put",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteLeagueTier = (leagueId: string) => {
|
||||||
|
return apiRequest<ApiResponse<null>>({
|
||||||
|
url: `/admin/league-tiers/${leagueId}`,
|
||||||
|
client: "admin",
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncLeagueTiers = () => {
|
||||||
|
return apiRequest<ApiResponse<{ count: number; path: string }>>({
|
||||||
|
url: "/admin/league-tiers/sync",
|
||||||
|
client: "admin",
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerRetrain = () => {
|
||||||
|
return apiRequest<ApiResponse<{ status: string; message: string }>>({
|
||||||
|
url: "/admin/league-tiers/retrain",
|
||||||
|
client: "admin",
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRetrainStatus = () => {
|
||||||
|
return apiRequest<ApiResponse<RetrainStatusDto>>({
|
||||||
|
url: "/admin/league-tiers/retrain/status",
|
||||||
|
client: "admin",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const adminService = {
|
export const adminService = {
|
||||||
getAnalyticsOverview,
|
getAnalyticsOverview,
|
||||||
getAllSettings,
|
getAllSettings,
|
||||||
@@ -134,4 +215,14 @@ export const adminService = {
|
|||||||
updateUserRole,
|
updateUserRole,
|
||||||
updateUserSubscription,
|
updateUserSubscription,
|
||||||
toggleUserActive,
|
toggleUserActive,
|
||||||
|
// League Tiers
|
||||||
|
getLeagueTiers,
|
||||||
|
getLeagueTierStats,
|
||||||
|
addLeagueTier,
|
||||||
|
updateLeagueTier,
|
||||||
|
deactivateLeagueTier,
|
||||||
|
deleteLeagueTier,
|
||||||
|
syncLeagueTiers,
|
||||||
|
triggerRetrain,
|
||||||
|
getRetrainStatus,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,6 +66,55 @@ export interface UpdateUserSubscriptionDto {
|
|||||||
expiresAt?: string | null;
|
expiresAt?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================
|
||||||
|
// League Tiers
|
||||||
|
// ========================
|
||||||
|
|
||||||
|
export interface LeagueTierDto {
|
||||||
|
id: number;
|
||||||
|
leagueId: string;
|
||||||
|
tier: number;
|
||||||
|
isActive: boolean;
|
||||||
|
addedBy?: string;
|
||||||
|
notes?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
league: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
country?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
code?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueTierStatsDto {
|
||||||
|
tiers: { tier: number; count: number }[];
|
||||||
|
totalActiveLeagues: number;
|
||||||
|
totalQualifiedMatches: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddLeagueTierDto {
|
||||||
|
leagueId: string;
|
||||||
|
tier?: number;
|
||||||
|
notes?: string;
|
||||||
|
addedBy?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateLeagueTierDto {
|
||||||
|
tier: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RetrainStatusDto {
|
||||||
|
running: boolean;
|
||||||
|
last_started?: string;
|
||||||
|
last_completed?: string;
|
||||||
|
last_status?: string;
|
||||||
|
last_error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ========================
|
// ========================
|
||||||
// Analytics
|
// Analytics
|
||||||
// ========================
|
// ========================
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { adminService } from "./service";
|
import { adminService } from "./service";
|
||||||
import type {
|
import type {
|
||||||
|
AddLeagueTierDto,
|
||||||
AdminPaginationParams,
|
AdminPaginationParams,
|
||||||
|
UpdateLeagueTierDto,
|
||||||
UpdateSettingDto,
|
UpdateSettingDto,
|
||||||
UpdateUserRoleDto,
|
UpdateUserRoleDto,
|
||||||
UpdateUserSubscriptionDto,
|
UpdateUserSubscriptionDto,
|
||||||
@@ -17,6 +19,8 @@ export const AdminQueryKeys = {
|
|||||||
users: (params?: AdminPaginationParams) =>
|
users: (params?: AdminPaginationParams) =>
|
||||||
[...AdminQueryKeys.usersList(), params] as const,
|
[...AdminQueryKeys.usersList(), params] as const,
|
||||||
user: (id: string) => [...AdminQueryKeys.all, "user", id] as const,
|
user: (id: string) => [...AdminQueryKeys.all, "user", id] as const,
|
||||||
|
leagueTiers: () => [...AdminQueryKeys.all, "leagueTiers"] as const,
|
||||||
|
leagueTierStats: () => [...AdminQueryKeys.all, "leagueTierStats"] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Analytics
|
// Analytics
|
||||||
@@ -144,3 +148,104 @@ export const useToggleUserActive = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ========================
|
||||||
|
// League Tiers
|
||||||
|
// ========================
|
||||||
|
|
||||||
|
export const useLeagueTiers = (enabled = true) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: AdminQueryKeys.leagueTiers(),
|
||||||
|
queryFn: () => adminService.getLeagueTiers(),
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLeagueTierStats = (enabled = true) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: AdminQueryKeys.leagueTierStats(),
|
||||||
|
queryFn: () => adminService.getLeagueTierStats(),
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAddLeagueTier = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (dto: AddLeagueTierDto) => adminService.addLeagueTier(dto),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.leagueTiers() });
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: AdminQueryKeys.leagueTierStats(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateLeagueTier = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
leagueId,
|
||||||
|
dto,
|
||||||
|
}: {
|
||||||
|
leagueId: string;
|
||||||
|
dto: UpdateLeagueTierDto;
|
||||||
|
}) => adminService.updateLeagueTier(leagueId, dto),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.leagueTiers() });
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: AdminQueryKeys.leagueTierStats(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeactivateLeagueTier = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (leagueId: string) =>
|
||||||
|
adminService.deactivateLeagueTier(leagueId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.leagueTiers() });
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: AdminQueryKeys.leagueTierStats(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteLeagueTier = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (leagueId: string) =>
|
||||||
|
adminService.deleteLeagueTier(leagueId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.leagueTiers() });
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: AdminQueryKeys.leagueTierStats(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSyncLeagueTiers = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => adminService.syncLeagueTiers(),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.leagueTiers() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTriggerRetrain = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => adminService.triggerRetrain(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user