переделка компании и департамента
This commit is contained in:
@@ -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="По проекту">
|
||||
|
||||
Reference in New Issue
Block a user