/* 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(client: AadHttpClient, baseUrl: string, path: string): Promise { 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 { const raw = await getJson(this.client, this.baseUrl, '/Dictionary/Companies'); return (raw ?? []).map(x => ({ id: x.Id, name: x.Name })); } public async departments(): Promise { const raw = await getJson(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 { const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) }); if (name.trim()) params.set('name', name.trim()); const raw = await getJson(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 { const raw = await getJson(this.client, this.baseUrl, '/Employee/All'); const byEmail = new Map(); (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()); } }