feat: история согласования по кнопке в строке таблиц заявок
В каждой строке таблиц заявок — кнопка «История согласования»: окно с той же лентой, что в карточке, без открытия самой заявки. Колонка действий теперь есть всегда (история у всех строк, корзина — у своих черновиков). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user