Фронтенд для сервиса #5
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user