переделка компании и департамента

This commit is contained in:
gorbenko.pavel
2026-07-29 15:50:53 +05:00
parent 8ef7acd93f
commit 3ed399becc
11 changed files with 446 additions and 20 deletions
@@ -1,7 +1,8 @@
import * as React from 'react';
import { ApprovalRouteApi } from '../../services/api';
import { ApiClientError } from '../../services/httpClient';
import { RequestType, routeTypeLabels, RouteType, TemplateRoute } from '../../models';
import { PortalDictionaryClient } from '../../services/portalDictionary';
import { Company, Department, RequestType, routeTypeLabels, RouteType, TemplateRoute } from '../../models';
import {
ActionsCell,
Alert,
@@ -17,12 +18,13 @@ import {
Modal,
PageHeader,
PrimaryCell,
Select,
TextInput
SearchableSelect,
Select
} from '../ui';
export interface IRoutesScreenProps {
api: ApprovalRouteApi;
dictionary?: PortalDictionaryClient;
onOpenSteps: (route: TemplateRoute) => void;
}
@@ -48,8 +50,12 @@ const ROUTE_TYPE_OPTIONS = Object.values(RouteType)
.filter(v => v !== RouteType.None)
.map(v => ({ value: v, label: routeTypeLabels[v] }));
export const RoutesScreen: React.FC<IRoutesScreenProps> = ({ api, onOpenSteps }) => {
export const RoutesScreen: React.FC<IRoutesScreenProps> = ({ api, dictionary, onOpenSteps }) => {
const [requestTypes, setRequestTypes] = React.useState<RequestType[]>([]);
const [companies, setCompanies] = React.useState<Company[]>([]);
const [departments, setDepartments] = React.useState<Department[]>([]);
const [dictLoading, setDictLoading] = React.useState(true);
const [dictError, setDictError] = React.useState<string | undefined>();
const [items, setItems] = React.useState<TemplateRoute[]>([]);
const [filterRequestTypeId, setFilterRequestTypeId] = React.useState<string>('');
const [loading, setLoading] = React.useState(true);
@@ -69,6 +75,22 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({ api, onOpenSteps })
api.requestTypes.list().then(setRequestTypes).catch(() => { /* показывается в общей ошибке ниже */ });
}, [api]);
// Справочники 1С грузим один раз на экран: они статичны, а отделов ~700 —
// поиск делаем на клиенте, без запроса на каждый символ.
React.useEffect(() => {
if (!dictionary) return;
setDictLoading(true);
setDictError(undefined);
Promise.all([dictionary.companies(), dictionary.departments()])
.then(([loadedCompanies, loadedDepartments]) => {
setCompanies(loadedCompanies);
setDepartments(loadedDepartments);
})
.catch((e: ApiClientError) => setDictError(e.message))
.finally(() => setDictLoading(false));
}, [dictionary]);
const load = React.useCallback(() => {
setLoading(true);
setLoadError(undefined);
@@ -84,6 +106,19 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({ api, onOpenSteps })
const requestTypeName = (id: number): string => requestTypes.find(rt => rt.id === id)?.name ?? `#${id}`;
// Пока справочник не загрузился (или маршрут ссылается на удалённую запись),
// показываем усечённый Guid — чтобы строка таблицы не выглядела пустой.
const companyName = (id: string): string => companies.find(c => c.id === id)?.name ?? shortGuid(id);
const departmentName = (id: string): string => departments.find(d => d.id === id)?.name ?? shortGuid(id);
// Отдел принадлежит компании, поэтому список сужаем под выбранную:
// иначе пользователь листает ~700 позиций из всех юрлиц сразу.
const departmentsForSelectedCompany = React.useMemo(
() => (form.companyId ? departments.filter(d => d.companyId === form.companyId) : []),
[departments, form.companyId]
);
const openCreate = (): void => {
setEditing(undefined);
setForm({ ...EMPTY_FORM, requestTypeId: filterRequestTypeId });
@@ -180,6 +215,7 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({ api, onOpenSteps })
</Field>
</div>
{dictError && <Alert kind="warn">Справочник компаний и отделов недоступен: {dictError}</Alert>}
{loadError && <Alert kind="err">{loadError}</Alert>}
{deleteError && <Alert kind="err">{deleteError}</Alert>}
@@ -195,8 +231,8 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({ api, onOpenSteps })
header: 'Тип маршрута',
render: r => <Badge variant={r.routeType === RouteType.Parallel ? 'blue' : 'gold'}>{routeTypeLabels[r.routeType]}</Badge>
},
{ key: 'company', header: 'Компания', render: r => <code>{shortGuid(r.companyId)}</code> },
{ key: 'department', header: 'Отдел', render: r => (r.departmentId ? <code>{shortGuid(r.departmentId)}</code> : '—') },
{ key: 'company', header: 'Компания', render: r => companyName(r.companyId) },
{ key: 'department', header: 'Отдел', render: r => (r.departmentId ? departmentName(r.departmentId) : '—') },
{
key: 'byProject',
header: 'По проекту',
@@ -260,18 +296,39 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({ api, onOpenSteps })
onChange={e => setForm({ ...form, routeType: e.target.value as RouteType })}
/>
</Field>
<Field label="Компания (Guid)" required full hint="Из справочника api.tnsu.kz.">
<TextInput
<Field label="Компания" required full hint="Справочник 1С (api.tnsu.kz).">
<SearchableSelect
options={companies.map(c => ({ value: c.id, label: c.name }))}
value={form.companyId}
onChange={e => setForm({ ...form, companyId: e.target.value })}
placeholder="00000000-0000-0000-0000-000000000000"
loading={dictLoading}
placeholder="Выберите компанию"
searchPlaceholder="Поиск по названию компании..."
onChange={companyId =>
// Смена компании обнуляет отдел: прежний принадлежал другой
// компании, и бэкенд не нашёл бы такой маршрут при resolve.
setForm({ ...form, companyId, departmentId: '' })
}
/>
</Field>
<Field label="Отдел (Guid)" full hint="Необязательно — маршрут «по умолчанию» для компании.">
<TextInput
<Field
label="Отдел"
full
hint={
form.companyId
? 'Необязательно — пусто означает маршрут «по умолчанию» для всей компании.'
: 'Сначала выберите компанию.'
}
>
<SearchableSelect
options={departmentsForSelectedCompany.map(d => ({ value: d.id, label: d.name }))}
value={form.departmentId}
onChange={e => setForm({ ...form, departmentId: e.target.value })}
placeholder="Оставьте пустым для маршрута по умолчанию"
loading={dictLoading}
disabled={!form.companyId}
allowClear
clearLabel="Без отдела (маршрут по умолчанию)"
placeholder="Без отдела (маршрут по умолчанию)"
searchPlaceholder="Поиск по названию отдела..."
onChange={departmentId => setForm({ ...form, departmentId })}
/>
</Field>
<Field label="По проекту">