Обходной лист (#12)
Tests / backend-test (push) Successful in 1m38s
Tests / frontend-test (push) Successful in 47s

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/12
This commit was merged in pull request #12.
This commit is contained in:
2026-08-17 05:43:52 +00:00
parent d076f4e98d
commit 48ede1420d
37 changed files with 1351 additions and 435 deletions
@@ -1,6 +1,7 @@
import * as React from 'react';
import styles from './RoutesScreen.module.scss';
import { RouteFormModal } from './RouteFormModal';
import { RouteCopyModal } from './RouteCopyModal';
import { ApprovalRouteApi } from '../../services/api';
import { PortalDictionaryClient } from '../../services/portalDictionary';
import { TemplateRoute } from '../../models';
@@ -10,6 +11,8 @@ import { useRequestTypes } from '../../hooks/useRequestTypes';
import { useTemplateRoutes } from '../../hooks/useTemplateRoutes';
import { usePortalDictionaries } from '../../hooks/usePortalDictionaries';
import { useDeleteTarget } from '../../hooks/useDeleteTarget';
import { useLocalStorageState } from '../../hooks/useLocalStorageState';
import { replaceRoute } from '../../routing';
import {
ActionsCell,
Alert,
@@ -28,18 +31,46 @@ import {
export interface IRoutesScreenProps {
api: ApprovalRouteApi;
dictionary?: PortalDictionaryClient;
/** Фильтр из ссылки (aarfilter) — перекрывает сохранённый, если задан. */
initialFilterRequestTypeId?: string;
onOpenSteps: (route: TemplateRoute) => void;
}
export const RoutesScreen: React.FC<IRoutesScreenProps> = ({ api, dictionary, onOpenSteps }) => {
// Фильтр по типу заявки переживает перезагрузку: администратор обычно работает
// с одним типом заявок подряд, и сбрасывать выбор на каждый заход неудобно.
const ROUTES_FILTER_STORAGE_KEY = 'approvalAdmin.routesFilter';
export const RoutesScreen: React.FC<IRoutesScreenProps> = ({
api,
dictionary,
initialFilterRequestTypeId,
onOpenSteps
}) => {
const requestTypes = useRequestTypes(api);
const portalDictionaries = usePortalDictionaries(dictionary);
const [filterRequestTypeId, setFilterRequestTypeId] = React.useState('');
const [filterRequestTypeId, setFilterRequestTypeId] = useLocalStorageState(ROUTES_FILTER_STORAGE_KEY, '');
// Открыли по ссылке с фильтром — она сильнее сохранённого значения. Только на
// монтировании: дальше источник истины — сам экран (он же пишет фильтр в URL
// ниже), иначе собственный replaceRoute тут же вернулся бы сюда как «внешний».
React.useEffect(() => {
if (initialFilterRequestTypeId) setFilterRequestTypeId(initialFilterRequestTypeId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Держим URL синхронным с фильтром — ссылку на отфильтрованный список можно
// скопировать. replaceRoute, а не navigate: смена фильтра не должна плодить
// записи в истории браузера.
React.useEffect(() => {
replaceRoute({ tab: 'routes', routesFilter: filterRequestTypeId });
}, [filterRequestTypeId]);
const routes = useTemplateRoutes(api, filterRequestTypeId ? Number(filterRequestTypeId) : undefined);
const [editing, setEditing] = React.useState<TemplateRoute | undefined>();
const [showForm, setShowForm] = React.useState(false);
const [copying, setCopying] = React.useState<TemplateRoute | undefined>();
const deleteTarget = useDeleteTarget<TemplateRoute>(item => api.templateRoutes.remove(item.id), routes.reload);
@@ -87,6 +118,7 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({ api, dictionary, on
<DataTable
columns={[
{ key: 'id', header: texts.routes.columnId, width: '90px', render: r => `#${r.id}` },
{
key: 'requestType',
header: texts.routes.columnRequestType,
@@ -115,6 +147,7 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({ api, dictionary, on
render: r => (
<ActionsCell>
<Button size="sm" variant="secondary" icon="clipboard" onClick={() => onOpenSteps(r)}>{texts.routes.openSteps}</Button>
<IconButton icon="copy" label={texts.routes.copy} onClick={() => setCopying(r)} />
<IconButton icon="edit" label={texts.common.edit} onClick={() => openEdit(r)} />
<IconButton icon="trash" label={texts.common.remove} danger onClick={() => deleteTarget.ask(r)} />
</ActionsCell>
@@ -140,6 +173,19 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({ api, dictionary, on
/>
)}
{copying && (
<RouteCopyModal
api={api}
source={copying}
requestTypeName={requestTypeName(copying.requestTypeId)}
companies={portalDictionaries.companies}
departments={portalDictionaries.departments}
dictionaryLoading={portalDictionaries.isLoading}
onCopied={routes.reload}
onClose={() => setCopying(undefined)}
/>
)}
{deleteTarget.target && (
<ConfirmDialog
title={texts.routes.deleteTitle}