Уведомления назначенным пользователям (#9)
Tests / frontend-test (push) Successful in 43s
Tests / backend-test (push) Successful in 1m54s

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/9
This commit was merged in pull request #9.
This commit is contained in:
2026-08-10 05:16:54 +00:00
parent 11738b6e4a
commit a07338314a
9 changed files with 157 additions and 19 deletions
@@ -1,7 +1,7 @@
import * as React from 'react';
import tableStyles from '../ui/DataTable.module.scss';
import styles from './ManageRequestsScreen.module.scss';
import { Employee, ManagedApprovalRow, isEditableRowStatus, isMovableRowStatus } from '../../models';
import { Employee, ManagedApprovalRow, REQUEST_STATUS, isEditableRowStatus, isMovableRowStatus } from '../../models';
import { texts } from '../../texts';
import { toEmployeeOption } from '../../utils/employee';
import { blockIndexOf, blockRanges, markParallel, withOrdersFrom } from '../../utils/orderBlocks';
@@ -27,6 +27,9 @@ export interface IManageRequestsPhaseTableProps {
statusVariant: (status: string) => BadgeVariant;
onReassign: (row: ManagedApprovalRow) => void;
onRemove: (row: ManagedApprovalRow) => void;
onNotify: (row: ManagedApprovalRow) => void;
/** Id строки, для которой сейчас в процессе отправка уведомления — кнопка на ней отключена. */
notifyingRowId?: number;
employees: Employee[];
employeesLoading: boolean;
/** Плейсхолдер плашки-пикера под таблицей — разный для согласования/исполнения. */
@@ -77,6 +80,8 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
statusVariant,
onReassign,
onRemove,
onNotify,
notifyingRowId,
employees,
employeesLoading,
addPlaceholder,
@@ -127,6 +132,13 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
// добавлять туда исполнителя всё равно бессмысленно).
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 renumbered = withOrdersFrom(mutate(withParallel), 0);
const updates = diffOrders(rows, renumbered);
@@ -186,6 +198,13 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
const isBlockStart = block.start === i;
const movable = blockIndex > lastLockedBlockIndex;
const rowEditable = isEditableRowStatus(r.status);
// Уведомлять можно только активный шаг (флоу шлёт письмо тому,
// чей RowID мы передали, независимо от статуса — сам он не
// проверяет очередь). «Ждёт очереди» — шаг ещё не наступил,
// слать по нему нечего: согласующий/исполнитель пока не
// должен действовать (см. request-portal/notify.service.ts,
// где по той же причине уведомляется только активный шаг).
const notifiable = r.status === REQUEST_STATUS.onApproval || r.status === REQUEST_STATUS.onExecution;
// Переключение параллельности — тоже перестановка (меняет order
// относительно соседней строки), поэтому смотрим на isMovableRowStatus,
// а не на rowEditable: у «Возвращена на доработку» кнопка
@@ -261,6 +280,13 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
disabled={!canToggle}
onClick={() => toggleParallel(i)}
/>
<IconButton
icon="mail"
className={styles.iconBtnVisible}
label={notifiable ? texts.manageRequests.notify : texts.manageRequests.notifyNotActive}
disabled={!notifiable || notifyingRowId === r.id}
onClick={() => onNotify(r)}
/>
<IconButton
icon="edit"
className={styles.iconBtnVisible}
@@ -303,7 +329,7 @@ export const ManageRequestsPhaseTable: React.FC<IManageRequestsPhaseTableProps>
{!readOnly && !phaseClosed && (
<div className={styles.addRow}>
<SearchableSelect
options={employees.map(toEmployeeOption)}
options={addableEmployees.map(toEmployeeOption)}
value=""
loading={employeesLoading}
placeholder={addPlaceholder}