feat: changed get time logic to approvers

This commit is contained in:
Тимур Городецкий
2026-08-13 10:01:08 +05:00
parent 2e6e138e8d
commit 8578653c16
55 changed files with 2249 additions and 46 deletions
@@ -130,7 +130,7 @@ export class ApprovalListService {
if (queuedRows.length === 0) return;
const minOrder = Math.min(...queuedRows.map((r) => r.order));
await Promise.all(
queuedRows.filter((r) => r.order === minOrder).map((r) => this.setStatus(r.id, activeStatus))
queuedRows.filter((r) => r.order === minOrder).map((r) => this.setActive(r.id, activeStatus))
);
};
@@ -146,6 +146,17 @@ export class ApprovalListService {
await this.list().items.getById(rowId).update({ [FIELDS.status]: status });
}
// Активация строки: статус + отметка времени, КОГДА строка стала текущей
// («получил на согласование/исполнение»). Отличается от setStatus именно
// датой — использовать только на переходе в активный статус, не для отката в
// «Ждёт очереди». Дату решения позже перезапишет setDecision. См.
// activateNextQueued (единая точка активации: отправка, одобрение, делегат).
private async setActive(rowId: number, status: string): Promise<void> {
await this.list()
.items.getById(rowId)
.update({ [FIELDS.status]: status, [FIELDS.date]: new Date().toISOString() });
}
// Добавляет одну строку согласования с заданным Order (для делегата).
// delegatedById — Id пользователя, который выполнил переназначение (не строки
// из маршрута — у них делегата нет). Он сохраняет право переназначить эту
@@ -262,10 +273,14 @@ export class ApprovalListService {
}
// Должность пользователя из профиля SharePoint (SPS-JobTitle). '' если недоступно.
// Принимает и клеймовый логин (i:0#.f|membership|…), и голый e-mail: пикер
// сотрудника отдаёт именно e-mail (см. pickedLogin), а getPropertiesFor ждёт
// account name — приводим e-mail к клеймовому виду так же, как ensure().
public async getJobTitle(loginName: string): Promise<string> {
if (!loginName) return '';
const account = loginName.indexOf('|') >= 0 ? loginName : `i:0#.f|membership|${loginName}`;
try {
const props = (await getSP().profiles.getPropertiesFor(loginName)) as {
const props = (await getSP().profiles.getPropertiesFor(account)) as {
UserProfileProperties?: unknown;
};
const raw = props.UserProfileProperties;
@@ -326,11 +341,17 @@ export class ApprovalListService {
return getSP().web.ensureUser(login);
}
// Формат «дд.мм.гггг чч:мм» в казахстанском поясе (UTC+5) — жёстко, а не по
// поясу браузера: время должно совпадать у всех участников. В SharePoint дата
// хранится в UTC (setDecision/setActive пишут toISOString); Казахстан без
// перехода на летнее время, поэтому достаточно прибавить 5 часов и читать
// UTC-компоненты (getUTC*), не завися от локального пояса среды.
private formatDate(iso: string): string {
if (!iso) return '';
const d = new Date(iso);
const dd = d.getDate() < 10 ? `0${d.getDate()}` : `${d.getDate()}`;
const mm = d.getMonth() + 1 < 10 ? `0${d.getMonth() + 1}` : `${d.getMonth() + 1}`;
return `${dd}.${mm}.${d.getFullYear()}`;
const KZ_OFFSET_MS = 5 * 60 * 60 * 1000;
const d = new Date(new Date(iso).getTime() + KZ_OFFSET_MS);
const p = (n: number): string => (n < 10 ? `0${n}` : `${n}`);
const date = `${p(d.getUTCDate())}.${p(d.getUTCMonth() + 1)}.${d.getUTCFullYear()}`;
return `${date} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`;
}
}
@@ -41,12 +41,16 @@ export async function resolveRoute(
userEmail: string,
status: ApprovalStatus = 'Approval',
departmentId: string | null = null,
fieldValues: IResolveFieldValue[] = []
fieldValues: IResolveFieldValue[] = [],
projectId: string | null = null
): Promise<IResolvedRoute> {
const c = await client(context);
const url = `${APPROVAL_API_BASE_URL}/api/template-routes/resolve`;
// Эндпоинт сменился с GET (query) на POST (JSON-тело).
// projectId (uuid проекта) — для форм, где согласующий зависит от проекта
// (напр. «руководитель проекта-источника» в СЗ на перемещение ТМЦ): бэкенд по
// нему подставляет исполнителя роли. Формы без привязки к проекту шлют null.
const response: HttpClientResponse = await c.post(url, AadHttpClient.configurations.v1, {
headers: {
'Content-Type': 'application/json',
@@ -55,7 +59,7 @@ export async function resolveRoute(
body: JSON.stringify({
requestTypeId,
userEmail,
projectId: null,
projectId: projectId || null,
status,
fieldValues,
departmentId: departmentId || null
@@ -16,10 +16,11 @@ export async function getPlannedMatrix(
approval: ApprovalListService,
status: ApprovalStatus = 'Approval',
departmentId?: string | null,
fieldValues?: IResolveFieldValue[]
fieldValues?: IResolveFieldValue[],
projectId?: string | null
): Promise<IMatrixRow[]> {
if (!userEmail) return [];
const route = await resolveRoute(context, requestTypeId, userEmail, status, departmentId, fieldValues);
const route = await resolveRoute(context, requestTypeId, userEmail, status, departmentId, fieldValues, projectId);
const phase = status === 'Execution' ? APPROVAL_PHASE.execution : APPROVAL_PHASE.approval;
const ordered = route.steps.slice().sort((a, b) => a.orderNo - b.orderNo);
return Promise.all(
@@ -18,6 +18,15 @@ export interface IApprovalMatrixProps {
emptyText?: string;
/** Плейсхолдер пикера добавления (только если editor.editable). */
pickerPlaceholder?: string;
/**
* Режим ожидания: маршрут ещё нельзя построить (не выбрана зависимость,
* напр. проект). Показываем только заголовок и информационную заглушку —
* без обращения к API, таблицы и пикера. Состояние loading/error/rows
* редактора при этом игнорируется.
*/
waiting?: boolean;
/** Текст заглушки в режиме waiting. */
waitingText?: string;
}
/** Таблица маршрута (согласование или исполнение) + (если editable) правка состава. */
@@ -28,6 +37,8 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
roleLabel = 'Согласующий',
emptyText = 'Согласующие не заданы.',
pickerPlaceholder = 'Добавить согласующего: начните вводить ФИО…',
waiting = false,
waitingText = '',
}) => {
const { rows, loading, error, editable, blocks } = editor;
@@ -86,12 +97,13 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
return (
<div className={styles.matrixWrap}>
<div className={styles.matrixTitle}>{title}</div>
{loading && <div className={styles.matrixNote}>Загрузка маршрута…</div>}
{!loading && error && <div className={styles.matrixNote}>Маршрут недоступен: {error}</div>}
{!loading && !error && rows.length === 0 && (
{waiting && <div className={styles.matrixNote}>{waitingText}</div>}
{!waiting && loading && <div className={styles.matrixNote}>Загрузка маршрута…</div>}
{!waiting && !loading && error && <div className={styles.matrixNote}>Маршрут недоступен: {error}</div>}
{!waiting && !loading && !error && rows.length === 0 && (
<div className={styles.matrixNote}>{emptyText}</div>
)}
{!loading && !error && rows.length > 0 && (
{!waiting && !loading && !error && rows.length > 0 && (
<div className={styles.matrixScroll}>
<table className={styles.matrix}>
<thead>
@@ -193,7 +205,7 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
</table>
</div>
)}
{editable && !loading && (
{editable && !waiting && !loading && (
<div className={styles.matrixPicker}>
<UserPicker search={search} onPick={addApprover} placeholder={pickerPlaceholder} />
</div>