feat: added status next in que and changed forms state machine
This commit is contained in:
@@ -8,7 +8,7 @@ import '@pnp/sp/profiles';
|
||||
import { IApprovalRow } from './models/IApprovalRow';
|
||||
import { IMatrixRow } from './models/IMatrixRow';
|
||||
import { ISiteUser } from './models/ISiteUser';
|
||||
import { APPROVAL_PHASE } from './models/statuses';
|
||||
import { APPROVAL_PHASE, REQUEST_STATUS } from './models/statuses';
|
||||
|
||||
// Внутренние имена колонок списка согласования (создаются провижинингом).
|
||||
const FIELDS = {
|
||||
@@ -187,6 +187,19 @@ export class ApprovalListService {
|
||||
});
|
||||
}
|
||||
|
||||
// Активирует следующий шаг маршрута: первую по очереди строку со статусом
|
||||
// «Ждёт очереди» переводит в активный статус её фазы (Согласование → «На
|
||||
// согласовании», Исполнение → «На исполнении»). Если ждущих нет — ничего.
|
||||
// Вызывается при отправке (активировать первый шаг) и после каждого решения.
|
||||
public async activateNextQueued(requestId: number): Promise<void> {
|
||||
const rows = await this.getForRequest(requestId); // отсортированы по StepOrder
|
||||
const next = rows.filter((r) => r.status === REQUEST_STATUS.queued)[0];
|
||||
if (!next) return;
|
||||
const active =
|
||||
next.phase === APPROVAL_PHASE.execution ? REQUEST_STATUS.onExecution : REQUEST_STATUS.onApproval;
|
||||
await this.list().items.getById(next.id).update({ [FIELDS.status]: active });
|
||||
}
|
||||
|
||||
// Удаляет все строки согласования по заявке (при возврате/повторной отправке).
|
||||
public async deleteForRequest(requestId: number): Promise<void> {
|
||||
const rows = await this.getForRequest(requestId);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { resolveRoute } from './approvalRoute.service';
|
||||
import { ApprovalListService, IApproverToWrite } from './approvalList.service';
|
||||
import { IMatrixRow } from './models/IMatrixRow';
|
||||
import { ApprovalStatus } from './models/IApprovalRouteApi';
|
||||
import { APPROVAL_PHASE } from './models/statuses';
|
||||
import { APPROVAL_PHASE, REQUEST_STATUS } from './models/statuses';
|
||||
|
||||
// Планируемый маршрут для НОВОЙ заявки: resolve по email инициатора → строки матрицы.
|
||||
// status выбирает фазу: 'Approval' (согласующие, по умолчанию) или 'Execution'
|
||||
@@ -58,12 +58,23 @@ export async function writeRoute(
|
||||
status: string
|
||||
): Promise<void> {
|
||||
await approval.deleteForRequest(requestId);
|
||||
await approval.createSteps(requestId, approvers, status, APPROVAL_PHASE.approval, 1);
|
||||
|
||||
// При ОТПРАВКЕ все шаги пишем «Ждёт очереди», затем активируем первый —
|
||||
// так активен ровно текущий шаг, остальные ждут. В черновике — статус заявки
|
||||
// (Черновик), без активации.
|
||||
const submitted = status === REQUEST_STATUS.onApproval;
|
||||
const rowStatus = submitted ? REQUEST_STATUS.queued : status;
|
||||
|
||||
await approval.createSteps(requestId, approvers, rowStatus, APPROVAL_PHASE.approval, 1);
|
||||
await approval.createSteps(
|
||||
requestId,
|
||||
executors,
|
||||
status,
|
||||
rowStatus,
|
||||
APPROVAL_PHASE.execution,
|
||||
approvers.length + 1
|
||||
);
|
||||
|
||||
if (submitted) {
|
||||
await approval.activateNextQueued(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,15 @@ import { IApprovalRow } from './models/IApprovalRow';
|
||||
import { ISiteUser } from './models/ISiteUser';
|
||||
import { emitRequestsChanged } from '../requestEvents';
|
||||
|
||||
export type ApproverDecision = 'approve' | 'return' | 'reject';
|
||||
// approve/return/reject — согласующий; execute — исполнитель отметил «Выполнено».
|
||||
export type ApproverDecision = 'approve' | 'return' | 'reject' | 'execute';
|
||||
|
||||
// Решение согласующего → статус его строки в списке согласования.
|
||||
// Решение → статус его строки в списке согласования.
|
||||
const ROW_STATUS: Record<ApproverDecision, string> = {
|
||||
approve: REQUEST_STATUS.approved,
|
||||
return: REQUEST_STATUS.returned,
|
||||
reject: REQUEST_STATUS.rejected,
|
||||
execute: REQUEST_STATUS.executed,
|
||||
};
|
||||
|
||||
// Действия согласующего: определение его строки, применение решения,
|
||||
@@ -34,7 +36,11 @@ export class ApproverService {
|
||||
public async getPendingRowForCurrentUser(requestId: number): Promise<IApprovalRow | undefined> {
|
||||
const rows = await this.approval.getForRequest(requestId);
|
||||
const userId = await this.approval.getCurrentUserId();
|
||||
const firstPending = rows.filter((r) => r.status === REQUEST_STATUS.onApproval)[0];
|
||||
// Активна первая незакрытая строка по очереди: согласующий («На согласовании»)
|
||||
// или, когда согласование пройдено, исполнитель («На исполнении»).
|
||||
const firstPending = rows.filter(
|
||||
(r) => r.status === REQUEST_STATUS.onApproval || r.status === REQUEST_STATUS.onExecution
|
||||
)[0];
|
||||
if (firstPending && firstPending.approverId === userId) return firstPending;
|
||||
return undefined;
|
||||
}
|
||||
@@ -53,12 +59,17 @@ export class ApproverService {
|
||||
const prevOrder = before.length > 0 ? before[before.length - 1].order : current.order - 1;
|
||||
const newOrder = (prevOrder + current.order) / 2;
|
||||
const position = await this.approval.getJobTitle(user.loginName);
|
||||
// Делегат наследует фазу той строки, вместо которой согласует (согласование/исполнение).
|
||||
// Делегат наследует фазу той строки, вместо которой действует, и её статус
|
||||
// ожидания (согласование → «На согласовании», исполнение → «На исполнении»).
|
||||
const pendingStatus =
|
||||
current.phase === APPROVAL_PHASE.execution
|
||||
? REQUEST_STATUS.onExecution
|
||||
: REQUEST_STATUS.onApproval;
|
||||
await this.approval.addApproverRow(
|
||||
requestId,
|
||||
{ email: user.email, position, allowReassign: false },
|
||||
newOrder,
|
||||
REQUEST_STATUS.onApproval,
|
||||
pendingStatus,
|
||||
current.phase
|
||||
);
|
||||
}
|
||||
@@ -72,6 +83,12 @@ export class ApproverService {
|
||||
comment: string
|
||||
): Promise<string> {
|
||||
await this.approval.setDecision(rowId, ROW_STATUS[decision], comment);
|
||||
// Положительное решение двигает очередь — активируем следующий ждущий шаг
|
||||
// (в т.ч. переход из согласования в исполнение). При возврате/отклонении
|
||||
// очередь не двигаем: заявка уходит на доработку.
|
||||
if (decision === 'approve' || decision === 'execute') {
|
||||
await this.approval.activateNextQueued(requestId);
|
||||
}
|
||||
const rows = await this.approval.getForRequest(requestId);
|
||||
const requestStatus = this.computeRequestStatus(rows);
|
||||
await this.updateRequestStatus(requestId, requestStatus);
|
||||
@@ -99,7 +116,7 @@ export class ApproverService {
|
||||
if (!approvalDone) return REQUEST_STATUS.onApproval;
|
||||
|
||||
if (execRows.length === 0) return REQUEST_STATUS.approved;
|
||||
if (execRows.every((r) => r.status === REQUEST_STATUS.approved)) return REQUEST_STATUS.closed;
|
||||
if (execRows.every((r) => r.status === REQUEST_STATUS.executed)) return REQUEST_STATUS.closed;
|
||||
return REQUEST_STATUS.execution;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,12 @@ export const REQUEST_STATUS = {
|
||||
approved: 'Согласовано',
|
||||
rejected: 'Отклонено',
|
||||
returned: 'Возвращена на доработку',
|
||||
// Статус СТРОКИ маршрута, до которой ещё не дошла очередь (активен только текущий шаг).
|
||||
queued: 'Ждёт очереди',
|
||||
// Статусы СТРОК фазы исполнения: активная очередь исполнителя и выполнено.
|
||||
onExecution: 'На исполнении',
|
||||
executed: 'Выполнено',
|
||||
// Статусы ЗАЯВКИ на этапе исполнения.
|
||||
execution: 'Выполнение',
|
||||
closed: 'Закрыта',
|
||||
} as const;
|
||||
@@ -27,13 +33,16 @@ export function isEditableStatus(status: string | undefined): boolean {
|
||||
return !status || EDITABLE_STATUSES.indexOf(status) !== -1;
|
||||
}
|
||||
|
||||
// Полный набор значений для Choice-колонки при провижининге.
|
||||
// Полный набор значений для Choice-колонки Status при провижининге.
|
||||
export const REQUEST_STATUS_CHOICES: string[] = [
|
||||
REQUEST_STATUS.draft,
|
||||
REQUEST_STATUS.onApproval,
|
||||
REQUEST_STATUS.approved,
|
||||
REQUEST_STATUS.rejected,
|
||||
REQUEST_STATUS.returned,
|
||||
REQUEST_STATUS.queued,
|
||||
REQUEST_STATUS.onExecution,
|
||||
REQUEST_STATUS.executed,
|
||||
REQUEST_STATUS.execution,
|
||||
REQUEST_STATUS.closed,
|
||||
];
|
||||
|
||||
@@ -3,6 +3,7 @@ import styles from './approval.module.scss';
|
||||
import { Icon } from '../../ui';
|
||||
import { IUseMatrixEditor } from '../useMatrixEditor';
|
||||
import { ISiteUser } from '../models/ISiteUser';
|
||||
import { StatusBadge } from './StatusBadge';
|
||||
import { UserPicker } from './UserPicker';
|
||||
|
||||
export interface IApprovalMatrixProps {
|
||||
@@ -53,7 +54,7 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
|
||||
<tr key={i}>
|
||||
<td>{r.who}</td>
|
||||
<td>{r.position}</td>
|
||||
<td>{r.status}</td>
|
||||
<td>{r.status && r.status !== '—' ? <StatusBadge status={r.status} /> : '—'}</td>
|
||||
<td>{r.date}</td>
|
||||
{editable && (
|
||||
<td className={styles.matrixActions}>
|
||||
|
||||
@@ -74,8 +74,10 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({ service, requestI
|
||||
|
||||
const isExecution = phase === APPROVAL_PHASE.execution;
|
||||
|
||||
if (checking) {
|
||||
return <div className={styles.state}>Проверяем вашу очередь…</div>;
|
||||
// Пока проверяем очередь или если действий нет (не твоя очередь / заявка
|
||||
// обработана / тебя нет в маршруте) — панель не показываем вовсе.
|
||||
if (checking || rowId === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (resultStatus) {
|
||||
@@ -89,14 +91,6 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({ service, requestI
|
||||
);
|
||||
}
|
||||
|
||||
if (rowId === undefined) {
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<Alert variant="warn">Сейчас не ваша очередь, либо заявка уже обработана.</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Фаза исполнения: одно действие «Выполнено» (без обязательного комментария),
|
||||
// строка помечается как «Согласовано» → computeRequestStatus переведёт заявку в
|
||||
// «Выполнение»/«Закрыта».
|
||||
@@ -114,7 +108,7 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({ service, requestI
|
||||
/>
|
||||
</FormField>
|
||||
<div className={styles.actions}>
|
||||
<Button variant="accent" disabled={submitting} onClick={() => decide('approve')}>
|
||||
<Button variant="accent" disabled={submitting} onClick={() => decide('execute')}>
|
||||
<Icon name="check" size="sm" />
|
||||
Выполнено
|
||||
</Button>
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
color: $gray-700;
|
||||
}
|
||||
|
||||
.queued {
|
||||
background: $gray-100;
|
||||
color: $gray-500;
|
||||
}
|
||||
|
||||
.onApproval {
|
||||
background: $blue-50;
|
||||
color: $blue-600;
|
||||
|
||||
@@ -5,8 +5,11 @@ import { REQUEST_STATUS } from '../models/statuses';
|
||||
// Цвет бейджа по статусу заявки. Неизвестный статус → нейтральный (как черновик).
|
||||
const VARIANT: Record<string, string> = {
|
||||
[REQUEST_STATUS.draft]: styles.draft,
|
||||
[REQUEST_STATUS.queued]: styles.queued,
|
||||
[REQUEST_STATUS.onApproval]: styles.onApproval,
|
||||
[REQUEST_STATUS.onExecution]: styles.onApproval, // активный шаг — как «на согласовании»
|
||||
[REQUEST_STATUS.approved]: styles.approved,
|
||||
[REQUEST_STATUS.executed]: styles.approved, // выполнено — как «согласовано» (зелёный)
|
||||
[REQUEST_STATUS.rejected]: styles.rejected,
|
||||
[REQUEST_STATUS.returned]: styles.returned,
|
||||
[REQUEST_STATUS.execution]: styles.execution,
|
||||
|
||||
Reference in New Issue
Block a user