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

Open
opened 2026-05-05 19:09:36 +03:00 by aleksey · 0 comments
Owner

План реализации фронтенда EventHub Admin

Этап 0: Инициализация проекта (1 день)

- Создать проект Vite + React + TypeScript
- Установить зависимости:
    antd
    @ant-design/icons
    react-router-dom
    @tanstack/react-query
    axios
    react-hook-form
    @hookform/resolvers
    zod
    react-i18next
    i18next
    dayjs
    zustand
- Настроить ESLint + Prettier
- Создать .env.example с переменными:
    VITE_API_BASE_URL
    VITE_WS_URL
    VITE_APP_TITLE
- Скопировать все типы из спецификации в src/types/api.ts

Этап 1: Аутентификация и HTTP-клиент (2 дня)

Axios инстанс (src/api/client.ts)

- Базовый URL из VITE_API_BASE_URL
- Request interceptor: добавлять Authorization: Bearer <access_token> из localStorage
- Response interceptor:
    при 401 попытаться обновить токен через POST /v1/refresh
    при успехе повторить исходный запрос
    при неудаче очистить localStorage и редирект на /login

Auth API (src/api/authApi.ts)

Функции:
    login(email: string, password: string): Promise<LoginResponse>
    logout(): Promise<void>
    getMe(): Promise<Admin>
    refreshToken(refreshToken: string): Promise<TokenPair>

Zustand хранилище (src/store/authStore.ts)

Состояние:
    user: Admin | null
    accessToken: string | null
    refreshToken: string | null
    isAuthenticated: boolean
Действия:
    login(email, password) - сохраняет токены в localStorage и состояние
    logout() - очищает localStorage и состояние, вызывает API logout
    checkAuth() - проверяет токен через getMe(), при ошибке делает logout

ProtectedRoute

Компонент, проверяющий isAuthenticated
Если не авторизован - редирект на /login
Опционально проверяет роль (для restricted маршрутов)

LoginPage

Форма (email, пароль)
Валидация через react-hook-form + zod
Вызов login из хранилища
Обработка ошибок, отображение сообщений
После успеха - редирект на /dashboard

Этап 2: Макет и навигация (1 день)

AdminLayout (antd Layout)

- Sider (боковое меню)
- Header (хедер с именем пользователя, переключателем языка, кнопкой выхода)
- Content (основная область)

Меню и роли

Пункты меню:
    Дашборд (/dashboard) — все роли
    Пользователи (/users) — superadmin, admin
    События (/events) — superadmin, admin
    Жалобы (/reports) — superadmin, admin, moderator
    Отзывы (/reviews) — superadmin, admin, moderator
    Бан-слова (/banned-words) — superadmin, admin
    Тикеты (/tickets) — superadmin, admin, support
    Подписки (/subscriptions) — superadmin, admin
    Администраторы (/admins) — только superadmin
    Аудит (/audit) — только superadmin

Скрывать пункты в зависимости от роли текущего пользователя

Маршруты (React Router v6)

/login -> LoginPage
/ -> редирект на /dashboard
/dashboard -> DashboardPage
/users -> UserListPage
/users/:id -> UserDetailPage
/events -> EventListPage
/events/:id -> EventDetailPage
/reports -> ReportListPage
/reports/:id -> ReportDetailPage
/reviews -> ReviewListPage
/banned-words -> BannedWordsPage
/tickets -> TicketListPage
/tickets/:id -> TicketDetailPage
/subscriptions -> SubscriptionListPage
/admins -> AdminListPage
/audit -> AuditPage

Этап 3: Интернационализация (1 день)

- Создать src/i18n/locales/ru.json и en.json
- Все ключи для текстов интерфейса (меню, заголовки, кнопки, сообщения)
- Настроить react-i18next
- Переключатель языка в хедере

Этап 4: API сервисы (1 день)

Для каждой сущности создать файл в src/api/

eventsApi.ts:
    getEvents(params: EventListParams): Promise<{data: Event[], total: number}>
    getEvent(id: string): Promise<Event>
    updateEvent(id: string, data: Partial<Event>): Promise<void>
    deleteEvent(id: string): Promise<void>

usersApi.ts:
    getUsers(params: UserListParams): Promise<{data: User[], total: number}>
    getUser(id: string): Promise<User>
    updateUser(id: string, data: Partial<User>): Promise<void>
    deleteUser(id: string): Promise<void>

reportsApi.ts:
    getReports(params: ReportListParams): Promise<{data: Report[], total: number}>
    getReport(id: string): Promise<Report>
    updateReport(id: string, data: {status: 'reviewed' | 'dismissed'}): Promise<void>

reviewsApi.ts:
    getReviews(params: ReviewListParams): Promise<{data: Review[], total: number}>
    getReview(id: string): Promise<Review>
    updateReview(id: string, data: Partial<Review>): Promise<void>
    bulkUpdateReviews(updates: {id: string, status: string}[]): Promise<number>

bannedWordsApi.ts:
    getBannedWords(params: PaginationParams): Promise<{data: BannedWord[], total: number}>
    addBannedWord(word: string): Promise<void>
    removeBannedWord(word: string): Promise<void>

ticketsApi.ts:
    getTickets(params: TicketListParams): Promise<{data: Ticket[], total: number}>
    getTicket(id: string): Promise<Ticket>
    updateTicket(id: string, data: Partial<Ticket>): Promise<void>
    deleteTicket(id: string): Promise<void>
    getTicketStats(): Promise<TicketStats>

subscriptionsApi.ts:
    getSubscriptions(params: SubscriptionListParams): Promise<{data: Subscription[], total: number}>
    getSubscription(id: string): Promise<Subscription>
    updateSubscription(id: string, data: Partial<Subscription>): Promise<void>
    deleteSubscription(id: string): Promise<void>

adminsApi.ts:
    getAdmins(params: AdminListParams): Promise<{data: Admin[], total: number}>
    getAdmin(id: string): Promise<Admin>
    createAdmin(data: {email: string, password: string, role: string}): Promise<void>
    updateAdmin(id: string, data: Partial<Admin>): Promise<void>
    deleteAdmin(id: string): Promise<void>

auditApi.ts:
    getAuditRecords(params: AuditListParams): Promise<{data: AuditRecord[], total: number}>

dashboardApi.ts:
    getStats(from?: string, to?: string): Promise<DashboardStats>

moderationApi.ts:
    moderate(targetType: string, id: string, action: string, reason?: string): Promise<void>

Этап 5: Кастомные хуки React Query (1 день)

Для каждой сущности создать хуки по шаблону:

useEvents(params):
    useQuery(['events', params], () => eventsApi.getEvents(params))

useEvent(id):
    useQuery(['events', id], () => eventsApi.getEvent(id), { enabled: !!id })

useUpdateEvent():
    useMutation(({id, data}) => eventsApi.updateEvent(id, data), {
        onSuccess: () => {
            queryClient.invalidateQueries(['events'])
        }
    })

Аналогично для всех остальных сущностей (User, Report, Review, Ticket, Subscription, Admin, Audit, BannedWord)
Для массовых операций (bulkUpdateReviews) - соответствующий useMutation

Этап 6: Переиспользуемые компоненты (1 день)

DataTable

- Обёртка над antd Table
- Принимает: columns, data, loading, total, limit, offset, onChange
- Синхронизация пагинации с параметрами запроса
- Отображение общего количества (из X-Total-Count)

StatusBadge

- Принимает статус и тип сущности
- Возвращает цветной Badge/Tag:
    Events: active (green), cancelled (red), completed (gray)
    Users: active (green), frozen (orange), deleted (red)
    Reports: pending (yellow), reviewed (green), dismissed (gray)
    Reviews: visible (green), hidden (orange), deleted (red)
    Tickets: open (red), in_progress (blue), resolved (green), closed (gray)
    Subscriptions: active (green), expired (orange), cancelled (red)

ConfirmModal

- Модальное окно подтверждения действий
- Принимает: title, content, onOk, onCancel

EmptyState

- Отображение при отсутствии данных

ErrorState

- Отображение ошибки загрузки

Этап 7: Страницы (10 дней)

7.1 Дашборд (1 день)

- GET /v1/admin/stats
- Карточки с метриками (antd Statistic):
    users, events, reviews, calendars, reports, tickets, subscriptions, active_subscriptions
- Возможность выбора диапазона дат (from, to)

7.2 Пользователи (1 день)

- Список (UserListPage):
    Таблица: email, nickname, role, status, last_login, created_at
    Фильтры: role, status, поиск (q)
    Пагинация через limit/offset
    Действия: редактировать, изменить статус (block/unblock)
- Детали/редактирование (UserDetailPage):
    Форма: role, status, reason
    Кнопка "Сохранить"

7.3 События (1 день)

- Список (EventListPage):
    Таблица: title, calendar_id, start_time, duration, status, rating_avg, rating_count
    Фильтры: status, calendar_id, from, to, поиск (q)
    Действия: редактировать, freeze/unfreeze
- Детали/редактирование (EventDetailPage):
    Форма: все поля из update-схемы (title, description, start_time, duration, status, specialist_id, location, tags, capacity, online_link, calendar_id, event_type, recurrence)
    Кнопка "Сохранить"

7.4 Жалобы (1 день)

- Список (ReportListPage):
    Таблица: reporter_id, target_type, target_id, reason, status, created_at
    Фильтры: status, target_type, поиск (q)
    Действия: просмотр деталей
- Детали (ReportDetailPage):
    Полная информация
    Кнопки: "Рассмотрено" (reviewed), "Отклонено" (dismissed)
    Модерация: кнопка "Скрыть контент" (PUT /v1/admin/:target_type/:id)

7.5 Отзывы (1 день)

- Список (ReviewListPage):
    Таблица: user_id, target_type, target_id, rating (звезды), comment, status, likes, dislikes
    Фильтры: target_type, target_id, user_id, status
    Массовое действие: выбор нескольких отзывов и изменение статуса (PATCH /v1/admin/reviews)
    Действия: скрыть/показать/удалить (индивидуально)
- Детали (ReviewDetailPage): форма редактирования (status, reason, comment, rating)

7.6 Бан-слова (1 день)

- Страница (BannedWordsPage):
    Таблица: word, added_by, added_at
    Форма добавления над таблицей: одно поле word
    Кнопка удаления в каждой строке

7.7 Тикеты (1 день)

- Список (TicketListPage):
    Карточки статистики сверху: open, in_progress, resolved, closed, total (из /tickets/stats)
    Таблица: error_message, status, assigned_to, count, first_seen, last_seen
    Фильтры: status, assigned_to, поиск (q)
    Действия: просмотр деталей
- Детали (TicketDetailPage):
    Полная информация
    Форма обновления: status, assigned_to, resolution_note
    Кнопка "Сохранить"

7.8 Подписки (1 день)

- Список (SubscriptionListPage):
    Таблица: user_id, plan, status, trial_used, started_at, expires_at
    Фильтры: plan, status
    Действия: редактировать
- Редактирование (модально или inline):
    Форма: plan, status, trial_used, expires_at

7.9 Администраторы (только superadmin) (1 день)

- Список (AdminListPage):
    Таблица: email, nickname, role, status, last_login, created_at
    Фильтры: role, status
    Кнопка "Добавить администратора"
    Действия: редактировать, удалить
- Форма создания/редактирования (AdminFormPage):
    Создание: email, password, role
    Редактирование: nickname, email, role, status, timezone, language, phone

7.10 Аудит (только superadmin) (1 день)

- Страница (AuditPage):
    Таблица: admin_id, email, role, action, entity_type, entity_id, timestamp, ip, reason
    Фильтры: admin_id, action, date_from, date_to
    Только просмотр

Этап 8: WebSocket real-time уведомления (1 день)

Хук useAdminWebSocket

- Подключение к wss://<домен>/admin/ws?token=<jwt>
- Подписка на каналы: reports, tickets
- Обработка входящих сообщений:
    { type: "new_report", data: ... } -> инвалидировать ['reports'], уведомление
    { type: "new_ticket", data: ... } -> инвалидировать ['tickets'], ['ticket-stats'], уведомление
- Автоматическое переподключение при обрыве (с exponential backoff)
- Пинг/понг каждые 30 секунд

Интеграция в интерфейс

- В хедере значок уведомлений со счётчиком непрочитанных
- При новом событии показывать antd notification и обновлять список через queryClient.invalidateQueries

Этап 9: Сборка и деплой (1 день)

- Настроить vite.config.ts с proxy для разработки
- Переменные окружения для dev/stage/prod
- Собрать production билд (npm run build)
- Настроить Nginx:
    - раздача статических файлов
    - reverse proxy к API (location /v1/ -> backend)
    - проксирование WebSocket (location /admin/ws -> wss://backend)
- Настройка CORS на бэкенде для домена фронтенда

Итоговая оценка времени

Этап 0: Инициализация проекта - 1 день
Этап 1: Аутентификация и HTTP-клиент - 2 дня
Этап 2: Макет и навигация - 1 день
Этап 3: i18n - 1 день
Этап 4: API сервисы - 1 день
Этап 5: Кастомные хуки - 1 день
Этап 6: Переиспользуемые компоненты - 1 день
Этап 7: Страницы - 10 дней
Этап 8: WebSocket - 1 день
Этап 9: Сборка и деплой - 1 день

Всего: 20 рабочих дней

Типы данных EventHub Admin API

Event (Событие)

interface Event {
    id: string;
    calendar_id: string;
    title: string;
    description: string;
    event_type: 'single' | 'recurring';
    start_time: string; // ISO8601
    duration: number;
    recurrence: object | null;
    master_id: string | null;
    is_instance: boolean;
    specialist_id: string | null;
    location: object | null;
    tags: string[];
    capacity: number | null;
    online_link: string | null;
    status: 'active' | 'cancelled' | 'completed';
    reason: string | null;
    rating_avg: number;
    rating_count: number;
    attachments: string[] | null;
    edit_history: object[] | null;
    created_at: string; // ISO8601
    updated_at: string; // ISO8601
}

// Параметры списка: from, to, status, calendar_id, title, q, limit, offset, sort, order
// Поля обновления: title, description, start_time, duration, status, specialist_id, location, tags, capacity, online_link, calendar_id, event_type, recurrence

User (Пользователь)

interface User {
    id: string;
    email: string;
    role: 'user' | 'bot';
    status: 'active' | 'frozen' | 'deleted';
    reason: string | null;
    nickname: string | null;
    avatar_url: string | null;
    timezone: string | null;
    language: string | null;
    social_links: string[] | null;
    phone: string | null;
    preferences: object | null;
    last_login: string; // ISO8601
    created_at: string; // ISO8601
    updated_at: string; // ISO8601
}

// Параметры списка: role, status, q, limit, offset
// Поля обновления: role, status, reason

Report (Жалоба)

interface Report {
    id: string;
    reporter_id: string;
    target_type: 'calendar' | 'event' | 'review';
    target_id: string;
    reason: string;
    status: 'pending' | 'reviewed' | 'dismissed';
    created_at: string;
    resolved_at: string | null;
    resolved_by: string | null;
}

// Параметры списка: status, target_type, q, limit, offset
// Поля обновления: status ('reviewed' | 'dismissed')

Review (Отзыв)

interface Review {
    id: string;
    user_id: string;
    target_type: 'calendar' | 'event';
    target_id: string;
    rating: number; // 1-5
    comment: string;
    status: 'visible' | 'hidden' | 'deleted';
    reason: string | null;
    likes: number;
    dislikes: number;
    created_at: string;
    updated_at: string;
}

// Параметры списка: target_type, target_id, user_id, status, limit, offset
// Массовое обновление: PATCH /v1/admin/reviews [{id, status}]
// Поля обновления для одного отзыва: status, reason, comment, rating

Banned Word (Бан-слово)

interface BannedWord {
    id: string;
    word: string;
    added_by: string | null;
    added_at: string | null;
}

// POST: {word: string}
// DELETE: /v1/admin/banned-words/:word

Ticket (Тикет баг-трекера)

interface Ticket {
    id: string;
    reporter_id: string;
    error_hash: string;
    error_message: string;
    stacktrace: string;
    context: string;
    count: number;
    first_seen: string;
    last_seen: string;
    status: 'open' | 'in_progress' | 'resolved' | 'closed';
    assigned_to: string | null;
    resolution_note: string | null;
}

// Параметры списка: status, assigned_to, q, limit, offset
// Поля обновления: status, assigned_to, resolution_note

Subscription (Подписка)

interface Subscription {
    id: string;
    user_id: string;
    plan: 'monthly' | 'quarterly' | 'biannual' | 'annual';
    status: 'active' | 'expired' | 'cancelled';
    trial_used: boolean;
    started_at: string;
    expires_at: string;
    created_at: string;
    updated_at: string;
}

// Параметры списка: plan, status, limit, offset
// Поля обновления: plan, status, trial_used, expires_at

Admin (Администратор)

interface Admin {
    id: string;
    email: string;
    role: 'superadmin' | 'admin' | 'moderator' | 'support';
    status: 'active' | 'blocked';
    nickname: string | null;
    avatar_url: string | null;
    timezone: string | null;
    language: string | null;
    phone: string | null;
    preferences: object | null;
    last_login: string;
    created_at: string;
    updated_at: string;
}

// POST создание: email, password, role
// Поля обновления: nickname, email, role, status, timezone, language, phone, preferences

Audit (Запись аудита)

interface AuditRecord {
    id: string;
    admin_id: string;
    email: string;
    role: string;
    action: string;
    entity_type: string;
    entity_id: string;
    timestamp: string;
    ip: string;
    reason: string | null;
}

// Параметры: admin_id, action, date_from, date_to, limit, offset

Stats (Статистика дашборда)

interface DashboardStats {
    users: number;
    events: number;
    reviews: number;
    calendars: number;
    reports: number;
    tickets: number;
    subscriptions: number;
    active_subscriptions: number;
}

Ticket Stats (Статистика тикетов)

interface TicketStats {
    open: number;
    in_progress: number;
    resolved: number;
    closed: number;
    total: number;
}

Moderation (Модерация)

type TargetType = 'calendar' | 'event' | 'review' | 'user';

type ModerationAction = {
    calendar: 'freeze' | 'unfreeze';
    event: 'freeze' | 'unfreeze';
    review: 'hide' | 'unhide';
    user: 'block' | 'unblock';
};

// PUT /v1/admin/:target_type/:id
// Body: {action: string, reason?: string}

Аутентификация

// POST /v1/admin/login
// Request: { email: string, password: string }
// Response: { access_token: string, refresh_token: string, ... }

// POST /v1/refresh
// Request: { refresh_token: string }
// Response: { access_token: string, refresh_token: string }

Пагинация

// Все списки возвращают заголовки:
// Content-Range: items 0-49/200
// X-Total-Count: 200
// Параметры: limit, offset
# План реализации фронтенда EventHub Admin ## Этап 0: Инициализация проекта (1 день) - Создать проект Vite + React + TypeScript - Установить зависимости: antd @ant-design/icons react-router-dom @tanstack/react-query axios react-hook-form @hookform/resolvers zod react-i18next i18next dayjs zustand - Настроить ESLint + Prettier - Создать .env.example с переменными: VITE_API_BASE_URL VITE_WS_URL VITE_APP_TITLE - Скопировать все типы из спецификации в src/types/api.ts ## Этап 1: Аутентификация и HTTP-клиент (2 дня) ### Axios инстанс (src/api/client.ts) - Базовый URL из VITE_API_BASE_URL - Request interceptor: добавлять Authorization: Bearer <access_token> из localStorage - Response interceptor: при 401 попытаться обновить токен через POST /v1/refresh при успехе повторить исходный запрос при неудаче очистить localStorage и редирект на /login ### Auth API (src/api/authApi.ts) Функции: login(email: string, password: string): Promise<LoginResponse> logout(): Promise<void> getMe(): Promise<Admin> refreshToken(refreshToken: string): Promise<TokenPair> ### Zustand хранилище (src/store/authStore.ts) Состояние: user: Admin | null accessToken: string | null refreshToken: string | null isAuthenticated: boolean Действия: login(email, password) - сохраняет токены в localStorage и состояние logout() - очищает localStorage и состояние, вызывает API logout checkAuth() - проверяет токен через getMe(), при ошибке делает logout ### ProtectedRoute Компонент, проверяющий isAuthenticated Если не авторизован - редирект на /login Опционально проверяет роль (для restricted маршрутов) ### LoginPage Форма (email, пароль) Валидация через react-hook-form + zod Вызов login из хранилища Обработка ошибок, отображение сообщений После успеха - редирект на /dashboard ## Этап 2: Макет и навигация (1 день) ### AdminLayout (antd Layout) - Sider (боковое меню) - Header (хедер с именем пользователя, переключателем языка, кнопкой выхода) - Content (основная область) ### Меню и роли Пункты меню: Дашборд (/dashboard) — все роли Пользователи (/users) — superadmin, admin События (/events) — superadmin, admin Жалобы (/reports) — superadmin, admin, moderator Отзывы (/reviews) — superadmin, admin, moderator Бан-слова (/banned-words) — superadmin, admin Тикеты (/tickets) — superadmin, admin, support Подписки (/subscriptions) — superadmin, admin Администраторы (/admins) — только superadmin Аудит (/audit) — только superadmin Скрывать пункты в зависимости от роли текущего пользователя ### Маршруты (React Router v6) /login -> LoginPage / -> редирект на /dashboard /dashboard -> DashboardPage /users -> UserListPage /users/:id -> UserDetailPage /events -> EventListPage /events/:id -> EventDetailPage /reports -> ReportListPage /reports/:id -> ReportDetailPage /reviews -> ReviewListPage /banned-words -> BannedWordsPage /tickets -> TicketListPage /tickets/:id -> TicketDetailPage /subscriptions -> SubscriptionListPage /admins -> AdminListPage /audit -> AuditPage ## Этап 3: Интернационализация (1 день) - Создать src/i18n/locales/ru.json и en.json - Все ключи для текстов интерфейса (меню, заголовки, кнопки, сообщения) - Настроить react-i18next - Переключатель языка в хедере ## Этап 4: API сервисы (1 день) Для каждой сущности создать файл в src/api/ eventsApi.ts: getEvents(params: EventListParams): Promise<{data: Event[], total: number}> getEvent(id: string): Promise<Event> updateEvent(id: string, data: Partial<Event>): Promise<void> deleteEvent(id: string): Promise<void> usersApi.ts: getUsers(params: UserListParams): Promise<{data: User[], total: number}> getUser(id: string): Promise<User> updateUser(id: string, data: Partial<User>): Promise<void> deleteUser(id: string): Promise<void> reportsApi.ts: getReports(params: ReportListParams): Promise<{data: Report[], total: number}> getReport(id: string): Promise<Report> updateReport(id: string, data: {status: 'reviewed' | 'dismissed'}): Promise<void> reviewsApi.ts: getReviews(params: ReviewListParams): Promise<{data: Review[], total: number}> getReview(id: string): Promise<Review> updateReview(id: string, data: Partial<Review>): Promise<void> bulkUpdateReviews(updates: {id: string, status: string}[]): Promise<number> bannedWordsApi.ts: getBannedWords(params: PaginationParams): Promise<{data: BannedWord[], total: number}> addBannedWord(word: string): Promise<void> removeBannedWord(word: string): Promise<void> ticketsApi.ts: getTickets(params: TicketListParams): Promise<{data: Ticket[], total: number}> getTicket(id: string): Promise<Ticket> updateTicket(id: string, data: Partial<Ticket>): Promise<void> deleteTicket(id: string): Promise<void> getTicketStats(): Promise<TicketStats> subscriptionsApi.ts: getSubscriptions(params: SubscriptionListParams): Promise<{data: Subscription[], total: number}> getSubscription(id: string): Promise<Subscription> updateSubscription(id: string, data: Partial<Subscription>): Promise<void> deleteSubscription(id: string): Promise<void> adminsApi.ts: getAdmins(params: AdminListParams): Promise<{data: Admin[], total: number}> getAdmin(id: string): Promise<Admin> createAdmin(data: {email: string, password: string, role: string}): Promise<void> updateAdmin(id: string, data: Partial<Admin>): Promise<void> deleteAdmin(id: string): Promise<void> auditApi.ts: getAuditRecords(params: AuditListParams): Promise<{data: AuditRecord[], total: number}> dashboardApi.ts: getStats(from?: string, to?: string): Promise<DashboardStats> moderationApi.ts: moderate(targetType: string, id: string, action: string, reason?: string): Promise<void> ## Этап 5: Кастомные хуки React Query (1 день) Для каждой сущности создать хуки по шаблону: useEvents(params): useQuery(['events', params], () => eventsApi.getEvents(params)) useEvent(id): useQuery(['events', id], () => eventsApi.getEvent(id), { enabled: !!id }) useUpdateEvent(): useMutation(({id, data}) => eventsApi.updateEvent(id, data), { onSuccess: () => { queryClient.invalidateQueries(['events']) } }) Аналогично для всех остальных сущностей (User, Report, Review, Ticket, Subscription, Admin, Audit, BannedWord) Для массовых операций (bulkUpdateReviews) - соответствующий useMutation ## Этап 6: Переиспользуемые компоненты (1 день) ### DataTable - Обёртка над antd Table - Принимает: columns, data, loading, total, limit, offset, onChange - Синхронизация пагинации с параметрами запроса - Отображение общего количества (из X-Total-Count) ### StatusBadge - Принимает статус и тип сущности - Возвращает цветной Badge/Tag: Events: active (green), cancelled (red), completed (gray) Users: active (green), frozen (orange), deleted (red) Reports: pending (yellow), reviewed (green), dismissed (gray) Reviews: visible (green), hidden (orange), deleted (red) Tickets: open (red), in_progress (blue), resolved (green), closed (gray) Subscriptions: active (green), expired (orange), cancelled (red) ### ConfirmModal - Модальное окно подтверждения действий - Принимает: title, content, onOk, onCancel ### EmptyState - Отображение при отсутствии данных ### ErrorState - Отображение ошибки загрузки ## Этап 7: Страницы (10 дней) ### 7.1 Дашборд (1 день) - GET /v1/admin/stats - Карточки с метриками (antd Statistic): users, events, reviews, calendars, reports, tickets, subscriptions, active_subscriptions - Возможность выбора диапазона дат (from, to) ### 7.2 Пользователи (1 день) - Список (UserListPage): Таблица: email, nickname, role, status, last_login, created_at Фильтры: role, status, поиск (q) Пагинация через limit/offset Действия: редактировать, изменить статус (block/unblock) - Детали/редактирование (UserDetailPage): Форма: role, status, reason Кнопка "Сохранить" ### 7.3 События (1 день) - Список (EventListPage): Таблица: title, calendar_id, start_time, duration, status, rating_avg, rating_count Фильтры: status, calendar_id, from, to, поиск (q) Действия: редактировать, freeze/unfreeze - Детали/редактирование (EventDetailPage): Форма: все поля из update-схемы (title, description, start_time, duration, status, specialist_id, location, tags, capacity, online_link, calendar_id, event_type, recurrence) Кнопка "Сохранить" ### 7.4 Жалобы (1 день) - Список (ReportListPage): Таблица: reporter_id, target_type, target_id, reason, status, created_at Фильтры: status, target_type, поиск (q) Действия: просмотр деталей - Детали (ReportDetailPage): Полная информация Кнопки: "Рассмотрено" (reviewed), "Отклонено" (dismissed) Модерация: кнопка "Скрыть контент" (PUT /v1/admin/:target_type/:id) ### 7.5 Отзывы (1 день) - Список (ReviewListPage): Таблица: user_id, target_type, target_id, rating (звезды), comment, status, likes, dislikes Фильтры: target_type, target_id, user_id, status Массовое действие: выбор нескольких отзывов и изменение статуса (PATCH /v1/admin/reviews) Действия: скрыть/показать/удалить (индивидуально) - Детали (ReviewDetailPage): форма редактирования (status, reason, comment, rating) ### 7.6 Бан-слова (1 день) - Страница (BannedWordsPage): Таблица: word, added_by, added_at Форма добавления над таблицей: одно поле word Кнопка удаления в каждой строке ### 7.7 Тикеты (1 день) - Список (TicketListPage): Карточки статистики сверху: open, in_progress, resolved, closed, total (из /tickets/stats) Таблица: error_message, status, assigned_to, count, first_seen, last_seen Фильтры: status, assigned_to, поиск (q) Действия: просмотр деталей - Детали (TicketDetailPage): Полная информация Форма обновления: status, assigned_to, resolution_note Кнопка "Сохранить" ### 7.8 Подписки (1 день) - Список (SubscriptionListPage): Таблица: user_id, plan, status, trial_used, started_at, expires_at Фильтры: plan, status Действия: редактировать - Редактирование (модально или inline): Форма: plan, status, trial_used, expires_at ### 7.9 Администраторы (только superadmin) (1 день) - Список (AdminListPage): Таблица: email, nickname, role, status, last_login, created_at Фильтры: role, status Кнопка "Добавить администратора" Действия: редактировать, удалить - Форма создания/редактирования (AdminFormPage): Создание: email, password, role Редактирование: nickname, email, role, status, timezone, language, phone ### 7.10 Аудит (только superadmin) (1 день) - Страница (AuditPage): Таблица: admin_id, email, role, action, entity_type, entity_id, timestamp, ip, reason Фильтры: admin_id, action, date_from, date_to Только просмотр ## Этап 8: WebSocket real-time уведомления (1 день) ### Хук useAdminWebSocket - Подключение к wss://<домен>/admin/ws?token=<jwt> - Подписка на каналы: reports, tickets - Обработка входящих сообщений: { type: "new_report", data: ... } -> инвалидировать ['reports'], уведомление { type: "new_ticket", data: ... } -> инвалидировать ['tickets'], ['ticket-stats'], уведомление - Автоматическое переподключение при обрыве (с exponential backoff) - Пинг/понг каждые 30 секунд ### Интеграция в интерфейс - В хедере значок уведомлений со счётчиком непрочитанных - При новом событии показывать antd notification и обновлять список через queryClient.invalidateQueries ## Этап 9: Сборка и деплой (1 день) - Настроить vite.config.ts с proxy для разработки - Переменные окружения для dev/stage/prod - Собрать production билд (npm run build) - Настроить Nginx: - раздача статических файлов - reverse proxy к API (location /v1/ -> backend) - проксирование WebSocket (location /admin/ws -> wss://backend) - Настройка CORS на бэкенде для домена фронтенда ## Итоговая оценка времени Этап 0: Инициализация проекта - 1 день Этап 1: Аутентификация и HTTP-клиент - 2 дня Этап 2: Макет и навигация - 1 день Этап 3: i18n - 1 день Этап 4: API сервисы - 1 день Этап 5: Кастомные хуки - 1 день Этап 6: Переиспользуемые компоненты - 1 день Этап 7: Страницы - 10 дней Этап 8: WebSocket - 1 день Этап 9: Сборка и деплой - 1 день Всего: 20 рабочих дней --- # Типы данных EventHub Admin API ## Event (Событие) interface Event { id: string; calendar_id: string; title: string; description: string; event_type: 'single' | 'recurring'; start_time: string; // ISO8601 duration: number; recurrence: object | null; master_id: string | null; is_instance: boolean; specialist_id: string | null; location: object | null; tags: string[]; capacity: number | null; online_link: string | null; status: 'active' | 'cancelled' | 'completed'; reason: string | null; rating_avg: number; rating_count: number; attachments: string[] | null; edit_history: object[] | null; created_at: string; // ISO8601 updated_at: string; // ISO8601 } // Параметры списка: from, to, status, calendar_id, title, q, limit, offset, sort, order // Поля обновления: title, description, start_time, duration, status, specialist_id, location, tags, capacity, online_link, calendar_id, event_type, recurrence ## User (Пользователь) interface User { id: string; email: string; role: 'user' | 'bot'; status: 'active' | 'frozen' | 'deleted'; reason: string | null; nickname: string | null; avatar_url: string | null; timezone: string | null; language: string | null; social_links: string[] | null; phone: string | null; preferences: object | null; last_login: string; // ISO8601 created_at: string; // ISO8601 updated_at: string; // ISO8601 } // Параметры списка: role, status, q, limit, offset // Поля обновления: role, status, reason ## Report (Жалоба) interface Report { id: string; reporter_id: string; target_type: 'calendar' | 'event' | 'review'; target_id: string; reason: string; status: 'pending' | 'reviewed' | 'dismissed'; created_at: string; resolved_at: string | null; resolved_by: string | null; } // Параметры списка: status, target_type, q, limit, offset // Поля обновления: status ('reviewed' | 'dismissed') ## Review (Отзыв) interface Review { id: string; user_id: string; target_type: 'calendar' | 'event'; target_id: string; rating: number; // 1-5 comment: string; status: 'visible' | 'hidden' | 'deleted'; reason: string | null; likes: number; dislikes: number; created_at: string; updated_at: string; } // Параметры списка: target_type, target_id, user_id, status, limit, offset // Массовое обновление: PATCH /v1/admin/reviews [{id, status}] // Поля обновления для одного отзыва: status, reason, comment, rating ## Banned Word (Бан-слово) interface BannedWord { id: string; word: string; added_by: string | null; added_at: string | null; } // POST: {word: string} // DELETE: /v1/admin/banned-words/:word ## Ticket (Тикет баг-трекера) interface Ticket { id: string; reporter_id: string; error_hash: string; error_message: string; stacktrace: string; context: string; count: number; first_seen: string; last_seen: string; status: 'open' | 'in_progress' | 'resolved' | 'closed'; assigned_to: string | null; resolution_note: string | null; } // Параметры списка: status, assigned_to, q, limit, offset // Поля обновления: status, assigned_to, resolution_note ## Subscription (Подписка) interface Subscription { id: string; user_id: string; plan: 'monthly' | 'quarterly' | 'biannual' | 'annual'; status: 'active' | 'expired' | 'cancelled'; trial_used: boolean; started_at: string; expires_at: string; created_at: string; updated_at: string; } // Параметры списка: plan, status, limit, offset // Поля обновления: plan, status, trial_used, expires_at ## Admin (Администратор) interface Admin { id: string; email: string; role: 'superadmin' | 'admin' | 'moderator' | 'support'; status: 'active' | 'blocked'; nickname: string | null; avatar_url: string | null; timezone: string | null; language: string | null; phone: string | null; preferences: object | null; last_login: string; created_at: string; updated_at: string; } // POST создание: email, password, role // Поля обновления: nickname, email, role, status, timezone, language, phone, preferences ## Audit (Запись аудита) interface AuditRecord { id: string; admin_id: string; email: string; role: string; action: string; entity_type: string; entity_id: string; timestamp: string; ip: string; reason: string | null; } // Параметры: admin_id, action, date_from, date_to, limit, offset ## Stats (Статистика дашборда) interface DashboardStats { users: number; events: number; reviews: number; calendars: number; reports: number; tickets: number; subscriptions: number; active_subscriptions: number; } ## Ticket Stats (Статистика тикетов) interface TicketStats { open: number; in_progress: number; resolved: number; closed: number; total: number; } ## Moderation (Модерация) type TargetType = 'calendar' | 'event' | 'review' | 'user'; type ModerationAction = { calendar: 'freeze' | 'unfreeze'; event: 'freeze' | 'unfreeze'; review: 'hide' | 'unhide'; user: 'block' | 'unblock'; }; // PUT /v1/admin/:target_type/:id // Body: {action: string, reason?: string} ## Аутентификация // POST /v1/admin/login // Request: { email: string, password: string } // Response: { access_token: string, refresh_token: string, ... } // POST /v1/refresh // Request: { refresh_token: string } // Response: { access_token: string, refresh_token: string } ## Пагинация // Все списки возвращают заголовки: // Content-Range: items 0-49/200 // X-Total-Count: 200 // Параметры: limit, offset
aleksey added the Epic label 2026-05-05 19:14:45 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: EventHub/EventHubFrontAdmin#1