Files
vidconf/frontend/src/api/admin.ts
Max Ronzhin 09215da22a feat(admin): вкладки фильтра по статусу в списке пользователей
Задача 3 переопределена оператором: вместо окончательного удаления
пользователя (упёрлось в CHECK-constraint'ы participant/chat_messages,
требующие миграции схемы — решили отложить) добавлены вкладки
«Активные»/«Заблокированные»/«Все» перед полем поиска в админке —
список фильтруется по `is_blocked` на бэкенде (GET /admin/users?status=).
2026-07-27 20:59:29 +03:00

294 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* API-клиент админ-эндпоинтов (`/api/v1/admin/*`).
* Списковые DTO (`AdminConferenceOut`/`AdminUserOut`) отдаются в стандартном
* конверте пагинации `items`/`total`.
*/
import { apiRequest } from '@/api/client'
import type { ConferenceRecurrence, ConferenceStatus, 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
}
/** Тело частичного обновления настроек инстанса — все поля опциональны. */
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
}
/** Тело запроса тестовой отправки письма (`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<T> {
items: T[]
total: number
}
function toQueryString(params: Record<string, string | number | undefined>): 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<PagedResult<AdminConferenceOut>> {
const qs = toQueryString({ status: query.status, q: query.q, limit: query.limit, offset: query.offset })
return apiRequest<PagedResult<AdminConferenceOut>>(`/admin/conferences${qs}`)
}
/** Частично обновить конференцию от имени администратора (реюз `ConferenceService.update`). */
export async function updateAdminConference(
id: string,
payload: AdminConferenceUpdatePayload,
): Promise<AdminConferenceOut> {
return apiRequest<AdminConferenceOut>(`/admin/conferences/${id}`, { method: 'PATCH', body: payload })
}
/** Удалить конференцию навсегда от имени администратора. 409 — конференция сейчас идёт. */
export async function deleteAdminConference(id: string): Promise<void> {
await apiRequest(`/admin/conferences/${id}`, { method: 'DELETE' })
}
/**
* Ручная рассылка .ics-приглашений по конференции. Без `emails` — получатели
* по умолчанию (владелец + участники прошлых сеансов для закреплённых).
* 202 — задача поставлена в очередь, письма отправляются асинхронно.
*/
export async function sendConferenceInvitations(id: string, emails?: string[]): Promise<void> {
await apiRequest(`/admin/conferences/${id}/invitations`, {
method: 'POST',
body: emails && emails.length > 0 ? { emails } : {},
})
}
/** Список пользователей для админки — с поиском и пагинацией. */
export async function listAdminUsers(query: AdminUserQuery = {}): Promise<PagedResult<AdminUserOut>> {
const qs = toQueryString({ status: query.status, q: query.q, limit: query.limit, offset: query.offset })
return apiRequest<PagedResult<AdminUserOut>>(`/admin/users${qs}`)
}
/** Сменить роль и/или блокировку пользователя. 409 — попытка изменить свою учётную запись. */
export async function updateAdminUser(id: string, payload: AdminUserUpdatePayload): Promise<AdminUserOut> {
return apiRequest<AdminUserOut>(`/admin/users/${id}`, { method: 'PATCH', body: payload })
}
/**
* Создать пользователя от имени администратора. 201 — email
* считается подтверждённым сразу. 409 — email уже зарегистрирован,
* 404 `team_not_found` — указанная команда не существует.
*/
export async function createAdminUser(payload: AdminUserCreateIn): Promise<AdminUserOut> {
return apiRequest<AdminUserOut>('/admin/users', { method: 'POST', body: payload })
}
/** Карточка профиля пользователя — открывается по клику на имя в таблице «Пользователи». */
export async function getAdminUser(id: string): Promise<AdminUserDetailOut> {
return apiRequest<AdminUserDetailOut>(`/admin/users/${id}`)
}
/** Загрузить аватар пользователя от имени администратора. 413/415 — лимиты размера/типа файла. */
export async function uploadAdminUserAvatar(id: string, file: File): Promise<AdminUserDetailOut> {
const form = new FormData()
form.append('file', file)
return apiRequest<AdminUserDetailOut>(`/admin/users/${id}/avatar`, { method: 'POST', body: form })
}
/** Текущие настройки инстанса. */
export async function getAdminSettings(): Promise<SettingsOut> {
return apiRequest<SettingsOut>('/admin/settings')
}
/** Частично обновить настройки инстанса. 400 — недоступный уровень AI/невалидная таймзона. */
export async function updateAdminSettings(payload: SettingsUpdateIn): Promise<SettingsOut> {
return apiRequest<SettingsOut>('/admin/settings', { method: 'PUT', body: payload })
}
/**
* Отправить тестовое письмо синхронно — проверка почтовой конфигурации.
* Результат приходит в теле ответа (`success`/`message`), не через HTTP-статус ошибки.
*/
export async function sendTestEmail(payload: TestEmailIn = {}): Promise<TestEmailOut> {
return apiRequest<TestEmailOut>('/admin/settings/test-email', { method: 'POST', body: payload })
}
/** Список команд для админки — с пагинацией. */
export async function listAdminTeams(query: AdminTeamQuery = {}): Promise<PagedResult<TeamOut>> {
const qs = toQueryString({ limit: query.limit, offset: query.offset })
return apiRequest<PagedResult<TeamOut>>(`/admin/teams${qs}`)
}
/** Создать команду. 409 — имя уже занято. */
export async function createAdminTeam(name: string): Promise<TeamOut> {
return apiRequest<TeamOut>('/admin/teams', { method: 'POST', body: { name } })
}
/** Переименовать команду. 409 — имя уже занято. */
export async function renameAdminTeam(id: string, name: string): Promise<TeamOut> {
return apiRequest<TeamOut>(`/admin/teams/${id}`, { method: 'PATCH', body: { name } })
}
/** Удалить команду навсегда. */
export async function deleteAdminTeam(id: string): Promise<void> {
await apiRequest(`/admin/teams/${id}`, { method: 'DELETE' })
}