Разработка админ-панели EventHubFrontAdmin v1.0 #1
This commit is contained in:
@@ -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]);
|
||||
};
|
||||
@@ -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('Ошибка удаления'),
|
||||
});
|
||||
};
|
||||
@@ -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 };
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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('Ошибка удаления'),
|
||||
});
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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('Ошибка удаления'),
|
||||
});
|
||||
};
|
||||
@@ -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('Ошибка обновления'),
|
||||
});
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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('Ошибка удаления'),
|
||||
});
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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('Ошибка удаления'),
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user