Feature/request type access #23
@@ -14,6 +14,7 @@ import { texts } from '../../texts';
|
||||
import { useLocalStorageState } from '../../hooks/useLocalStorageState';
|
||||
import { DashboardScreen } from '../screens/DashboardScreen';
|
||||
import { RequestTypesScreen } from '../screens/RequestTypesScreen';
|
||||
import { RequestTypeAccessScreen } from '../screens/RequestTypeAccessScreen';
|
||||
import { RoutesScreen } from '../screens/RoutesScreen';
|
||||
import { RouteStepsScreen } from '../screens/RouteStepsScreen';
|
||||
import { ResolveScreen } from '../screens/ResolveScreen';
|
||||
@@ -27,6 +28,7 @@ import { ManageByUserScreen } from '../screens/ManageByUserScreen';
|
||||
const NAV_ITEMS: ISidebarItem[] = [
|
||||
{ id: 'dashboard', title: texts.nav.dashboard, icon: 'chart' },
|
||||
{ id: 'requestTypes', title: texts.nav.requestTypes, icon: 'document' },
|
||||
{ id: 'requestTypeAccess', title: texts.nav.requestTypeAccess, icon: 'people' },
|
||||
{ id: 'routes', title: texts.nav.routes, icon: 'send' },
|
||||
{ id: 'resolve', title: texts.nav.resolve, icon: 'search' },
|
||||
{ id: 'manageRequests', title: texts.nav.manageRequests, icon: 'clipboard' },
|
||||
@@ -147,6 +149,7 @@ export const AdminApp: React.FC<IAdminAppProps> = ({ api, dictionary, sharePoint
|
||||
<div className={styles.content}>
|
||||
{route.tab === 'dashboard' && <DashboardScreen api={api} onNavigate={navigateTab} />}
|
||||
{route.tab === 'requestTypes' && <RequestTypesScreen api={api} sharePoint={sharePoint} />}
|
||||
{route.tab === 'requestTypeAccess' && <RequestTypeAccessScreen api={api} dictionary={dictionary} />}
|
||||
{route.tab === 'routes' && !route.routeId && (
|
||||
<RoutesScreen
|
||||
api={api}
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
export type ScreenKey = 'dashboard' | 'requestTypes' | 'routes' | 'routeSteps' | 'resolve' | 'manageRequests' | 'manageByUser';
|
||||
export type ScreenKey =
|
||||
| 'dashboard'
|
||||
| 'requestTypes'
|
||||
| 'requestTypeAccess'
|
||||
| 'routes'
|
||||
| 'routeSteps'
|
||||
| 'resolve'
|
||||
| 'manageRequests'
|
||||
| 'manageByUser';
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import * as React from 'react';
|
||||
import { ApprovalRouteApi } from '../../services/api';
|
||||
import { PortalDictionaryClient } from '../../services/portalDictionary';
|
||||
import { texts } from '../../texts';
|
||||
import { useRequestTypes } from '../../hooks/useRequestTypes';
|
||||
import { useRequestTypeAccess } from '../../hooks/useRequestTypeAccess';
|
||||
import { usePortalEmployees } from '../../hooks/usePortalEmployees';
|
||||
import { useAsyncSubmit } from '../../hooks/useAsyncSubmit';
|
||||
import { useDeleteTarget } from '../../hooks/useDeleteTarget';
|
||||
import { toEmployeeOption } from '../../utils/employee';
|
||||
import { RequestTypeAccess } from '../../models';
|
||||
import {
|
||||
ActionsCell,
|
||||
Alert,
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
DataTable,
|
||||
Field,
|
||||
FormGrid,
|
||||
FormModal,
|
||||
IconButton,
|
||||
PageHeader,
|
||||
PrimaryCell,
|
||||
SearchableSelect,
|
||||
Select
|
||||
} from '../ui';
|
||||
|
||||
export interface IRequestTypeAccessScreenProps {
|
||||
api: ApprovalRouteApi;
|
||||
dictionary?: PortalDictionaryClient;
|
||||
}
|
||||
|
||||
export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> = ({ api, dictionary }) => {
|
||||
const requestTypes = useRequestTypes(api);
|
||||
const access = useRequestTypeAccess(api);
|
||||
const employees = usePortalEmployees(dictionary);
|
||||
|
||||
const [showForm, setShowForm] = React.useState(false);
|
||||
const [requestTypeId, setRequestTypeId] = React.useState('');
|
||||
const [email, setEmail] = React.useState('');
|
||||
const submit = useAsyncSubmit();
|
||||
|
||||
const deleteTarget = useDeleteTarget<RequestTypeAccess>(item => api.requestTypeAccess.remove(item.id), access.reload);
|
||||
|
||||
const requestTypeName = (id: number): string => requestTypes.items.find(rt => rt.id === id)?.name ?? `#${id}`;
|
||||
|
||||
const openCreate = (): void => {
|
||||
setRequestTypeId('');
|
||||
setEmail('');
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const submitForm = (): void => {
|
||||
if (!requestTypeId) {
|
||||
submit.fail(texts.requestTypeAccess.validationRequestType);
|
||||
return;
|
||||
}
|
||||
if (!email.trim()) {
|
||||
submit.fail(texts.requestTypeAccess.validationEmail);
|
||||
return;
|
||||
}
|
||||
|
||||
submit.run(() =>
|
||||
api.requestTypeAccess.create({ requestTypeId: Number(requestTypeId), email: email.trim() }).then(() => {
|
||||
setShowForm(false);
|
||||
access.reload();
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Breadcrumbs items={[{ label: texts.common.home }, { label: texts.requestTypeAccess.breadcrumb }]} />
|
||||
<PageHeader
|
||||
icon="people"
|
||||
subtitle={texts.requestTypeAccess.subtitle}
|
||||
title={texts.requestTypeAccess.title}
|
||||
actions={<Button variant="accent" icon="plus" onClick={openCreate}>{texts.requestTypeAccess.add}</Button>}
|
||||
/>
|
||||
|
||||
{access.error && <Alert kind="err">{access.error}</Alert>}
|
||||
{deleteTarget.error && <Alert kind="err">{deleteTarget.error}</Alert>}
|
||||
|
||||
<DataTable
|
||||
columns={[
|
||||
{ key: 'requestType', header: texts.requestTypeAccess.columnRequestType, render: r => <PrimaryCell>{requestTypeName(r.requestTypeId)}</PrimaryCell> },
|
||||
{ key: 'email', header: texts.requestTypeAccess.columnEmail, render: r => r.email },
|
||||
{
|
||||
key: 'actions',
|
||||
header: '',
|
||||
width: '100px',
|
||||
align: 'right',
|
||||
sticky: true,
|
||||
render: r => (
|
||||
<ActionsCell>
|
||||
<IconButton icon="trash" label={texts.common.remove} danger onClick={() => deleteTarget.ask(r)} />
|
||||
</ActionsCell>
|
||||
)
|
||||
}
|
||||
]}
|
||||
rows={access.items}
|
||||
keyOf={r => r.id}
|
||||
emptyMessage={access.isLoading ? texts.common.loading : texts.requestTypeAccess.emptyList}
|
||||
/>
|
||||
|
||||
{showForm && (
|
||||
<FormModal
|
||||
title={texts.requestTypeAccess.createTitle}
|
||||
error={submit.error}
|
||||
isSaving={submit.isBusy}
|
||||
onSubmit={submitForm}
|
||||
onClose={() => setShowForm(false)}
|
||||
>
|
||||
<FormGrid>
|
||||
<Field label={texts.requestTypeAccess.requestTypeLabel} required full>
|
||||
<Select
|
||||
options={requestTypes.items.map(rt => ({ value: String(rt.id), label: rt.name }))}
|
||||
placeholder={texts.requestTypeAccess.requestTypePlaceholder}
|
||||
value={requestTypeId}
|
||||
onChange={e => setRequestTypeId(e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={texts.requestTypeAccess.emailLabel} required full>
|
||||
<SearchableSelect
|
||||
options={employees.items.map(toEmployeeOption)}
|
||||
value={email}
|
||||
loading={employees.isLoading}
|
||||
placeholder={texts.requestTypeAccess.emailPlaceholder}
|
||||
searchPlaceholder={texts.select.employeeSearchPlaceholder}
|
||||
emptyMessage={texts.select.employeesEmpty}
|
||||
onChange={setEmail}
|
||||
/>
|
||||
</Field>
|
||||
</FormGrid>
|
||||
</FormModal>
|
||||
)}
|
||||
|
||||
{deleteTarget.target && (
|
||||
<ConfirmDialog
|
||||
title={texts.requestTypeAccess.deleteTitle}
|
||||
text={texts.requestTypeAccess.deleteText(deleteTarget.target.email, requestTypeName(deleteTarget.target.requestTypeId))}
|
||||
confirmLabel={texts.common.remove}
|
||||
busy={deleteTarget.isBusy}
|
||||
onCancel={deleteTarget.cancel}
|
||||
onConfirm={deleteTarget.confirm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { RequestTypeAccess } from '../models';
|
||||
import { ApprovalRouteApi } from '../services/api';
|
||||
import { AsyncList, useAsyncList } from './useAsyncList';
|
||||
|
||||
/** Реестр доступа «видеть все заявки типа» — целиком, по всем типам заявок. */
|
||||
export function useRequestTypeAccess(api: ApprovalRouteApi): AsyncList<RequestTypeAccess> {
|
||||
const load = React.useCallback(() => api.requestTypeAccess.list(), [api]);
|
||||
return useAsyncList(load);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
export * from './enums';
|
||||
export * from './dictionaries';
|
||||
export * from './requestTypes';
|
||||
export * from './requestTypeAccess';
|
||||
export * from './requestTypeLists';
|
||||
export * from './templateRoutes';
|
||||
export * from './templateRouteSteps';
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// DTO эндпоинта api/request-type-access (Application/RequestTypeAccesses/*.cs).
|
||||
|
||||
export interface RequestTypeAccess {
|
||||
id: number;
|
||||
requestTypeId: number;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface CreateRequestTypeAccessInput {
|
||||
requestTypeId: number;
|
||||
email: string;
|
||||
}
|
||||
@@ -81,7 +81,7 @@ const ROUTE_CHANGE_EVENT = 'aa-route-change';
|
||||
|
||||
// 'routeSteps' сюда намеренно не входит — это не отдельная вкладка URL, а
|
||||
// состояние вкладки 'routes' при наличии aaroute (см. parseRoute).
|
||||
const TABS: ScreenKey[] = ['dashboard', 'requestTypes', 'routes', 'resolve', 'manageRequests', 'manageByUser'];
|
||||
const TABS: ScreenKey[] = ['dashboard', 'requestTypes', 'requestTypeAccess', 'routes', 'resolve', 'manageRequests', 'manageByUser'];
|
||||
|
||||
const PARAM_KEYS = [
|
||||
'aatab', 'aaroute', 'aarfilter',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IHttpClient } from './httpClient';
|
||||
import {
|
||||
CreateRequestTypeAccessInput,
|
||||
CreateRequestTypeInput,
|
||||
CreateTemplateRouteInput,
|
||||
CreateTemplateRouteStepInput,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
PatchTemplateRouteStepInput,
|
||||
RequestListPartition,
|
||||
RequestType,
|
||||
RequestTypeAccess,
|
||||
RequestTypeListSeries,
|
||||
ResolveTemplateRouteQuery,
|
||||
ResolveTemplateRouteResult,
|
||||
@@ -80,6 +82,21 @@ export class ApprovalRouteApi {
|
||||
this.http.get<RequestListPartition[]>(`/api/request-types/${requestTypeId}/lists`)
|
||||
};
|
||||
|
||||
// Доступ «видеть все заявки типа» (Application/RequestTypeAccesses/*.cs) —
|
||||
// тот же список email по типу, что и ручка для Power Automate
|
||||
// (GET .../{requestTypeId}/emails), только здесь читается ВЕСЬ реестр разом
|
||||
// (requestTypeId не передан) — админке нужна одна таблица по всем типам.
|
||||
public readonly requestTypeAccess = {
|
||||
list: (requestTypeId?: number): Promise<RequestTypeAccess[]> =>
|
||||
this.http.get<RequestTypeAccess[]>('/api/request-type-access', { requestTypeId }),
|
||||
|
||||
create: (body: CreateRequestTypeAccessInput): Promise<RequestTypeAccess> =>
|
||||
this.http.post<RequestTypeAccess>('/api/request-type-access', body),
|
||||
|
||||
remove: (id: number): Promise<void> =>
|
||||
this.http.delete(`/api/request-type-access/${id}`)
|
||||
};
|
||||
|
||||
public readonly templateRoutes = {
|
||||
list: (requestTypeId?: number): Promise<TemplateRoute[]> =>
|
||||
this.http.get<TemplateRoute[]>('/api/template-routes', { requestTypeId }),
|
||||
|
||||
@@ -93,6 +93,7 @@ export const texts = {
|
||||
title: 'Панель настройки маршрутов',
|
||||
dashboard: 'Дашборд',
|
||||
requestTypes: 'Типы заявок',
|
||||
requestTypeAccess: 'Доступ к заявкам',
|
||||
routes: 'Маршруты',
|
||||
resolve: 'Проверка маршрута',
|
||||
manageRequests: 'Редактирование заявок',
|
||||
@@ -178,6 +179,26 @@ export const texts = {
|
||||
`Тип «${name}» будет удалён безвозвратно. Связанные маршруты могут перестать разрешаться.`
|
||||
},
|
||||
|
||||
requestTypeAccess: {
|
||||
breadcrumb: 'Доступ к заявкам',
|
||||
subtitle: 'Кто видит все заявки типа, а не только свои',
|
||||
title: 'Доступ к заявкам',
|
||||
add: 'Выдать доступ',
|
||||
emptyList: 'Пока никому не выдан доступ ни к одному типу заявок.',
|
||||
columnRequestType: 'Тип заявки',
|
||||
columnEmail: 'Сотрудник',
|
||||
createTitle: 'Выдать доступ',
|
||||
requestTypeLabel: 'Тип заявки',
|
||||
requestTypePlaceholder: 'Выберите тип заявки',
|
||||
emailLabel: 'Сотрудник',
|
||||
emailPlaceholder: 'Найдите по ФИО или email',
|
||||
validationRequestType: 'Выберите тип заявки.',
|
||||
validationEmail: 'Выберите сотрудника.',
|
||||
deleteTitle: 'Отозвать доступ?',
|
||||
deleteText: (email: string, requestTypeName: string): string =>
|
||||
`${email} больше не будет видеть все заявки типа «${requestTypeName}».`
|
||||
},
|
||||
|
||||
routes: {
|
||||
breadcrumb: 'Маршруты согласования',
|
||||
subtitle: 'Настройка согласований',
|
||||
|
||||
Reference in New Issue
Block a user