diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx index 3f601c2..a77df7e 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx @@ -110,18 +110,25 @@ const RecruitingSelectionForm: React.FC = ({ // data/requests.ts) → числовой RequestTypeId, нужный approval-route API. const requestTypeIdState = useRequestTypeId(context, requestType.id); const requestTypeId = requestTypeIdState.id; + // Маршрут подбора зависит от проекта (ByProject): без projectId бэкенд отвечает + // resolve: 404. projectId = oid выбранного объекта/проекта (поле ObjectProject). + const projectId = data.ObjectProjectId || null; + // Новая заявка: матрицу резолвим только когда есть и тип, и проект — иначе + // 404 «маршрут не найден». При редактировании читаем уже записанные строки. + const matrixReady = !!editId || (!!projectId && !!requestTypeId); const loadMatrix = useCallback( () => editId ? approval.getMatrixForRequest(editId) - : getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval'), - [context, userEmail, approval, editId, requestTypeId] + : getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', null, undefined, projectId), + [context, userEmail, approval, editId, requestTypeId, projectId] ); const matrix = useMatrixEditor( loadMatrix, - `${userEmail}-${editId ?? 'new'}`, + // projectId в ключе → при смене проекта маршрут перезапрашивается. + `${userEmail}-${editId ?? 'new'}-${projectId}`, !readOnly, - !!editId || !!requestTypeId, + matrixReady, (login) => approval.getJobTitle(login) ); @@ -278,7 +285,12 @@ const RecruitingSelectionForm: React.FC = ({ )} - approval.searchUsers(q)} /> + approval.searchUsers(q)} + waiting={!matrixReady} + waitingText="Выберите «Объект/Проект» (п. 1.6), чтобы построить маршрут согласования." + /> = ({ data, set, context /> - { + onChange={(name, id, project) => { set('ObjectProject', name); - set('ObjectProjectId', id); - set('ObjectProjectCode', code); + set('ObjectProjectId', id); // oid из /Project — им же резолвится ByProject-маршрут + set('ObjectProjectCode', project ? project.code : ''); }} placeholder="Начните вводить название или код проекта…" /> diff --git a/src/webparts/requestPortal/shared/dictionary/index.ts b/src/webparts/requestPortal/shared/dictionary/index.ts index 99de6d5..0fb3b8f 100644 --- a/src/webparts/requestPortal/shared/dictionary/index.ts +++ b/src/webparts/requestPortal/shared/dictionary/index.ts @@ -10,7 +10,3 @@ export { getCompanies } from './companies.service'; export type { ICompany } from './models/ICompany'; export { CompanyPickerMulti } from './ui/CompanyPickerMulti'; export type { ICompanyPickerMultiProps } from './ui/CompanyPickerMulti'; -export { getProjects1C } from './projects1c.service'; -export type { IProject1C } from './models/IProject1C'; -export { Project1CPicker } from './ui/Project1CPicker'; -export type { IProject1CPickerProps } from './ui/Project1CPicker'; diff --git a/src/webparts/requestPortal/shared/dictionary/models/IProject1C.ts b/src/webparts/requestPortal/shared/dictionary/models/IProject1C.ts deleted file mode 100644 index 6175256..0000000 --- a/src/webparts/requestPortal/shared/dictionary/models/IProject1C.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GET https://api.tnsu.kz/Dictionary/Projects1C → плоский массив -// { Id, Code, ProjectName } (PascalCase, без обёртки — как /Dictionary/Companies). -// Это отдельный справочник 1С-проектов; не путать с /Project (approval-route). -export interface IProject1C { - /** GUID проекта — стабильный идентификатор. */ - Id: string; - /** Код проекта (напр. «БК0000069»). */ - Code: string; - /** Наименование проекта — то, что видит пользователь. */ - ProjectName: string; -} diff --git a/src/webparts/requestPortal/shared/dictionary/projects1c.service.ts b/src/webparts/requestPortal/shared/dictionary/projects1c.service.ts deleted file mode 100644 index 91fab70..0000000 --- a/src/webparts/requestPortal/shared/dictionary/projects1c.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http'; -import { WebPartContext } from '@microsoft/sp-webpart-base'; -import { APPROVAL_API_CLIENT_ID, DICTIONARY_API_BASE_URL } from '../config'; -import { IProject1C } from './models/IProject1C'; - -// GET /Dictionary/Projects1C — справочник проектов 1С, ответ — плоский массив -// { Id, Code, ProjectName } (без обёртки, как /Dictionary/Companies). -async function fetchProjects1C(context: WebPartContext): Promise { - const client = await context.aadHttpClientFactory.getClient(APPROVAL_API_CLIENT_ID); - const response: HttpClientResponse = await client.get( - `${DICTIONARY_API_BASE_URL}/Dictionary/Projects1C`, - AadHttpClient.configurations.v1 - ); - if (!response.ok) { - throw new Error(`Dictionary/Projects1C: ${response.status} ${response.statusText}`); - } - return response.json(); -} - -// Справочник в рамках сессии не меняется — грузим один раз и отдаём тот же -// промис повторным вызовам (как в companies/departments.service). -let cache: Promise | undefined; -export function getProjects1C(context: WebPartContext): Promise { - if (!cache) { - cache = fetchProjects1C(context).catch((e) => { - cache = undefined; - throw e; - }); - } - return cache; -} diff --git a/src/webparts/requestPortal/shared/dictionary/ui/Project1CPicker.module.scss b/src/webparts/requestPortal/shared/dictionary/ui/Project1CPicker.module.scss deleted file mode 100644 index c7f87ec..0000000 --- a/src/webparts/requestPortal/shared/dictionary/ui/Project1CPicker.module.scss +++ /dev/null @@ -1,90 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.wrap { - position: relative; -} - -.input { - box-sizing: border-box; - width: 100%; - // справа место под крестик-очистку - padding: 11px 36px 11px 13px; - border: 1px solid $gray-300; - border-radius: $r-sm; - background: $white; - font-family: $font-sans; - font-size: 13.5px; - color: $gray-900; - transition: .18s; - - @include input-focus-gold; - @include readonly-field(':read-only'); - - &::placeholder { - color: $gray-400; - } -} - -.clear { - position: absolute; - top: 50%; - right: 8px; - transform: translateY(-50%); - display: flex; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - padding: 0; - border: 0; - border-radius: $r-sm; - background: transparent; - color: $gray-400; - font-size: 18px; - line-height: 1; - cursor: pointer; - transition: .15s; - - &:hover { - background: $gray-100; - color: $gray-700; - } -} - -.list { - position: absolute; - z-index: 20; - left: 0; - right: 0; - margin-top: $sp-1; - max-height: 260px; - overflow-y: auto; - background: $white; - border: 1px solid $gray-200; - border-radius: $r-sm; - box-shadow: $sh-2; -} - -.item { - display: flex; - flex-direction: column; - gap: 2px; - width: 100%; - padding: 9px 12px; - border: 0; - background: transparent; - text-align: left; - font-family: $font-sans; - font-size: $fs-sm; - color: $gray-900; - cursor: pointer; - - &:hover { - background: $navy-50; - } -} - -.meta { - font-size: $fs-caption; - color: $gray-500; -} diff --git a/src/webparts/requestPortal/shared/dictionary/ui/Project1CPicker.tsx b/src/webparts/requestPortal/shared/dictionary/ui/Project1CPicker.tsx deleted file mode 100644 index 25ee6a0..0000000 --- a/src/webparts/requestPortal/shared/dictionary/ui/Project1CPicker.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import * as React from 'react'; -import type { WebPartContext } from '@microsoft/sp-webpart-base'; -import styles from './Project1CPicker.module.scss'; -import { useFormReadOnly } from '@tnsu/ui-kit'; -import { getProjects1C } from '../projects1c.service'; -import { IProject1C } from '../models/IProject1C'; - -export interface IProject1CPickerProps { - context: WebPartContext; - id?: string; - /** Текущее наименование проекта (то, что видно в поле). */ - value: string; - /** - * name — введённый/выбранный текст; id — GUID проекта ('' при ручном вводе); - * code — код проекта ('' при ручном вводе). При выборе из списка приходят все три. - */ - onChange: (name: string, id: string, code: string) => void; - placeholder?: string; - /** Только чтение (режим согласующего). */ - readOnly?: boolean; -} - -const MAX_SUGGESTIONS = 30; - -/** Поле выбора проекта из справочника 1С (GET /Dictionary/Projects1C): ввод текста фильтрует список по названию/коду. */ -export const Project1CPicker: React.FC = ({ - context, - id, - value, - onChange, - placeholder, - readOnly -}) => { - const contextReadOnly = useFormReadOnly(); - const effectiveReadOnly = readOnly ?? contextReadOnly; - - const [projects, setProjects] = React.useState([]); - const [open, setOpen] = React.useState(false); - - React.useEffect(() => { - let alive = true; - getProjects1C(context) - .then((list) => { - if (alive) setProjects(list); - }) - .catch(() => undefined); - return () => { - alive = false; - }; - }, [context]); - - const suggestions = React.useMemo(() => { - const query = value.trim().toLowerCase(); - if (!query) return []; - return projects - .filter( - (p) => - p.ProjectName.toLowerCase().indexOf(query) !== -1 || - (p.Code && p.Code.toLowerCase().indexOf(query) !== -1) - ) - .slice(0, MAX_SUGGESTIONS); - }, [projects, value]); - - const handlePick = (project: IProject1C): void => { - onChange(project.ProjectName, project.Id, project.Code); - setOpen(false); - }; - - return ( -
- { - onChange(e.target.value, '', ''); - setOpen(true); - }} - onFocus={() => setOpen(true)} - onBlur={() => setOpen(false)} - /> - {!effectiveReadOnly && value.length > 0 && ( - - )} - {!effectiveReadOnly && open && suggestions.length > 0 && ( -
- {suggestions.map((p) => ( - - ))} -
- )} -
- ); -};