Доступ к заявкам: выдача доступа сразу нескольким сотрудникам по одному типу заявки
В форме можно добавить несколько сотрудников списком; уже имеющие доступ к выбранному типу не предлагаются. Ошибки по отдельным сотрудникам не отменяют остальных — неудавшиеся остаются в списке. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+21
@@ -10,3 +10,24 @@
|
|||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chosen {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chosenItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 4px 8px 4px 12px;
|
||||||
|
border: 1px solid #e1e4ea;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chosenEmpty {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|||||||
+60
-17
@@ -53,7 +53,7 @@ export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> =
|
|||||||
|
|
||||||
const [showForm, setShowForm] = React.useState(false);
|
const [showForm, setShowForm] = React.useState(false);
|
||||||
const [requestTypeId, setRequestTypeId] = React.useState('');
|
const [requestTypeId, setRequestTypeId] = React.useState('');
|
||||||
const [email, setEmail] = React.useState('');
|
const [emails, setEmails] = React.useState<string[]>([]);
|
||||||
const submit = useAsyncSubmit();
|
const submit = useAsyncSubmit();
|
||||||
|
|
||||||
const [filterRequestTypeId, setFilterRequestTypeId] = useLocalStorageState(ACCESS_FILTER_TYPE_STORAGE_KEY, '');
|
const [filterRequestTypeId, setFilterRequestTypeId] = useLocalStorageState(ACCESS_FILTER_TYPE_STORAGE_KEY, '');
|
||||||
@@ -97,7 +97,7 @@ export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> =
|
|||||||
|
|
||||||
const openCreate = (): void => {
|
const openCreate = (): void => {
|
||||||
setRequestTypeId('');
|
setRequestTypeId('');
|
||||||
setEmail('');
|
setEmails([]);
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,17 +106,45 @@ export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> =
|
|||||||
submit.fail(texts.requestTypeAccess.validationRequestType);
|
submit.fail(texts.requestTypeAccess.validationRequestType);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!email.trim()) {
|
if (emails.length === 0) {
|
||||||
submit.fail(texts.requestTypeAccess.validationEmail);
|
submit.fail(texts.requestTypeAccess.validationEmail);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
submit.run(() =>
|
// По одному запросу на сотрудника (бэкенд выдаёт доступ по одному). Падение на
|
||||||
api.requestTypeAccess.create({ requestTypeId: Number(requestTypeId), email: email.trim() }).then(() => {
|
// одном не отменяет остальных: неудавшиеся остаются в списке с причиной, чтобы
|
||||||
|
// можно было поправить и нажать ещё раз.
|
||||||
|
submit.run(async () => {
|
||||||
|
const failures: string[] = [];
|
||||||
|
const failedEmails: string[] = [];
|
||||||
|
for (const email of emails) {
|
||||||
|
try {
|
||||||
|
await api.requestTypeAccess.create({ requestTypeId: Number(requestTypeId), email });
|
||||||
|
} catch (e) {
|
||||||
|
failedEmails.push(email);
|
||||||
|
failures.push(`${email}: ${(e as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedEmails.length < emails.length) access.reload();
|
||||||
|
if (failures.length === 0) {
|
||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
access.reload();
|
return;
|
||||||
})
|
}
|
||||||
);
|
setEmails(failedEmails);
|
||||||
|
throw new Error(texts.requestTypeAccess.createFailed(failures));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Уже выбранным и тем, у кого доступ к этому типу есть, в списке делать нечего.
|
||||||
|
const unavailableEmails = new Set(
|
||||||
|
[...emails, ...access.items.filter(a => String(a.requestTypeId) === requestTypeId).map(a => a.email)]
|
||||||
|
.map(e => e.toLowerCase())
|
||||||
|
);
|
||||||
|
const addableEmployees = employees.items.filter(e => !unavailableEmails.has(e.email.toLowerCase()));
|
||||||
|
const employeeLabel = (email: string): string => {
|
||||||
|
const employee = employees.items.find(e => e.email.toLowerCase() === email.toLowerCase());
|
||||||
|
return employee ? `${employee.fio} · ${employee.email}` : email;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -207,15 +235,30 @@ export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> =
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={texts.requestTypeAccess.emailLabel} required full>
|
<Field label={texts.requestTypeAccess.emailLabel} required full>
|
||||||
<SearchableSelect
|
<div>
|
||||||
options={employees.items.map(toEmployeeOption)}
|
<SearchableSelect
|
||||||
value={email}
|
options={addableEmployees.map(toEmployeeOption)}
|
||||||
loading={employees.isLoading}
|
value=""
|
||||||
placeholder={texts.requestTypeAccess.emailPlaceholder}
|
loading={employees.isLoading}
|
||||||
searchPlaceholder={texts.select.employeeSearchPlaceholder}
|
placeholder={texts.requestTypeAccess.emailPlaceholder}
|
||||||
emptyMessage={texts.select.employeesEmpty}
|
searchPlaceholder={texts.select.employeeSearchPlaceholder}
|
||||||
onChange={setEmail}
|
emptyMessage={texts.select.employeesEmpty}
|
||||||
/>
|
onChange={value => value && setEmails(current => [...current, value])}
|
||||||
|
/>
|
||||||
|
<div className={styles.chosen}>
|
||||||
|
{emails.length === 0 && <span className={styles.chosenEmpty}>{texts.requestTypeAccess.noEmailsSelected}</span>}
|
||||||
|
{emails.map(email => (
|
||||||
|
<div key={email} className={styles.chosenItem}>
|
||||||
|
<span>{employeeLabel(email)}</span>
|
||||||
|
<IconButton
|
||||||
|
icon="close"
|
||||||
|
label={texts.requestTypeAccess.removeEmail}
|
||||||
|
onClick={() => setEmails(current => current.filter(x => x !== email))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
</FormGrid>
|
</FormGrid>
|
||||||
</FormModal>
|
</FormModal>
|
||||||
|
|||||||
@@ -198,10 +198,14 @@ export const texts = {
|
|||||||
createTitle: 'Выдать доступ',
|
createTitle: 'Выдать доступ',
|
||||||
requestTypeLabel: 'Тип заявки',
|
requestTypeLabel: 'Тип заявки',
|
||||||
requestTypePlaceholder: 'Выберите тип заявки',
|
requestTypePlaceholder: 'Выберите тип заявки',
|
||||||
emailLabel: 'Сотрудник',
|
emailLabel: 'Сотрудники',
|
||||||
emailPlaceholder: 'Найдите по ФИО или email',
|
emailPlaceholder: 'Добавьте сотрудника — найдите по ФИО или email',
|
||||||
|
removeEmail: 'Убрать из списка',
|
||||||
|
noEmailsSelected: 'Пока никого не выбрано.',
|
||||||
|
createFailed: (failures: string[]): string =>
|
||||||
|
`Не удалось выдать доступ (остались в списке): ${failures.join('; ')}`,
|
||||||
validationRequestType: 'Выберите тип заявки.',
|
validationRequestType: 'Выберите тип заявки.',
|
||||||
validationEmail: 'Выберите сотрудника.',
|
validationEmail: 'Выберите хотя бы одного сотрудника.',
|
||||||
deleteTitle: 'Отозвать доступ?',
|
deleteTitle: 'Отозвать доступ?',
|
||||||
deleteText: (email: string, requestTypeName: string): string =>
|
deleteText: (email: string, requestTypeName: string): string =>
|
||||||
`${email} больше не будет видеть все заявки типа «${requestTypeName}».`
|
`${email} больше не будет видеть все заявки типа «${requestTypeName}».`
|
||||||
|
|||||||
Reference in New Issue
Block a user