fix: удалил эндпоинт Projects1C и перевел на актуальный Project
Tests / frontend-test (push) Successful in 1m9s
Tests / frontend-test (push) Successful in 1m9s
This commit is contained in:
@@ -110,18 +110,25 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
|||||||
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
||||||
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
||||||
const requestTypeId = requestTypeIdState.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(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
editId
|
editId
|
||||||
? approval.getMatrixForRequest(editId)
|
? approval.getMatrixForRequest(editId)
|
||||||
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval'),
|
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', null, undefined, projectId),
|
||||||
[context, userEmail, approval, editId, requestTypeId]
|
[context, userEmail, approval, editId, requestTypeId, projectId]
|
||||||
);
|
);
|
||||||
const matrix = useMatrixEditor(
|
const matrix = useMatrixEditor(
|
||||||
loadMatrix,
|
loadMatrix,
|
||||||
`${userEmail}-${editId ?? 'new'}`,
|
// projectId в ключе → при смене проекта маршрут перезапрашивается.
|
||||||
|
`${userEmail}-${editId ?? 'new'}-${projectId}`,
|
||||||
!readOnly,
|
!readOnly,
|
||||||
!!editId || !!requestTypeId,
|
matrixReady,
|
||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -278,7 +285,12 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
|||||||
</CollapseSection>
|
</CollapseSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix
|
||||||
|
editor={matrix}
|
||||||
|
search={(q) => approval.searchUsers(q)}
|
||||||
|
waiting={!matrixReady}
|
||||||
|
waitingText="Выберите «Объект/Проект» (п. 1.6), чтобы построить маршрут согласования."
|
||||||
|
/>
|
||||||
|
|
||||||
<SubmitActions
|
<SubmitActions
|
||||||
submit={submit}
|
submit={submit}
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { CollapseSection, FormField, TextInput, Select, PeopleField } from '../../../shared/ui';
|
import { CollapseSection, FormField, TextInput, Select, PeopleField } from '../../../shared/ui';
|
||||||
import { CompanyPickerMulti, Project1CPicker } from '../../../shared/dictionary';
|
import { CompanyPickerMulti, ProjectPicker } from '../../../shared/dictionary';
|
||||||
import {
|
import {
|
||||||
ISectionProps,
|
ISectionProps,
|
||||||
PERSON_PLACEHOLDER,
|
PERSON_PLACEHOLDER,
|
||||||
@@ -68,14 +68,14 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="1.6 Объект/Проект" htmlFor="ObjectProject" full>
|
<FormField label="1.6 Объект/Проект" htmlFor="ObjectProject" full>
|
||||||
<Project1CPicker
|
<ProjectPicker
|
||||||
context={context}
|
context={context}
|
||||||
id="ObjectProject"
|
id="ObjectProject"
|
||||||
value={data.ObjectProject}
|
value={data.ObjectProject}
|
||||||
onChange={(name, id, code) => {
|
onChange={(name, id, project) => {
|
||||||
set('ObjectProject', name);
|
set('ObjectProject', name);
|
||||||
set('ObjectProjectId', id);
|
set('ObjectProjectId', id); // oid из /Project — им же резолвится ByProject-маршрут
|
||||||
set('ObjectProjectCode', code);
|
set('ObjectProjectCode', project ? project.code : '');
|
||||||
}}
|
}}
|
||||||
placeholder="Начните вводить название или код проекта…"
|
placeholder="Начните вводить название или код проекта…"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,7 +10,3 @@ export { getCompanies } from './companies.service';
|
|||||||
export type { ICompany } from './models/ICompany';
|
export type { ICompany } from './models/ICompany';
|
||||||
export { CompanyPickerMulti } from './ui/CompanyPickerMulti';
|
export { CompanyPickerMulti } from './ui/CompanyPickerMulti';
|
||||||
export type { ICompanyPickerMultiProps } 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';
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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<IProject1C[]> {
|
|
||||||
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<IProject1C[]> | undefined;
|
|
||||||
export function getProjects1C(context: WebPartContext): Promise<IProject1C[]> {
|
|
||||||
if (!cache) {
|
|
||||||
cache = fetchProjects1C(context).catch((e) => {
|
|
||||||
cache = undefined;
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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<IProject1CPickerProps> = ({
|
|
||||||
context,
|
|
||||||
id,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
placeholder,
|
|
||||||
readOnly
|
|
||||||
}) => {
|
|
||||||
const contextReadOnly = useFormReadOnly();
|
|
||||||
const effectiveReadOnly = readOnly ?? contextReadOnly;
|
|
||||||
|
|
||||||
const [projects, setProjects] = React.useState<IProject1C[]>([]);
|
|
||||||
const [open, setOpen] = React.useState<boolean>(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 (
|
|
||||||
<div className={styles.wrap}>
|
|
||||||
<input
|
|
||||||
id={id}
|
|
||||||
className={styles.input}
|
|
||||||
value={value}
|
|
||||||
placeholder={placeholder}
|
|
||||||
readOnly={effectiveReadOnly}
|
|
||||||
onChange={(e) => {
|
|
||||||
onChange(e.target.value, '', '');
|
|
||||||
setOpen(true);
|
|
||||||
}}
|
|
||||||
onFocus={() => setOpen(true)}
|
|
||||||
onBlur={() => setOpen(false)}
|
|
||||||
/>
|
|
||||||
{!effectiveReadOnly && value.length > 0 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.clear}
|
|
||||||
aria-label="Очистить"
|
|
||||||
title="Очистить"
|
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
|
||||||
onClick={() => {
|
|
||||||
onChange('', '', '');
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!effectiveReadOnly && open && suggestions.length > 0 && (
|
|
||||||
<div className={styles.list}>
|
|
||||||
{suggestions.map((p) => (
|
|
||||||
<button
|
|
||||||
key={p.Id}
|
|
||||||
type="button"
|
|
||||||
className={styles.item}
|
|
||||||
// preventDefault на mousedown — иначе input теряет фокус (onBlur) раньше onClick.
|
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
|
||||||
onClick={() => handlePick(p)}
|
|
||||||
>
|
|
||||||
{p.ProjectName}
|
|
||||||
<span className={styles.meta}>{p.Code}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user