Добавить фильтры по типу заявки и сотруднику в «Доступ к заявкам»
Персистятся тем же приёмом, что и routesFilter у маршрутов: живут в адресе (aaacctype/aaaccemail) и в localStorage, переживают перезагрузку и возврат на вкладку. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -149,7 +149,14 @@ export const AdminApp: React.FC<IAdminAppProps> = ({ api, dictionary, sharePoint
|
|||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{route.tab === 'dashboard' && <DashboardScreen api={api} onNavigate={navigateTab} />}
|
{route.tab === 'dashboard' && <DashboardScreen api={api} onNavigate={navigateTab} />}
|
||||||
{route.tab === 'requestTypes' && <RequestTypesScreen api={api} sharePoint={sharePoint} />}
|
{route.tab === 'requestTypes' && <RequestTypesScreen api={api} sharePoint={sharePoint} />}
|
||||||
{route.tab === 'requestTypeAccess' && <RequestTypeAccessScreen api={api} dictionary={dictionary} />}
|
{route.tab === 'requestTypeAccess' && (
|
||||||
|
<RequestTypeAccessScreen
|
||||||
|
api={api}
|
||||||
|
dictionary={dictionary}
|
||||||
|
initialFilterRequestTypeId={route.accessFilterRequestTypeId}
|
||||||
|
initialFilterEmail={route.accessFilterEmail}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{route.tab === 'routes' && !route.routeId && (
|
{route.tab === 'routes' && !route.routeId && (
|
||||||
<RoutesScreen
|
<RoutesScreen
|
||||||
api={api}
|
api={api}
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 18px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterField {
|
||||||
|
min-width: 220px;
|
||||||
|
max-width: 320px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import styles from './RequestTypeAccessScreen.module.scss';
|
||||||
import { ApprovalRouteApi } from '../../services/api';
|
import { ApprovalRouteApi } from '../../services/api';
|
||||||
import { PortalDictionaryClient } from '../../services/portalDictionary';
|
import { PortalDictionaryClient } from '../../services/portalDictionary';
|
||||||
import { texts } from '../../texts';
|
import { texts } from '../../texts';
|
||||||
@@ -7,7 +8,9 @@ import { useRequestTypeAccess } from '../../hooks/useRequestTypeAccess';
|
|||||||
import { usePortalEmployees } from '../../hooks/usePortalEmployees';
|
import { usePortalEmployees } from '../../hooks/usePortalEmployees';
|
||||||
import { useAsyncSubmit } from '../../hooks/useAsyncSubmit';
|
import { useAsyncSubmit } from '../../hooks/useAsyncSubmit';
|
||||||
import { useDeleteTarget } from '../../hooks/useDeleteTarget';
|
import { useDeleteTarget } from '../../hooks/useDeleteTarget';
|
||||||
|
import { useLocalStorageState } from '../../hooks/useLocalStorageState';
|
||||||
import { toEmployeeOption } from '../../utils/employee';
|
import { toEmployeeOption } from '../../utils/employee';
|
||||||
|
import { replaceRoute } from '../../routing';
|
||||||
import { RequestTypeAccess } from '../../models';
|
import { RequestTypeAccess } from '../../models';
|
||||||
import {
|
import {
|
||||||
ActionsCell,
|
ActionsCell,
|
||||||
@@ -23,15 +26,28 @@ import {
|
|||||||
PageHeader,
|
PageHeader,
|
||||||
PrimaryCell,
|
PrimaryCell,
|
||||||
SearchableSelect,
|
SearchableSelect,
|
||||||
Select
|
Select,
|
||||||
|
TextInput
|
||||||
} from '../ui';
|
} from '../ui';
|
||||||
|
|
||||||
export interface IRequestTypeAccessScreenProps {
|
export interface IRequestTypeAccessScreenProps {
|
||||||
api: ApprovalRouteApi;
|
api: ApprovalRouteApi;
|
||||||
dictionary?: PortalDictionaryClient;
|
dictionary?: PortalDictionaryClient;
|
||||||
|
/** Фильтры из ссылки (aaacctype/aaaccemail) — перекрывают сохранённые, если заданы. */
|
||||||
|
initialFilterRequestTypeId?: string;
|
||||||
|
initialFilterEmail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> = ({ api, dictionary }) => {
|
// Фильтры переживают перезагрузку — тот же приём, что и ROUTES_FILTER_STORAGE_KEY в RoutesScreen.
|
||||||
|
const ACCESS_FILTER_TYPE_STORAGE_KEY = 'approvalAdmin.accessFilterRequestTypeId';
|
||||||
|
const ACCESS_FILTER_EMAIL_STORAGE_KEY = 'approvalAdmin.accessFilterEmail';
|
||||||
|
|
||||||
|
export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> = ({
|
||||||
|
api,
|
||||||
|
dictionary,
|
||||||
|
initialFilterRequestTypeId,
|
||||||
|
initialFilterEmail
|
||||||
|
}) => {
|
||||||
const requestTypes = useRequestTypes(api);
|
const requestTypes = useRequestTypes(api);
|
||||||
const access = useRequestTypeAccess(api);
|
const access = useRequestTypeAccess(api);
|
||||||
const employees = usePortalEmployees(dictionary);
|
const employees = usePortalEmployees(dictionary);
|
||||||
@@ -41,10 +57,37 @@ export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> =
|
|||||||
const [email, setEmail] = React.useState('');
|
const [email, setEmail] = React.useState('');
|
||||||
const submit = useAsyncSubmit();
|
const submit = useAsyncSubmit();
|
||||||
|
|
||||||
|
const [filterRequestTypeId, setFilterRequestTypeId] = useLocalStorageState(ACCESS_FILTER_TYPE_STORAGE_KEY, '');
|
||||||
|
const [filterEmail, setFilterEmail] = useLocalStorageState(ACCESS_FILTER_EMAIL_STORAGE_KEY, '');
|
||||||
|
|
||||||
|
// Ссылка с фильтром сильнее сохранённого значения — только на монтировании
|
||||||
|
// (см. тот же приём в RoutesScreen).
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (initialFilterRequestTypeId) setFilterRequestTypeId(initialFilterRequestTypeId);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (initialFilterEmail) setFilterEmail(initialFilterEmail);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Держим адрес синхронным с фильтрами — отфильтрованную таблицу можно
|
||||||
|
// передать ссылкой. replaceRoute, а не navigate: смена фильтра не должна
|
||||||
|
// плодить записи в истории браузера.
|
||||||
|
React.useEffect(() => {
|
||||||
|
replaceRoute({ tab: 'requestTypeAccess', accessFilterRequestTypeId: filterRequestTypeId, accessFilterEmail: filterEmail });
|
||||||
|
}, [filterRequestTypeId, filterEmail]);
|
||||||
|
|
||||||
const deleteTarget = useDeleteTarget<RequestTypeAccess>(item => api.requestTypeAccess.remove(item.id), access.reload);
|
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 requestTypeName = (id: number): string => requestTypes.items.find(rt => rt.id === id)?.name ?? `#${id}`;
|
||||||
|
|
||||||
|
const filteredItems = access.items.filter(item => {
|
||||||
|
if (filterRequestTypeId && String(item.requestTypeId) !== filterRequestTypeId) return false;
|
||||||
|
if (filterEmail && item.email.toLowerCase().indexOf(filterEmail.trim().toLowerCase()) === -1) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// Список SharePoint нужно вручную открыть на чтение этому email — иначе доступ
|
// Список SharePoint нужно вручную открыть на чтение этому email — иначе доступ
|
||||||
// здесь настраивает только видимость в UI approval-route, но не в самом
|
// здесь настраивает только видимость в UI approval-route, но не в самом
|
||||||
// SharePoint (поиск по порталу отфильтрует чужие заявки правами списка).
|
// SharePoint (поиск по порталу отфильтрует чужие заявки правами списка).
|
||||||
@@ -87,6 +130,28 @@ export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> =
|
|||||||
actions={<Button variant="accent" icon="plus" onClick={openCreate}>{texts.requestTypeAccess.add}</Button>}
|
actions={<Button variant="accent" icon="plus" onClick={openCreate}>{texts.requestTypeAccess.add}</Button>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className={styles.filters}>
|
||||||
|
<div className={styles.filterField}>
|
||||||
|
<Field label={texts.requestTypeAccess.filterRequestTypeLabel}>
|
||||||
|
<Select
|
||||||
|
options={requestTypes.items.map(rt => ({ value: String(rt.id), label: rt.name }))}
|
||||||
|
placeholder={texts.requestTypeAccess.filterRequestTypeAll}
|
||||||
|
value={filterRequestTypeId}
|
||||||
|
onChange={e => setFilterRequestTypeId(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
<div className={styles.filterField}>
|
||||||
|
<Field label={texts.requestTypeAccess.filterEmailLabel}>
|
||||||
|
<TextInput
|
||||||
|
value={filterEmail}
|
||||||
|
onChange={e => setFilterEmail(e.target.value)}
|
||||||
|
placeholder={texts.requestTypeAccess.filterEmailPlaceholder}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{access.error && <Alert kind="err">{access.error}</Alert>}
|
{access.error && <Alert kind="err">{access.error}</Alert>}
|
||||||
{deleteTarget.error && <Alert kind="err">{deleteTarget.error}</Alert>}
|
{deleteTarget.error && <Alert kind="err">{deleteTarget.error}</Alert>}
|
||||||
|
|
||||||
@@ -108,9 +173,15 @@ export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> =
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
rows={access.items}
|
rows={filteredItems}
|
||||||
keyOf={r => r.id}
|
keyOf={r => r.id}
|
||||||
emptyMessage={access.isLoading ? texts.common.loading : texts.requestTypeAccess.emptyList}
|
emptyMessage={
|
||||||
|
access.isLoading
|
||||||
|
? texts.common.loading
|
||||||
|
: access.items.length > 0 && filteredItems.length === 0
|
||||||
|
? texts.requestTypeAccess.emptyFiltered
|
||||||
|
: texts.requestTypeAccess.emptyList
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showForm && (
|
{showForm && (
|
||||||
|
|||||||
@@ -69,6 +69,14 @@ export interface IAdminRoute {
|
|||||||
* к тому же отфильтрованному списку, а не сбросить фильтр.
|
* к тому же отфильтрованному списку, а не сбросить фильтр.
|
||||||
*/
|
*/
|
||||||
routesFilter?: string;
|
routesFilter?: string;
|
||||||
|
/**
|
||||||
|
* Фильтры таблицы «Доступ к заявкам» (id типа как строка, email) — только при
|
||||||
|
* tab='requestTypeAccess'. Та же логика, что и у routesFilter: живут в адресе,
|
||||||
|
* чтобы отфильтрованную ссылку можно было передать и чтобы фильтр не терялся
|
||||||
|
* при возврате на вкладку.
|
||||||
|
*/
|
||||||
|
accessFilterRequestTypeId?: string;
|
||||||
|
accessFilterEmail?: string;
|
||||||
/** Задан только при tab='resolve' и непустом requestTypeId. */
|
/** Задан только при tab='resolve' и непустом requestTypeId. */
|
||||||
resolve?: IResolveRouteParams;
|
resolve?: IResolveRouteParams;
|
||||||
/** Задан только при tab='manageRequests' и непустых requestTypeId+requestId. */
|
/** Задан только при tab='manageRequests' и непустых requestTypeId+requestId. */
|
||||||
@@ -85,6 +93,7 @@ const TABS: ScreenKey[] = ['dashboard', 'requestTypes', 'requestTypeAccess', 'ro
|
|||||||
|
|
||||||
const PARAM_KEYS = [
|
const PARAM_KEYS = [
|
||||||
'aatab', 'aaroute', 'aarfilter',
|
'aatab', 'aaroute', 'aarfilter',
|
||||||
|
'aaacctype', 'aaaccemail',
|
||||||
'aartype', 'aauser', 'aaproject', 'aaprojectname', 'aastatus', 'aafields', 'aadept',
|
'aartype', 'aauser', 'aaproject', 'aaprojectname', 'aastatus', 'aafields', 'aadept',
|
||||||
'aamtype', 'aamreq', 'aamlist',
|
'aamtype', 'aamreq', 'aamlist',
|
||||||
'aabuser', 'aabtarget'
|
'aabuser', 'aabtarget'
|
||||||
@@ -146,6 +155,8 @@ export function parseRoute(search: string = window.location.search): IAdminRoute
|
|||||||
tab,
|
tab,
|
||||||
routeId: routeId && !isNaN(routeId) ? routeId : undefined,
|
routeId: routeId && !isNaN(routeId) ? routeId : undefined,
|
||||||
routesFilter: (tab === 'routes' ? params.get('aarfilter') : null) || undefined,
|
routesFilter: (tab === 'routes' ? params.get('aarfilter') : null) || undefined,
|
||||||
|
accessFilterRequestTypeId: (tab === 'requestTypeAccess' ? params.get('aaacctype') : null) || undefined,
|
||||||
|
accessFilterEmail: (tab === 'requestTypeAccess' ? params.get('aaaccemail') : null) || undefined,
|
||||||
resolve,
|
resolve,
|
||||||
manage,
|
manage,
|
||||||
manageByUser: byUserEmail
|
manageByUser: byUserEmail
|
||||||
@@ -167,6 +178,11 @@ function buildQuery(route: IAdminRoute): string {
|
|||||||
if (route.routesFilter) params.set('aarfilter', route.routesFilter);
|
if (route.routesFilter) params.set('aarfilter', route.routesFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (route.tab === 'requestTypeAccess') {
|
||||||
|
if (route.accessFilterRequestTypeId) params.set('aaacctype', route.accessFilterRequestTypeId);
|
||||||
|
if (route.accessFilterEmail) params.set('aaaccemail', route.accessFilterEmail);
|
||||||
|
}
|
||||||
|
|
||||||
if (route.tab === 'resolve' && route.resolve && route.resolve.requestTypeId) {
|
if (route.tab === 'resolve' && route.resolve && route.resolve.requestTypeId) {
|
||||||
params.set('aartype', route.resolve.requestTypeId);
|
params.set('aartype', route.resolve.requestTypeId);
|
||||||
if (route.resolve.userEmail) params.set('aauser', route.resolve.userEmail);
|
if (route.resolve.userEmail) params.set('aauser', route.resolve.userEmail);
|
||||||
|
|||||||
@@ -185,6 +185,11 @@ export const texts = {
|
|||||||
title: 'Доступ к заявкам',
|
title: 'Доступ к заявкам',
|
||||||
add: 'Выдать доступ',
|
add: 'Выдать доступ',
|
||||||
emptyList: 'Пока никому не выдан доступ ни к одному типу заявок.',
|
emptyList: 'Пока никому не выдан доступ ни к одному типу заявок.',
|
||||||
|
emptyFiltered: 'Ничего не найдено по заданным фильтрам.',
|
||||||
|
filterRequestTypeLabel: 'Фильтр по типу заявки',
|
||||||
|
filterRequestTypeAll: 'Все типы заявок',
|
||||||
|
filterEmailLabel: 'Фильтр по сотруднику',
|
||||||
|
filterEmailPlaceholder: 'Email сотрудника',
|
||||||
columnRequestType: 'Тип заявки',
|
columnRequestType: 'Тип заявки',
|
||||||
columnEmail: 'Сотрудник',
|
columnEmail: 'Сотрудник',
|
||||||
columnSharePointLists: 'Списки SharePoint (выдать доступ вручную)',
|
columnSharePointLists: 'Списки SharePoint (выдать доступ вручную)',
|
||||||
|
|||||||
Reference in New Issue
Block a user