From e5d2e96a38c54b0279b2df4c2bd16b555d9e6556 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 7 Jan 2026 16:24:30 +0300 Subject: [PATCH] voice --- .claude/settings.local.json | 3 +- admin/src/api/analytics.ts | 54 ++---- admin/src/api/packs.ts | 8 +- admin/src/api/users.ts | 30 ++-- admin/src/pages/DashboardPage.tsx | 179 +++++-------------- admin/src/types/models.ts | 28 ++- backend/src/admin/auth/admin-auth.service.ts | 54 ++++-- src/pages/CreateRoom.jsx | 38 +++- src/pages/RoomPage.jsx | 30 +++- 9 files changed, 188 insertions(+), 236 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 6da3c8b..1edfa5e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,8 @@ "Bash(npx prisma generate:*)", "Bash(npm run build:*)", "Bash(docker-compose up:*)", - "Bash(npx prisma migrate:*)" + "Bash(npx prisma migrate:*)", + "Bash(tree:*)" ] } } diff --git a/admin/src/api/analytics.ts b/admin/src/api/analytics.ts index e218403..c7f4963 100644 --- a/admin/src/api/analytics.ts +++ b/admin/src/api/analytics.ts @@ -1,55 +1,21 @@ import { adminApiClient } from './client' +// Dashboard stats from backend /api/admin/analytics/dashboard export interface DashboardStats { users: number - cards: number - packs: number - enabledPacks: number - payments: number -} - -export interface RecentUser { - id: number - name?: string - email?: string - createdAt?: string -} - -export interface TopPack { - id: number - title: string - cards: number - enabled: boolean -} - -export interface ChartDataPoint { - date: string - registrations?: number - revenue?: number -} - -export interface DashboardData { - stats: DashboardStats - recentUsers: RecentUser[] - topPacks: TopPack[] + activeUsers: number + rooms: number + activeRooms: number + questionPacks: number + publicPacks: number + gamesPlayed: number + gamesToday: number } export const analyticsApi = { // Get dashboard analytics - getDashboard: async (): Promise => { - const response = await adminApiClient.get('/api/v2/admin/analytics/dashboard') - return response.data - }, - - // Get user registration chart data - getUsersChart: async (): Promise<{ data: ChartDataPoint[]; period: string }> => { - const response = await adminApiClient.get('/api/v2/admin/analytics/users/chart') - return response.data - }, - - // Get revenue chart data - getRevenueChart: async (): Promise<{ data: ChartDataPoint[]; period: string; currency: string }> => { - const response = await adminApiClient.get('/api/v2/admin/analytics/revenue/chart') + getDashboard: async (): Promise => { + const response = await adminApiClient.get('/api/admin/analytics/dashboard') return response.data }, } diff --git a/admin/src/api/packs.ts b/admin/src/api/packs.ts index fae965d..c7faae7 100644 --- a/admin/src/api/packs.ts +++ b/admin/src/api/packs.ts @@ -43,7 +43,7 @@ export const packsApi = { showDisabled?: boolean }): Promise> => { try { - const response = await adminApiClient.get('/api/v2/admin/packs', { + const response = await adminApiClient.get('/api/admin/packs', { params: { page: params?.page || 1, limit: params?.limit || 20, @@ -82,7 +82,7 @@ export const packsApi = { // Get pack details by ID for editing getPack: async (packId: string): Promise => { try { - const response = await adminApiClient.get(`/api/v2/admin/packs/${packId}`) + const response = await adminApiClient.get(`/api/admin/packs/${packId}`) return response.data } catch (error) { const axiosError = error as AxiosError<{ error?: string; message?: string }> @@ -110,7 +110,7 @@ export const packsApi = { // Create or update pack upsertPack: async (pack: EditCardPackDto): Promise<{ success: boolean; pack: EditCardPackDto }> => { try { - const response = await adminApiClient.post('/api/v2/admin/packs', pack) + const response = await adminApiClient.post('/api/admin/packs', pack) return response.data } catch (error) { const axiosError = error as AxiosError<{ error?: string; message?: string; field?: string; details?: string }> @@ -146,7 +146,7 @@ export const packsApi = { // Delete pack deletePack: async (packId: string): Promise<{ success: boolean; message: string }> => { try { - const response = await adminApiClient.delete(`/api/v2/admin/packs/${packId}`) + const response = await adminApiClient.delete(`/api/admin/packs/${packId}`) return response.data } catch (error) { const axiosError = error as AxiosError<{ error?: string; message?: string }> diff --git a/admin/src/api/users.ts b/admin/src/api/users.ts index c478a71..6759481 100644 --- a/admin/src/api/users.ts +++ b/admin/src/api/users.ts @@ -1,44 +1,36 @@ import { adminApiClient } from './client' -import type { UserDto, PaginatedResponse, PaymentDto } from '@/types/models' +import type { UserDto, PaginatedResponse } from '@/types/models' export const usersApi = { // Get all users with pagination getUsers: async (params?: { page?: number limit?: number - ids?: string[] }): Promise> => { - const response = await adminApiClient.get('/api/v2/admin/users', { + const response = await adminApiClient.get('/api/admin/users', { params: { page: params?.page || 1, limit: params?.limit || 20, - ids: params?.ids?.join(','), }, }) return response.data }, - // Get user IDs list - getUserIds: async (): Promise => { - const response = await adminApiClient.get('/api/v2/admin/users/ids') - return response.data.ids + // Get single user by ID + getUser: async (userId: string): Promise => { + const response = await adminApiClient.get(`/api/admin/users/${userId}`) + return response.data }, - // Get user purchases - getUserPurchases: async (userId: string): Promise => { - const response = await adminApiClient.get(`/api/v2/admin/users/${userId}/purchases`) - return response.data.payments - }, - - // Create or update user - upsertUser: async (user: UserDto): Promise<{ result: boolean }> => { - const response = await adminApiClient.post('/api/v2/admin/users', user) + // Update user + updateUser: async (userId: string, user: Partial): Promise => { + const response = await adminApiClient.patch(`/api/admin/users/${userId}`, user) return response.data }, // Delete user - deleteUser: async (userId: string): Promise<{ result: boolean }> => { - const response = await adminApiClient.delete(`/api/v2/admin/users/${userId}`) + deleteUser: async (userId: string): Promise<{ message: string }> => { + const response = await adminApiClient.delete(`/api/admin/users/${userId}`) return response.data }, } diff --git a/admin/src/pages/DashboardPage.tsx b/admin/src/pages/DashboardPage.tsx index 89acbc1..087c7dd 100644 --- a/admin/src/pages/DashboardPage.tsx +++ b/admin/src/pages/DashboardPage.tsx @@ -1,35 +1,21 @@ import { useQuery } from '@tanstack/react-query' -import { Users, FileText, Package, DollarSign, TrendingUp, Clock } from 'lucide-react' -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts' +import { Users, Package, TrendingUp, Activity } from 'lucide-react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Badge } from '@/components/ui/badge' -import { analyticsApi, type DashboardData, type ChartDataPoint } from '@/api/analytics' +import { analyticsApi, type DashboardStats } from '@/api/analytics' import { useAuthStore } from '@/stores/authStore' export default function DashboardPage() { const { isAuthenticated, token } = useAuthStore() - + // Only make requests if authenticated and token exists const isReady = isAuthenticated && !!token && !!localStorage.getItem('admin_token') - - const { data: dashboardData, isLoading: dashboardLoading } = useQuery({ + + const { data: dashboardData, isLoading: dashboardLoading } = useQuery({ queryKey: ['dashboard'], queryFn: analyticsApi.getDashboard, enabled: isReady, }) - const { data: usersChartData } = useQuery<{ data: ChartDataPoint[] }>({ - queryKey: ['users-chart'], - queryFn: analyticsApi.getUsersChart, - enabled: isReady, - }) - - const { data: revenueChartData } = useQuery<{ data: ChartDataPoint[] }>({ - queryKey: ['revenue-chart'], - queryFn: analyticsApi.getRevenueChart, - enabled: isReady, - }) - if (dashboardLoading) { return (
@@ -72,7 +58,7 @@ export default function DashboardPage() { -
{dashboardData?.stats.users || 0}
+
{dashboardData?.users || 0}

Registered users

@@ -81,165 +67,96 @@ export default function DashboardPage() { - Total Cards - + Active Users + -
{dashboardData?.stats.cards || 0}
+
{dashboardData?.activeUsers || 0}

- Game cards created + Active in last 7 days

- Active Packs + Question Packs -
{dashboardData?.stats.enabledPacks || 0}
+
{dashboardData?.publicPacks || 0}

- of {dashboardData?.stats.packs || 0} total packs + of {dashboardData?.questionPacks || 0} total packs

- Total Payments - + Games Today + -
{dashboardData?.stats.payments || 0}
+
{dashboardData?.gamesToday || 0}

- Successful transactions + of {dashboardData?.gamesPlayed || 0} total games

- {/* Charts */} + {/* Rooms Stats */}
- User Registrations + Room Statistics - Daily user registrations for the last 30 days - - - - {usersChartData?.data && ( - - - - new Date(value).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - /> - - new Date(value).toLocaleDateString()} - formatter={(value: number | undefined) => [value ?? 0, 'Registrations']} - /> - - - - )} - - - - - - Revenue Trend - - Daily revenue for the last 30 days - - - - {revenueChartData?.data && ( - - - - new Date(value).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - /> - - new Date(value).toLocaleDateString()} - formatter={(value: number | undefined) => [`$${value ?? 0}`, 'Revenue']} - /> - - - - )} - - -
- - {/* Recent Users and Top Packs */} -
- - - - - Recent Users - - - Latest user registrations + Current room status overview
- {dashboardData?.recentUsers.slice(0, 5).map((user) => ( -
-
-

{user.name || 'No name'}

-

{user.email || 'No email'}

-
-
- {user.createdAt ? new Date(user.createdAt).toLocaleDateString() : 'N/A'} -
+
+
+

Total Rooms

+

All time

- )) || ( -

No recent users

- )} +
{dashboardData?.rooms || 0}
+
+
+
+

Active Rooms

+

Currently playing or waiting

+
+
{dashboardData?.activeRooms || 0}
+
- - - Popular Packs - + Game Statistics - Most popular card packs + Overall game activity
- {dashboardData?.topPacks.map((pack) => ( -
-
-

{pack.title}

-

{pack.cards} cards

-
- - {pack.enabled ? 'Active' : 'Disabled'} - +
+
+

Total Games

+

All time completed games

- )) || ( -

No packs available

- )} +
{dashboardData?.gamesPlayed || 0}
+
+
+
+

Today's Games

+

Completed today

+
+
{dashboardData?.gamesToday || 0}
+
diff --git a/admin/src/types/models.ts b/admin/src/types/models.ts index 7055e6b..0428517 100644 --- a/admin/src/types/models.ts +++ b/admin/src/types/models.ts @@ -29,28 +29,20 @@ export interface CardPackPreviewDto { order?: number } +// User model matching backend Prisma schema export interface UserDto { - id?: number - name?: string + id: string email?: string - admin: boolean - packs: string[] - purchases: string[] - subscription?: boolean - subscriptionFeatures: string[] - userDataDto?: unknown - userSettingsDto?: unknown + name?: string + role: 'USER' | 'ADMIN' + telegramId?: string + createdAt: string + gamesPlayed: number + gamesWon: number + totalPoints: number } -export interface PaymentDto { - id: string - userId: string - amount: number - currency: string - status: string - createdAt: string - updatedAt: string -} +// Note: PaymentDto removed - no payment system exists in the backend export interface SubscriptionPlanAdminDto { id: string diff --git a/backend/src/admin/auth/admin-auth.service.ts b/backend/src/admin/auth/admin-auth.service.ts index a9435f8..55789bf 100644 --- a/backend/src/admin/auth/admin-auth.service.ts +++ b/backend/src/admin/auth/admin-auth.service.ts @@ -362,22 +362,46 @@ export class AdminAuthService implements OnModuleInit { }); if (!user) { - // Create admin user for password login - user = await this.prisma.user.create({ - data: { - telegramId: passwordAdminTelegramId, - role: 'ADMIN', - name: 'Admin', - email: adminUsername, - }, - select: { - id: true, - email: true, - name: true, - role: true, - telegramId: true, - }, + // Check if email is already taken by another user + const existingUserWithEmail = await this.prisma.user.findUnique({ + where: { email: adminUsername }, }); + + if (existingUserWithEmail) { + // If email exists but with different telegramId, update it + user = await this.prisma.user.update({ + where: { id: existingUserWithEmail.id }, + data: { + telegramId: passwordAdminTelegramId, + role: 'ADMIN', + name: 'Admin', + }, + select: { + id: true, + email: true, + name: true, + role: true, + telegramId: true, + }, + }); + } else { + // Create new admin user for password login + user = await this.prisma.user.create({ + data: { + telegramId: passwordAdminTelegramId, + role: 'ADMIN', + name: 'Admin', + email: adminUsername, + }, + select: { + id: true, + email: true, + name: true, + role: true, + telegramId: true, + }, + }); + } } else if (user.role !== 'ADMIN') { // Upgrade to admin if needed await this.prisma.user.update({ diff --git a/src/pages/CreateRoom.jsx b/src/pages/CreateRoom.jsx index a6b0ea3..faa60d2 100644 --- a/src/pages/CreateRoom.jsx +++ b/src/pages/CreateRoom.jsx @@ -3,10 +3,11 @@ import { useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import { useRoom } from '../hooks/useRoom'; import { questionsApi } from '../services/api'; +import NameInputModal from '../components/NameInputModal'; const CreateRoom = () => { const navigate = useNavigate(); - const { user } = useAuth(); + const { user, loginAnonymous, loading: authLoading } = useAuth(); const { createRoom, loading: roomLoading } = useRoom(); const [questionPacks, setQuestionPacks] = useState([]); @@ -18,6 +19,27 @@ const CreateRoom = () => { timerDuration: 30, }); const [loading, setLoading] = useState(true); + const [isNameModalOpen, setIsNameModalOpen] = useState(false); + + // Проверка авторизации и показ модального окна для ввода имени + useEffect(() => { + if (!authLoading && !user) { + setIsNameModalOpen(true); + } else if (user) { + setIsNameModalOpen(false); + } + }, [authLoading, user]); + + // Обработка ввода имени и авторизация + const handleNameSubmit = async (name) => { + try { + await loginAnonymous(name); + setIsNameModalOpen(false); + } catch (error) { + console.error('Login error:', error); + alert('Ошибка при авторизации. Попробуйте еще раз.'); + } + }; useEffect(() => { const fetchPacks = async () => { @@ -31,12 +53,16 @@ const CreateRoom = () => { } }; - fetchPacks(); + if (user) { + fetchPacks(); + } else { + setLoading(false); + } }, [user]); const handleCreateRoom = async () => { if (!user) { - alert('Войдите в систему для создания комнаты'); + setIsNameModalOpen(true); return; } @@ -142,6 +168,12 @@ const CreateRoom = () => {
+ +
); }; diff --git a/src/pages/RoomPage.jsx b/src/pages/RoomPage.jsx index 66ddefe..7f9f9a9 100644 --- a/src/pages/RoomPage.jsx +++ b/src/pages/RoomPage.jsx @@ -5,11 +5,12 @@ import { useRoom } from '../hooks/useRoom'; import { questionsApi } from '../services/api'; import QRCode from 'qrcode'; import QRModal from '../components/QRModal'; +import NameInputModal from '../components/NameInputModal'; const RoomPage = () => { const { roomCode } = useParams(); const navigate = useNavigate(); - const { user } = useAuth(); + const { user, loginAnonymous, loading: authLoading } = useAuth(); const { room, participants, @@ -22,6 +23,7 @@ const RoomPage = () => { const [qrCode, setQrCode] = useState(''); const [joined, setJoined] = useState(false); const [isQRModalOpen, setIsQRModalOpen] = useState(false); + const [isNameModalOpen, setIsNameModalOpen] = useState(false); const [questionPacks, setQuestionPacks] = useState([]); const [selectedPackId, setSelectedPackId] = useState(''); const [loadingPacks, setLoadingPacks] = useState(false); @@ -51,6 +53,26 @@ const RoomPage = () => { } }, [roomCode]); + // Проверка авторизации и показ модального окна для ввода имени + useEffect(() => { + if (!authLoading && !user && room && !loading) { + setIsNameModalOpen(true); + } else if (user) { + setIsNameModalOpen(false); + } + }, [authLoading, user, room, loading]); + + // Обработка ввода имени и авторизация + const handleNameSubmit = async (name) => { + try { + await loginAnonymous(name); + setIsNameModalOpen(false); + } catch (error) { + console.error('Login error:', error); + alert('Ошибка при авторизации. Попробуйте еще раз.'); + } + }; + useEffect(() => { const handleJoin = async () => { if (room && user && !joined) { @@ -247,6 +269,12 @@ const RoomPage = () => { qrCode={qrCode} roomCode={roomCode} /> + + ); };