This commit is contained in:
@@ -40,8 +40,9 @@ import {
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
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
|
||||
@@ -140,6 +141,7 @@ export default function AdminContent() {
|
||||
const tabs: { key: AdminTab; label: string }[] = [
|
||||
{ key: "overview", label: t("overview") },
|
||||
{ key: "users", label: t("user-management") },
|
||||
{ key: "league-tiers", label: "Lig Tier" },
|
||||
];
|
||||
|
||||
const getUserDisplayName = (user: AdminUserDto) => {
|
||||
@@ -548,6 +550,9 @@ export default function AdminContent() {
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* League Tiers Tab */}
|
||||
{activeTab === "league-tiers" && <LeagueTiersContent />}
|
||||
|
||||
<EditUserModal
|
||||
user={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 {
|
||||
AdminPaginationParams,
|
||||
AdminUserDto,
|
||||
AddLeagueTierDto,
|
||||
AnalyticsOverviewDto,
|
||||
LeagueTierDto,
|
||||
LeagueTierStatsDto,
|
||||
RetrainStatusDto,
|
||||
SettingDto,
|
||||
UpdateLeagueTierDto,
|
||||
UpdateSettingDto,
|
||||
UpdateUserRoleDto,
|
||||
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 = {
|
||||
getAnalyticsOverview,
|
||||
getAllSettings,
|
||||
@@ -134,4 +215,14 @@ export const adminService = {
|
||||
updateUserRole,
|
||||
updateUserSubscription,
|
||||
toggleUserActive,
|
||||
// League Tiers
|
||||
getLeagueTiers,
|
||||
getLeagueTierStats,
|
||||
addLeagueTier,
|
||||
updateLeagueTier,
|
||||
deactivateLeagueTier,
|
||||
deleteLeagueTier,
|
||||
syncLeagueTiers,
|
||||
triggerRetrain,
|
||||
getRetrainStatus,
|
||||
};
|
||||
|
||||
@@ -66,6 +66,55 @@ export interface UpdateUserSubscriptionDto {
|
||||
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
|
||||
// ========================
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { adminService } from "./service";
|
||||
import type {
|
||||
AddLeagueTierDto,
|
||||
AdminPaginationParams,
|
||||
UpdateLeagueTierDto,
|
||||
UpdateSettingDto,
|
||||
UpdateUserRoleDto,
|
||||
UpdateUserSubscriptionDto,
|
||||
@@ -17,6 +19,8 @@ export const AdminQueryKeys = {
|
||||
users: (params?: AdminPaginationParams) =>
|
||||
[...AdminQueryKeys.usersList(), params] as const,
|
||||
user: (id: string) => [...AdminQueryKeys.all, "user", id] as const,
|
||||
leagueTiers: () => [...AdminQueryKeys.all, "leagueTiers"] as const,
|
||||
leagueTierStats: () => [...AdminQueryKeys.all, "leagueTierStats"] as const,
|
||||
};
|
||||
|
||||
// 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