Merge remote-tracking branch 'origin/main' into left-panel
This commit is contained in:
@@ -39,18 +39,27 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
const approval = useMemo(() => hireApprovalService(), []);
|
const approval = useMemo(() => hireApprovalService(), []);
|
||||||
const userEmail = context.pageContext.user.email;
|
const userEmail = context.pageContext.user.email;
|
||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() => getPlannedMatrix(context, HIRE_REQUEST_TYPE_ID, userEmail, approval),
|
() => getPlannedMatrix(context, HIRE_REQUEST_TYPE_ID, userEmail, approval, 'Approval'),
|
||||||
[context, userEmail, approval]
|
[context, userEmail, approval]
|
||||||
);
|
);
|
||||||
const matrix = useMatrixEditor(loadMatrix, userEmail, true, true, (login) =>
|
const matrix = useMatrixEditor(loadMatrix, userEmail, true, true, (login) =>
|
||||||
approval.getJobTitle(login)
|
approval.getJobTitle(login)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Фаза исполнения: исполнители из маршрута (status='Execution'), только для чтения.
|
||||||
|
const loadExecutors = useCallback(
|
||||||
|
() => getPlannedMatrix(context, HIRE_REQUEST_TYPE_ID, userEmail, approval, 'Execution'),
|
||||||
|
[context, userEmail, approval]
|
||||||
|
);
|
||||||
|
const executors = useMatrixEditor(loadExecutors, userEmail, false, true, (login) =>
|
||||||
|
approval.getJobTitle(login)
|
||||||
|
);
|
||||||
|
|
||||||
const saving = actionStatus === 'saving';
|
const saving = actionStatus === 'saving';
|
||||||
const isValid = data.CandidateName.trim().length > 0 && data.Position.trim().length > 0;
|
const isValid = data.CandidateName.trim().length > 0 && data.Position.trim().length > 0;
|
||||||
|
|
||||||
const matrixApprovers = (): IApproverToWrite[] =>
|
const toApprovers = (rows: typeof matrix.rows): IApproverToWrite[] =>
|
||||||
matrix.rows.map((r) => ({ email: r.email, position: r.position, allowReassign: r.allowReassign }));
|
rows.map((r) => ({ email: r.email, position: r.position, allowReassign: r.allowReassign }));
|
||||||
|
|
||||||
async function run(kind: ActionKind, requestStatus: string): Promise<void> {
|
async function run(kind: ActionKind, requestStatus: string): Promise<void> {
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
@@ -61,7 +70,12 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
setActionKind(kind);
|
setActionKind(kind);
|
||||||
setActionStatus('saving');
|
setActionStatus('saving');
|
||||||
try {
|
try {
|
||||||
const id = await saveHireRequest(data, matrixApprovers(), requestStatus);
|
const id = await saveHireRequest(
|
||||||
|
data,
|
||||||
|
toApprovers(matrix.rows),
|
||||||
|
toApprovers(executors.rows),
|
||||||
|
requestStatus
|
||||||
|
);
|
||||||
await uploadAttachments(DATA_LIST_TITLE, id, files); // файлы — после создания элемента
|
await uploadAttachments(DATA_LIST_TITLE, id, files); // файлы — после создания элемента
|
||||||
setStatus(requestStatus);
|
setStatus(requestStatus);
|
||||||
setActionStatus('success');
|
setActionStatus('success');
|
||||||
@@ -115,6 +129,16 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
|
|
||||||
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
|
{!executors.loading && executors.rows.length > 0 && (
|
||||||
|
<ApprovalMatrix
|
||||||
|
editor={executors}
|
||||||
|
search={(q) => approval.searchUsers(q)}
|
||||||
|
title="Исполнение (после согласования)"
|
||||||
|
roleLabel="Исполнитель"
|
||||||
|
emptyText="Исполнители не заданы по маршруту."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button variant="secondary" onClick={onCancel} disabled={saving}>
|
<Button variant="secondary" onClick={onCancel} disabled={saving}>
|
||||||
Отмена
|
Отмена
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
{ "internalName": "Ticket", "title": "Заявка", "type": "Lookup", "lookupList": "Прием на работу — данные", "lookupShowField": "Title" },
|
{ "internalName": "Ticket", "title": "Заявка", "type": "Lookup", "lookupList": "Прием на работу — данные", "lookupShowField": "Title" },
|
||||||
{ "internalName": "Date", "title": "Дата", "type": "DateTime" },
|
{ "internalName": "Date", "title": "Дата", "type": "DateTime" },
|
||||||
{ "internalName": "Comment", "title": "Комментарий", "type": "Note" },
|
{ "internalName": "Comment", "title": "Комментарий", "type": "Note" },
|
||||||
{ "internalName": "Order", "title": "Порядок", "type": "Number" },
|
{ "internalName": "StepOrder", "title": "Порядок", "type": "Number" },
|
||||||
{ "internalName": "AllowReassignToOtherApprovers", "title": "Можно перепоручать", "type": "Boolean" }
|
{ "internalName": "AllowReassignToOtherApprovers", "title": "Можно перепоручать", "type": "Boolean" },
|
||||||
|
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { ApprovalListService, IApproverToWrite, writeApprovers } from '../../shared/approval';
|
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
||||||
import { IFormData } from './model';
|
import { IFormData } from './model';
|
||||||
|
|
||||||
export const DATA_LIST_TITLE = 'Прием на работу — данные';
|
export const DATA_LIST_TITLE = 'Прием на работу — данные';
|
||||||
@@ -40,16 +40,18 @@ async function writeContent(data: IFormData, status: string): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сохранить заявку «на прием»: контент + маршрут согласующих со статусом.
|
* Сохранить заявку «на прием»: контент + маршрут (согласующие + исполнители) со статусом.
|
||||||
* status = «Черновик» (сохранение) или «На согласовании» (отправка).
|
* status = «Черновик» (сохранение) или «На согласовании» (отправка).
|
||||||
|
* executors может быть пустым — тогда после согласования заявка станет «Согласовано».
|
||||||
* Возвращает Id заявки.
|
* Возвращает Id заявки.
|
||||||
*/
|
*/
|
||||||
export async function saveHireRequest(
|
export async function saveHireRequest(
|
||||||
data: IFormData,
|
data: IFormData,
|
||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
|
executors: IApproverToWrite[],
|
||||||
status: string
|
status: string
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
const id = await writeContent(data, status);
|
const id = await writeContent(data, status);
|
||||||
await writeApprovers(hireApprovalService(), id, approvers, status);
|
await writeRoute(hireApprovalService(), id, approvers, executors, status);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,16 +62,25 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
const approval = useMemo(() => workplaceApprovalService(), []);
|
const approval = useMemo(() => workplaceApprovalService(), []);
|
||||||
const userEmail = context.pageContext.user.email;
|
const userEmail = context.pageContext.user.email;
|
||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() => getPlannedMatrix(context, WORKPLACE_REQUEST_TYPE_ID, userEmail, approval),
|
() => getPlannedMatrix(context, WORKPLACE_REQUEST_TYPE_ID, userEmail, approval, 'Approval'),
|
||||||
[context, userEmail, approval]
|
[context, userEmail, approval]
|
||||||
);
|
);
|
||||||
const matrix = useMatrixEditor(loadMatrix, userEmail, true, true, (login) => approval.getJobTitle(login));
|
const matrix = useMatrixEditor(loadMatrix, userEmail, true, true, (login) => approval.getJobTitle(login));
|
||||||
|
|
||||||
|
// Фаза исполнения: исполнители из маршрута (status='Execution'), только для чтения.
|
||||||
|
const loadExecutors = useCallback(
|
||||||
|
() => getPlannedMatrix(context, WORKPLACE_REQUEST_TYPE_ID, userEmail, approval, 'Execution'),
|
||||||
|
[context, userEmail, approval]
|
||||||
|
);
|
||||||
|
const executors = useMatrixEditor(loadExecutors, userEmail, false, true, (login) =>
|
||||||
|
approval.getJobTitle(login)
|
||||||
|
);
|
||||||
|
|
||||||
const saving = actionStatus === 'saving';
|
const saving = actionStatus === 'saving';
|
||||||
const isValid = data.FullName.trim().length > 0 && data.Position.trim().length > 0;
|
const isValid = data.FullName.trim().length > 0 && data.Position.trim().length > 0;
|
||||||
|
|
||||||
const matrixApprovers = (): IApproverToWrite[] =>
|
const toApprovers = (rows: typeof matrix.rows): IApproverToWrite[] =>
|
||||||
matrix.rows.map((r) => ({ email: r.email, position: r.position, allowReassign: r.allowReassign }));
|
rows.map((r) => ({ email: r.email, position: r.position, allowReassign: r.allowReassign }));
|
||||||
|
|
||||||
async function run(kind: ActionKind, requestStatus: string): Promise<void> {
|
async function run(kind: ActionKind, requestStatus: string): Promise<void> {
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
@@ -82,7 +91,13 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
setActionKind(kind);
|
setActionKind(kind);
|
||||||
setActionStatus('saving');
|
setActionStatus('saving');
|
||||||
try {
|
try {
|
||||||
const id = await saveWorkplaceRequest(data, matrixApprovers(), requestStatus, sourceId);
|
const id = await saveWorkplaceRequest(
|
||||||
|
data,
|
||||||
|
toApprovers(matrix.rows),
|
||||||
|
toApprovers(executors.rows),
|
||||||
|
requestStatus,
|
||||||
|
sourceId
|
||||||
|
);
|
||||||
await uploadAttachments(DATA_LIST_TITLE, id, files);
|
await uploadAttachments(DATA_LIST_TITLE, id, files);
|
||||||
setActionStatus('success');
|
setActionStatus('success');
|
||||||
if (onSaved) {
|
if (onSaved) {
|
||||||
@@ -125,6 +140,16 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
|
|
||||||
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
||||||
|
|
||||||
|
{!executors.loading && executors.rows.length > 0 && (
|
||||||
|
<ApprovalMatrix
|
||||||
|
editor={executors}
|
||||||
|
search={(q) => approval.searchUsers(q)}
|
||||||
|
title="Исполнение (после согласования)"
|
||||||
|
roleLabel="Исполнитель"
|
||||||
|
emptyText="Исполнители не заданы по маршруту."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button variant="secondary" onClick={onCancel} disabled={saving}>
|
<Button variant="secondary" onClick={onCancel} disabled={saving}>
|
||||||
Отмена
|
Отмена
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
{ "internalName": "Ticket", "title": "Заявка", "type": "Lookup", "lookupList": "Организация рабочего места — данные", "lookupShowField": "Title" },
|
{ "internalName": "Ticket", "title": "Заявка", "type": "Lookup", "lookupList": "Организация рабочего места — данные", "lookupShowField": "Title" },
|
||||||
{ "internalName": "Date", "title": "Дата", "type": "DateTime" },
|
{ "internalName": "Date", "title": "Дата", "type": "DateTime" },
|
||||||
{ "internalName": "Comment", "title": "Комментарий", "type": "Note" },
|
{ "internalName": "Comment", "title": "Комментарий", "type": "Note" },
|
||||||
{ "internalName": "Order", "title": "Порядок", "type": "Number" },
|
{ "internalName": "StepOrder", "title": "Порядок", "type": "Number" },
|
||||||
{ "internalName": "AllowReassignToOtherApprovers", "title": "Можно перепоручать", "type": "Boolean" }
|
{ "internalName": "AllowReassignToOtherApprovers", "title": "Можно перепоручать", "type": "Boolean" },
|
||||||
|
{ "internalName": "Phase", "title": "Фаза", "type": "Choice", "choices": ["Согласование", "Исполнение"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { ApprovalListService, IApproverToWrite, writeApprovers } from '../../shared/approval';
|
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
||||||
import { IFormData } from './model';
|
import { IFormData } from './model';
|
||||||
|
|
||||||
export const DATA_LIST_TITLE = 'Организация рабочего места — данные';
|
export const DATA_LIST_TITLE = 'Организация рабочего места — данные';
|
||||||
@@ -42,16 +42,18 @@ async function writeContent(data: IFormData, status: string, sourceHireId?: numb
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сохранить заявку «орг. рабочего места»: контент + маршрут согласующих.
|
* Сохранить заявку «орг. рабочего места»: контент + маршрут (согласующие + исполнители).
|
||||||
|
* Исполнители пишутся сразу — фаза исполнения стартует автоматически после согласования.
|
||||||
* sourceHireId — если заявка создана на основе «прием».
|
* sourceHireId — если заявка создана на основе «прием».
|
||||||
*/
|
*/
|
||||||
export async function saveWorkplaceRequest(
|
export async function saveWorkplaceRequest(
|
||||||
data: IFormData,
|
data: IFormData,
|
||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
|
executors: IApproverToWrite[],
|
||||||
status: string,
|
status: string,
|
||||||
sourceHireId?: number
|
sourceHireId?: number
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
const id = await writeContent(data, status, sourceHireId);
|
const id = await writeContent(data, status, sourceHireId);
|
||||||
await writeApprovers(workplaceApprovalService(), id, approvers, status);
|
await writeRoute(workplaceApprovalService(), id, approvers, executors, status);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import '@pnp/sp/profiles';
|
|||||||
import { IApprovalRow } from './models/IApprovalRow';
|
import { IApprovalRow } from './models/IApprovalRow';
|
||||||
import { IMatrixRow } from './models/IMatrixRow';
|
import { IMatrixRow } from './models/IMatrixRow';
|
||||||
import { ISiteUser } from './models/ISiteUser';
|
import { ISiteUser } from './models/ISiteUser';
|
||||||
|
import { APPROVAL_PHASE } from './models/statuses';
|
||||||
|
|
||||||
// Внутренние имена колонок списка согласования (создаются провижинингом).
|
// Внутренние имена колонок списка согласования (создаются провижинингом).
|
||||||
const FIELDS = {
|
const FIELDS = {
|
||||||
@@ -16,8 +17,10 @@ const FIELDS = {
|
|||||||
ticket: 'Ticket', // «Заявка» (Lookup) → TicketId
|
ticket: 'Ticket', // «Заявка» (Lookup) → TicketId
|
||||||
date: 'Date', // «Дата» (DateTime)
|
date: 'Date', // «Дата» (DateTime)
|
||||||
comment: 'Comment', // «Комментарий» (Text)
|
comment: 'Comment', // «Комментарий» (Text)
|
||||||
order: 'Order', // «Порядок» (Number)
|
order: 'StepOrder', // «Порядок» (Number). НЕ 'Order' — это зарезервированное
|
||||||
|
// скрытое системное поле SharePoint (не видно в UI списка).
|
||||||
allowReassign: 'AllowReassignToOtherApprovers', // «Можно перепоручать» (Да/Нет)
|
allowReassign: 'AllowReassignToOtherApprovers', // «Можно перепоручать» (Да/Нет)
|
||||||
|
phase: 'Phase', // «Фаза» (Choice): Согласование | Исполнение
|
||||||
};
|
};
|
||||||
|
|
||||||
// Согласующий для записи в список.
|
// Согласующий для записи в список.
|
||||||
@@ -35,12 +38,15 @@ export class ApprovalListService {
|
|||||||
return getSP().web.lists.getByTitle(this.listTitle);
|
return getSP().web.lists.getByTitle(this.listTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пишет по строке на каждого согласующего с указанным статусом.
|
// Пишет по строке на каждого участника фазы с указанным статусом.
|
||||||
// Порядок (Order) = позиция в массиве.
|
// Порядок (Order) = orderStart + позиция в массиве (фазы идут подряд в одном списке:
|
||||||
|
// сначала согласующие 1..N, затем исполнители N+1..N+M).
|
||||||
public async createSteps(
|
public async createSteps(
|
||||||
requestId: number,
|
requestId: number,
|
||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
status: string
|
status: string,
|
||||||
|
phase: string = APPROVAL_PHASE.approval,
|
||||||
|
orderStart: number = 1
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
for (let i = 0; i < approvers.length; i++) {
|
for (let i = 0; i < approvers.length; i++) {
|
||||||
const a = approvers[i];
|
const a = approvers[i];
|
||||||
@@ -51,8 +57,9 @@ export class ApprovalListService {
|
|||||||
[`${FIELDS.approver}Id`]: approverId,
|
[`${FIELDS.approver}Id`]: approverId,
|
||||||
[FIELDS.status]: status,
|
[FIELDS.status]: status,
|
||||||
[`${FIELDS.ticket}Id`]: requestId,
|
[`${FIELDS.ticket}Id`]: requestId,
|
||||||
[FIELDS.order]: i + 1,
|
[FIELDS.order]: orderStart + i,
|
||||||
[FIELDS.allowReassign]: a.allowReassign,
|
[FIELDS.allowReassign]: a.allowReassign,
|
||||||
|
[FIELDS.phase]: phase,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +69,8 @@ export class ApprovalListService {
|
|||||||
requestId: number,
|
requestId: number,
|
||||||
approver: IApproverToWrite,
|
approver: IApproverToWrite,
|
||||||
order: number,
|
order: number,
|
||||||
status: string
|
status: string,
|
||||||
|
phase: string = APPROVAL_PHASE.approval
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const approverId = await this.resolveUserId(approver.email);
|
const approverId = await this.resolveUserId(approver.email);
|
||||||
await this.list().items.add({
|
await this.list().items.add({
|
||||||
@@ -72,6 +80,7 @@ export class ApprovalListService {
|
|||||||
[`${FIELDS.ticket}Id`]: requestId,
|
[`${FIELDS.ticket}Id`]: requestId,
|
||||||
[FIELDS.order]: order,
|
[FIELDS.order]: order,
|
||||||
[FIELDS.allowReassign]: approver.allowReassign,
|
[FIELDS.allowReassign]: approver.allowReassign,
|
||||||
|
[FIELDS.phase]: phase,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +88,7 @@ export class ApprovalListService {
|
|||||||
public async getForRequest(requestId: number): Promise<IApprovalRow[]> {
|
public async getForRequest(requestId: number): Promise<IApprovalRow[]> {
|
||||||
const rows = (await this.list()
|
const rows = (await this.list()
|
||||||
.items.filter(`${FIELDS.ticket}Id eq ${requestId}`)
|
.items.filter(`${FIELDS.ticket}Id eq ${requestId}`)
|
||||||
.select('Id', FIELDS.status, FIELDS.order, FIELDS.allowReassign, `${FIELDS.approver}/Id`)
|
.select('Id', FIELDS.status, FIELDS.order, FIELDS.allowReassign, FIELDS.phase, `${FIELDS.approver}/Id`)
|
||||||
.expand(FIELDS.approver)
|
.expand(FIELDS.approver)
|
||||||
.orderBy(FIELDS.order, true)()) as Array<Record<string, unknown>>;
|
.orderBy(FIELDS.order, true)()) as Array<Record<string, unknown>>;
|
||||||
|
|
||||||
@@ -91,6 +100,8 @@ export class ApprovalListService {
|
|||||||
approverId: approver?.Id ?? 0,
|
approverId: approver?.Id ?? 0,
|
||||||
order: (r[FIELDS.order] as number) ?? 0,
|
order: (r[FIELDS.order] as number) ?? 0,
|
||||||
allowReassign: (r[FIELDS.allowReassign] as boolean) ?? false,
|
allowReassign: (r[FIELDS.allowReassign] as boolean) ?? false,
|
||||||
|
// Пустая фаза (старые строки без колонки) трактуется как «Согласование».
|
||||||
|
phase: (r[FIELDS.phase] as string) || APPROVAL_PHASE.approval,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -107,6 +118,7 @@ export class ApprovalListService {
|
|||||||
FIELDS.comment,
|
FIELDS.comment,
|
||||||
FIELDS.order,
|
FIELDS.order,
|
||||||
FIELDS.allowReassign,
|
FIELDS.allowReassign,
|
||||||
|
FIELDS.phase,
|
||||||
`${FIELDS.approver}/Title`
|
`${FIELDS.approver}/Title`
|
||||||
)
|
)
|
||||||
.expand(FIELDS.approver)
|
.expand(FIELDS.approver)
|
||||||
@@ -123,6 +135,7 @@ export class ApprovalListService {
|
|||||||
date: this.formatDate((r[FIELDS.date] as string) || ''),
|
date: this.formatDate((r[FIELDS.date] as string) || ''),
|
||||||
comment: (r[FIELDS.comment] as string) || '',
|
comment: (r[FIELDS.comment] as string) || '',
|
||||||
allowReassign: (r[FIELDS.allowReassign] as boolean) ?? false,
|
allowReassign: (r[FIELDS.allowReassign] as boolean) ?? false,
|
||||||
|
phase: (r[FIELDS.phase] as string) || APPROVAL_PHASE.approval,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
|
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
|
||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import { APPROVAL_API_CLIENT_ID, APPROVAL_API_BASE_URL } from '../config';
|
import { APPROVAL_API_CLIENT_ID, APPROVAL_API_BASE_URL } from '../config';
|
||||||
import { IApiResponse, IRequestType, IResolvedRoute } from './models/IApprovalRouteApi';
|
import { ApprovalStatus, IApiResponse, IRequestType, IResolvedRoute } from './models/IApprovalRouteApi';
|
||||||
|
|
||||||
// Все обращения к внешнему approval-route API — только здесь.
|
// Все обращения к внешнему approval-route API — только здесь.
|
||||||
// Клиент авторизуется через AadHttpClient (SSO по текущему пользователю).
|
// Клиент авторизуется через AadHttpClient (SSO по текущему пользователю).
|
||||||
@@ -24,13 +24,17 @@ export async function getRequestTypes(context: WebPartContext): Promise<IRequest
|
|||||||
return body.data ?? [];
|
return body.data ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/template-routes/resolve — планируемый маршрут согласования для типа
|
// POST /api/template-routes/resolve — планируемый маршрут для типа заявки и
|
||||||
// заявки и пользователя. Бэкенд сам подставляет согласующих (executorEmail/position)
|
// пользователя. Бэкенд сам подставляет исполнителей шага (executorEmail/position)
|
||||||
// по ролям исходя из userEmail.
|
// по ролям исходя из userEmail. Параметр status выбирает ФАЗУ маршрута:
|
||||||
|
// 'Approval' — согласующие (кто одобряет),
|
||||||
|
// 'Execution' — исполнители (кто выполняет после согласования).
|
||||||
|
// Один и тот же маршрут, разные наборы шагов.
|
||||||
export async function resolveRoute(
|
export async function resolveRoute(
|
||||||
context: WebPartContext,
|
context: WebPartContext,
|
||||||
requestTypeId: number,
|
requestTypeId: number,
|
||||||
userEmail: string
|
userEmail: string,
|
||||||
|
status: ApprovalStatus = 'Approval'
|
||||||
): Promise<IResolvedRoute> {
|
): Promise<IResolvedRoute> {
|
||||||
const c = await client(context);
|
const c = await client(context);
|
||||||
const url = `${APPROVAL_API_BASE_URL}/api/template-routes/resolve`;
|
const url = `${APPROVAL_API_BASE_URL}/api/template-routes/resolve`;
|
||||||
@@ -45,7 +49,7 @@ export async function resolveRoute(
|
|||||||
requestTypeId,
|
requestTypeId,
|
||||||
userEmail,
|
userEmail,
|
||||||
projectId: null,
|
projectId: null,
|
||||||
status: 'Approval',
|
status,
|
||||||
fieldValues: []
|
fieldValues: []
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,17 +2,22 @@ import { WebPartContext } from '@microsoft/sp-webpart-base';
|
|||||||
import { resolveRoute } from './approvalRoute.service';
|
import { resolveRoute } from './approvalRoute.service';
|
||||||
import { ApprovalListService, IApproverToWrite } from './approvalList.service';
|
import { ApprovalListService, IApproverToWrite } from './approvalList.service';
|
||||||
import { IMatrixRow } from './models/IMatrixRow';
|
import { IMatrixRow } from './models/IMatrixRow';
|
||||||
|
import { ApprovalStatus } from './models/IApprovalRouteApi';
|
||||||
|
import { APPROVAL_PHASE } from './models/statuses';
|
||||||
|
|
||||||
// Планируемый маршрут для НОВОЙ заявки: resolve по email инициатора → строки матрицы.
|
// Планируемый маршрут для НОВОЙ заявки: resolve по email инициатора → строки матрицы.
|
||||||
// Имена согласующих подтягиваются по executorEmail (бэкенд отдаёт email + должность).
|
// status выбирает фазу: 'Approval' (согласующие, по умолчанию) или 'Execution'
|
||||||
|
// (исполнители). Имена подтягиваются по executorEmail (бэкенд отдаёт email + должность).
|
||||||
export async function getPlannedMatrix(
|
export async function getPlannedMatrix(
|
||||||
context: WebPartContext,
|
context: WebPartContext,
|
||||||
requestTypeId: number,
|
requestTypeId: number,
|
||||||
userEmail: string,
|
userEmail: string,
|
||||||
approval: ApprovalListService
|
approval: ApprovalListService,
|
||||||
|
status: ApprovalStatus = 'Approval'
|
||||||
): Promise<IMatrixRow[]> {
|
): Promise<IMatrixRow[]> {
|
||||||
if (!userEmail) return [];
|
if (!userEmail) return [];
|
||||||
const route = await resolveRoute(context, requestTypeId, userEmail);
|
const route = await resolveRoute(context, requestTypeId, userEmail, status);
|
||||||
|
const phase = status === 'Execution' ? APPROVAL_PHASE.execution : APPROVAL_PHASE.approval;
|
||||||
const ordered = route.steps.slice().sort((a, b) => a.orderNo - b.orderNo);
|
const ordered = route.steps.slice().sort((a, b) => a.orderNo - b.orderNo);
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
ordered.map(async (s) => ({
|
ordered.map(async (s) => ({
|
||||||
@@ -24,11 +29,12 @@ export async function getPlannedMatrix(
|
|||||||
date: '',
|
date: '',
|
||||||
comment: '',
|
comment: '',
|
||||||
allowReassign: s.allowReassignToOtherApprovers,
|
allowReassign: s.allowReassignToOtherApprovers,
|
||||||
|
phase,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Записать строки согласующих для заявки с указанным статусом.
|
// Записать строки согласующих для заявки с указанным статусом (только фаза согласования).
|
||||||
// Матрица перезаписывается целиком (для повторной отправки/черновика).
|
// Матрица перезаписывается целиком (для повторной отправки/черновика).
|
||||||
export async function writeApprovers(
|
export async function writeApprovers(
|
||||||
approval: ApprovalListService,
|
approval: ApprovalListService,
|
||||||
@@ -37,5 +43,27 @@ export async function writeApprovers(
|
|||||||
status: string
|
status: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await approval.deleteForRequest(requestId);
|
await approval.deleteForRequest(requestId);
|
||||||
await approval.createSteps(requestId, approvers, status);
|
await approval.createSteps(requestId, approvers, status, APPROVAL_PHASE.approval, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Записать ОБЕ фазы маршрута заявки: согласующих (Order 1..N) и исполнителей
|
||||||
|
// (Order N+1..N+M) в один список. Исполнители пишутся сразу при отправке —
|
||||||
|
// стороне согласующего внешний API уже не нужен, переход в фазу исполнения
|
||||||
|
// делает computeRequestStatus, когда все согласующие одобрили.
|
||||||
|
export async function writeRoute(
|
||||||
|
approval: ApprovalListService,
|
||||||
|
requestId: number,
|
||||||
|
approvers: IApproverToWrite[],
|
||||||
|
executors: IApproverToWrite[],
|
||||||
|
status: string
|
||||||
|
): Promise<void> {
|
||||||
|
await approval.deleteForRequest(requestId);
|
||||||
|
await approval.createSteps(requestId, approvers, status, APPROVAL_PHASE.approval, 1);
|
||||||
|
await approval.createSteps(
|
||||||
|
requestId,
|
||||||
|
executors,
|
||||||
|
status,
|
||||||
|
APPROVAL_PHASE.execution,
|
||||||
|
approvers.length + 1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import '@pnp/sp/webs';
|
|||||||
import '@pnp/sp/lists';
|
import '@pnp/sp/lists';
|
||||||
import '@pnp/sp/items';
|
import '@pnp/sp/items';
|
||||||
import { ApprovalListService } from './approvalList.service';
|
import { ApprovalListService } from './approvalList.service';
|
||||||
import { REQUEST_STATUS } from './models/statuses';
|
import { REQUEST_STATUS, APPROVAL_PHASE } from './models/statuses';
|
||||||
import { IApprovalRow } from './models/IApprovalRow';
|
import { IApprovalRow } from './models/IApprovalRow';
|
||||||
import { ISiteUser } from './models/ISiteUser';
|
import { ISiteUser } from './models/ISiteUser';
|
||||||
|
|
||||||
@@ -52,11 +52,13 @@ export class ApproverService {
|
|||||||
const prevOrder = before.length > 0 ? before[before.length - 1].order : current.order - 1;
|
const prevOrder = before.length > 0 ? before[before.length - 1].order : current.order - 1;
|
||||||
const newOrder = (prevOrder + current.order) / 2;
|
const newOrder = (prevOrder + current.order) / 2;
|
||||||
const position = await this.approval.getJobTitle(user.loginName);
|
const position = await this.approval.getJobTitle(user.loginName);
|
||||||
|
// Делегат наследует фазу той строки, вместо которой согласует (согласование/исполнение).
|
||||||
await this.approval.addApproverRow(
|
await this.approval.addApproverRow(
|
||||||
requestId,
|
requestId,
|
||||||
{ email: user.email, position, allowReassign: false },
|
{ email: user.email, position, allowReassign: false },
|
||||||
newOrder,
|
newOrder,
|
||||||
REQUEST_STATUS.onApproval
|
REQUEST_STATUS.onApproval,
|
||||||
|
current.phase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,13 +77,28 @@ export class ApproverService {
|
|||||||
return requestStatus;
|
return requestStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Статус заявки из строк маршрута с учётом фазы:
|
||||||
|
// 1) фаза согласования: отклонение/возврат → соответствующий статус;
|
||||||
|
// не все согласовали → «На согласовании».
|
||||||
|
// 2) согласование завершено: нет исполнителей → «Согласовано» (финал);
|
||||||
|
// исполнители есть и не все выполнили → «Выполнение»;
|
||||||
|
// все выполнили → «Закрыта».
|
||||||
|
// Строка «выполнена» исполнителем помечается тем же статусом «Согласовано»
|
||||||
|
// (действие «Выполнено» в DecisionPanel), фазы различаются колонкой Phase.
|
||||||
private computeRequestStatus(rows: IApprovalRow[]): string {
|
private computeRequestStatus(rows: IApprovalRow[]): string {
|
||||||
if (rows.some((r) => r.status === REQUEST_STATUS.rejected)) return REQUEST_STATUS.rejected;
|
const execRows = rows.filter((r) => r.phase === APPROVAL_PHASE.execution);
|
||||||
if (rows.some((r) => r.status === REQUEST_STATUS.returned)) return REQUEST_STATUS.returned;
|
const approvalRows = rows.filter((r) => r.phase !== APPROVAL_PHASE.execution);
|
||||||
if (rows.length > 0 && rows.every((r) => r.status === REQUEST_STATUS.approved)) {
|
|
||||||
return REQUEST_STATUS.approved;
|
if (approvalRows.some((r) => r.status === REQUEST_STATUS.rejected)) return REQUEST_STATUS.rejected;
|
||||||
}
|
if (approvalRows.some((r) => r.status === REQUEST_STATUS.returned)) return REQUEST_STATUS.returned;
|
||||||
return REQUEST_STATUS.onApproval;
|
|
||||||
|
const approvalDone =
|
||||||
|
approvalRows.length === 0 || approvalRows.every((r) => r.status === REQUEST_STATUS.approved);
|
||||||
|
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;
|
||||||
|
return REQUEST_STATUS.execution;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateRequestStatus(id: number, status: string): Promise<void> {
|
private async updateRequestStatus(id: number, status: string): Promise<void> {
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
// Единая точка импорта механизма согласования.
|
// Единая точка импорта механизма согласования.
|
||||||
export { REQUEST_STATUS, REQUEST_STATUS_CHOICES } from './models/statuses';
|
export {
|
||||||
|
REQUEST_STATUS,
|
||||||
|
REQUEST_STATUS_CHOICES,
|
||||||
|
APPROVAL_PHASE,
|
||||||
|
APPROVAL_PHASE_CHOICES,
|
||||||
|
} from './models/statuses';
|
||||||
export type { ISiteUser } from './models/ISiteUser';
|
export type { ISiteUser } from './models/ISiteUser';
|
||||||
export type { IMatrixRow } from './models/IMatrixRow';
|
export type { IMatrixRow } from './models/IMatrixRow';
|
||||||
export type { IApprovalRow } from './models/IApprovalRow';
|
export type { IApprovalRow } from './models/IApprovalRow';
|
||||||
export type {
|
export type {
|
||||||
|
ApprovalStatus,
|
||||||
IRequestType,
|
IRequestType,
|
||||||
IResolvedRoute,
|
IResolvedRoute,
|
||||||
IResolvedRouteStep,
|
IResolvedRouteStep,
|
||||||
@@ -14,7 +20,7 @@ export { ApprovalListService } from './approvalList.service';
|
|||||||
export type { IApproverToWrite } from './approvalList.service';
|
export type { IApproverToWrite } from './approvalList.service';
|
||||||
export { ApproverService } from './approverDecision.service';
|
export { ApproverService } from './approverDecision.service';
|
||||||
export type { ApproverDecision } from './approverDecision.service';
|
export type { ApproverDecision } from './approverDecision.service';
|
||||||
export { getPlannedMatrix, writeApprovers } from './approvalSubmission.service';
|
export { getPlannedMatrix, writeApprovers, writeRoute } from './approvalSubmission.service';
|
||||||
export { useMatrixEditor } from './useMatrixEditor';
|
export { useMatrixEditor } from './useMatrixEditor';
|
||||||
export type { IUseMatrixEditor } from './useMatrixEditor';
|
export type { IUseMatrixEditor } from './useMatrixEditor';
|
||||||
export { ApprovalMatrix } from './ui/ApprovalMatrix';
|
export { ApprovalMatrix } from './ui/ApprovalMatrix';
|
||||||
|
|||||||
@@ -34,4 +34,7 @@ export interface IResolvedRouteStep {
|
|||||||
executorEmail: string | null;
|
executorEmail: string | null;
|
||||||
position: string | null;
|
position: string | null;
|
||||||
allowReassignToOtherApprovers: boolean;
|
allowReassignToOtherApprovers: boolean;
|
||||||
|
// Предупреждение маршрута по этому шагу (например, роль не разрешилась в
|
||||||
|
// конкретного человека) — приходит от API, показываем автору при желании.
|
||||||
|
warning: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ export interface IApprovalRow {
|
|||||||
approverId: number; // Id пользователя сайта в поле «Согласующий»
|
approverId: number; // Id пользователя сайта в поле «Согласующий»
|
||||||
order: number; // колонка Order (очередь согласования)
|
order: number; // колонка Order (очередь согласования)
|
||||||
allowReassign: boolean; // AllowReassignToOtherApprovers
|
allowReassign: boolean; // AllowReassignToOtherApprovers
|
||||||
|
phase: string; // Phase: «Согласование» | «Исполнение» (см. APPROVAL_PHASE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ export interface IMatrixRow {
|
|||||||
date: string; // дата решения (дд.мм.гггг) или ''
|
date: string; // дата решения (дд.мм.гггг) или ''
|
||||||
comment: string;
|
comment: string;
|
||||||
allowReassign: boolean; // может ли этот согласующий добавить делеганта
|
allowReassign: boolean; // может ли этот согласующий добавить делеганта
|
||||||
|
phase?: string; // фаза строки: «Согласование» | «Исполнение» (см. APPROVAL_PHASE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,3 +23,18 @@ export const REQUEST_STATUS_CHOICES: string[] = [
|
|||||||
REQUEST_STATUS.execution,
|
REQUEST_STATUS.execution,
|
||||||
REQUEST_STATUS.closed,
|
REQUEST_STATUS.closed,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Фаза строки в списке маршрута: согласование (кто одобряет) и исполнение
|
||||||
|
// (кто выполняет после согласования). Обе фазы живут в одном списке «— согласование»,
|
||||||
|
// строки различаются колонкой Phase. Значения приходят из внешнего API как
|
||||||
|
// Approval/Execution, но в списке храним по-русски для читаемости колонки.
|
||||||
|
export const APPROVAL_PHASE = {
|
||||||
|
approval: 'Согласование',
|
||||||
|
execution: 'Исполнение',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Значения Choice-колонки Phase при провижининге.
|
||||||
|
export const APPROVAL_PHASE_CHOICES: string[] = [
|
||||||
|
APPROVAL_PHASE.approval,
|
||||||
|
APPROVAL_PHASE.execution,
|
||||||
|
];
|
||||||
|
|||||||
@@ -8,25 +8,40 @@ import { UserPicker } from './UserPicker';
|
|||||||
export interface IApprovalMatrixProps {
|
export interface IApprovalMatrixProps {
|
||||||
editor: IUseMatrixEditor;
|
editor: IUseMatrixEditor;
|
||||||
search: (query: string) => Promise<ISiteUser[]>;
|
search: (query: string) => Promise<ISiteUser[]>;
|
||||||
|
/** Заголовок блока. По умолчанию — «Маршрут согласования». */
|
||||||
|
title?: string;
|
||||||
|
/** Подпись роли в шапке таблицы. По умолчанию — «Согласующий». */
|
||||||
|
roleLabel?: string;
|
||||||
|
/** Текст, когда участников нет. По умолчанию — «Согласующие не заданы.». */
|
||||||
|
emptyText?: string;
|
||||||
|
/** Плейсхолдер пикера добавления (только если editor.editable). */
|
||||||
|
pickerPlaceholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Таблица маршрута согласования + (если editable) правка порядка/состава. */
|
/** Таблица маршрута (согласование или исполнение) + (если editable) правка состава. */
|
||||||
export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({ editor, search }) => {
|
export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
|
||||||
|
editor,
|
||||||
|
search,
|
||||||
|
title = 'Маршрут согласования',
|
||||||
|
roleLabel = 'Согласующий',
|
||||||
|
emptyText = 'Согласующие не заданы.',
|
||||||
|
pickerPlaceholder = 'Добавить согласующего: начните вводить ФИО…',
|
||||||
|
}) => {
|
||||||
const { rows, loading, error, editable } = editor;
|
const { rows, loading, error, editable } = editor;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.matrixWrap}>
|
<div className={styles.matrixWrap}>
|
||||||
<div className={styles.matrixTitle}>Маршрут согласования</div>
|
<div className={styles.matrixTitle}>{title}</div>
|
||||||
{loading && <div className={styles.matrixNote}>Загрузка маршрута…</div>}
|
{loading && <div className={styles.matrixNote}>Загрузка маршрута…</div>}
|
||||||
{!loading && error && <div className={styles.matrixNote}>Маршрут недоступен: {error}</div>}
|
{!loading && error && <div className={styles.matrixNote}>Маршрут недоступен: {error}</div>}
|
||||||
{!loading && !error && rows.length === 0 && (
|
{!loading && !error && rows.length === 0 && (
|
||||||
<div className={styles.matrixNote}>Согласующие не заданы.</div>
|
<div className={styles.matrixNote}>{emptyText}</div>
|
||||||
)}
|
)}
|
||||||
{!loading && !error && rows.length > 0 && (
|
{!loading && !error && rows.length > 0 && (
|
||||||
<table className={styles.matrix}>
|
<table className={styles.matrix}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Согласующий</th>
|
<th>{roleLabel}</th>
|
||||||
<th>Должность</th>
|
<th>Должность</th>
|
||||||
<th>Статус</th>
|
<th>Статус</th>
|
||||||
<th>Дата</th>
|
<th>Дата</th>
|
||||||
@@ -77,11 +92,7 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({ editor, search
|
|||||||
)}
|
)}
|
||||||
{editable && !loading && (
|
{editable && !loading && (
|
||||||
<div className={styles.matrixPicker}>
|
<div className={styles.matrixPicker}>
|
||||||
<UserPicker
|
<UserPicker search={search} onPick={editor.addApprover} placeholder={pickerPlaceholder} />
|
||||||
search={search}
|
|
||||||
onPick={editor.addApprover}
|
|
||||||
placeholder="Добавить согласующего: начните вводить ФИО…"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import styles from './DecisionPanel.module.scss';
|
import styles from './DecisionPanel.module.scss';
|
||||||
import { Button, Alert, FormField, TextArea, Icon } from '../../ui';
|
import { Button, Alert, FormField, TextArea, Icon } from '../../ui';
|
||||||
import { ApproverService, ApproverDecision } from '../approverDecision.service';
|
import { ApproverService, ApproverDecision } from '../approverDecision.service';
|
||||||
|
import { APPROVAL_PHASE } from '../models/statuses';
|
||||||
|
|
||||||
interface IDecisionPanelProps {
|
interface IDecisionPanelProps {
|
||||||
service: ApproverService;
|
service: ApproverService;
|
||||||
@@ -17,6 +18,7 @@ interface IDecisionPanelProps {
|
|||||||
export const DecisionPanel: React.FC<IDecisionPanelProps> = ({ service, requestId, onDecided }) => {
|
export const DecisionPanel: React.FC<IDecisionPanelProps> = ({ service, requestId, onDecided }) => {
|
||||||
const [checking, setChecking] = useState<boolean>(true);
|
const [checking, setChecking] = useState<boolean>(true);
|
||||||
const [rowId, setRowId] = useState<number | undefined>(undefined);
|
const [rowId, setRowId] = useState<number | undefined>(undefined);
|
||||||
|
const [phase, setPhase] = useState<string>(APPROVAL_PHASE.approval);
|
||||||
const [comment, setComment] = useState<string>('');
|
const [comment, setComment] = useState<string>('');
|
||||||
const [submitting, setSubmitting] = useState<boolean>(false);
|
const [submitting, setSubmitting] = useState<boolean>(false);
|
||||||
const [resultStatus, setResultStatus] = useState<string | undefined>(undefined);
|
const [resultStatus, setResultStatus] = useState<string | undefined>(undefined);
|
||||||
@@ -30,6 +32,9 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({ service, requestI
|
|||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (alive) {
|
if (alive) {
|
||||||
setRowId(row?.id);
|
setRowId(row?.id);
|
||||||
|
if (row) {
|
||||||
|
setPhase(row.phase);
|
||||||
|
}
|
||||||
setChecking(false);
|
setChecking(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -67,14 +72,19 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({ service, requestI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isExecution = phase === APPROVAL_PHASE.execution;
|
||||||
|
|
||||||
if (checking) {
|
if (checking) {
|
||||||
return <div className={styles.state}>Проверяем вашу очередь на согласование…</div>;
|
return <div className={styles.state}>Проверяем вашу очередь…</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resultStatus) {
|
if (resultStatus) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.panel}>
|
<div className={styles.panel}>
|
||||||
<Alert variant="ok">Решение сохранено. Статус заявки: «{resultStatus}».</Alert>
|
<Alert variant="ok">
|
||||||
|
{isExecution ? 'Отмечено как выполнено' : 'Решение сохранено'}. Статус заявки: «
|
||||||
|
{resultStatus}».
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -82,9 +92,33 @@ export const DecisionPanel: React.FC<IDecisionPanelProps> = ({ service, requestI
|
|||||||
if (rowId === undefined) {
|
if (rowId === undefined) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.panel}>
|
<div className={styles.panel}>
|
||||||
<Alert variant="warn">
|
<Alert variant="warn">Сейчас не ваша очередь, либо заявка уже обработана.</Alert>
|
||||||
Сейчас не ваша очередь на согласование, либо заявка уже обработана.
|
</div>
|
||||||
</Alert>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Фаза исполнения: одно действие «Выполнено» (без обязательного комментария),
|
||||||
|
// строка помечается как «Согласовано» → computeRequestStatus переведёт заявку в
|
||||||
|
// «Выполнение»/«Закрыта».
|
||||||
|
if (isExecution) {
|
||||||
|
return (
|
||||||
|
<div className={styles.panel}>
|
||||||
|
<div className={styles.title}>Исполнение</div>
|
||||||
|
{error && <Alert variant="err">{error}</Alert>}
|
||||||
|
<FormField label="Комментарий" htmlFor="decisionComment">
|
||||||
|
<TextArea
|
||||||
|
id="decisionComment"
|
||||||
|
value={comment}
|
||||||
|
onChange={setComment}
|
||||||
|
placeholder="Комментарий (необязательно)"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Button variant="accent" disabled={submitting} onClick={() => decide('approve')}>
|
||||||
|
<Icon name="check" size="sm" />
|
||||||
|
Выполнено
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user