48ede1420d
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/12
136 lines
5.5 KiB
TypeScript
136 lines
5.5 KiB
TypeScript
/* eslint-disable @rushstack/no-new-null -- поля "сырых" IPortal*-интерфейсов 1:1 зеркалят nullable JSON-контракт портала api.tnsu.kz */
|
|
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
|
|
import { Company, Department, Employee, Project } from '../models';
|
|
import { ApiClientError } from './httpClient';
|
|
|
|
// Справочники берём напрямую из api.tnsu.kz — тем же AAD-токеном, что и наш API:
|
|
// в appsettings.json бэкенда Entra:Audience и TnsuPortal:ClientId — один и тот же
|
|
// GUID, то есть обе службы живут на одной регистрации Entra. Прокси не нужен.
|
|
//
|
|
// Два отличия от нашего API, из-за которых здесь отдельный клиент:
|
|
// 1) портал отдаёт «голый» массив, без конверта { data: ... };
|
|
// 2) ключи в PascalCase (Id/Name/CompanyId/Company).
|
|
|
|
interface IPortalCompany {
|
|
Id: string;
|
|
Name: string;
|
|
}
|
|
|
|
interface IPortalDepartment {
|
|
Id: string;
|
|
Name: string;
|
|
CompanyId: string;
|
|
Company: string;
|
|
}
|
|
|
|
// GET /Project?name=&page=&pageSize= — в отличие от /Dictionary/*, здесь портал
|
|
// уже отдаёт camelCase и постраничный конверт (данные идут из своей БД, а не
|
|
// из сырых 1С-справочников).
|
|
interface IPortalProject {
|
|
oid: string;
|
|
name: string;
|
|
code: string | null;
|
|
status: string | null;
|
|
}
|
|
|
|
interface IPortalProjectPage {
|
|
items: IPortalProject[];
|
|
totalCount: number;
|
|
page: number;
|
|
pageSize: number;
|
|
totalPages: number;
|
|
}
|
|
|
|
export interface IProjectSearchResult {
|
|
items: Project[];
|
|
hasMore: boolean;
|
|
}
|
|
|
|
// GET /Employee/All — уже camelCase, как /Project (не PascalCase, как /Dictionary/*).
|
|
// Строка на каждого сотрудника в каждом его отделе, поэтому один email может
|
|
// повторяться несколько раз; сотрудники без email в подборе исполнителя бесполезны.
|
|
interface IPortalEmployee {
|
|
fio: string;
|
|
email: string | null;
|
|
position: string | null;
|
|
company: string | null;
|
|
}
|
|
|
|
async function getJson<T>(client: AadHttpClient, baseUrl: string, path: string): Promise<T> {
|
|
const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
|
const url = new URL(path.replace(/^\//, ''), normalizedBase).toString();
|
|
|
|
const response: HttpClientResponse = await client.get(url, AadHttpClient.configurations.v1);
|
|
const text = await response.text();
|
|
|
|
if (!response.ok) {
|
|
throw new ApiClientError(response.status, [
|
|
`Справочник api.tnsu.kz вернул HTTP ${response.status}${text ? `: ${text}` : ''}`
|
|
]);
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(text) as T;
|
|
} catch {
|
|
throw new ApiClientError(response.status, ['Справочник api.tnsu.kz вернул не-JSON ответ.']);
|
|
}
|
|
}
|
|
|
|
export class PortalDictionaryClient {
|
|
constructor(private readonly client: AadHttpClient, private readonly baseUrl: string) {}
|
|
|
|
public async companies(): Promise<Company[]> {
|
|
const raw = await getJson<IPortalCompany[]>(this.client, this.baseUrl, '/Dictionary/Companies');
|
|
return (raw ?? []).map(x => ({ id: x.Id, name: x.Name }));
|
|
}
|
|
|
|
public async departments(): Promise<Department[]> {
|
|
const raw = await getJson<IPortalDepartment[]>(this.client, this.baseUrl, '/Dictionary/Departments');
|
|
return (raw ?? []).map(x => ({
|
|
id: x.Id,
|
|
name: x.Name,
|
|
companyId: x.CompanyId,
|
|
company: x.Company
|
|
}));
|
|
}
|
|
|
|
public async searchProjects(name: string, page: number, pageSize = 20): Promise<IProjectSearchResult> {
|
|
const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) });
|
|
if (name.trim()) params.set('name', name.trim());
|
|
|
|
const raw = await getJson<IPortalProjectPage>(this.client, this.baseUrl, `/Project?${params.toString()}`);
|
|
return {
|
|
items: (raw.items ?? []).map(x => ({ oid: x.oid, name: x.name, code: x.code, status: x.status })),
|
|
hasMore: raw.page < raw.totalPages
|
|
};
|
|
}
|
|
|
|
public async employees(): Promise<Employee[]> {
|
|
const raw = await getJson<IPortalEmployee[]>(this.client, this.baseUrl, '/Employee/All');
|
|
|
|
const byEmail = new Map<string, Employee>();
|
|
(raw ?? []).forEach(x => {
|
|
if (!x.email) return;
|
|
const existing = byEmail.get(x.email);
|
|
if (!existing) {
|
|
byEmail.set(x.email, {
|
|
fio: x.fio,
|
|
email: x.email,
|
|
position: x.position || '',
|
|
company: x.company || '',
|
|
// Одна строка на каждую компанию, где числится сотрудник (совместители/
|
|
// бухгалтерия на несколько юрлиц — не редкость), companies собирает их
|
|
// все, а company остаётся первой встреченной (для пикера/отображения,
|
|
// где нужна ровно одна строка на человека).
|
|
companies: x.company ? [x.company] : []
|
|
});
|
|
} else {
|
|
if (!existing.position && x.position) existing.position = x.position;
|
|
if (!existing.company && x.company) existing.company = x.company;
|
|
if (x.company && existing.companies.indexOf(x.company) === -1) existing.companies.push(x.company);
|
|
}
|
|
});
|
|
return Array.from(byEmail.values());
|
|
}
|
|
}
|