Files
chatballs/apps/internal-ui/src/features/employees/api.ts
T
AndreyandClaude Fable 5.1 145c44a17d ✨ feat(internal-ui): несколько организаций — переключатель, разделы администратора установки, приглашения
Переключатель организаций в сайдбаре (дизайн-базлайн v2, A1) показывает все
организации человека, текущая отмечена, выбор другой пересобирает Shell по
ключу организации. Вход без организации в адресе открывает последнюю
открытую или первую по списку вместо экрана «нет доступа»; предпочтение
переехало в localStorage.

Разделы «Платформа» и «Хранилище файлов» видны только администратору
установки, relay для звонков у остальных менеджеров только на чтение;
настройки установки запрашиваются с /api/v1/instance/.

Ссылка-приглашение /join: вошедший принимает приглашение и попадает в новую
организацию, гость без учётной записи задаёт имя и пароль полями мастера
первого запуска. Ожидающие приглашения показаны в списке сотрудников
строками «Приглашён» с меню «отправить ещё раз» и «отозвать» — теми же
элементами, что строка сотрудника.

e2e: проекту internal-ui задана русская локаль браузера, моки сессии отдают
язык установки — сценарии перестали зависеть от языка машины; добавлены
сценарии переключателя, строки приглашения и регистрации гостя.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 14:37:52 +03:00

99 lines
4.4 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.
import { api } from "../../api/client";
import type { PagedPayload } from "../../shared/usePagedResource";
import type { Employee, EmployeeInvitation, Role } from "../../types";
import type { EmployeeRoleFilter } from "./model";
// Действия над сотрудником (дизайн-базлайн v2, кадры E2, E3, E5–E8).
// Пароль первичного доступа сервер отдаёт ровно один раз — открытым он нигде
// не хранится, поэтому ответ показывается сразу в диалоге.
export type PasswordMode = "mail" | "show";
export type IssuedPassword = {
employee: Employee;
password: string;
reason: "created" | "reset";
};
export type EmployeeCreateInput = {
fullName: string;
phone: string;
email: string;
role: Role;
positionTitle: string;
groupIds: number[];
passwordMode: PasswordMode;
};
export async function createEmployee(input: EmployeeCreateInput): Promise<IssuedPassword | null> {
// Если адрес уже принадлежит учётной записи из другой организации, сервер
// вместо сотрудника выписывает приглашение: employee и password пусты,
// человек появится в списке после того, как примет его.
const payload = await api<{ employee: Employee | null; password: string | null; invited?: boolean }>("/api/v1/employees/operators/", {
method: "POST",
body: JSON.stringify(input),
});
return payload.password && payload.employee
? { employee: payload.employee, password: payload.password, reason: "created" }
: null;
}
export async function resetEmployeePassword(userId: number, mode: PasswordMode): Promise<IssuedPassword | null> {
const payload = await api<{ employee: Employee; password: string | null }>(`/api/v1/employees/${userId}/reset-password/`, {
method: "POST",
body: JSON.stringify({ mode }),
});
return payload.password ? { employee: payload.employee, password: payload.password, reason: "reset" } : null;
}
export async function terminateEmployeeSessions(userId: number): Promise<void> {
await api(`/api/v1/employees/${userId}/revoke-sessions/`, { method: "POST" });
}
export async function blockEmployee(userId: number, block: boolean): Promise<void> {
await api(`/api/v1/employees/${userId}/${block ? "block" : "unblock"}/`, { method: "POST" });
}
// Список сотрудников: страница, роль, группа и поиск считает сервер (кадры E1/E2).
export type EmployeeListQuery = {
role: EmployeeRoleFilter;
groupId: number | "all";
query: string;
};
// Вместе со страницей сервер отдаёт ожидающие приглашения — их мало и они
// не листаются.
export type EmployeesPayload = PagedPayload<Employee> & { invitations?: EmployeeInvitation[] };
export function fetchEmployees(
{ role, groupId, query }: EmployeeListQuery,
page: number,
): Promise<EmployeesPayload> {
const params = new URLSearchParams({ page: String(page) });
if (role !== "all") params.set("role", role);
if (groupId !== "all") params.set("group", String(groupId));
if (query.trim()) params.set("q", query.trim());
return api<EmployeesPayload>(`/api/v1/employees/?${params.toString()}`);
}
export async function resendInvitation(invitationId: number): Promise<void> {
await api(`/api/v1/employees/invitations/${invitationId}/resend/`, { method: "POST" });
}
export async function revokeInvitation(invitationId: number): Promise<void> {
await api(`/api/v1/employees/invitations/${invitationId}/revoke/`, { method: "POST" });
}
/** Кандидаты на владение (кадр E9) — только действующие администраторы.
* Роль и поиск отбирает сервер; если админов больше страницы, у выбора
* появляется строка поиска. */
export function fetchOwnershipCandidates(query = ""): Promise<PagedPayload<Employee>> {
const params = new URLSearchParams({ role: "ADMIN", pageSize: "100" });
if (query.trim()) params.set("q", query.trim());
return api<PagedPayload<Employee>>(`/api/v1/employees/?${params.toString()}`);
}
export function fetchOwner(): Promise<PagedPayload<Employee>> {
return api<PagedPayload<Employee>>("/api/v1/employees/?role=OWNER");
}