160 lines
11 KiB
TypeScript
160 lines
11 KiB
TypeScript
// Чистые правила экранов консолидаций (спека `consolidations-ui`): подписи и пояснения статусов версии, ручные
|
||
// переходы, блокировка, «Версия», фильтр «Только в работе». Без React, store и API.
|
||
import type { CounterPairOut } from '../types/api';
|
||
import type { ConsRussianStatus, ConsStatus, LockOut } from '../types/consolidations/common_types';
|
||
import { pluralRu } from './labels';
|
||
|
||
/** Подписи статусов версии — одни для списка, фильтра «Статус», шапки карточки и модалки «Изменить статус». */
|
||
export const CONS_STATUS_LABEL: Record<ConsStatus, ConsRussianStatus> = {
|
||
INITIATED: 'Инициация',
|
||
DOCUMENTS_PROCESSING: 'Обработка документов',
|
||
DATA_CORRECTION: 'Корректировка данных',
|
||
CONSOLIDATION_RF: 'Консолидация РФ',
|
||
VERIFICATION_RF: 'Верификация РФ',
|
||
CONSOLIDATION_GO: 'Консолидация ГО',
|
||
VERIFICATION_GO: 'Верификация ГО',
|
||
VERIFIED: 'Верифицирован',
|
||
CANCELLED: 'Отменена',
|
||
ADJUSTMENT: 'Корректировка',
|
||
ARCHIVED: 'В архиве',
|
||
REJECTED: 'Отклонён',
|
||
};
|
||
|
||
/**
|
||
* Пояснение статуса под заголовком карточки версии. `CONSOLIDATION_RF`, `VERIFIED` — тексты макета [30], [33-01];
|
||
* `INITIATED`, `DOCUMENTS_PROCESSING`, `DATA_CORRECTION`, `CANCELLED` — временные из ТЗ «Статусная модель», пока нет
|
||
* текстов аналитика (design.md change consolidations-ui, решение (б)).
|
||
*/
|
||
export const CONS_STATUS_DESCRIPTION: Partial<Record<ConsStatus, string>> = {
|
||
INITIATED: 'Система зафиксировала состав ГСК на отчетную дату и инициатора (КА РФ или КА ГО)',
|
||
DOCUMENTS_PROCESSING:
|
||
'КА загружает документы; система отправляет pdf файлы в ИС Сканирование, производит парсинг ОСВ, бухгалтерского баланса и ОФР',
|
||
DATA_CORRECTION:
|
||
'Выявлены некорректные данные в ИС Сканирование, проводится ручная корректировка. Пользователь валидирует распарсенные данные и вносит правки',
|
||
CONSOLIDATION_RF: 'Автоматическая консолидация, ручные проверки и сравнение КФО клиента и банка',
|
||
CONSOLIDATION_GO: 'Формирование и проверка КФО, внесение корректировок на уровне ГО',
|
||
VERIFICATION_RF: 'Проверка и подтверждение КФО на уровне РФ',
|
||
VERIFICATION_GO: 'Проверка и подтверждение КФО на уровне ГО',
|
||
VERIFIED: 'Отчет верифицирован ГО, КА ГО внес в систему номер и дату решения УОБ',
|
||
CANCELLED: 'Заявка на формирование/подтверждение КФО аннулирована по инициативе пользователя',
|
||
};
|
||
|
||
/** Пояснение статуса; нет текста — пусто (название статуса под заголовком не повторяем). */
|
||
export const consStatusDescription = (status: ConsStatus): string => CONS_STATUS_DESCRIPTION[status] ?? '';
|
||
|
||
/**
|
||
* Ручные переходы статуса версии — зеркало `MANUAL_TRANSITIONS` из `api/src/services/consolidation_status.py`
|
||
* (API допустимые переходы не отдаёт). Расхождение с бэком сервер ловит 409 `TRANSITION_NOT_ALLOWED`. С маршрутом
|
||
* согласования (Срез 7) заменяется ответом API.
|
||
*/
|
||
export const MANUAL_TRANSITIONS: Partial<Record<ConsStatus, readonly ConsStatus[]>> = {
|
||
INITIATED: ['CANCELLED'],
|
||
DOCUMENTS_PROCESSING: ['CANCELLED', 'INITIATED'],
|
||
DATA_CORRECTION: ['CANCELLED', 'INITIATED'],
|
||
};
|
||
|
||
/** Допустимые ручные переходы из статуса; нет — пустой список. */
|
||
export const manualTransitions = (status: ConsStatus): readonly ConsStatus[] => MANUAL_TRANSITIONS[status] ?? [];
|
||
|
||
/** Версию держит текущий пользователь. */
|
||
export const isLockedBy = (lock: LockOut | null | undefined, userId: string): boolean => !!lock && lock.locked_by === userId;
|
||
|
||
/** Кнопка «Изменить статус» в шапке карточки: своя блокировка и есть ручные переходы. */
|
||
export const canChangeStatus = (status: ConsStatus, lock: LockOut | null | undefined, userId: string): boolean =>
|
||
isLockedBy(lock, userId) && manualTransitions(status).length > 0;
|
||
|
||
/** Лимит причины перехвата и комментария к смене статуса (`MAX_TAKEOVER_REASON`, `MAX_STATUS_COMMENT` бэка). */
|
||
export const MAX_REASON_LENGTH = 1000;
|
||
|
||
/** Причина перехвата / комментарий: непусто после обрезки пробелов и не длиннее лимита. */
|
||
export const isValidReason = (text: string | null | undefined): boolean => {
|
||
const trimmed = (text ?? '').trim();
|
||
return trimmed.length > 0 && trimmed.length <= MAX_REASON_LENGTH;
|
||
};
|
||
|
||
/** «Изменить статус» в модалке: выбран новый этап и комментарий непуст. */
|
||
export const canSubmitStatusChange = (toStatus: ConsStatus | null | undefined, comment: string | null | undefined): boolean =>
|
||
!!toStatus && isValidReason(comment);
|
||
|
||
/** Колонка «Версия»: у первой версии пусто, у последующих — номер. */
|
||
export const versionLabel = (versionNo: number): string => (versionNo === 1 ? '' : String(versionNo));
|
||
|
||
/** Флажок «Только в работе» из `?in_work_only=`: включён только явным `true` (по умолчанию выключен, как в API). */
|
||
export const parseInWorkOnly = (param: string | null): boolean => param === 'true';
|
||
|
||
/** «Взять в работу» недоступна у отменённой версии — по коду статуса, а не по подписи. */
|
||
export const canTakeInWork = (status: ConsStatus): boolean => status !== 'CANCELLED';
|
||
|
||
/** Точка в конце фразы после ФИО: «Соколов А. В.» уже заканчивается точкой — вторую не ставим. */
|
||
export const endSentence = (text: string): string => (text.endsWith('.') ? text : `${text}.`);
|
||
|
||
/** «+ Создать» в модалке «Новая консолидация»: в консолидации не меньше двух ЮЛ (иначе 422 `PERIMETER_TOO_SMALL`). */
|
||
export const MIN_PERIMETER = 2;
|
||
|
||
/** Модалка [31-00] нужна при уходе со вкладки «ОСВ», пока есть несопоставленные строки. */
|
||
export const needsUnmappedWarning = (unmappedTotal: number | null | undefined): boolean => (unmappedTotal ?? 0) > 0;
|
||
|
||
/**
|
||
* Переход на другую вкладку отчёта (`/versions/:version_id/:tab`): сегмент вкладки заменяется, запись истории — тоже,
|
||
* чтобы «Назад» вёл со страницы отчёта, а не по переключениям вкладок (AURORA-1851, как переключатель документа в AURORA-1692).
|
||
*/
|
||
export const reportTabNavigation = (pathname: string, activeKey: string, newKey: string) => ({
|
||
to: pathname.replace(new RegExp(`/${activeKey}/?$`), `/${newKey}`),
|
||
options: { replace: true },
|
||
});
|
||
|
||
/** Статусы, в которых версия закрыта для правок: модалку о парах без встречных записей не показываем. */
|
||
export const FINISHED_STATUSES: readonly ConsStatus[] = ['VERIFIED', 'CANCELLED', 'ARCHIVED', 'REJECTED'];
|
||
|
||
/** Префикс ключа `sessionStorage`: `armka:counter-pairs:<version_id>`, значение — `counterPairsKey`. */
|
||
export const COUNTER_PAIRS_STORAGE_PREFIX = 'armka:counter-pairs:';
|
||
|
||
/** Ключ списка пар — не зависит от порядка: `holder>missing`, отсортированные, через запятую. */
|
||
export const counterPairsKey = (pairs: readonly CounterPairOut[]): string =>
|
||
pairs
|
||
.map((p) => `${p.holder_id}>${p.missing_id}`)
|
||
.sort()
|
||
.join(',');
|
||
|
||
/**
|
||
* Показывать ли модалку о парах без встречных записей (`05-domain-rules.md` §3 п. 9): есть пары, версия не закрыта и
|
||
* этот же список пар в сессии ещё не показывали. Хранилище недоступно или бросает — показываем.
|
||
*/
|
||
export const shouldWarnCounterPairs = (
|
||
versionId: string,
|
||
status: ConsStatus | undefined,
|
||
pairs: readonly CounterPairOut[],
|
||
storage: Pick<Storage, 'getItem'> | null,
|
||
): boolean => {
|
||
if (pairs.length === 0) return false;
|
||
if (status && FINISHED_STATUSES.includes(status)) return false;
|
||
try {
|
||
return storage?.getItem(`${COUNTER_PAIRS_STORAGE_PREFIX}${versionId}`) !== counterPairsKey(pairs);
|
||
} catch {
|
||
return true;
|
||
}
|
||
};
|
||
|
||
/** Запоминает показанный список пар; ошибки хранилища (приватный режим) глотаем. */
|
||
export const rememberCounterPairsShown = (
|
||
versionId: string,
|
||
pairs: readonly CounterPairOut[],
|
||
storage: Pick<Storage, 'setItem'> | null,
|
||
): void => {
|
||
try {
|
||
storage?.setItem(`${COUNTER_PAIRS_STORAGE_PREFIX}${versionId}`, counterPairsKey(pairs));
|
||
} catch {
|
||
// хранилище недоступно — модалка просто покажется снова
|
||
}
|
||
};
|
||
|
||
/** Тексты модалки «Проверьте встречные записи участников». */
|
||
export const counterPairsWarning = (pairs: readonly CounterPairOut[], unmappedTotal: number): { headline: string; details: string } => {
|
||
const missing = new Set(pairs.map((p) => p.missing_id)).size;
|
||
const base = 'Возможно, контрагент среди строк без ИНН, его ИНН не найден в справочнике или это пропуск в учёте.';
|
||
return {
|
||
headline: `Нет встречных записей: ${pairs.length} ${pluralRu(pairs.length, ['пара', 'пары', 'пар'])} у ${missing} ${pluralRu(missing, ['участника', 'участников', 'участников'])}.`,
|
||
details: unmappedTotal > 0 ? `${base} Ещё ${unmappedTotal} строк без ИНН учтены как внешние.` : base,
|
||
};
|
||
};
|