Разработка админ-панели EventHubFrontAdmin v1.0 #1

This commit is contained in:
2026-05-23 20:43:21 +03:00
parent 36c95b71a4
commit f5961c5529
68 changed files with 10374 additions and 3 deletions
+101
View File
@@ -0,0 +1,101 @@
import { useEffect, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useAuthStore } from '../store/authStore';
type WsMessage = {
type: 'report_created' | 'ticket_created';
data?: {
report_id?: string;
ticket_id?: string;
target_type?: string;
target_id?: string;
reason?: string;
};
status?: string;
channel?: string;
timestamp?: number;
};
export const useAdminWebSocket = () => {
const queryClient = useQueryClient();
const accessToken = useAuthStore((s) => s.accessToken);
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const pingIntervalRef = useRef<ReturnType<typeof setInterval>>();
const lastMessageTimestamp = useRef<number>(0);
const wsRef = useRef<WebSocket | null>(null);
useEffect(() => {
if (!isAuthenticated || !accessToken) return;
let isMounted = true;
const connect = () => {
const wsUrl = import.meta.env.DEV
? `ws://localhost:5173/admin/ws?token=${accessToken}`
: `${import.meta.env.VITE_WS_URL || 'wss://admin-ws.eventhub.local'}/admin/ws?token=${accessToken}`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.onopen = () => {
if (!isMounted) return;
console.log('[WS] Connected');
ws.send(JSON.stringify({ action: 'subscribe', channel: 'reports' }));
ws.send(JSON.stringify({ action: 'subscribe', channel: 'tickets' }));
pingIntervalRef.current = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ action: 'ping' }));
}
}, 30_000);
};
ws.onmessage = (event) => {
if (!isMounted) return;
try {
const msg: WsMessage = JSON.parse(event.data);
console.log('[WS] Message:', msg);
if (msg.timestamp && msg.timestamp === lastMessageTimestamp.current) {
return;
}
if (msg.timestamp) {
lastMessageTimestamp.current = msg.timestamp;
}
if (msg.type === 'report_created' || msg.type === 'ticket_created') {
window.dispatchEvent(
new CustomEvent('admin-ws-message', { detail: msg })
);
if (msg.type === 'report_created') {
queryClient.invalidateQueries({ queryKey: ['reports'] });
} else if (msg.type === 'ticket_created') {
queryClient.invalidateQueries({ queryKey: ['tickets'] });
queryClient.invalidateQueries({ queryKey: ['ticket-stats'] });
}
}
} catch (e) {
console.warn('[WS] Failed to parse message:', e);
}
};
ws.onclose = () => {
if (!isMounted) return;
console.log('[WS] Disconnected, will reconnect in 5s');
if (pingIntervalRef.current) clearInterval(pingIntervalRef.current);
reconnectTimeoutRef.current = setTimeout(connect, 5000);
};
// Не вызываем ws.close() при ошибке, браузер закроет сокет сам
};
connect();
return () => {
isMounted = false;
if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
if (pingIntervalRef.current) clearInterval(pingIntervalRef.current);
// Никакого принудительного закрытия wsRef.current – браузер сам управится
};
}, [isAuthenticated, accessToken, queryClient]);
};
+62
View File
@@ -0,0 +1,62 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { adminsApi } from '../api/adminsApi';
import { AdminListParams } from '../types/api';
import { message } from 'antd';
import { normalizeData } from '../utils/normalize';
export const useAdmins = (params: AdminListParams) => {
return useQuery({
queryKey: ['admins', params],
queryFn: async () => {
const res = await adminsApi.getAdmins(params);
return { data: normalizeData(res.data), total: res.total };
},
});
};
export const useAdmin = (id: string) => {
return useQuery({
queryKey: ['admins', id],
queryFn: () => adminsApi.getAdmin(id),
enabled: !!id && id !== '-' && id !== 'undefined',
retry: false,
});
};
export const useCreateAdmin = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: { email: string; password: string; role: string }) =>
adminsApi.createAdmin(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admins'] });
message.success('Администратор создан');
},
onError: () => message.error('Ошибка создания'),
});
};
export const useUpdateAdmin = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<Admin> }) =>
adminsApi.updateAdmin(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admins'] });
message.success('Администратор обновлён');
},
onError: () => message.error('Ошибка обновления'),
});
};
export const useDeleteAdmin = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => adminsApi.deleteAdmin(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admins'] });
message.success('Администратор удалён');
},
onError: () => message.error('Ошибка удаления'),
});
};
+14
View File
@@ -0,0 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import { auditApi } from '../api/auditApi';
import { AuditListParams } from '../types/api';
import { normalizeData } from '../utils/normalize';
export const useAudit = (params: AuditListParams) => {
return useQuery({
queryKey: ['audit', params],
queryFn: async () => {
const res = await auditApi.getAuditRecords(params);
return { data: normalizeData(res.data), total: res.total };
},
});
};
+38
View File
@@ -0,0 +1,38 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { bannedWordsApi, BannedWordListParams } from '../api/bannedWordsApi';
import { message } from 'antd';
import { normalizeData } from '../utils/normalize';
export const useBannedWords = (params?: BannedWordListParams) => {
return useQuery({
queryKey: ['banned-words', params],
queryFn: async () => {
const res = await bannedWordsApi.getBannedWords(params);
return { data: normalizeData(res.data), total: res.total };
},
});
};
export const useAddBannedWord = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (word: string) => bannedWordsApi.addBannedWord(word),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['banned-words'] });
message.success('Слово добавлено');
},
onError: () => message.error('Ошибка добавления'),
});
};
export const useRemoveBannedWord = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (word: string) => bannedWordsApi.removeBannedWord(word),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['banned-words'] });
message.success('Слово удалено');
},
onError: () => message.error('Ошибка удаления'),
});
};
+13
View File
@@ -0,0 +1,13 @@
import { useQuery } from '@tanstack/react-query';
import { dashboardApi } from '../api/dashboardApi';
import { normalizeData } from '../utils/normalize';
export const useDashboardStats = (from?: string, to?: string) => {
return useQuery({
queryKey: ['dashboard', from, to],
queryFn: async () => {
const data = await dashboardApi.getStats(from, to);
return normalizeData(data);
},
});
};
+48
View File
@@ -0,0 +1,48 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { eventsApi } from '../api/eventsApi';
import { EventListParams } from '../types/api';
import { message } from 'antd';
import { normalizeData } from '../utils/normalize';
export const useEvents = (params: EventListParams) => {
return useQuery({
queryKey: ['events', params],
queryFn: async () => {
const res = await eventsApi.getEvents(params);
return { data: normalizeData(res.data), total: res.total };
},
});
};
export const useEvent = (id: string) => {
return useQuery({
queryKey: ['events', id],
queryFn: () => eventsApi.getEvent(id), // сырые данные для формы
enabled: !!id && id !== '-',
});
};
export const useUpdateEvent = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<Event> }) =>
eventsApi.updateEvent(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['events'] });
message.success('Событие обновлено');
},
onError: () => message.error('Ошибка обновления'),
});
};
export const useDeleteEvent = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => eventsApi.deleteEvent(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['events'] });
message.success('Событие удалено');
},
onError: () => message.error('Ошибка удаления'),
});
};
+36
View File
@@ -0,0 +1,36 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { reportsApi } from '../api/reportsApi';
import { ReportListParams } from '../types/api';
import { message } from 'antd';
import { normalizeData } from '../utils/normalize';
export const useReports = (params: ReportListParams) => {
return useQuery({
queryKey: ['reports', params],
queryFn: async () => {
const res = await reportsApi.getReports(params);
return { data: normalizeData(res.data), total: res.total };
},
});
};
export const useReport = (id: string) => {
return useQuery({
queryKey: ['reports', id],
queryFn: () => reportsApi.getReport(id),
enabled: !!id,
});
};
export const useUpdateReport = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: { status: 'reviewed' | 'dismissed' } }) =>
reportsApi.updateReport(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reports'] });
message.success('Статус обновлён');
},
onError: () => message.error('Ошибка обновления'),
});
};
+54
View File
@@ -0,0 +1,54 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { reviewsApi } from '../api/reviewsApi';
import { ReviewListParams } from '../types/api';
import { message } from 'antd';
import { normalizeData } from '../utils/normalize';
export const useReviews = (params: ReviewListParams) => {
return useQuery({
queryKey: ['reviews', params],
queryFn: async () => {
const res = await reviewsApi.getReviews(params);
return { data: normalizeData(res.data), total: res.total };
},
});
};
// Вот этот хук отсутствовал
export const useReview = (id: string) => {
return useQuery({
queryKey: ['reviews', id],
queryFn: () => reviewsApi.getReview(id),
enabled: !!id && id !== '-',
retry: false,
});
};
export const useUpdateReview = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<Review> }) =>
reviewsApi.updateReview(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reviews'] });
message.success('Отзыв обновлён');
},
onError: () => message.error('Ошибка обновления'),
});
};
export const useBulkUpdateReviews = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (updates: { id: string; status: string }[]) =>
reviewsApi.bulkUpdateReviews(updates),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reviews'] });
message.success('Статусы обновлены');
},
onError: (error) => {
message.error('Ошибка массового обновления');
console.error(error);
},
});
};
+48
View File
@@ -0,0 +1,48 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { subscriptionsApi } from '../api/subscriptionsApi';
import { SubscriptionListParams } from '../types/api';
import { message } from 'antd';
import { normalizeData } from '../utils/normalize';
export const useSubscriptions = (params: SubscriptionListParams) => {
return useQuery({
queryKey: ['subscriptions', params],
queryFn: async () => {
const res = await subscriptionsApi.getSubscriptions(params);
return { data: normalizeData(res.data), total: res.total };
},
});
};
export const useSubscription = (id: string) => {
return useQuery({
queryKey: ['subscriptions', id],
queryFn: () => subscriptionsApi.getSubscription(id), // сырые данные
enabled: !!id,
});
};
export const useUpdateSubscription = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<Subscription> }) =>
subscriptionsApi.updateSubscription(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['subscriptions'] });
message.success('Подписка обновлена');
},
onError: () => message.error('Ошибка обновления'),
});
};
export const useDeleteSubscription = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => subscriptionsApi.deleteSubscription(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['subscriptions'] });
message.success('Подписка удалена');
},
onError: () => message.error('Ошибка удаления'),
});
};
+58
View File
@@ -0,0 +1,58 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { ticketsApi } from '../api/ticketsApi';
import { TicketListParams } from '../types/api';
import { message } from 'antd';
import { normalizeData } from '../utils/normalize';
export const useTickets = (params: TicketListParams) => {
return useQuery({
queryKey: ['tickets', params],
queryFn: async () => {
const res = await ticketsApi.getTickets(params);
return { data: normalizeData(res.data), total: res.total };
},
});
};
export const useTicket = (id: string) => {
return useQuery({
queryKey: ['tickets', id],
queryFn: () => ticketsApi.getTicket(id), // сырые данные
enabled: !!id,
});
};
export const useUpdateTicket = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<Ticket> }) =>
ticketsApi.updateTicket(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tickets'] });
message.success('Тикет обновлён');
},
onError: () => message.error('Ошибка обновления'),
});
};
export const useDeleteTicket = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => ticketsApi.deleteTicket(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tickets'] });
message.success('Тикет удалён');
},
onError: () => message.error('Ошибка удаления'),
});
};
export const useTicketStats = () => {
return useQuery({
queryKey: ['ticket-stats'],
queryFn: async () => {
const data = await ticketsApi.getTicketStats();
return normalizeData(data);
},
});
};
+47
View File
@@ -0,0 +1,47 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { usersApi } from '../api/usersApi';
import { UserListParams } from '../types/api';
import { message } from 'antd';
import { normalizeData } from '../utils/normalize';
export const useUsers = (params: UserListParams) => {
return useQuery({
queryKey: ['users', params],
queryFn: async () => {
const res = await usersApi.getUsers(params);
return { data: normalizeData(res.data), total: res.total };
},
});
};
export const useUser = (id: string) => {
return useQuery({
queryKey: ['users', id],
queryFn: () => usersApi.getUser(id),
enabled: !!id && id !== '-',
});
};
export const useUpdateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<User> }) => usersApi.updateUser(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
message.success('Пользователь обновлен');
},
onError: () => message.error('Ошибка обновления'),
});
};
export const useDeleteUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => usersApi.deleteUser(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
message.success('Пользователь удален');
},
onError: () => message.error('Ошибка удаления'),
});
};