feat: история согласования — маршрут пересохраняется патчем

При доработке и повторной отправке writeRoute раньше удалял все строки
маршрута и создавал заново — кто вернул или отклонил заявку и с каким
комментарием, терялось. Теперь маршрут пересохраняется патчем
(planRoutePatch + ApprovalListService.patchSteps): строки, что остаются в
маршруте, обновляются на месте, и их прошлые решения лежат в истории
версий; недостающие шаги создаются, выпавшие и строки делегатов уходят в
корзину. Всё одним пакетом. Проверено пилотами на увольнении и рабочем
месте (обе фазы, уход в корзину). Аварийный откат для всех форм —
ROUTE_PATCH_ENABLED = false.

Из версий строк маршрута и заявки собирается лента «История
согласования» (shared/approval/history): отправки, решения с
комментариями и временем на шаге, делегирование, замена исполнителя,
у кого заявка сейчас. Свёрнутый раздел в карточке и в форме доработки
всех форм, грузится только при раскрытии.

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 5b1e94bf42
commit 153c966f0b
40 changed files with 1334 additions and 20 deletions
@@ -14,6 +14,7 @@ import {
} from '../../shared/ui';
import {
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
useMatrixEditor,
getPlannedMatrix,
@@ -339,6 +340,19 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({
/>
)}
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
{editId && (
<div className="rp-no-print">
<ApprovalHistory
approval={approval}
dataListRef={editListId || dataListTitle(FORM_ID)}
requestId={editId}
reloadKey={requestStatus}
/>
</div>
)}
<SubmitActions
onDelete={handleDelete}
submit={submit}
@@ -8,6 +8,7 @@ import {
DecisionPanel,
StatusBadge,
ApprovalMatrix,
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
useRequestTypeId,
@@ -319,6 +320,12 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
</div>
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
{/* История согласования: маршрут пересохраняется патчем (см.
writeRoute): прошлые решения лежат в версиях строк. */}
<div className="rp-no-print">
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
</div>
<div className="rp-no-print">
<DecisionPanel
service={approverService}
@@ -14,6 +14,7 @@ import {
} from '../../shared/ui';
import {
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
useMatrixEditor,
getPlannedMatrix,
@@ -327,6 +328,19 @@ const ConcreteRequestForm: React.FC<IRequestFormProps> = ({
waitingText="Укажите начальника участка и проект, чтобы построить маршрут согласования."
/>
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
{editId && (
<div className="rp-no-print">
<ApprovalHistory
approval={approval}
dataListRef={editListId || dataListTitle(FORM_ID)}
requestId={editId}
reloadKey={requestStatus}
/>
</div>
)}
<SubmitActions
onDelete={handleDelete}
submit={submit}
@@ -8,6 +8,7 @@ import {
DecisionPanel,
StatusBadge,
ApprovalMatrix,
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
useRequestTypeId
@@ -155,6 +156,12 @@ const ConcreteRequestView: React.FC<IConcreteRequestViewProps> = ({ context, ite
</div>
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
{/* История согласования: маршрут пересохраняется патчем (см.
writeRoute): прошлые решения лежат в версиях строк. */}
<div className="rp-no-print">
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
</div>
<div className="rp-no-print">
<DecisionPanel
service={approverService}
@@ -14,6 +14,7 @@ import {
} from '../../shared/ui';
import {
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
useMatrixEditor,
getPlannedMatrix,
@@ -341,6 +342,20 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили, — поэтому история и в форме, а не только в просмотре.
Только у уже существующей заявки: у новой истории нет. */}
{editId && (
<div className="rp-no-print">
<ApprovalHistory
approval={approval}
dataListRef={editListId || dataListTitle(FORM_ID)}
requestId={editId}
reloadKey={requestStatus}
/>
</div>
)}
<SubmitActions
onDelete={handleDelete}
submit={submit}
@@ -8,6 +8,7 @@ import {
DecisionPanel,
StatusBadge,
ApprovalMatrix,
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
useRequestTypeId,
@@ -283,6 +284,12 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
</div>
{/* История согласования: маршрут пересохраняется патчем (см.
writeRoute): прошлые решения лежат в версиях строк. */}
<div className="rp-no-print">
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
</div>
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый).
Итог автосоздания обходного листа — тут же, а не в шапке: он появляется
именно как следствие решения «Согласовать», логичнее видеть его рядом. */}
@@ -14,6 +14,7 @@ import {
} from '../../shared/ui';
import {
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
useMatrixEditor,
getPlannedMatrix,
@@ -303,6 +304,19 @@ const EmployeeTransferForm: React.FC<IRequestFormProps> = ({
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
{editId && (
<div className="rp-no-print">
<ApprovalHistory
approval={approval}
dataListRef={editListId || dataListTitle(FORM_ID)}
requestId={editId}
reloadKey={requestStatus}
/>
</div>
)}
<SubmitActions
onDelete={handleDelete}
submit={submit}
@@ -8,6 +8,7 @@ import {
DecisionPanel,
StatusBadge,
ApprovalMatrix,
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
useRequestTypeId
@@ -152,6 +153,12 @@ const EmployeeTransferView: React.FC<IEmployeeTransferViewProps> = ({ context, i
</div>
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
{/* История согласования: маршрут пересохраняется патчем (см.
writeRoute): прошлые решения лежат в версиях строк. */}
<div className="rp-no-print">
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
</div>
<div className="rp-no-print">
<DecisionPanel
service={approverService}
@@ -21,6 +21,7 @@ import {
import { IFieldChange, toChangeMap } from '../../shared/fieldChanges/fieldChanges';
import {
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
useMatrixEditor,
getPlannedMatrix,
@@ -469,6 +470,19 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
/>
)}
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
{editId && (
<div className="rp-no-print">
<ApprovalHistory
approval={approval}
dataListRef={editListId || dataListTitle(FORM_ID)}
requestId={editId}
reloadKey={requestStatus}
/>
</div>
)}
<SubmitActions
onDelete={handleDelete}
submit={submit}
@@ -23,6 +23,7 @@ import {
DecisionPanel,
StatusBadge,
ApprovalMatrix,
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
useRequestTypeId,
@@ -417,6 +418,12 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
</div>
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
{/* История согласования: маршрут пересохраняется патчем (см.
writeRoute): прошлые решения лежат в версиях строк. */}
<div className="rp-no-print">
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
</div>
<div className="rp-no-print">
<DecisionPanel
service={approverService}
@@ -21,6 +21,7 @@ import {
import { IFieldChange, toChangeMap } from '../../shared/fieldChanges/fieldChanges';
import {
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
useMatrixEditor,
getPlannedMatrix,
@@ -459,6 +460,19 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
/>
)}
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
{editId && (
<div className="rp-no-print">
<ApprovalHistory
approval={approval}
dataListRef={editListId || dataListTitle(FORM_ID)}
requestId={editId}
reloadKey={requestStatus}
/>
</div>
)}
<SubmitActions
onDelete={handleDelete}
submit={submit}
@@ -22,6 +22,7 @@ import {
DecisionPanel,
StatusBadge,
ApprovalMatrix,
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
useRequestTypeId,
@@ -380,6 +381,12 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
</div>
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
{/* История согласования: маршрут пересохраняется патчем (см.
writeRoute): прошлые решения лежат в версиях строк. */}
<div className="rp-no-print">
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
</div>
<div className="rp-no-print">
<DecisionPanel
service={approverService}
@@ -18,6 +18,7 @@ import {
} from '../../shared/ui';
import {
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
useMatrixEditor,
getPlannedMatrix,
@@ -478,6 +479,19 @@ const ReferralForm: React.FC<IRequestFormProps> = ({
/>
)}
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
{editId && (
<div className="rp-no-print">
<ApprovalHistory
approval={approval}
dataListRef={editListId || dataListTitle(FORM_ID)}
requestId={editId}
reloadKey={requestStatus}
/>
</div>
)}
<SubmitActions
onDelete={handleDelete}
submit={submit}
@@ -18,6 +18,7 @@ import {
DecisionPanel,
StatusBadge,
ApprovalMatrix,
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
useRequestTypeId,
@@ -238,6 +239,12 @@ const ReferralView: React.FC<IReferralViewProps> = ({ context, itemId, formKey,
</div>
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
{/* История согласования: маршрут пересохраняется патчем (см.
writeRoute): прошлые решения лежат в версиях строк. */}
<div className="rp-no-print">
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
</div>
<div className="rp-no-print">
<DecisionPanel
service={approverService}
@@ -14,6 +14,7 @@ import {
} from '../../shared/ui';
import {
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
useMatrixEditor,
getPlannedMatrix,
@@ -387,6 +388,19 @@ const ServiceRequestForm: React.FC<IRequestFormProps> = ({
waitingText="Укажите заявителя и проект, чтобы построить маршрут согласования."
/>
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
{editId && (
<div className="rp-no-print">
<ApprovalHistory
approval={approval}
dataListRef={editListId || dataListTitle(FORM_ID)}
requestId={editId}
reloadKey={requestStatus}
/>
</div>
)}
<SubmitActions
onDelete={handleDelete}
submit={submit}
@@ -18,6 +18,7 @@ import {
DecisionPanel,
StatusBadge,
ApprovalMatrix,
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
useRequestTypeId,
@@ -181,6 +182,12 @@ const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemI
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
</div>
{/* История согласования: маршрут пересохраняется патчем (см.
writeRoute): прошлые решения лежат в версиях строк. */}
<div className="rp-no-print">
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
</div>
<div className="rp-no-print">
<DecisionPanel
service={approverService}
@@ -18,6 +18,7 @@ import {
} from '../../shared/ui';
import {
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
useMatrixEditor,
getPlannedMatrix,
@@ -421,6 +422,19 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
emptyText="Для этого вида работ согласование не требуется — заявка сразу поступит в отдел 1С."
/>
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
{editId && (
<div className="rp-no-print">
<ApprovalHistory
approval={approval}
dataListRef={editListId || dataListTitle(FORM_ID)}
requestId={editId}
reloadKey={requestStatus}
/>
</div>
)}
<SubmitActions
onDelete={handleDelete}
submit={submit}
@@ -20,6 +20,7 @@ import {
DecisionPanel,
StatusBadge,
ApprovalMatrix,
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
useRequestTypeId,
@@ -247,6 +248,12 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey,
/>
</div>
{/* История согласования: маршрут пересохраняется патчем (см.
writeRoute): прошлые решения лежат в версиях строк. */}
<div className="rp-no-print">
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
</div>
<div className="rp-no-print">
<DecisionPanel
service={approverService}
@@ -18,6 +18,7 @@ import {
} from '../../shared/ui';
import {
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
useMatrixEditor,
getPlannedMatrix,
@@ -514,6 +515,19 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
/>
)}
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
{editId && (
<div className="rp-no-print">
<ApprovalHistory
approval={approval}
dataListRef={editListId || dataListTitle(FORM_ID)}
requestId={editId}
reloadKey={requestStatus}
/>
</div>
)}
<SubmitActions
onDelete={handleDelete}
submit={submit}
@@ -21,6 +21,7 @@ import {
DecisionPanel,
StatusBadge,
ApprovalMatrix,
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
useRequestTypeId,
@@ -207,6 +208,12 @@ const TmcTransferView: React.FC<ITmcTransferViewProps> = ({ context, itemId, for
)}
</div>
{/* История согласования: маршрут пересохраняется патчем (см.
writeRoute): прошлые решения лежат в версиях строк. */}
<div className="rp-no-print">
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
</div>
<div className="rp-no-print">
<DecisionPanel
service={approverService}
@@ -14,6 +14,7 @@ import {
} from '../../shared/ui';
import {
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
useMatrixEditor,
getPlannedMatrix,
@@ -315,6 +316,19 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
{editId && (
<div className="rp-no-print">
<ApprovalHistory
approval={approval}
dataListRef={editListId || dataListTitle(FORM_ID)}
requestId={editId}
reloadKey={requestStatus}
/>
</div>
)}
<SubmitActions
onDelete={handleDelete}
submit={submit}
@@ -8,6 +8,7 @@ import {
DecisionPanel,
StatusBadge,
ApprovalMatrix,
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
useRequestTypeId
@@ -152,6 +153,12 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
</div>
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
{/* История согласования: маршрут пересохраняется патчем (см.
writeRoute): прошлые решения лежат в версиях строк. */}
<div className="rp-no-print">
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
</div>
<div className="rp-no-print">
<DecisionPanel
service={approverService}
@@ -14,6 +14,7 @@ import {
} from '../../shared/ui';
import {
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
useMatrixEditor,
getPlannedMatrix,
@@ -314,6 +315,19 @@ const VacationForm: React.FC<IRequestFormProps> = ({
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
{editId && (
<div className="rp-no-print">
<ApprovalHistory
approval={approval}
dataListRef={editListId || dataListTitle(FORM_ID)}
requestId={editId}
reloadKey={requestStatus}
/>
</div>
)}
<SubmitActions
onDelete={handleDelete}
submit={submit}
@@ -8,6 +8,7 @@ import {
DecisionPanel,
StatusBadge,
ApprovalMatrix,
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
useRequestTypeId
@@ -152,6 +153,12 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
</div>
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
{/* История согласования: маршрут пересохраняется патчем (см.
writeRoute): прошлые решения лежат в версиях строк. */}
<div className="rp-no-print">
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
</div>
<div className="rp-no-print">
<DecisionPanel
service={approverService}
@@ -18,6 +18,7 @@ import {
} from '../../shared/ui';
import {
ApprovalMatrix,
ApprovalHistory,
StatusBadge,
useMatrixEditor,
getPlannedMatrix,
@@ -402,6 +403,19 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
/>
)}
{/* Автору на доработке важнее всего видеть, почему вернули или
отклонили. Только у уже существующей заявки: у новой истории нет. */}
{editId && (
<div className="rp-no-print">
<ApprovalHistory
approval={approval}
dataListRef={editListId || dataListTitle(FORM_ID)}
requestId={editId}
reloadKey={requestStatus}
/>
</div>
)}
<SubmitActions
onDelete={handleDelete}
submit={submit}
@@ -21,6 +21,7 @@ import {
DecisionPanel,
StatusBadge,
ApprovalMatrix,
ApprovalHistory,
useMatrixEditor,
notifyActiveStep,
useRequestTypeId,
@@ -274,6 +275,12 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
)}
</div>
{/* История согласования: маршрут пересохраняется патчем (см.
writeRoute): прошлые решения лежат в версиях строк. */}
<div className="rp-no-print">
<ApprovalHistory approval={approval} dataListRef={dataListRef} requestId={itemId} reloadKey={decisionSeq} />
</div>
<div className="rp-no-print">
<DecisionPanel
service={approverService}