Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/6
This commit was merged in pull request #6.
This commit is contained in:
@@ -2,11 +2,14 @@ import * as React from 'react';
|
||||
import styles from './ResolveScreen.module.scss';
|
||||
import { ApprovalRouteApi } from '../../services/api';
|
||||
import { PortalDictionaryClient } from '../../services/portalDictionary';
|
||||
import { Project, RouteStepStatus } from '../../models';
|
||||
import { routeStepStatusLabels, routeTypeLabels, texts } from '../../texts';
|
||||
import { isNumericFieldType, SharePointListsClient } from '../../services/sharePointLists';
|
||||
import { Project, ResolveFieldValue, RouteStepStatus } from '../../models';
|
||||
import { routeStepStatusLabels, texts } from '../../texts';
|
||||
import { useRequestTypes } from '../../hooks/useRequestTypes';
|
||||
import { useResolveRoute } from '../../hooks/useResolveRoute';
|
||||
import { usePortalEmployees } from '../../hooks/usePortalEmployees';
|
||||
import { useSharePointFields } from '../../hooks/useSharePointFields';
|
||||
import { useLocalStorageState } from '../../hooks/useLocalStorageState';
|
||||
import { findEmployeeByEmail, toEmployeeOption } from '../../utils/employee';
|
||||
import {
|
||||
Alert,
|
||||
@@ -17,30 +20,58 @@ import {
|
||||
DataTable,
|
||||
Field,
|
||||
FormGrid,
|
||||
IconButton,
|
||||
PageHeader,
|
||||
Panel,
|
||||
PrimaryCell,
|
||||
SearchableSelect,
|
||||
Select
|
||||
Select,
|
||||
TextInput
|
||||
} from '../ui';
|
||||
|
||||
export interface IResolveScreenProps {
|
||||
api: ApprovalRouteApi;
|
||||
/** Undefined, пока SPFx выдаёт AAD-клиент для api.tnsu.kz. */
|
||||
dictionary?: PortalDictionaryClient;
|
||||
sharePoint: SharePointListsClient;
|
||||
currentUserEmail?: string;
|
||||
}
|
||||
|
||||
const STATUS_OPTIONS = [RouteStepStatus.Approval, RouteStepStatus.Execution]
|
||||
.map(v => ({ value: v, label: routeStepStatusLabels[v] }));
|
||||
|
||||
export const ResolveScreen: React.FC<IResolveScreenProps> = ({ api, dictionary, currentUserEmail }) => {
|
||||
// Форма проверки маршрута заполняется одними и теми же параметрами раз за разом
|
||||
// (тип заявки, инициатор, проект, статус) — персистим её в localStorage, чтобы
|
||||
// не вводить всё заново при каждом открытии экрана.
|
||||
const RESOLVE_FORM_STORAGE_KEY = 'approvalAdmin.resolveForm';
|
||||
|
||||
interface IResolveFormState {
|
||||
requestTypeId: string;
|
||||
userEmail: string;
|
||||
projectId: string;
|
||||
projectLabel: string;
|
||||
status: RouteStepStatus;
|
||||
fieldValues: ResolveFieldValue[];
|
||||
}
|
||||
|
||||
const EMPTY_RESOLVE_FORM: IResolveFormState = {
|
||||
requestTypeId: '',
|
||||
userEmail: '',
|
||||
projectId: '',
|
||||
projectLabel: '',
|
||||
status: RouteStepStatus.Approval,
|
||||
fieldValues: []
|
||||
};
|
||||
|
||||
export const ResolveScreen: React.FC<IResolveScreenProps> = ({ api, dictionary, sharePoint, currentUserEmail }) => {
|
||||
const requestTypes = useRequestTypes(api);
|
||||
const [requestTypeId, setRequestTypeId] = React.useState('');
|
||||
const [userEmail, setUserEmail] = React.useState('');
|
||||
const [projectId, setProjectId] = React.useState('');
|
||||
const [projectLabel, setProjectLabel] = React.useState('');
|
||||
const [status, setStatus] = React.useState<RouteStepStatus>(RouteStepStatus.Approval);
|
||||
const [form, setForm] = useLocalStorageState<IResolveFormState>(RESOLVE_FORM_STORAGE_KEY, EMPTY_RESOLVE_FORM);
|
||||
const { requestTypeId, userEmail, projectId, projectLabel, status, fieldValues = [] } = form;
|
||||
|
||||
const requestType = requestTypes.items.find(rt => rt.id === Number(requestTypeId));
|
||||
const hasListName = Boolean(requestType?.listName);
|
||||
const sharePointFields = useSharePointFields(sharePoint, requestType?.siteUrl, requestType?.listName);
|
||||
const fieldNameOptions = sharePointFields.items.map(f => ({ value: f.internalName, label: f.title }));
|
||||
|
||||
const resolved = useResolveRoute(api);
|
||||
const employees = usePortalEmployees(dictionary);
|
||||
@@ -55,16 +86,35 @@ export const ResolveScreen: React.FC<IResolveScreenProps> = ({ api, dictionary,
|
||||
[dictionary]
|
||||
);
|
||||
|
||||
const updateFieldValue = (index: number, patch: Partial<ResolveFieldValue>): void => {
|
||||
setForm({ ...form, fieldValues: fieldValues.map((fv, i) => (i === index ? { ...fv, ...patch } : fv)) });
|
||||
};
|
||||
|
||||
const addFieldValue = (): void => {
|
||||
setForm({ ...form, fieldValues: [...fieldValues, { fieldName: '', value: '' }] });
|
||||
};
|
||||
|
||||
const removeFieldValue = (index: number): void => {
|
||||
setForm({ ...form, fieldValues: fieldValues.filter((_, i) => i !== index) });
|
||||
};
|
||||
|
||||
const run = (): void => {
|
||||
if (!requestTypeId) {
|
||||
resolved.fail(texts.resolve.validationRequestType);
|
||||
return;
|
||||
}
|
||||
const conditions = fieldValues.filter(fv => fv.fieldName.trim() || fv.value.trim());
|
||||
if (conditions.some(fv => !fv.fieldName.trim() || !fv.value.trim())) {
|
||||
resolved.fail(texts.resolve.validationConditionField);
|
||||
return;
|
||||
}
|
||||
|
||||
resolved.resolve({
|
||||
requestTypeId: Number(requestTypeId),
|
||||
userEmail: userEmail.trim() || undefined,
|
||||
projectId: projectId.trim() || undefined,
|
||||
status
|
||||
status,
|
||||
fieldValues: conditions.length > 0 ? conditions : undefined
|
||||
});
|
||||
};
|
||||
|
||||
@@ -83,7 +133,7 @@ export const ResolveScreen: React.FC<IResolveScreenProps> = ({ api, dictionary,
|
||||
options={requestTypes.items.map(rt => ({ value: String(rt.id), label: rt.name }))}
|
||||
placeholder={texts.resolve.requestTypePlaceholder}
|
||||
value={requestTypeId}
|
||||
onChange={e => setRequestTypeId(e.target.value)}
|
||||
onChange={e => setForm({ ...form, requestTypeId: e.target.value, fieldValues: [] })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
@@ -99,7 +149,7 @@ export const ResolveScreen: React.FC<IResolveScreenProps> = ({ api, dictionary,
|
||||
placeholder={texts.resolve.userEmailPlaceholder}
|
||||
searchPlaceholder={texts.select.employeeSearchPlaceholder}
|
||||
emptyMessage={texts.select.employeesEmpty}
|
||||
onChange={setUserEmail}
|
||||
onChange={userEmail => setForm({ ...form, userEmail })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={texts.resolve.projectLabel} hint={texts.resolve.projectHint}>
|
||||
@@ -115,16 +165,62 @@ export const ResolveScreen: React.FC<IResolveScreenProps> = ({ api, dictionary,
|
||||
getValue={p => p.oid}
|
||||
getLabel={p => p.name}
|
||||
getSublabel={p => [p.code, p.status].filter(Boolean).join(' · ') || undefined}
|
||||
onChange={(nextId, item) => {
|
||||
setProjectId(nextId);
|
||||
setProjectLabel(item ? item.name : '');
|
||||
}}
|
||||
onChange={(nextId, item) => setForm({ ...form, projectId: nextId, projectLabel: item ? item.name : '' })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={texts.resolve.statusLabel} required>
|
||||
<Select options={STATUS_OPTIONS} value={status} onChange={e => setStatus(e.target.value as RouteStepStatus)} />
|
||||
<Select
|
||||
options={STATUS_OPTIONS}
|
||||
value={status}
|
||||
onChange={e => setForm({ ...form, status: e.target.value as RouteStepStatus })}
|
||||
/>
|
||||
</Field>
|
||||
</FormGrid>
|
||||
|
||||
<div className={styles.conditions}>
|
||||
<div className={styles.conditionsHeader}>
|
||||
<span className={styles.conditionsTitle}>{texts.resolve.conditionsLabel}</span>
|
||||
{requestTypeId && !hasListName && (
|
||||
<span className={styles.conditionsHint}>{texts.resolve.conditionsDisabledHint}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasListName && (
|
||||
<>
|
||||
{fieldValues.map((fv, index) => {
|
||||
const field = sharePointFields.items.find(f => f.internalName === fv.fieldName);
|
||||
const isNumeric = field ? isNumericFieldType(field.typeAsString) : false;
|
||||
return (
|
||||
// eslint-disable-next-line react/no-array-index-key -- строки не переупорядочиваются, только добавляются/удаляются с конца или явным индексом
|
||||
<div className={styles.conditionRow} key={index}>
|
||||
<Select
|
||||
options={fieldNameOptions}
|
||||
value={fv.fieldName}
|
||||
disabled={sharePointFields.isLoading}
|
||||
placeholder={
|
||||
sharePointFields.isLoading
|
||||
? texts.resolve.conditionFieldLoading
|
||||
: texts.resolve.conditionFieldPlaceholder
|
||||
}
|
||||
onChange={e => updateFieldValue(index, { fieldName: e.target.value })}
|
||||
/>
|
||||
<TextInput
|
||||
type={isNumeric ? 'number' : 'text'}
|
||||
value={fv.value}
|
||||
placeholder={texts.resolve.conditionValuePlaceholder}
|
||||
onChange={e => updateFieldValue(index, { value: e.target.value })}
|
||||
/>
|
||||
<IconButton icon="trash" label={texts.common.remove} danger onClick={() => removeFieldValue(index)} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button variant="secondary" size="sm" icon="plus" onClick={addFieldValue}>
|
||||
{texts.resolve.conditionAdd}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.runButtonRow}>
|
||||
<Button variant="accent" icon="search" onClick={run} disabled={resolved.isBusy}>
|
||||
{resolved.isBusy ? texts.resolve.running : texts.resolve.run}
|
||||
@@ -137,7 +233,7 @@ export const ResolveScreen: React.FC<IResolveScreenProps> = ({ api, dictionary,
|
||||
{result && (
|
||||
<>
|
||||
<Alert kind="ok">
|
||||
{texts.resolve.summary(result.routeId, routeTypeLabels[result.routeType], result.requiresControl)}
|
||||
{texts.resolve.summary(result.routeId, result.requiresControl)}
|
||||
</Alert>
|
||||
{unresolvedCount > 0 && <Alert kind="warn">{texts.resolve.unresolved(unresolvedCount)}</Alert>}
|
||||
<DataTable
|
||||
|
||||
Reference in New Issue
Block a user