This commit is contained in:
Generated
+4
-4
@@ -17,7 +17,7 @@
|
|||||||
"@microsoft/sp-webpart-base": "1.22.1",
|
"@microsoft/sp-webpart-base": "1.22.1",
|
||||||
"@pnp/sp": "^4.20.0",
|
"@pnp/sp": "^4.20.0",
|
||||||
"@pnp/spfx-controls-react": "^3.25.0",
|
"@pnp/spfx-controls-react": "^3.25.0",
|
||||||
"@tnsu/ui-kit": "^0.3.0",
|
"@tnsu/ui-kit": "^0.4.0",
|
||||||
"react": "17.0.1",
|
"react": "17.0.1",
|
||||||
"react-dom": "17.0.1",
|
"react-dom": "17.0.1",
|
||||||
"tslib": "2.3.1"
|
"tslib": "2.3.1"
|
||||||
@@ -9870,9 +9870,9 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/@tnsu/ui-kit": {
|
"node_modules/@tnsu/ui-kit": {
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"resolved": "http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/%40tnsu%2Fui-kit/-/0.3.0/ui-kit-0.3.0.tgz",
|
"resolved": "http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/%40tnsu%2Fui-kit/-/0.4.0/ui-kit-0.4.0.tgz",
|
||||||
"integrity": "sha512-V9pTS/rBmU9KInczI8jJ/KHT96hfX0OlnjIJ2NZ/iEQTOE93I9xMfmgpj83PSRrI6/xzyybu+8k/qVO6LP926w==",
|
"integrity": "sha512-J3cBvoR2vV0SC5GihOagLAqvC0txQE342qIh6viabMt+Um6Jg4mhTff+ZkVahG0g+bF6sqSCUl3x813aO82KuQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@fluentui/react": "^8.106.4",
|
"@fluentui/react": "^8.106.4",
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
"@microsoft/sp-webpart-base": "1.22.1",
|
"@microsoft/sp-webpart-base": "1.22.1",
|
||||||
"@pnp/sp": "^4.20.0",
|
"@pnp/sp": "^4.20.0",
|
||||||
"@pnp/spfx-controls-react": "^3.25.0",
|
"@pnp/spfx-controls-react": "^3.25.0",
|
||||||
"@tnsu/ui-kit": "^0.3.0",
|
"@tnsu/ui-kit": "^0.4.0",
|
||||||
"react": "17.0.1",
|
"react": "17.0.1",
|
||||||
"react-dom": "17.0.1",
|
"react-dom": "17.0.1",
|
||||||
"tslib": "2.3.1"
|
"tslib": "2.3.1"
|
||||||
|
|||||||
@@ -66,9 +66,8 @@
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Цвет пунктов списка — те же цвета, что у StatusBadge (см. statusOptionClassName
|
// Цвет пунктов списка — те же цвета, что у StatusBadge. Только color:
|
||||||
// в RequestsTableView.tsx). Только color: background-color нативный <option>
|
// background-color нативный <option> показывает не во всех браузерах одинаково.
|
||||||
// показывает не во всех браузерах одинаково, поэтому не используем.
|
|
||||||
.optionDraft { color: $gray-700; }
|
.optionDraft { color: $gray-700; }
|
||||||
.optionOnApproval { color: $blue-600; }
|
.optionOnApproval { color: $blue-600; }
|
||||||
.optionApproved { color: $green-600; }
|
.optionApproved { color: $green-600; }
|
||||||
@@ -112,6 +111,8 @@
|
|||||||
background: $red-50;
|
background: $red-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Карточка-контейнер вокруг таблицы. Сама таблица (заголовки, строки, ховер,
|
||||||
|
// горизонтальный скролл) теперь в @tnsu/ui-kit DataTable.
|
||||||
.tableWrap {
|
.tableWrap {
|
||||||
background: $white;
|
background: $white;
|
||||||
border: 1px solid $gray-200;
|
border: 1px solid $gray-200;
|
||||||
@@ -120,97 +121,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
// Пустой комментарий в колонке «Причина возврата/отклонения».
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: $fs-sm;
|
|
||||||
|
|
||||||
th {
|
|
||||||
text-align: left;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: $gray-50;
|
|
||||||
border-bottom: 1px solid $gray-200;
|
|
||||||
color: $gray-500;
|
|
||||||
font-size: $fs-label;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: .04em;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 14px 16px;
|
|
||||||
border-bottom: 1px solid $gray-100;
|
|
||||||
color: $gray-900;
|
|
||||||
vertical-align: middle;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:last-child td {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Кликабельна вся строка (см. RequestsTableView — onClick на <tr>).
|
|
||||||
.row {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background .16s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $gold-50;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 2px solid $gold-500;
|
|
||||||
outline-offset: -2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.colId {
|
|
||||||
width: 70px;
|
|
||||||
color: $gray-400;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colType {
|
|
||||||
width: 200px;
|
|
||||||
color: $navy-700;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colTitle {
|
|
||||||
min-width: 240px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colDate {
|
|
||||||
width: 110px;
|
|
||||||
color: $gray-500;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colAuthor {
|
|
||||||
width: 280px;
|
|
||||||
color: $gray-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colStatus {
|
|
||||||
width: 190px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Причина возврата — свободный текст согласующего, поэтому переносим по словам
|
|
||||||
// (в т.ч. длинное слово без пробелов, иначе оно растянет таблицу).
|
|
||||||
.colComment {
|
|
||||||
white-space: normal;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
color: $gray-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commentEmpty {
|
.commentEmpty {
|
||||||
color: $gray-400;
|
color: $gray-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Раскраска статуса — теперь только в shared/approval/ui/StatusBadge (см.
|
|
||||||
// использование ниже с size="sm"). Раньше здесь была своя копия этих цветов —
|
|
||||||
// она разошлась с эталоном (На согласовании/Возвращена на доработку оказались
|
|
||||||
// перепутаны), поэтому копию убрали, а не подправили.
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
searchOwnRequests,
|
searchOwnRequests,
|
||||||
} from './myRequests.service';
|
} from './myRequests.service';
|
||||||
import { IMyRequest } from './models/IMyRequest';
|
import { IMyRequest } from './models/IMyRequest';
|
||||||
|
import { DataTable, IDataTableColumn } from '@tnsu/ui-kit';
|
||||||
import { REQUEST_STATUS, REQUEST_STATUS_CHOICES, IRequestType, StatusBadge } from '../../shared/approval';
|
import { REQUEST_STATUS, REQUEST_STATUS_CHOICES, IRequestType, StatusBadge } from '../../shared/approval';
|
||||||
import { onRequestsChanged } from '../../shared/requestEvents';
|
import { onRequestsChanged } from '../../shared/requestEvents';
|
||||||
import { useRoutedQuery } from '../../shared/usePersistedQuery';
|
import { useRoutedQuery } from '../../shared/usePersistedQuery';
|
||||||
@@ -201,61 +202,50 @@ const Table: React.FC<{
|
|||||||
statusFilter: string;
|
statusFilter: string;
|
||||||
onOpen: (request: IMyRequest) => void;
|
onOpen: (request: IMyRequest) => void;
|
||||||
}> = ({ rows, view, statusFilter, onOpen }) => {
|
}> = ({ rows, view, statusFilter, onOpen }) => {
|
||||||
const columns = columnsFor(view, statusFilter);
|
const cols = columnsFor(view, statusFilter);
|
||||||
|
|
||||||
|
// Колонки для DataTable строим динамически: базовые всегда, автор/статус/
|
||||||
|
// комментарий — по columnsFor (зависит от раздела и фильтра). Кастомные ячейки
|
||||||
|
// (статус → StatusBadge, дата → формат) задаются через render.
|
||||||
|
const columns: IDataTableColumn<IMyRequest>[] = [
|
||||||
|
{ key: 'id', title: 'ID', width: '70px' },
|
||||||
|
{ key: 'type', title: 'Тип', width: '200px', render: (r) => r.requestTypeName },
|
||||||
|
{ key: 'title', title: 'Название', render: (r) => r.title || `#${r.id}` },
|
||||||
|
{ key: 'date', title: 'Дата', width: '110px', render: (r) => formatDate(r.created) }
|
||||||
|
];
|
||||||
|
if (cols.author) {
|
||||||
|
columns.push({
|
||||||
|
key: 'author',
|
||||||
|
title: 'Автор',
|
||||||
|
width: '280px',
|
||||||
|
render: (r) => [r.authorName, r.authorEmail].filter(Boolean).join(' · ') || '—'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (cols.status) {
|
||||||
|
columns.push({
|
||||||
|
key: 'status',
|
||||||
|
title: 'Статус',
|
||||||
|
width: '190px',
|
||||||
|
render: (r) => (r.status ? <StatusBadge status={r.status} size="sm" /> : '—')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (cols.commentLabel) {
|
||||||
|
columns.push({
|
||||||
|
key: 'comment',
|
||||||
|
title: cols.commentLabel,
|
||||||
|
render: (r) => r.decisionComment || <span className={styles.commentEmpty}>Без комментария</span>
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tableWrap}>
|
<div className={styles.tableWrap}>
|
||||||
<table className={styles.table}>
|
<DataTable
|
||||||
<thead>
|
columns={columns}
|
||||||
<tr>
|
rows={rows}
|
||||||
<th className={styles.colId}>ID</th>
|
rowKey={(r) => `${r.requestTypeId}-${r.id}`}
|
||||||
<th className={styles.colType}>Тип</th>
|
onRowClick={onOpen}
|
||||||
<th className={styles.colTitle}>Название</th>
|
/>
|
||||||
<th className={styles.colDate}>Дата</th>
|
</div>
|
||||||
{columns.author && <th className={styles.colAuthor}>Автор</th>}
|
|
||||||
{columns.status && <th className={styles.colStatus}>Статус</th>}
|
|
||||||
{columns.commentLabel && <th className={styles.colComment}>{columns.commentLabel}</th>}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{rows.map((r) => (
|
|
||||||
<tr
|
|
||||||
className={styles.row}
|
|
||||||
key={`${r.requestTypeId}-${r.id}`}
|
|
||||||
tabIndex={0}
|
|
||||||
role="button"
|
|
||||||
aria-label={`Открыть заявку «${r.title || r.id}»`}
|
|
||||||
onClick={() => onOpen(r)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
onOpen(r);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<td className={styles.colId}>{r.id}</td>
|
|
||||||
<td className={styles.colType}>{r.requestTypeName}</td>
|
|
||||||
<td className={styles.colTitle}>{r.title || `#${r.id}`}</td>
|
|
||||||
<td className={styles.colDate}>{formatDate(r.created)}</td>
|
|
||||||
{columns.author && (
|
|
||||||
<td className={styles.colAuthor}>
|
|
||||||
{[r.authorName, r.authorEmail].filter(Boolean).join(' · ') || '—'}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{columns.status && (
|
|
||||||
<td className={styles.colStatus}>
|
|
||||||
{r.status ? <StatusBadge status={r.status} size="sm" /> : '—'}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
{columns.commentLabel && (
|
|
||||||
<td className={styles.colComment}>
|
|
||||||
{r.decisionComment || <span className={styles.commentEmpty}>Без комментария</span>}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -278,13 +278,15 @@ export class ApprovalListService {
|
|||||||
FIELDS.slaHours,
|
FIELDS.slaHours,
|
||||||
FIELDS.askOverdueReason,
|
FIELDS.askOverdueReason,
|
||||||
`${FIELDS.approver}/Title`,
|
`${FIELDS.approver}/Title`,
|
||||||
`${FIELDS.approver}/EMail`
|
`${FIELDS.approver}/EMail`,
|
||||||
|
`${FIELDS.delegatedBy}/Title`
|
||||||
)
|
)
|
||||||
.expand(FIELDS.approver)
|
.expand(FIELDS.approver, FIELDS.delegatedBy)
|
||||||
.orderBy(FIELDS.order, true)()) as Array<Record<string, unknown>>;
|
.orderBy(FIELDS.order, true)()) as Array<Record<string, unknown>>;
|
||||||
|
|
||||||
return rows.map((r, i) => {
|
return rows.map((r, i) => {
|
||||||
const approver = r[FIELDS.approver] as { Title?: string; EMail?: string } | undefined;
|
const approver = r[FIELDS.approver] as { Title?: string; EMail?: string } | undefined;
|
||||||
|
const delegatedBy = r[FIELDS.delegatedBy] as { Title?: string } | undefined;
|
||||||
return {
|
return {
|
||||||
order: (r[FIELDS.order] as number) ?? i + 1,
|
order: (r[FIELDS.order] as number) ?? i + 1,
|
||||||
who: approver?.Title || '',
|
who: approver?.Title || '',
|
||||||
@@ -294,6 +296,9 @@ 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,
|
||||||
|
// Пусто у строк из штатного маршрута; заполнено только у делегатов
|
||||||
|
// (кого согласующий добавил перед собой) — матрица пометит их.
|
||||||
|
addedBy: delegatedBy?.Title || '',
|
||||||
phase: (r[FIELDS.phase] as string) || APPROVAL_PHASE.approval,
|
phase: (r[FIELDS.phase] as string) || APPROVAL_PHASE.approval,
|
||||||
// Читаем, чтобы не потерять при пересохранении: writeRoute удаляет
|
// Читаем, чтобы не потерять при пересохранении: writeRoute удаляет
|
||||||
// строки и создаёт их заново из матрицы, и без этих полей SLA,
|
// строки и создаёт их заново из матрицы, и без этих полей SLA,
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ export interface IMatrixRow {
|
|||||||
date: string; // дата решения (дд.мм.гггг) или ''
|
date: string; // дата решения (дд.мм.гггг) или ''
|
||||||
comment: string;
|
comment: string;
|
||||||
allowReassign: boolean; // может ли этот согласующий добавить делеганта
|
allowReassign: boolean; // может ли этот согласующий добавить делеганта
|
||||||
|
// ФИО согласующего, который добавил ЭТУ строку перед собой (доп. согласующий,
|
||||||
|
// добавленный другим согласующим с правом переназначения — DelegatedBy). ''
|
||||||
|
// для обычных строк маршрута. Матрица показывает это пометкой под именем.
|
||||||
|
addedBy?: string;
|
||||||
phase?: string; // фаза строки: «Согласование» | «Исполнение» (см. APPROVAL_PHASE)
|
phase?: string; // фаза строки: «Согласование» | «Исполнение» (см. APPROVAL_PHASE)
|
||||||
// Шаг идёт ОДНОВРЕМЕННО с предыдущим (у обоих одинаковый order — так
|
// Шаг идёт ОДНОВРЕМЕННО с предыдущим (у обоих одинаковый order — так
|
||||||
// activateNextQueued активирует их вместе). У первой строки всегда false.
|
// activateNextQueued активирует их вместе). У первой строки всегда false.
|
||||||
|
|||||||
@@ -141,6 +141,16 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
|
|||||||
)}
|
)}
|
||||||
<td data-label={roleLabel}>
|
<td data-label={roleLabel}>
|
||||||
{r.who}
|
{r.who}
|
||||||
|
{/* Доп. согласующий, добавленный другим согласующим (с правом
|
||||||
|
переназначения) — помечаем, кто его добавил. Пусто у обычных
|
||||||
|
строк маршрута. Как и комментарий — inline под именем, чтобы
|
||||||
|
не ломать rowSpan колонки «Шаг» у параллельных шагов. */}
|
||||||
|
{r.addedBy && (
|
||||||
|
<span className={styles.addedByInline}>
|
||||||
|
<span className={styles.addedByLabel}>Доп. согласующий, добавил:</span>{' '}
|
||||||
|
{r.addedBy}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{/* Комментарий решения — мелким текстом под именем (не отдельной
|
{/* Комментарий решения — мелким текстом под именем (не отдельной
|
||||||
строкой: иначе ломается rowSpan колонки «Шаг» у параллельных). */}
|
строкой: иначе ломается rowSpan колонки «Шаг» у параллельных). */}
|
||||||
{r.comment && (
|
{r.comment && (
|
||||||
|
|||||||
@@ -142,6 +142,20 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Пометка «доп. согласующий, добавил: …» — под именем согласующего. Золотой
|
||||||
|
// акцент, чтобы отличать таких участников от штатных строк маршрута.
|
||||||
|
.addedByInline {
|
||||||
|
display: block;
|
||||||
|
margin-top: 3px;
|
||||||
|
font-size: $fs-caption;
|
||||||
|
color: $gold-600;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addedByLabel {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
// ВАЖНО: не display:flex — это сняло бы с ячейки display:table-cell, и она
|
// ВАЖНО: не display:flex — это сняло бы с ячейки display:table-cell, и она
|
||||||
// перестала бы вставать в строку рядом с соседней (кнопки уезжали на вторую
|
// перестала бы вставать в строку рядом с соседней (кнопки уезжали на вторую
|
||||||
// строку). Ячейка остаётся табличной, кнопки держатся в строке через nowrap.
|
// строку). Ячейка остаётся табличной, кнопки держатся в строке через nowrap.
|
||||||
@@ -287,7 +301,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Комментарий под именем — на всю ширину ячейки (своя строка).
|
// Комментарий под именем — на всю ширину ячейки (своя строка).
|
||||||
.commentInline {
|
.commentInline,
|
||||||
|
.addedByInline {
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user