/** * API-клиент админ-эндпоинтов (`/api/v1/admin/*`). * Списковые DTO (`AdminConferenceOut`/`AdminUserOut`) отдаются в стандартном * конверте пагинации `items`/`total`. */ import { apiRequest } from '@/api/client' import type { ConferenceRecurrence, ConferenceStatus, PublishQualityCap, SummaryRecipientsMode } from '@/api/conferences' /** Уровень качества AI-обработки (транскрибация + суммаризация). */ export type AiLevel = 'min' | 'medium' | 'max' /** Доступность одного уровня AI с причиной отказа (если недоступен). */ export interface AiLevelStatus { level: AiLevel available: boolean reason: string | null } /** Эффективные настройки инстанса. */ export interface SettingsOut { chat_enabled: boolean /** Единый переключатель модуля AI (транскрибация + суммаризация). */ transcription_enabled: boolean /** Есть ли хотя бы один Celery-воркер, обслуживающий очередь транскрибации. */ transcription_queue_served: boolean ai_level: AiLevel ai_levels: AiLevelStatus[] summary_recipients: SummaryRecipientsMode /** IANA-таймзона отображения времени в письмах/.ics (напр. `Europe/Moscow`). */ display_timezone: string /** Разрешён ли выбор команды на экране регистрации. */ registration_team_choice: boolean /** Включена ли верификация регистрации по домену корпоративной почты. */ registration_email_domain_enabled: boolean /** Эталонные домены для верификации (напр. `['company.ru']`) — email подходит, если * совпадает с любым из списка; пуст, если верификация выключена. */ registration_email_domains: string[] /** Включён ли контактный адрес инстанса (подставляется в `Reply-To` исходящих писем). */ contact_email_enabled: boolean /** Контактный адрес — `null`, если не задан/выключен. */ contact_email: string | null /** Потолок качества исходящего видео публикующего — см. `PublishQualityCap`. */ publish_quality_cap: PublishQualityCap /** Максимум одновременно видимых плиток сцены (`StageGrid`). */ stage_max_tiles: number } /** Тело частичного обновления настроек инстанса — все поля опциональны. */ export interface SettingsUpdateIn { chat_enabled?: boolean transcription_enabled?: boolean /** Недоступный уровень (см. `ai_levels`) — backend отвечает 400. */ ai_level?: AiLevel summary_recipients?: SummaryRecipientsMode display_timezone?: string registration_team_choice?: boolean /** Включение с пустым списком или невалидным доменом — backend отвечает 400. */ registration_email_domain_enabled?: boolean registration_email_domains?: string[] /** Включение без email или невалидный email — backend отвечает 400. */ contact_email_enabled?: boolean contact_email?: string | null publish_quality_cap?: PublishQualityCap stage_max_tiles?: number } /** Тело запроса тестовой отправки письма (`POST /admin/settings/test-email`). */ export interface TestEmailIn { /** Не задан — backend отправит на email текущего администратора. */ to?: string | null } /** Результат тестовой отправки — успех или текст ошибки транспорта. */ export interface TestEmailOut { success: boolean message: string /** Хост/порт SMTP — только при `EMAIL_BACKEND=smtp`, без логина/пароля. */ smtp_host: string | null smtp_port: number | null } /** Конференция в ответе админ-списка — `ConferenceOut` + сведения о владельце. */ export interface AdminConferenceOut { id: string number: string slug: string title: string | null status: ConferenceStatus is_pinned: boolean is_closed: boolean scheduled_at: string | null duration_minutes: number | null recurrence: ConferenceRecurrence | null next_occurrence: string | null created_at: string summary_recipients: SummaryRecipientsMode | null owner_name: string | null owner_email: string | null } /** Параметры выборки списка конференций. */ export interface AdminConferenceQuery { status?: ConferenceStatus q?: string limit?: number offset?: number } /** Тело правки конференции админом — те же поля, что и в пользовательском API. */ export interface AdminConferenceUpdatePayload { title?: string scheduled_at?: string duration_minutes?: number is_pinned?: boolean recurrence?: ConferenceRecurrence is_closed?: boolean password?: string summary_recipients?: SummaryRecipientsMode | null } /** Пользователь в ответе админ-списка. */ export interface AdminUserOut { id: string email: string name_user: string role: 'admin' | 'user' is_blocked: boolean email_verified: boolean created_at: string /** Команда пользователя — `null`, если не назначена. */ team_id: string | null /** URL аватара с cache-busting (`?v=`) — `null`, если фото не загружено. */ avatar_url: string | null team_name: string | null } /** * Карточка профиля пользователя для админки — тот же * набор полей, что и в списке (backend отдаёт аватар и команду и в списке, * и в карточке). */ export type AdminUserDetailOut = AdminUserOut /** Параметры выборки списка пользователей. */ export interface AdminUserQuery { /** Фильтр по блокировке — без параметра отдаются все пользователи. */ status?: 'active' | 'blocked' q?: string limit?: number offset?: number } /** Тело правки пользователя — роль, блокировка, ФИО и/или команда, все поля опциональны. */ export interface AdminUserUpdatePayload { role?: 'admin' | 'user' is_blocked?: boolean name_user?: string /** `null` — снять пользователя с команды. */ team_id?: string | null } /** Тело создания пользователя администратором. */ export interface AdminUserCreateIn { name_user: string email: string /** Та же политика минимальной длины, что и при самостоятельной регистрации. */ password: string team_id?: string | null } /** Команда в ответе админ-списка. */ export interface TeamOut { id: string name: string created_at: string } /** Параметры выборки списка команд. */ export interface AdminTeamQuery { limit?: number offset?: number } /** Стандартный конверт пагинации списковых админ-эндпоинтов. */ export interface PagedResult { items: T[] total: number } function toQueryString(params: Record): string { const search = new URLSearchParams() for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== '') search.set(key, String(value)) } const qs = search.toString() return qs ? `?${qs}` : '' } /** Список конференций для админки — с фильтром по статусу, поиском и пагинацией. */ export async function listAdminConferences(query: AdminConferenceQuery = {}): Promise> { const qs = toQueryString({ status: query.status, q: query.q, limit: query.limit, offset: query.offset }) return apiRequest>(`/admin/conferences${qs}`) } /** Частично обновить конференцию от имени администратора (реюз `ConferenceService.update`). */ export async function updateAdminConference( id: string, payload: AdminConferenceUpdatePayload, ): Promise { return apiRequest(`/admin/conferences/${id}`, { method: 'PATCH', body: payload }) } /** Удалить конференцию навсегда от имени администратора. 409 — конференция сейчас идёт. */ export async function deleteAdminConference(id: string): Promise { await apiRequest(`/admin/conferences/${id}`, { method: 'DELETE' }) } /** * Ручная рассылка .ics-приглашений по конференции. Без `emails` — получатели * по умолчанию (владелец + участники прошлых сеансов для закреплённых). * 202 — задача поставлена в очередь, письма отправляются асинхронно. */ export async function sendConferenceInvitations(id: string, emails?: string[]): Promise { await apiRequest(`/admin/conferences/${id}/invitations`, { method: 'POST', body: emails && emails.length > 0 ? { emails } : {}, }) } /** Список пользователей для админки — с поиском и пагинацией. */ export async function listAdminUsers(query: AdminUserQuery = {}): Promise> { const qs = toQueryString({ status: query.status, q: query.q, limit: query.limit, offset: query.offset }) return apiRequest>(`/admin/users${qs}`) } /** Сменить роль и/или блокировку пользователя. 409 — попытка изменить свою учётную запись. */ export async function updateAdminUser(id: string, payload: AdminUserUpdatePayload): Promise { return apiRequest(`/admin/users/${id}`, { method: 'PATCH', body: payload }) } /** * Создать пользователя от имени администратора. 201 — email * считается подтверждённым сразу. 409 — email уже зарегистрирован, * 404 `team_not_found` — указанная команда не существует. */ export async function createAdminUser(payload: AdminUserCreateIn): Promise { return apiRequest('/admin/users', { method: 'POST', body: payload }) } /** Карточка профиля пользователя — открывается по клику на имя в таблице «Пользователи». */ export async function getAdminUser(id: string): Promise { return apiRequest(`/admin/users/${id}`) } /** Загрузить аватар пользователя от имени администратора. 413/415 — лимиты размера/типа файла. */ export async function uploadAdminUserAvatar(id: string, file: File): Promise { const form = new FormData() form.append('file', file) return apiRequest(`/admin/users/${id}/avatar`, { method: 'POST', body: form }) } /** Текущие настройки инстанса. */ export async function getAdminSettings(): Promise { return apiRequest('/admin/settings') } /** Частично обновить настройки инстанса. 400 — недоступный уровень AI/невалидная таймзона. */ export async function updateAdminSettings(payload: SettingsUpdateIn): Promise { return apiRequest('/admin/settings', { method: 'PUT', body: payload }) } /** * Отправить тестовое письмо синхронно — проверка почтовой конфигурации. * Результат приходит в теле ответа (`success`/`message`), не через HTTP-статус ошибки. */ export async function sendTestEmail(payload: TestEmailIn = {}): Promise { return apiRequest('/admin/settings/test-email', { method: 'POST', body: payload }) } /** Список команд для админки — с пагинацией. */ export async function listAdminTeams(query: AdminTeamQuery = {}): Promise> { const qs = toQueryString({ limit: query.limit, offset: query.offset }) return apiRequest>(`/admin/teams${qs}`) } /** Создать команду. 409 — имя уже занято. */ export async function createAdminTeam(name: string): Promise { return apiRequest('/admin/teams', { method: 'POST', body: { name } }) } /** Переименовать команду. 409 — имя уже занято. */ export async function renameAdminTeam(id: string, name: string): Promise { return apiRequest(`/admin/teams/${id}`, { method: 'PATCH', body: { name } }) } /** Удалить команду навсегда. */ export async function deleteAdminTeam(id: string): Promise { await apiRequest(`/admin/teams/${id}`, { method: 'DELETE' }) }