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:
@@ -0,0 +1,185 @@
|
||||
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 { useRequestTypes } from '../../hooks/useRequestTypes';
|
||||
import { useResolveRoute } from '../../hooks/useResolveRoute';
|
||||
import { usePortalEmployees } from '../../hooks/usePortalEmployees';
|
||||
import { findEmployeeByEmail, toEmployeeOption } from '../../utils/employee';
|
||||
import {
|
||||
Alert,
|
||||
AsyncSearchableSelect,
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
CellNote,
|
||||
DataTable,
|
||||
Field,
|
||||
FormGrid,
|
||||
PageHeader,
|
||||
Panel,
|
||||
PrimaryCell,
|
||||
SearchableSelect,
|
||||
Select
|
||||
} from '../ui';
|
||||
|
||||
export interface IResolveScreenProps {
|
||||
api: ApprovalRouteApi;
|
||||
/** Undefined, пока SPFx выдаёт AAD-клиент для api.tnsu.kz. */
|
||||
dictionary?: PortalDictionaryClient;
|
||||
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 }) => {
|
||||
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 resolved = useResolveRoute(api);
|
||||
const employees = usePortalEmployees(dictionary);
|
||||
|
||||
const searchProjects = React.useCallback(
|
||||
(query: string, page: number) => {
|
||||
if (!dictionary) {
|
||||
return Promise.reject(new Error(texts.resolve.projectDictionaryLoading));
|
||||
}
|
||||
return dictionary.searchProjects(query, page);
|
||||
},
|
||||
[dictionary]
|
||||
);
|
||||
|
||||
const run = (): void => {
|
||||
if (!requestTypeId) {
|
||||
resolved.fail(texts.resolve.validationRequestType);
|
||||
return;
|
||||
}
|
||||
resolved.resolve({
|
||||
requestTypeId: Number(requestTypeId),
|
||||
userEmail: userEmail.trim() || undefined,
|
||||
projectId: projectId.trim() || undefined,
|
||||
status
|
||||
});
|
||||
};
|
||||
|
||||
const result = resolved.result;
|
||||
const unresolvedCount = result ? result.steps.filter(s => !s.executorEmail).length : 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Breadcrumbs items={[{ label: texts.common.home }, { label: texts.resolve.breadcrumb }]} />
|
||||
<PageHeader icon="search" subtitle={texts.resolve.subtitle} title={texts.resolve.title} />
|
||||
|
||||
<Panel>
|
||||
<FormGrid>
|
||||
<Field label={texts.resolve.requestTypeLabel} required full>
|
||||
<Select
|
||||
options={requestTypes.items.map(rt => ({ value: String(rt.id), label: rt.name }))}
|
||||
placeholder={texts.resolve.requestTypePlaceholder}
|
||||
value={requestTypeId}
|
||||
onChange={e => setRequestTypeId(e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={texts.resolve.userEmailLabel}
|
||||
hint={currentUserEmail ? texts.resolve.userEmailHint(currentUserEmail) : texts.resolve.userEmailHintUnknown}
|
||||
>
|
||||
<SearchableSelect
|
||||
options={employees.items.map(toEmployeeOption)}
|
||||
value={userEmail}
|
||||
loading={employees.isLoading}
|
||||
allowClear
|
||||
clearLabel={texts.resolve.userEmailClear}
|
||||
placeholder={texts.resolve.userEmailPlaceholder}
|
||||
searchPlaceholder={texts.select.employeeSearchPlaceholder}
|
||||
emptyMessage={texts.select.employeesEmpty}
|
||||
onChange={setUserEmail}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={texts.resolve.projectLabel} hint={texts.resolve.projectHint}>
|
||||
<AsyncSearchableSelect<Project>
|
||||
value={projectId}
|
||||
selectedLabel={projectLabel}
|
||||
disabled={!dictionary}
|
||||
allowClear
|
||||
clearLabel={texts.resolve.projectClear}
|
||||
placeholder={dictionary ? texts.resolve.projectPlaceholder : texts.resolve.projectLoadingPlaceholder}
|
||||
searchPlaceholder={texts.resolve.projectSearchPlaceholder}
|
||||
search={searchProjects}
|
||||
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 : '');
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={texts.resolve.statusLabel} required>
|
||||
<Select options={STATUS_OPTIONS} value={status} onChange={e => setStatus(e.target.value as RouteStepStatus)} />
|
||||
</Field>
|
||||
</FormGrid>
|
||||
<div className={styles.runButtonRow}>
|
||||
<Button variant="accent" icon="search" onClick={run} disabled={resolved.isBusy}>
|
||||
{resolved.isBusy ? texts.resolve.running : texts.resolve.run}
|
||||
</Button>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
{resolved.error && <Alert kind="err">{resolved.error}</Alert>}
|
||||
|
||||
{result && (
|
||||
<>
|
||||
<Alert kind="ok">
|
||||
{texts.resolve.summary(result.routeId, routeTypeLabels[result.routeType], result.requiresControl)}
|
||||
</Alert>
|
||||
{unresolvedCount > 0 && <Alert kind="warn">{texts.resolve.unresolved(unresolvedCount)}</Alert>}
|
||||
<DataTable
|
||||
columns={[
|
||||
{ key: 'order', header: texts.resolve.columnOrderNo, width: '50px', render: r => r.orderNo },
|
||||
{
|
||||
key: 'executor',
|
||||
header: texts.resolve.columnExecutor,
|
||||
render: r => {
|
||||
const employee = findEmployeeByEmail(employees.items, r.executorEmail);
|
||||
return (
|
||||
<div>
|
||||
<PrimaryCell>{employee ? employee.fio : (r.executorEmail ?? texts.common.empty)}</PrimaryCell>
|
||||
{employee && <CellNote>{employee.email}</CellNote>}
|
||||
{r.warning && <CellNote kind="warn">{r.warning}</CellNote>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'position',
|
||||
header: texts.resolve.columnPosition,
|
||||
render: r => (
|
||||
<div>
|
||||
{r.position ?? texts.common.empty}
|
||||
{!r.position && !r.warning && r.executorEmail && <CellNote>{texts.resolve.positionMissing}</CellNote>}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'reassign',
|
||||
header: texts.resolve.columnReassign,
|
||||
align: 'center',
|
||||
render: r => (r.allowReassignToOtherApprovers ? texts.resolve.reassignAllowed : texts.common.no)
|
||||
}
|
||||
]}
|
||||
rows={result.steps}
|
||||
keyOf={r => r.id}
|
||||
emptyMessage={texts.resolve.emptySteps}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user