feat: история согласования по кнопке в строке таблиц заявок

В каждой строке таблиц заявок — кнопка «История согласования»: окно с
той же лентой, что в карточке, без открытия самой заявки. Колонка
действий теперь есть всегда (история у всех строк, корзина — у своих
черновиков).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-09-29 12:42:42 +05:00
parent 153c966f0b
commit ab3593c170
3 changed files with 82 additions and 12 deletions
@@ -0,0 +1,42 @@
import * as React from 'react';
import { Alert, Dialog } from '@tnsu/ui-kit';
import { ApprovalHistoryFeed, ApprovalListService, approvalServiceFor, HISTORY_TEXT } from '../../shared/approval';
import { friendlyErrorMessage } from '../../shared/diagnostics';
import { IMyRequest } from './models/IMyRequest';
interface Props {
/** Заявка, чью историю показываем; undefined — окно закрыто. */
request: IMyRequest | undefined;
onClose: () => void;
}
/**
* Список согласования ИМЕННО той партиции, где лежит заявка (listId строки), —
* как и остальные дочитывания в таблице. Если партицию не удалось сопоставить
* с формой, approvalServiceFor бросает — показываем ошибку в окне, а не роняем таблицу.
*/
function approvalFor(request: IMyRequest): { approval?: ApprovalListService; error?: string } {
try {
return { approval: approvalServiceFor(request.formKey, request.listId) };
} catch (e) {
return { error: friendlyErrorMessage(e) };
}
}
/**
* «История согласования» заявки прямо из таблицы — по кнопке в строке, без
* открытия самой заявки. Та же лента, что в карточке (ApprovalHistoryFeed).
*/
export const RequestHistoryDialog: React.FC<Props> = ({ request, onClose }) => {
const target = React.useMemo(() => (request ? approvalFor(request) : {}), [request]);
const number = request ? request.code || `#${request.id}` : '';
return (
<Dialog open={!!request} title={`${HISTORY_TEXT.title} · №${number}`} onDismiss={onClose} width="md">
{request && target.error && <Alert variant="err">{target.error}</Alert>}
{request && target.approval && (
<ApprovalHistoryFeed approval={target.approval} dataListRef={request.listId} requestId={request.id} />
)}
</Dialog>
);
};
@@ -79,6 +79,13 @@
overflow: hidden; overflow: hidden;
} }
// Кнопки действий строки (история, корзина) — в ряд, прижаты вправо.
.rowActions {
display: flex;
justify-content: flex-end;
gap: $sp-1;
}
// Пустой комментарий в колонке «Причина возврата/отклонения». // Пустой комментарий в колонке «Причина возврата/отклонения».
.commentEmpty { .commentEmpty {
color: $gray-400; color: $gray-400;
@@ -29,6 +29,7 @@ import { useRoutedQuery } from '../../shared/usePersistedQuery';
import { IPage, pageKey, pageSignature, readPage, writePage } from './pageCache'; import { IPage, pageKey, pageSignature, readPage, writePage } from './pageCache';
import { Pager } from './Pager'; import { Pager } from './Pager';
import { RequestsFilterBar } from './RequestsFilterBar'; import { RequestsFilterBar } from './RequestsFilterBar';
import { RequestHistoryDialog } from './RequestHistoryDialog';
import { SortableHeader } from './SortableHeader'; import { SortableHeader } from './SortableHeader';
import { filterRows, hasActiveFilters, ISortState, SortKey, sortRows } from './requestsTableFilters'; import { filterRows, hasActiveFilters, ISortState, SortKey, sortRows } from './requestsTableFilters';
import { useTableRefinement } from './useTableRefinement'; import { useTableRefinement } from './useTableRefinement';
@@ -237,9 +238,11 @@ const Table: React.FC<{
sort: ISortState; sort: ISortState;
onSort: (key: SortKey) => void; onSort: (key: SortKey) => void;
onOpen: (request: IMyRequest) => void; onOpen: (request: IMyRequest) => void;
/** Показать историю согласования заявки (окно, без открытия заявки). */
onShowHistory: (request: IMyRequest) => void;
/** Удалить черновик; задан только там, где в таблице бывают свои черновики. */ /** Удалить черновик; задан только там, где в таблице бывают свои черновики. */
onDelete?: (request: IMyRequest) => void; onDelete?: (request: IMyRequest) => void;
}> = ({ rows, view, statusFilter, sort, onSort, onOpen, onDelete }) => { }> = ({ rows, view, statusFilter, sort, onSort, onOpen, onShowHistory, onDelete }) => {
const cols = columnsFor(view, statusFilter); const cols = columnsFor(view, statusFilter);
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} />
@@ -297,27 +300,41 @@ const Table: React.FC<{
render: (r) => r.decisionComment || <span className={styles.commentEmpty}>Без комментария</span> render: (r) => r.decisionComment || <span className={styles.commentEmpty}>Без комментария</span>
}); });
} }
if (onDelete && rows.some(canDeleteRow)) { // Действия строки: история согласования — у всех, корзина — только у своих
columns.push({ // черновиков. Клик по всей строке открывает заявку — кнопкам это не нужно,
key: 'actions', // поэтому stopPropagation.
title: '', const hasDeletable = !!onDelete && rows.some(canDeleteRow);
width: '56px', columns.push({
render: (r) => key: 'actions',
canDeleteRow(r) ? ( title: '',
// Место под одну или две иконки-кнопки (sm) с зазором.
width: hasDeletable ? '92px' : '56px',
render: (r) => (
<div className={styles.rowActions}>
<IconButton
icon="calendar"
title="История согласования"
size="sm"
onClick={(e) => {
e.stopPropagation();
onShowHistory(r);
}}
/>
{onDelete && canDeleteRow(r) && (
<IconButton <IconButton
icon="trash" icon="trash"
title="Удалить черновик" title="Удалить черновик"
danger danger
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
// Клик по всей строке открывает заявку — кнопке это не нужно.
e.stopPropagation(); e.stopPropagation();
onDelete(r); onDelete(r);
}} }}
/> />
) : null )}
}); </div>
} )
});
return ( return (
<div className={styles.tableWrap}> <div className={styles.tableWrap}>
@@ -379,6 +396,8 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
const [totalRows, setTotalRows] = React.useState<number>(0); const [totalRows, setTotalRows] = React.useState<number>(0);
const [error, setError] = React.useState<string>(''); const [error, setError] = React.useState<string>('');
const [deleteError, setDeleteError] = React.useState<string>(''); const [deleteError, setDeleteError] = React.useState<string>('');
// Заявка, чья история согласования открыта в окне; undefined — окно закрыто.
const [historyFor, setHistoryFor] = React.useState<IMyRequest | undefined>(undefined);
// Список перечитается сам: confirmAndDeleteDraft шлёт rp-requests-changed. // Список перечитается сам: confirmAndDeleteDraft шлёт rp-requests-changed.
const handleDelete = (request: IMyRequest): void => { const handleDelete = (request: IMyRequest): void => {
@@ -729,11 +748,13 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
sort={sort} sort={sort}
onSort={refinement.onSort} onSort={refinement.onSort}
onOpen={onOpen} onOpen={onOpen}
onShowHistory={setHistoryFor}
onDelete={view === 'own:created' ? handleDelete : undefined} onDelete={view === 'own:created' ? handleDelete : undefined}
/> />
<Pager page={page} totalPages={totalPages} onChange={onPageChange} /> <Pager page={page} totalPages={totalPages} onChange={onPageChange} />
</> </>
)} )}
<RequestHistoryDialog request={historyFor} onClose={() => setHistoryFor(undefined)} />
</section> </section>
); );
}; };