Переработка списков #5
@@ -38,18 +38,22 @@ const OWN_REST_TOP = 200;
|
||||
// ApproverOWSUSER в схеме поиска не находилось (разбирались 2026-08-06).
|
||||
// Суффикс — тип исходного поля: OWSUSER для Person/Group.
|
||||
const MP_APPROVER = 'TansuSiteApproverOWSUSER';
|
||||
// Статус строки маршрута — Choice-поле, отсюда суффикс OWSCHCS (подтверждено
|
||||
// отладочным перебором 2026-08-06; у Person-полей он OWSUSER, см. MP_APPROVER).
|
||||
const MP_STATUS = 'RequestStatusOWSCHCS';
|
||||
// Статус СТРОКИ МАРШРУТА (колонка ApprovalStatus списка «— согласование») —
|
||||
// Choice-поле, отсюда суффикс OWSCHCS (подтверждено отладочным перебором
|
||||
// 2026-08-06; у Person-полей он OWSUSER, см. MP_APPROVER). Колонку не путать с
|
||||
// RequestStatus списка ДАННЫХ — это статус ЗАЯВКИ целиком, у него своя
|
||||
// managed property (см. MP_TICKET_STATUS ниже).
|
||||
const MP_STATUS = 'ApprovalStatusOWSCHCS';
|
||||
|
||||
// То же самое для Choice-колонки «Статус» списка ДАННЫХ (внутреннее имя Status,
|
||||
// см. forms/*/provisioning.json) — по ней отбираются «Доработка» и фильтр по
|
||||
// статусу в «Моих заявках». Кириллица в значении проблемы не создаёт: KQL — не
|
||||
// $filter (там она действительно подводит, см. indexApprovalsForType), и по
|
||||
// MP_STATUS выше точно так же ищутся русские статусы строк маршрута.
|
||||
// То же самое для Choice-колонки «Статус» списка ДАННЫХ (внутреннее имя
|
||||
// RequestStatus, см. forms/*/provisioning.json) — по ней отбираются
|
||||
// «Доработка» и фильтр по статусу в «Моих заявках». Кириллица в значении
|
||||
// проблемы не создаёт: KQL — не $filter (там она действительно подводит, см.
|
||||
// indexApprovalsForType), и по MP_STATUS выше точно так же ищутся русские
|
||||
// статусы строк маршрута.
|
||||
// Если отбор по статусу вдруг начнёт стабильно возвращать 0 — проверять надо в
|
||||
// первую очередь ЭТО имя в схеме поиска (см. предупреждение в searchOwnRequests).
|
||||
const MP_TICKET_STATUS = 'StatusOWSCHCS';
|
||||
const MP_TICKET_STATUS = 'RequestStatusOWSCHCS';
|
||||
|
||||
export function currentUserEmail(context: WebPartContext): string {
|
||||
return (context.pageContext.user.email || '').toLowerCase();
|
||||
@@ -99,11 +103,11 @@ const MY_TURN_STATUSES: string[] = [REQUEST_STATUS.onApproval, REQUEST_STATUS.on
|
||||
// (иначе на клиенте нечем было посчитать «чья очередь»). Теперь очередь
|
||||
// проставляется при создании/решении (см. approvalList.service.ts/createSteps,
|
||||
// approverDecision.service.ts/decide) — «чья сейчас очередь» это просто
|
||||
// значение колонки Status, поэтому здесь достаточно ОДНОГО отфильтрованного
|
||||
// запроса: ApproverId (+ Status для 'myTurn'). Список согласования больше НЕ
|
||||
// сканируется целиком, независимо от его размера.
|
||||
// значение колонки ApprovalStatus, поэтому здесь достаточно ОДНОГО
|
||||
// отфильтрованного запроса: ApproverId (+ ApprovalStatus для 'myTurn').
|
||||
// Список согласования больше НЕ сканируется целиком, независимо от его размера.
|
||||
//
|
||||
// Заявку (Title/Status/Created) читаем сразу через expand по Lookup-полю
|
||||
// Заявку (Title/RequestStatus/Created) читаем сразу через expand по Lookup-полю
|
||||
// Ticket — отдельный запрос к списку данных нужен только за Author
|
||||
// (двухуровневый expand Ticket/Author у SharePoint REST ненадёжен), и то —
|
||||
// ТОЛЬКО для строк текущей страницы (см. hydrateAuthors ниже), а не для всего
|
||||
@@ -227,7 +231,7 @@ export async function hydratePendingAuthors(
|
||||
}
|
||||
|
||||
// Лёгкий индекс совпадающих заявок для одного типа: строки согласования по
|
||||
// ApproverId → Title/Status/Created заявки отдельным запросом к списку ДАННЫХ
|
||||
// ApproverId → Title/RequestStatus/Created заявки отдельным запросом к списку ДАННЫХ
|
||||
// (НЕ через expand по Lookup-полю Ticket — SharePoint REST не отдаёт так
|
||||
// произвольные поля связанного элемента, только через Person-поля вроде
|
||||
// Author; попытка `expand('Ticket').select('Ticket/Status', ...)` падает с 400).
|
||||
@@ -267,8 +271,8 @@ async function indexApprovalsForType(
|
||||
// SharePoint REST — резолвим email в числовой Id пользователя (как и везде в
|
||||
// проекте, см. ensureUser) и фильтруем по TansuSiteApproverId.
|
||||
//
|
||||
// RequestStatus в $filter НЕ используем: подтверждено на практике — фильтр
|
||||
// вида `RequestStatus eq 'На согласовании'` (кириллица в $filter) молча
|
||||
// ApprovalStatus в $filter НЕ используем: подтверждено на практике — фильтр
|
||||
// вида `ApprovalStatus eq 'На согласовании'` (кириллица в $filter) молча
|
||||
// возвращает 0 строк, даже когда без него те же строки находятся с этим же
|
||||
// значением статуса. Поэтому фильтруем только по TansuSiteApproverId
|
||||
// (числовое значение, проблем нет), а статус сравниваем в JS после
|
||||
@@ -289,7 +293,7 @@ async function indexApprovalsForType(
|
||||
const cutoffIso = new Date(Date.now() - RECENT_MS).toISOString();
|
||||
const recentRows = (await approvalList.items
|
||||
.filter(`TansuSiteApproverId eq ${approver.Id} and Modified ge '${cutoffIso}'`)
|
||||
.select('Id', 'TicketId', 'RequestStatus')
|
||||
.select('Id', 'TicketId', 'ApprovalStatus')
|
||||
.top(2000)()) as Array<Record<string, unknown>>;
|
||||
|
||||
// Статусы уходят в KQL: сервер сразу отдаёт только нужные строки,
|
||||
@@ -300,7 +304,7 @@ async function indexApprovalsForType(
|
||||
|
||||
// REST-довесок берётся по Modified без учёта статуса (кириллица в $filter
|
||||
// ненадёжна, см. выше) — отбор по статусу делаем здесь, на клиенте.
|
||||
stepRows = allRows.filter((r) => statuses.indexOf(r.RequestStatus as string) !== -1);
|
||||
stepRows = allRows.filter((r) => statuses.indexOf(r.ApprovalStatus as string) !== -1);
|
||||
} else {
|
||||
// 'involved' — используется ТОЛЬКО для текстового поиска (см.
|
||||
// getPendingApprovalIndex/searchMyRequests) — здесь важна не мгновенная
|
||||
@@ -328,7 +332,7 @@ async function indexApprovalsForType(
|
||||
stepRows.forEach((r) => {
|
||||
const ticketId = r.TicketId as number | undefined;
|
||||
if (ticketId && !stepStatusByTicket.has(ticketId)) {
|
||||
stepStatusByTicket.set(ticketId, (r.RequestStatus as string) || '');
|
||||
stepStatusByTicket.set(ticketId, (r.ApprovalStatus as string) || '');
|
||||
}
|
||||
});
|
||||
if (ticketIds.length === 0) {
|
||||
@@ -341,7 +345,7 @@ async function indexApprovalsForType(
|
||||
const ticketFilter = ticketIds.map((id) => `Id eq ${id}`).join(' or ');
|
||||
const ticketRows = (await list.items
|
||||
.filter(ticketFilter)
|
||||
.select('Id', 'Title', 'Status', 'Created')
|
||||
.select('Id', 'Title', 'RequestStatus', 'Created')
|
||||
.top(ticketIds.length)()) as Array<Record<string, unknown>>;
|
||||
|
||||
const rows = ticketRows.map((r) => ({
|
||||
@@ -430,7 +434,7 @@ async function getInvolvedRowsViaSearch(
|
||||
|
||||
return (await approvalList.items
|
||||
.filter(ids.map((id) => `Id eq ${id}`).join(' or '))
|
||||
.select('Id', 'TicketId', 'RequestStatus')
|
||||
.select('Id', 'TicketId', 'ApprovalStatus')
|
||||
.top(ids.length)()) as Array<Record<string, unknown>>;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
@@ -573,7 +577,7 @@ async function loadByIds(
|
||||
const rows = (await sp.web
|
||||
.getList(type.listId as string)
|
||||
.items.filter(ids.map((id) => `Id eq ${id}`).join(' or '))
|
||||
.select('Id', 'Title', 'Status', 'Created', 'Author/EMail', 'Author/Title')
|
||||
.select('Id', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title')
|
||||
.expand('Author')
|
||||
.top(ids.length)()) as Array<Record<string, unknown>>;
|
||||
rows.forEach((r) => {
|
||||
@@ -599,7 +603,7 @@ async function getOwnRowsForType(context: WebPartContext, type: IRequestType): P
|
||||
const rows = (await sp.web
|
||||
.getList(type.listId as string)
|
||||
.items.filter(`AuthorId eq ${me.Id}`)
|
||||
.select('Id', 'Title', 'Status', 'Created', 'Author/EMail', 'Author/Title')
|
||||
.select('Id', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title')
|
||||
.expand('Author')
|
||||
.orderBy('Created', false)
|
||||
.top(OWN_REST_TOP)()) as Array<Record<string, unknown>>;
|
||||
@@ -861,7 +865,7 @@ function toMyRequest(
|
||||
return {
|
||||
id: r.Id as number,
|
||||
title: (r.Title as string) || '',
|
||||
status: (r.Status as string) || '',
|
||||
status: (r.RequestStatus as string) || '',
|
||||
authorEmail: author?.EMail || '',
|
||||
authorName: author?.Title || '',
|
||||
created: (r.Created as string) || '',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"dataListTitle": "Прием на работу — согласование",
|
||||
"columns": [
|
||||
{ "internalName": "TansuSiteApprover", "title": "Согласующий", "type": "User", "userSelectionMode": "PeopleOnly" },
|
||||
{ "internalName": "RequestStatus", "title": "Статус", "type": "Choice", "choices": ["Черновик", "Ждёт очереди", "На согласовании", "На исполнении", "Согласовано", "Выполнено", "Отклонено", "Возвращена на доработку"] },
|
||||
{ "internalName": "ApprovalStatus", "title": "Статус", "type": "Choice", "choices": ["Черновик", "Ждёт очереди", "На согласовании", "На исполнении", "Согласовано", "Выполнено", "Отклонено", "Возвращена на доработку"] },
|
||||
{ "internalName": "Ticket", "title": "Заявка", "type": "Lookup", "lookupList": "Прием на работу — данные", "lookupShowField": "Title" },
|
||||
{ "internalName": "Date", "title": "Дата", "type": "DateTime" },
|
||||
{ "internalName": "Comment", "title": "Комментарий", "type": "Note" },
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function loadHireRequest(itemId: number): Promise<ILoadedRequest> {
|
||||
const item = (await sp.web.lists
|
||||
.getByTitle(LIST_TITLE)
|
||||
.items.getById(itemId)
|
||||
.select(...SCALAR_FIELDS, 'Status')()) as Record<string, unknown>;
|
||||
.select(...SCALAR_FIELDS, 'RequestStatus')()) as Record<string, unknown>;
|
||||
|
||||
const data: IFormData = { ...EMPTY_DATA };
|
||||
const writable = data as unknown as Record<string, string>;
|
||||
@@ -37,5 +37,5 @@ export async function loadHireRequest(itemId: number): Promise<ILoadedRequest> {
|
||||
}
|
||||
});
|
||||
|
||||
return { data, status: (item.Status as string) || '' };
|
||||
return { data, status: (item.RequestStatus as string) || '' };
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"dataListTitle": "Прием на работу — данные",
|
||||
"columns": [
|
||||
{ "internalName": "RequestUniqueCode", "title": "Уникальный код заявки", "type": "Text" },
|
||||
{ "internalName": "Status", "title": "Статус", "type": "Choice", "choices": ["Черновик", "На согласовании", "Согласовано", "Отклонено", "Возвращена на доработку", "Выполнение", "Закрыта"] },
|
||||
{ "internalName": "RequestStatus", "title": "Статус", "type": "Choice", "choices": ["Черновик", "На согласовании", "Согласовано", "Отклонено", "Возвращена на доработку", "Выполнение", "Закрыта"] },
|
||||
|
||||
{ "internalName": "Phone", "title": "Телефон", "type": "Text" },
|
||||
{ "internalName": "HireType", "title": "Тип приёма", "type": "Choice", "choices": ["Новый сотрудник", "Вместо уволенного"] },
|
||||
|
||||
@@ -23,7 +23,7 @@ function buildTitle(data: IFormData): string {
|
||||
|
||||
/** Поля элемента списка данных из модели формы (текст/выбор/даты/числа). */
|
||||
function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
||||
const item: Record<string, unknown> = { Title: buildTitle(data), Status: status };
|
||||
const item: Record<string, unknown> = { Title: buildTitle(data), RequestStatus: status };
|
||||
|
||||
(Object.keys(data) as (keyof IFormData)[]).forEach((key) => {
|
||||
const value = data[key];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"dataListTitle": "Организация рабочего места — согласование",
|
||||
"columns": [
|
||||
{ "internalName": "TansuSiteApprover", "title": "Согласующий", "type": "User", "userSelectionMode": "PeopleOnly" },
|
||||
{ "internalName": "RequestStatus", "title": "Статус", "type": "Choice", "choices": ["Черновик", "Ждёт очереди", "На согласовании", "На исполнении", "Согласовано", "Выполнено", "Отклонено", "Возвращена на доработку"] },
|
||||
{ "internalName": "ApprovalStatus", "title": "Статус", "type": "Choice", "choices": ["Черновик", "Ждёт очереди", "На согласовании", "На исполнении", "Согласовано", "Выполнено", "Отклонено", "Возвращена на доработку"] },
|
||||
{ "internalName": "Ticket", "title": "Заявка", "type": "Lookup", "lookupList": "Организация рабочего места — данные", "lookupShowField": "Title" },
|
||||
{ "internalName": "Date", "title": "Дата", "type": "DateTime" },
|
||||
{ "internalName": "Comment", "title": "Комментарий", "type": "Note" },
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function loadWorkplaceRequest(itemId: number): Promise<ILoadedReque
|
||||
const item = (await getSP()
|
||||
.web.lists.getByTitle(LIST_TITLE)
|
||||
.items.getById(itemId)
|
||||
.select(...SCALAR_FIELDS, 'NeedsEquipment', 'Status')()) as Record<string, unknown>;
|
||||
.select(...SCALAR_FIELDS, 'NeedsEquipment', 'RequestStatus')()) as Record<string, unknown>;
|
||||
|
||||
const data: IFormData = { ...EMPTY_DATA };
|
||||
const writable = data as unknown as Record<string, string>;
|
||||
@@ -36,5 +36,5 @@ export async function loadWorkplaceRequest(itemId: number): Promise<ILoadedReque
|
||||
|
||||
data.NeedsEquipment = item.NeedsEquipment === true;
|
||||
|
||||
return { data, status: (item.Status as string) || '' };
|
||||
return { data, status: (item.RequestStatus as string) || '' };
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"dataListTitle": "Организация рабочего места — данные",
|
||||
"columns": [
|
||||
{ "internalName": "RequestUniqueCode", "title": "Уникальный код заявки", "type": "Text" },
|
||||
{ "internalName": "Status", "title": "Статус", "type": "Choice", "choices": ["Черновик", "На согласовании", "Согласовано", "Отклонено", "Возвращена на доработку", "Выполнение", "Закрыта"] },
|
||||
{ "internalName": "RequestStatus", "title": "Статус", "type": "Choice", "choices": ["Черновик", "На согласовании", "Согласовано", "Отклонено", "Возвращена на доработку", "Выполнение", "Закрыта"] },
|
||||
|
||||
{ "internalName": "FullName", "title": "Ф.И.О.", "type": "Text", "required": true },
|
||||
{ "internalName": "NameLatin", "title": "Фамилия и имя (латиница)", "type": "Text" },
|
||||
|
||||
@@ -33,7 +33,7 @@ function buildTitle(data: IFormData): string {
|
||||
|
||||
/** Поля элемента списка данных из модели формы. */
|
||||
function buildItem(data: IFormData, status: string, sourceHireId?: number): Record<string, unknown> {
|
||||
const item: Record<string, unknown> = { Title: buildTitle(data), Status: status };
|
||||
const item: Record<string, unknown> = { Title: buildTitle(data), RequestStatus: status };
|
||||
|
||||
(Object.keys(data) as (keyof IFormData)[]).forEach((key) => {
|
||||
const value = data[key];
|
||||
|
||||
@@ -12,12 +12,18 @@ import { APPROVAL_PHASE, REQUEST_STATUS } from './models/statuses';
|
||||
|
||||
// Внутренние имена колонок списка согласования (создаются провижинингом).
|
||||
const FIELDS = {
|
||||
// Внутренние имена с префиксом группы колонок сайта (TansuSite) и «Request» —
|
||||
// так они заведены в SharePoint. Из них SharePoint выводит имена managed
|
||||
// properties для поиска: TansuSiteApproverOWSUSER и RequestStatusOWSCHCS
|
||||
// (см. MP_APPROVER/MP_STATUS в features/myRequests/myRequests.service.ts).
|
||||
// Внутренние имена с префиксом группы колонок сайта (TansuSite) там, где он
|
||||
// есть, — так они заведены в SharePoint. Из них SharePoint выводит имена
|
||||
// managed properties для поиска: TansuSiteApproverOWSUSER и
|
||||
// ApprovalStatusOWSCHCS (см. MP_APPROVER/MP_STATUS в
|
||||
// features/myRequests/myRequests.service.ts).
|
||||
//
|
||||
// ВАЖНО: колонка называется ApprovalStatus, а не Status и не RequestStatus —
|
||||
// это статус СТРОКИ маршрута (список «— согласование»); статус ЗАЯВКИ в
|
||||
// списке «— данных» лежит в отдельной колонке RequestStatus (см.
|
||||
// models/statuses.ts — там оба набора значений сведены в одну картину).
|
||||
approver: 'TansuSiteApprover', // «Согласующий» (User) → TansuSiteApproverId
|
||||
status: 'RequestStatus', // «Статус» (Choice)
|
||||
status: 'ApprovalStatus', // «Статус» (Choice) строки маршрута
|
||||
ticket: 'Ticket', // «Заявка» (Lookup) → TicketId
|
||||
date: 'Date', // «Дата» (DateTime)
|
||||
comment: 'Comment', // «Комментарий» (Text)
|
||||
|
||||
@@ -127,7 +127,7 @@ export class ApproverService {
|
||||
|
||||
private async updateRequestStatus(id: number, status: string): Promise<void> {
|
||||
await getSP().web.lists.getByTitle(this.dataListTitle).items.getById(id).update({
|
||||
Status: status,
|
||||
RequestStatus: status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,16 +45,21 @@ export function isEditableStatus(status: string | undefined): boolean {
|
||||
|
||||
// Наборов значений ДВА, и они не совпадают — это разные сущности:
|
||||
//
|
||||
// 1) статус ЗАЯВКИ — колонка Status в списке «… — данные». Пишется формой
|
||||
// (Черновик/На согласовании) и computeRequestStatus после каждого решения.
|
||||
// «Ждёт очереди»/«На исполнении»/«Выполнено» здесь не бывают: это состояния
|
||||
// отдельного шага маршрута, а не заявки целиком.
|
||||
// 1) статус ЗАЯВКИ — колонка RequestStatus в списке «… — данные». Пишется
|
||||
// формой (Черновик/На согласовании) и computeRequestStatus после каждого
|
||||
// решения. «Ждёт очереди»/«На исполнении»/«Выполнено» здесь не бывают: это
|
||||
// состояния отдельного шага маршрута, а не заявки целиком.
|
||||
//
|
||||
// 2) статус СТРОКИ МАРШРУТА — колонка RequestStatus в списке «… — согласование».
|
||||
// 2) статус СТРОКИ МАРШРУТА — колонка ApprovalStatus в списке «… — согласование».
|
||||
// Пишется createSteps/activateNextQueued/setDecision. «Выполнение»/«Закрыта»
|
||||
// здесь не бывают: это агрегатные состояния заявки, у отдельного шага
|
||||
// смысла не имеют.
|
||||
//
|
||||
// Обе колонки переименованы (были Status в списке данных и RequestStatus в
|
||||
// списке согласования соответственно) — переименование внутреннего имени в
|
||||
// SharePoint не трогает уже сохранённые значения, отдельная миграция данных
|
||||
// не нужна.
|
||||
//
|
||||
// Держать их в синхроне с forms/*/provisioning.json и forms/*/approval/provisioning.json.
|
||||
export const REQUEST_STATUS_CHOICES: string[] = [
|
||||
REQUEST_STATUS.draft,
|
||||
|
||||
Reference in New Issue
Block a user