feat: дата выхода сотрудника в закладке «Все заявки» по организации рабочего места #24
@@ -17,12 +17,14 @@ import {
|
|||||||
hydratePendingAuthors,
|
hydratePendingAuthors,
|
||||||
searchOwnRequests,
|
searchOwnRequests,
|
||||||
} from './myRequests.service';
|
} from './myRequests.service';
|
||||||
|
import { hydrateStartDates } from './startDates.service';
|
||||||
import { IMyRequest } from './models/IMyRequest';
|
import { IMyRequest } from './models/IMyRequest';
|
||||||
import { DataTable, IDataTableColumn, IconButton } from '@tnsu/ui-kit';
|
import { DataTable, IDataTableColumn, IconButton } from '@tnsu/ui-kit';
|
||||||
import { REQUEST_STATUS, IRequestType, StatusBadge } from '../../shared/approval';
|
import { REQUEST_STATUS, IRequestType, StatusBadge } from '../../shared/approval';
|
||||||
import { confirmAndDeleteDraft, isDeletableStatus } from '../../shared/draftDeletion';
|
import { confirmAndDeleteDraft, isDeletableStatus } from '../../shared/draftDeletion';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import { getFormComponent } from '../../forms/registry';
|
import { getFormComponent } from '../../forms/registry';
|
||||||
|
import { FORM_ID as WORKPLACE_SETUP_FORM_ID } from '../../forms/WorkplaceSetup/model';
|
||||||
import { onRequestsChanged } from '../../shared/requestEvents';
|
import { onRequestsChanged } from '../../shared/requestEvents';
|
||||||
import { perfScope } from '../../shared/perfTrace';
|
import { perfScope } from '../../shared/perfTrace';
|
||||||
import { useRoutedQuery } from '../../shared/usePersistedQuery';
|
import { useRoutedQuery } from '../../shared/usePersistedQuery';
|
||||||
@@ -135,6 +137,8 @@ interface ITableColumns {
|
|||||||
status: boolean;
|
status: boolean;
|
||||||
/** Колонка «Исполнитель» — у кого заявка сейчас (см. showsAssignee). */
|
/** Колонка «Исполнитель» — у кого заявка сейчас (см. showsAssignee). */
|
||||||
assignee: boolean;
|
assignee: boolean;
|
||||||
|
/** Колонка «Дата выхода» — только «Все заявки» по организации рабочего места. */
|
||||||
|
startDate?: boolean;
|
||||||
/** Заголовок колонки с комментарием; undefined — колонки нет. */
|
/** Заголовок колонки с комментарием; undefined — колонки нет. */
|
||||||
commentLabel?: string;
|
commentLabel?: string;
|
||||||
}
|
}
|
||||||
@@ -157,7 +161,12 @@ function showsAssignee(view: ListView, statusFilter: string): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function columnsFor(view: ListView, statusFilter: string): ITableColumns {
|
/** Дата выхода сотрудника есть только у заявки на организацию рабочего места, и показываем её во «Все заявки». */
|
||||||
|
function showsStartDate(view: ListView, type: IRequestType | undefined): boolean {
|
||||||
|
return view === 'all' && type?.pageUrl === WORKPLACE_SETUP_FORM_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
function columnsFor(view: ListView, statusFilter: string, startDate = false): ITableColumns {
|
||||||
const assignee = showsAssignee(view, statusFilter);
|
const assignee = showsAssignee(view, statusFilter);
|
||||||
const decisionStatus = decisionStatusFor(view, statusFilter);
|
const decisionStatus = decisionStatusFor(view, statusFilter);
|
||||||
const commentLabel =
|
const commentLabel =
|
||||||
@@ -171,7 +180,7 @@ function columnsFor(view: ListView, statusFilter: string): ITableColumns {
|
|||||||
if (view === 'rework') return { author: false, status: false, assignee, commentLabel };
|
if (view === 'rework') return { author: false, status: false, assignee, commentLabel };
|
||||||
if (view === 'own:created') return { author: false, status: !statusFilter, assignee, commentLabel };
|
if (view === 'own:created') return { author: false, status: !statusFilter, assignee, commentLabel };
|
||||||
// 'all' — заявки всех авторов вперемешку, поэтому нужны и автор, и статус.
|
// 'all' — заявки всех авторов вперемешку, поэтому нужны и автор, и статус.
|
||||||
if (view === 'all') return { author: true, status: true, assignee };
|
if (view === 'all') return { author: true, status: true, assignee, startDate };
|
||||||
// История решений: заявка чужая, поэтому нужен автор. Статус — только если он
|
// История решений: заявка чужая, поэтому нужен автор. Статус — только если он
|
||||||
// не предопределён самой вкладкой («Отклонённые мной» — это всегда «Отклонено»,
|
// не предопределён самой вкладкой («Отклонённые мной» — это всегда «Отклонено»,
|
||||||
// а вот у согласованных/выполненных заявка могла уйти дальше по маршруту).
|
// а вот у согласованных/выполненных заявка могла уйти дальше по маршруту).
|
||||||
@@ -235,6 +244,8 @@ const Table: React.FC<{
|
|||||||
rows: IMyRequest[];
|
rows: IMyRequest[];
|
||||||
view: ListView;
|
view: ListView;
|
||||||
statusFilter: string;
|
statusFilter: string;
|
||||||
|
/** Показывать колонку «Дата выхода» (см. showsStartDate). */
|
||||||
|
withStartDate: boolean;
|
||||||
sort: ISortState;
|
sort: ISortState;
|
||||||
onSort: (key: SortKey) => void;
|
onSort: (key: SortKey) => void;
|
||||||
onOpen: (request: IMyRequest) => void;
|
onOpen: (request: IMyRequest) => void;
|
||||||
@@ -242,8 +253,8 @@ const Table: React.FC<{
|
|||||||
onShowHistory: (request: IMyRequest) => void;
|
onShowHistory: (request: IMyRequest) => void;
|
||||||
/** Удалить черновик; задан только там, где в таблице бывают свои черновики. */
|
/** Удалить черновик; задан только там, где в таблице бывают свои черновики. */
|
||||||
onDelete?: (request: IMyRequest) => void;
|
onDelete?: (request: IMyRequest) => void;
|
||||||
}> = ({ rows, view, statusFilter, sort, onSort, onOpen, onShowHistory, onDelete }) => {
|
}> = ({ rows, view, statusFilter, withStartDate, sort, onSort, onOpen, onShowHistory, onDelete }) => {
|
||||||
const cols = columnsFor(view, statusFilter);
|
const cols = columnsFor(view, statusFilter, withStartDate);
|
||||||
const sortable = (label: string, key: SortKey): React.ReactNode => (
|
const sortable = (label: string, key: SortKey): React.ReactNode => (
|
||||||
<SortableHeader label={label} sortKey={key} sort={sort} onSort={onSort} />
|
<SortableHeader label={label} sortKey={key} sort={sort} onSort={onSort} />
|
||||||
);
|
);
|
||||||
@@ -261,6 +272,14 @@ const Table: React.FC<{
|
|||||||
{ key: 'title', title: 'Название', render: (r) => r.title || r.code || `#${r.id}` },
|
{ key: 'title', title: 'Название', render: (r) => r.title || r.code || `#${r.id}` },
|
||||||
{ key: 'date', title: sortable('Дата', 'date'), width: '110px', render: (r) => formatDate(r.created) }
|
{ key: 'date', title: sortable('Дата', 'date'), width: '110px', render: (r) => formatDate(r.created) }
|
||||||
];
|
];
|
||||||
|
if (cols.startDate) {
|
||||||
|
columns.push({
|
||||||
|
key: 'startDate',
|
||||||
|
title: 'Дата выхода',
|
||||||
|
width: '120px',
|
||||||
|
render: (r) => formatDate(r.startDate ?? '') || '—'
|
||||||
|
});
|
||||||
|
}
|
||||||
if (cols.author) {
|
if (cols.author) {
|
||||||
columns.push({
|
columns.push({
|
||||||
key: 'author',
|
key: 'author',
|
||||||
@@ -588,6 +607,7 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
decisionStatus ? hydrateDecisionComments(context, types, result.rows, decisionStatus) : undefined,
|
decisionStatus ? hydrateDecisionComments(context, types, result.rows, decisionStatus) : undefined,
|
||||||
showsAssignee(view, statusFilter) ? hydrateCurrentAssignees(context, types, result.rows) : undefined,
|
showsAssignee(view, statusFilter) ? hydrateCurrentAssignees(context, types, result.rows) : undefined,
|
||||||
|
showsStartDate(view, allRequestsType) ? hydrateStartDates(context, result.rows) : undefined,
|
||||||
]);
|
]);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
@@ -745,6 +765,7 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
|||||||
rows={visible}
|
rows={visible}
|
||||||
view={view}
|
view={view}
|
||||||
statusFilter={statusFilter}
|
statusFilter={statusFilter}
|
||||||
|
withStartDate={showsStartDate(view, allRequestsType)}
|
||||||
sort={sort}
|
sort={sort}
|
||||||
onSort={refinement.onSort}
|
onSort={refinement.onSort}
|
||||||
onOpen={onOpen}
|
onOpen={onOpen}
|
||||||
|
|||||||
@@ -76,4 +76,12 @@ export interface IMyRequest {
|
|||||||
* hydrateCurrentAssignees).
|
* hydrateCurrentAssignees).
|
||||||
*/
|
*/
|
||||||
currentAssignees?: IAssignee[];
|
currentAssignees?: IAssignee[];
|
||||||
|
/**
|
||||||
|
* Дата выхода сотрудника на работу (колонка StartDate) — только у заявок на
|
||||||
|
* организацию рабочего места, колонка «Дата выхода» во «Все заявки». Как и
|
||||||
|
* decisionComment, живёт в списке данных и дочитывается только для видимой
|
||||||
|
* страницы (см. hydrateStartDates). ISO-строка; '' — не заполнена,
|
||||||
|
* undefined — не дочитывалось.
|
||||||
|
*/
|
||||||
|
startDate?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export function pageSignature(page: IPage): string {
|
|||||||
r.created,
|
r.created,
|
||||||
r.authorName,
|
r.authorName,
|
||||||
r.decisionComment ?? '',
|
r.decisionComment ?? '',
|
||||||
|
r.startDate ?? '',
|
||||||
(r.currentAssignees ?? []).map((a) => `${a.name}/${a.email}`).join(','),
|
(r.currentAssignees ?? []).map((a) => `${a.name}/${a.email}`).join(','),
|
||||||
].join('~')
|
].join('~')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import '@pnp/sp/webs';
|
||||||
|
import '@pnp/sp/lists';
|
||||||
|
import '@pnp/sp/items';
|
||||||
|
import { spForSite } from '../../shared/pnp/pnpjs';
|
||||||
|
import { traced } from '../../shared/perfTrace';
|
||||||
|
import { IMyRequest } from './models/IMyRequest';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проставляет startDate — дату выхода сотрудника (колонка StartDate списка
|
||||||
|
* данных). Только для видимой страницы; группировка по партиции строки, как в
|
||||||
|
* hydrateAuthors: в другой партиции тот же Id — чужая заявка. Мутирует строки.
|
||||||
|
*/
|
||||||
|
export async function hydrateStartDates(context: WebPartContext, rows: IMyRequest[]): Promise<void> {
|
||||||
|
if (rows.length === 0) return;
|
||||||
|
return traced('дата выхода (REST)', () => hydrateStartDatesInner(context, rows));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hydrateStartDatesInner(context: WebPartContext, rows: IMyRequest[]): Promise<void> {
|
||||||
|
const byPartition = new Map<string, { siteUrl: string; listId: string; typeName: string; ids: number[] }>();
|
||||||
|
rows.forEach((r) => {
|
||||||
|
const bucket = byPartition.get(r.listId.toLowerCase()) || {
|
||||||
|
siteUrl: r.siteUrl,
|
||||||
|
listId: r.listId,
|
||||||
|
typeName: r.requestTypeName,
|
||||||
|
ids: [],
|
||||||
|
};
|
||||||
|
bucket.ids.push(r.id);
|
||||||
|
byPartition.set(r.listId.toLowerCase(), bucket);
|
||||||
|
});
|
||||||
|
|
||||||
|
const byKey = new Map<string, string>();
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(byPartition.values()).map(async ({ siteUrl, listId, typeName, ids }) => {
|
||||||
|
try {
|
||||||
|
const sp = spForSite(siteUrl, context);
|
||||||
|
const dataRows = (await sp.web
|
||||||
|
.getList(listId)
|
||||||
|
.items.filter(ids.map((id) => `Id eq ${id}`).join(' or '))
|
||||||
|
.select('Id', 'StartDate')
|
||||||
|
.top(ids.length)()) as Array<Record<string, unknown>>;
|
||||||
|
dataRows.forEach((r) => byKey.set(`${listId.toLowerCase()}-${r.Id as number}`, (r.StartDate as string) || ''));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`[Все заявки] Не удалось дочитать дату выхода для «${typeName}» (${listId}):`,
|
||||||
|
(e as Error).message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
rows.forEach((r) => {
|
||||||
|
r.startDate = byKey.get(`${r.listId.toLowerCase()}-${r.id}`) ?? '';
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user