Фронтенд для сервиса #5

Merged
gorbenko.pavel merged 29 commits from trynode22 into main 2026-07-30 06:11:24 +00:00
4 changed files with 64 additions and 2 deletions
Showing only changes of commit 7458020880 - Show all commits
@@ -15,6 +15,7 @@ import {
AsyncSearchableSelect, AsyncSearchableSelect,
Breadcrumbs, Breadcrumbs,
Button, Button,
CellNote,
DataTable, DataTable,
Field, Field,
FormGrid, FormGrid,
@@ -34,6 +35,14 @@ export interface IResolveScreenProps {
const STATUS_OPTIONS = [RouteStepStatus.Approval, RouteStepStatus.Execution] const STATUS_OPTIONS = [RouteStepStatus.Approval, RouteStepStatus.Execution]
.map(v => ({ value: v, label: routeStepStatusLabels[v] })); .map(v => ({ value: v, label: routeStepStatusLabels[v] }));
function stepsWord(count: number): string {
const mod100 = count % 100;
const mod10 = count % 10;
if (mod100 >= 11 && mod100 <= 14) return 'шагах';
if (mod10 === 1) return 'шаге';
return 'шагах';
}
export const ResolveScreen: React.FC<IResolveScreenProps> = ({ api, dictionary, currentUserEmail }) => { export const ResolveScreen: React.FC<IResolveScreenProps> = ({ api, dictionary, currentUserEmail }) => {
const [requestTypes, setRequestTypes] = React.useState<RequestType[]>([]); const [requestTypes, setRequestTypes] = React.useState<RequestType[]>([]);
const [requestTypeId, setRequestTypeId] = React.useState(''); const [requestTypeId, setRequestTypeId] = React.useState('');
@@ -60,6 +69,8 @@ export const ResolveScreen: React.FC<IResolveScreenProps> = ({ api, dictionary,
[dictionary] [dictionary]
); );
const unresolvedCount = result ? result.steps.filter(s => !s.executorEmail).length : 0;
const run = (): void => { const run = (): void => {
if (!requestTypeId) { if (!requestTypeId) {
setError('Выберите тип заявки.'); setError('Выберите тип заявки.');
@@ -140,11 +151,36 @@ export const ResolveScreen: React.FC<IResolveScreenProps> = ({ api, dictionary,
Маршрут #{result.routeId} · {routeTypeLabels[result.routeType]} Маршрут #{result.routeId} · {routeTypeLabels[result.routeType]}
{result.requiresControl ? ' · требуется контроль исполнения' : ''} {result.requiresControl ? ' · требуется контроль исполнения' : ''}
</Alert> </Alert>
{unresolvedCount > 0 && (
<Alert kind="warn">
Исполнитель не определён на {unresolvedCount} {stepsWord(unresolvedCount)} — причина указана в строке шага.
</Alert>
)}
<DataTable <DataTable
columns={[ columns={[
{ key: 'order', header: '№', width: '50px', render: r => r.orderNo }, { key: 'order', header: '№', width: '50px', render: r => r.orderNo },
{ key: 'executor', header: 'Исполнитель', render: r => <PrimaryCell>{r.executorEmail ?? '—'}</PrimaryCell> }, {
{ key: 'position', header: 'Должность', render: r => r.position ?? '—' }, key: 'executor',
header: 'Исполнитель',
render: r => (
<div>
<PrimaryCell>{r.executorEmail ?? '—'}</PrimaryCell>
{r.warning && <CellNote kind="warn">{r.warning}</CellNote>}
</div>
)
},
{
key: 'position',
header: 'Должность',
render: r => (
<div>
{r.position ?? '—'}
{!r.position && !r.warning && r.executorEmail && (
<CellNote>Должность не указана в карточке сотрудника.</CellNote>
)}
</div>
)
},
{ {
key: 'reassign', key: 'reassign',
header: 'Переназначение', header: 'Переназначение',
@@ -70,6 +70,21 @@
gap: 10px; gap: 10px;
} }
.cellNote {
display: flex;
align-items: flex-start;
gap: 6px;
margin-top: 4px;
max-width: 420px;
font-size: 11.5px;
line-height: 1.4;
color: $gray-500;
svg { margin-top: 1px; }
}
.cellNoteWarn { color: $amber-700; }
.actionsCell { .actionsCell {
display: flex; display: flex;
gap: 4px; gap: 4px;
@@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import styles from './DataTable.module.scss'; import styles from './DataTable.module.scss';
import { Icon } from './Icon';
export interface IColumn<T> { export interface IColumn<T> {
key: string; key: string;
@@ -66,3 +67,11 @@ export const PrimaryCell: React.FC<{ children: React.ReactNode }> = ({ children
export const ActionsCell: React.FC<{ children: React.ReactNode }> = ({ children }) => ( export const ActionsCell: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<span className={styles.actionsCell}>{children}</span> <span className={styles.actionsCell}>{children}</span>
); );
// Пояснение под значением ячейки — например, причина, по которой значение пустое.
export const CellNote: React.FC<{ children: React.ReactNode; kind?: 'warn' | 'muted' }> = ({ children, kind = 'muted' }) => (
<span className={`${styles.cellNote} ${kind === 'warn' ? styles.cellNoteWarn : ''}`}>
{kind === 'warn' && <Icon name="warning" size="sm" />}
<span>{children}</span>
</span>
);
@@ -192,6 +192,8 @@ export interface ResolvedRouteStep {
executorEmail: string | null; executorEmail: string | null;
position: string | null; position: string | null;
allowReassignToOtherApprovers: boolean; allowReassignToOtherApprovers: boolean;
/** Причина, по которой исполнитель шага не определился (заполняется бэкендом). */
warning: string | null;
} }
export interface ResolveTemplateRouteResult { export interface ResolveTemplateRouteResult {