Уведомления назначенным пользователям #9
+13
-1
@@ -10,6 +10,8 @@ export interface IManageRequestsApproverModalProps {
|
|||||||
initialEmail?: string;
|
initialEmail?: string;
|
||||||
employees: Employee[];
|
employees: Employee[];
|
||||||
employeesLoading: boolean;
|
employeesLoading: boolean;
|
||||||
|
/** Email'ы (в любом регистре), уже занятые другими шагами той же фазы — этот человек не может встать в фазу второй раз. */
|
||||||
|
excludedEmails: string[];
|
||||||
onSave: (email: string, position: string) => Promise<void>;
|
onSave: (email: string, position: string) => Promise<void>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
@@ -27,18 +29,28 @@ export const ManageRequestsApproverModal: React.FC<IManageRequestsApproverModalP
|
|||||||
initialEmail = '',
|
initialEmail = '',
|
||||||
employees,
|
employees,
|
||||||
employeesLoading,
|
employeesLoading,
|
||||||
|
excludedEmails,
|
||||||
onSave,
|
onSave,
|
||||||
onClose
|
onClose
|
||||||
}) => {
|
}) => {
|
||||||
const [email, setEmail] = React.useState(initialEmail);
|
const [email, setEmail] = React.useState(initialEmail);
|
||||||
const submit = useAsyncSubmit();
|
const submit = useAsyncSubmit();
|
||||||
|
|
||||||
|
// Тот, кто уже занят в этой фазе, исключён из списка выбора — дубликат
|
||||||
|
// физически нельзя выбрать (см. excludedEmails в ManageRequestsScreen).
|
||||||
|
const excludedSet = new Set(excludedEmails.map(e => e.toLowerCase()));
|
||||||
|
const selectableEmployees = employees.filter(e => !excludedSet.has(e.email.toLowerCase()));
|
||||||
|
|
||||||
const handleSubmit = (): void => {
|
const handleSubmit = (): void => {
|
||||||
const trimmedEmail = email.trim();
|
const trimmedEmail = email.trim();
|
||||||
if (!trimmedEmail) {
|
if (!trimmedEmail) {
|
||||||
submit.fail(texts.manageRequests.validationApprover);
|
submit.fail(texts.manageRequests.validationApprover);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (excludedSet.has(trimmedEmail.toLowerCase())) {
|
||||||
|
submit.fail(texts.manageRequests.validationDuplicateApprover);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const position = findEmployeeByEmail(employees, trimmedEmail)?.position || '';
|
const position = findEmployeeByEmail(employees, trimmedEmail)?.position || '';
|
||||||
submit.run(() => onSave(trimmedEmail, position));
|
submit.run(() => onSave(trimmedEmail, position));
|
||||||
};
|
};
|
||||||
@@ -48,7 +60,7 @@ export const ManageRequestsApproverModal: React.FC<IManageRequestsApproverModalP
|
|||||||
<FormGrid>
|
<FormGrid>
|
||||||
<Field label={texts.manageRequests.approverLabel} required full>
|
<Field label={texts.manageRequests.approverLabel} required full>
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
options={employees.map(toEmployeeOption)}
|
options={selectableEmployees.map(toEmployeeOption)}
|
||||||
value={email}
|
value={email}
|
||||||
loading={employeesLoading}
|
loading={employeesLoading}
|
||||||
placeholder={texts.manageRequests.approverPlaceholder}
|
placeholder={texts.manageRequests.approverPlaceholder}
|
||||||
|
|||||||
@@ -132,6 +132,13 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
|
|||||||
// добавлять туда исполнителя всё равно бессмысленно).
|
// добавлять туда исполнителя всё равно бессмысленно).
|
||||||
const phaseClosed = requestRejected || (rows.length > 0 && rows.every(r => !isEditableRowStatus(r.status)));
|
const phaseClosed = requestRejected || (rows.length > 0 && rows.every(r => !isEditableRowStatus(r.status)));
|
||||||
|
|
||||||
|
// Один и тот же человек не может стоять в фазе дважды (в т.ч. призрачной
|
||||||
|
// строкой, которая ещё не подтверждена сервером, — см. handleAddApprover в
|
||||||
|
// ManageRequestsScreen) — исключаем уже занятых из пикера, а не проверяем
|
||||||
|
// постфактум: так дубликат физически нельзя выбрать.
|
||||||
|
const usedEmails = new Set(rows.map(r => r.approverEmail.toLowerCase()).filter(Boolean));
|
||||||
|
const addableEmployees = employees.filter(e => !usedEmails.has(e.email.toLowerCase()));
|
||||||
|
|
||||||
const applyReorder = (mutate: (rows: Row[]) => Row[]): void => {
|
const applyReorder = (mutate: (rows: Row[]) => Row[]): void => {
|
||||||
const renumbered = withOrdersFrom(mutate(withParallel), 0);
|
const renumbered = withOrdersFrom(mutate(withParallel), 0);
|
||||||
const updates = diffOrders(rows, renumbered);
|
const updates = diffOrders(rows, renumbered);
|
||||||
@@ -322,7 +329,7 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
|
|||||||
{!readOnly && !phaseClosed && (
|
{!readOnly && !phaseClosed && (
|
||||||
<div className={styles.addRow}>
|
<div className={styles.addRow}>
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
options={employees.map(toEmployeeOption)}
|
options={addableEmployees.map(toEmployeeOption)}
|
||||||
value=""
|
value=""
|
||||||
loading={employeesLoading}
|
loading={employeesLoading}
|
||||||
placeholder={addPlaceholder}
|
placeholder={addPlaceholder}
|
||||||
|
|||||||
@@ -349,6 +349,18 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
|||||||
const openReassign = (row: ManagedApprovalRow): void => setModal(row);
|
const openReassign = (row: ManagedApprovalRow): void => setModal(row);
|
||||||
const closeModal = (): void => setModal(undefined);
|
const closeModal = (): void => setModal(undefined);
|
||||||
|
|
||||||
|
// Кого нельзя выбрать при переназначении — все остальные согласующие/
|
||||||
|
// исполнители ТОЙ ЖЕ фазы (сама строка modal исключена, иначе нельзя было
|
||||||
|
// бы оставить текущего человека на месте). Один человек не может стоять в
|
||||||
|
// фазе дважды — та же гарантия, что и addableEmployees в
|
||||||
|
// ManageRequestsPhaseTable для добавления нового шага.
|
||||||
|
const reassignExcludedEmails = matrix && modal
|
||||||
|
? matrix.rows
|
||||||
|
.filter(r => r.phase === modal.phase && r.id !== modal.id)
|
||||||
|
.map(r => r.approverEmail.toLowerCase())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
// Переназначение — тоже оптимистично: patch конкретной строки сразу по ответу
|
// Переназначение — тоже оптимистично: patch конкретной строки сразу по ответу
|
||||||
// сервера, без похода за всей матрицей заново. approverName берём из уже
|
// сервера, без похода за всей матрицей заново. approverName берём из уже
|
||||||
// загруженного справочника сотрудников (тот же источник, что подставляет
|
// загруженного справочника сотрудников (тот же источник, что подставляет
|
||||||
@@ -572,6 +584,7 @@ export const ManageRequestsScreen: React.FC<IManageRequestsScreenProps> = ({ api
|
|||||||
initialEmail={modal.approverEmail}
|
initialEmail={modal.approverEmail}
|
||||||
employees={employees.items}
|
employees={employees.items}
|
||||||
employeesLoading={employees.isLoading}
|
employeesLoading={employees.isLoading}
|
||||||
|
excludedEmails={reassignExcludedEmails}
|
||||||
onSave={saveModal}
|
onSave={saveModal}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -321,6 +321,7 @@ export const texts = {
|
|||||||
approverLabel: 'Сотрудник',
|
approverLabel: 'Сотрудник',
|
||||||
approverPlaceholder: 'Выберите сотрудника',
|
approverPlaceholder: 'Выберите сотрудника',
|
||||||
validationApprover: 'Выберите сотрудника.',
|
validationApprover: 'Выберите сотрудника.',
|
||||||
|
validationDuplicateApprover: 'Этот сотрудник уже стоит в этой фазе на другом шаге.',
|
||||||
deleteTitle: 'Удалить шаг?',
|
deleteTitle: 'Удалить шаг?',
|
||||||
deleteText: (position: string): string => `Шаг «${position || 'без должности'}» будет удалён безвозвратно.`,
|
deleteText: (position: string): string => `Шаг «${position || 'без должности'}» будет удалён безвозвратно.`,
|
||||||
orderFloorHint: (order: number): string =>
|
orderFloorHint: (order: number): string =>
|
||||||
|
|||||||
Reference in New Issue
Block a user