Фронтенд для сервиса (#5)
Tests / test (push) Successful in 2m26s

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/5
This commit was merged in pull request #5.
This commit is contained in:
2026-07-30 06:11:24 +00:00
parent 5476c1bab5
commit da158cfd84
114 changed files with 26045 additions and 2 deletions
@@ -0,0 +1,160 @@
import * as React from 'react';
import styles from './RoutesScreen.module.scss';
import { RouteFormModal } from './RouteFormModal';
import { ApprovalRouteApi } from '../../services/api';
import { PortalDictionaryClient } from '../../services/portalDictionary';
import { RouteType, TemplateRoute } from '../../models';
import { routeTypeLabels, texts } from '../../texts';
import { shortGuid } from '../../utils/guid';
import { useRequestTypes } from '../../hooks/useRequestTypes';
import { useTemplateRoutes } from '../../hooks/useTemplateRoutes';
import { usePortalDictionaries } from '../../hooks/usePortalDictionaries';
import { useDeleteTarget } from '../../hooks/useDeleteTarget';
import {
ActionsCell,
Alert,
Badge,
Breadcrumbs,
Button,
ConfirmDialog,
DataTable,
Field,
IconButton,
PageHeader,
PrimaryCell,
Select
} from '../ui';
export interface IRoutesScreenProps {
api: ApprovalRouteApi;
dictionary?: PortalDictionaryClient;
onOpenSteps: (route: TemplateRoute) => void;
}
export const RoutesScreen: React.FC<IRoutesScreenProps> = ({ api, dictionary, onOpenSteps }) => {
const requestTypes = useRequestTypes(api);
const portalDictionaries = usePortalDictionaries(dictionary);
const [filterRequestTypeId, setFilterRequestTypeId] = React.useState('');
const routes = useTemplateRoutes(api, filterRequestTypeId ? Number(filterRequestTypeId) : undefined);
const [editing, setEditing] = React.useState<TemplateRoute | undefined>();
const [showForm, setShowForm] = React.useState(false);
const deleteTarget = useDeleteTarget<TemplateRoute>(item => api.templateRoutes.remove(item.id), routes.reload);
const requestTypeName = (id: number): string => requestTypes.items.find(rt => rt.id === id)?.name ?? `#${id}`;
// Пока справочник не загрузился (или маршрут ссылается на удалённую запись),
// показываем усечённый Guid — чтобы строка таблицы не выглядела пустой.
const companyName = (id: string): string => portalDictionaries.companies.find(c => c.id === id)?.name ?? shortGuid(id);
const departmentName = (id: string): string => portalDictionaries.departments.find(d => d.id === id)?.name ?? shortGuid(id);
const openCreate = (): void => {
setEditing(undefined);
setShowForm(true);
};
const openEdit = (item: TemplateRoute): void => {
setEditing(item);
setShowForm(true);
};
return (
<div>
<Breadcrumbs items={[{ label: texts.common.home }, { label: texts.routes.breadcrumb }]} />
<PageHeader
icon="route"
subtitle={texts.routes.subtitle}
title={texts.routes.title}
actions={<Button variant="accent" icon="plus" onClick={openCreate}>{texts.routes.add}</Button>}
/>
<div className={styles.filter}>
<Field label={texts.routes.filterLabel}>
<Select
options={requestTypes.items.map(rt => ({ value: String(rt.id), label: rt.name }))}
placeholder={texts.routes.filterAll}
value={filterRequestTypeId}
onChange={e => setFilterRequestTypeId(e.target.value)}
/>
</Field>
</div>
{portalDictionaries.error && <Alert kind="warn">{texts.routes.dictionaryFailed(portalDictionaries.error)}</Alert>}
{routes.error && <Alert kind="err">{routes.error}</Alert>}
{deleteTarget.error && <Alert kind="err">{deleteTarget.error}</Alert>}
<DataTable
columns={[
{
key: 'requestType',
header: texts.routes.columnRequestType,
render: r => <PrimaryCell>{requestTypeName(r.requestTypeId)}</PrimaryCell>
},
{
key: 'routeType',
header: texts.routes.columnRouteType,
render: r => <Badge variant={r.routeType === RouteType.Parallel ? 'blue' : 'gold'}>{routeTypeLabels[r.routeType]}</Badge>
},
{ key: 'company', header: texts.routes.columnCompany, render: r => companyName(r.companyId) },
{ key: 'department', header: texts.routes.columnDepartment, render: r => (r.departmentId ? departmentName(r.departmentId) : texts.common.empty) },
{
key: 'byProject',
header: texts.routes.columnByProject,
align: 'center',
render: r => (r.byProject ? <Badge variant="ok">{texts.common.yes}</Badge> : <Badge variant="neutral">{texts.common.no}</Badge>)
},
{
key: 'control',
header: texts.routes.columnControl,
align: 'center',
render: r => (r.requiresControl ? <Badge variant="gold">{texts.routes.controlRequired}</Badge> : <Badge variant="neutral">{texts.common.no}</Badge>)
},
{
key: 'actions',
header: '',
width: '160px',
align: 'right',
sticky: true,
render: r => (
<ActionsCell>
<Button size="sm" variant="secondary" icon="clipboard" onClick={() => onOpenSteps(r)}>{texts.routes.openSteps}</Button>
<IconButton icon="edit" label={texts.common.edit} onClick={() => openEdit(r)} />
<IconButton icon="trash" label={texts.common.remove} danger onClick={() => deleteTarget.ask(r)} />
</ActionsCell>
)
}
]}
rows={routes.items}
keyOf={r => r.id}
emptyMessage={routes.isLoading ? texts.common.loading : texts.routes.emptyList}
/>
{showForm && (
<RouteFormModal
api={api}
editing={editing}
initialRequestTypeId={filterRequestTypeId}
requestTypes={requestTypes.items}
companies={portalDictionaries.companies}
departments={portalDictionaries.departments}
dictionaryLoading={portalDictionaries.isLoading}
onSaved={routes.reload}
onClose={() => setShowForm(false)}
/>
)}
{deleteTarget.target && (
<ConfirmDialog
title={texts.routes.deleteTitle}
text={texts.routes.deleteText(deleteTarget.target.id)}
confirmLabel={texts.common.remove}
busy={deleteTarget.isBusy}
onCancel={deleteTarget.cancel}
onConfirm={deleteTarget.confirm}
/>
)}
</div>
);
};