Поле включения/адреса в карточке «Контактный адрес» и кнопка «Отправить тестовое письмо» с выводом результата — рядом, по образцу соседних настроек AdminSettingsTab.
291 lines
13 KiB
TypeScript
291 lines
13 KiB
TypeScript
/**
|
||
* 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`) — `null`, если верификация выключена. */
|
||
registration_email_domain: string | null
|
||
/** Включён ли контактный адрес инстанса (подставляется в `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_domain?: string | null
|
||
/** Включение без 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 {
|
||
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({ 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' })
|
||
}
|