Files
request-portal/src/webparts/requestPortal/forms/Dismissal
gorbenko.pavel cf57b40513
Tests / frontend-test (push) Failing after 38s
feat(автосогласование): колонка AutoApprove во всех списках согласования (#20)
Провижининг (Provision-Lists.ps1 читает эти манифесты) теперь заводит колонку
«Автосогласование» (AutoApprove, Да/Нет) в каждом из 12 списков согласования —
рядом с уже существующими SLAhours/AskOverdueReason.

Значение приходит из resolve (approval-route API, шаг маршрута настраивается
только в админке — портал само значение не выбирает, только переносит его в
строку списка при отправке заявки/сохранении черновика, тем же путём, что и
slaHours/askOverdueReason). Фактическое автосогласование по истечении SLA
делает фоновая задача бэкенда (Hangfire), не портал.

В ApprovalMatrix добавлена пометка «(авто)» рядом со значением SLA — чисто
информативно, чтобы участник видел, что решение по шагу может принять не
только человек.

Примечание: ServiceRequest и TmcTransfer не передают ни slaHours/
askOverdueReason, ни (теперь) autoApprove из resolve в строку — это
существовавший пробел (SLA на уровне шага у этих двух форм и раньше не
работал), автосогласование для них наследует только это же ограничение.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/20
2026-09-14 09:02:14 +00:00
..
2026-08-26 11:10:26 +00:00
2026-08-26 11:10:26 +00:00
2026-08-26 11:10:26 +00:00
2026-08-26 11:10:26 +00:00
2026-08-26 11:10:26 +00:00
2026-08-26 11:10:26 +00:00

Заявление на увольнение (hr-dismissal)

Назначение: заявление работника о расторжении трудового договора по собственному желанию (штатный сотрудник). Бумажный первоисточник — «Шаблон Заявление на увольнение ШТАТ.docx». Категория: Отдел кадров Списки SP: «Заявление на увольнение 1 — данные» / «Заявление на увольнение 1 — согласование» RequestTypeId (внешний API маршрутов): не хардкодится — резолвится в рантайме по pageUrl («hr-dismissal») через useRequestTypeId (см. «Формы больше не хардкодят RequestTypeId» в docs/forms.md). Тип заведён в админке approval-route (id 16, список данных /sites/requests/Lists/18).

Маршрут

  • Фазы: только согласование. Фазы исполнения нет — после согласования заявка получает статус «Согласовано» (как «Обходной лист» / «Отпуск без содержания»).
  • Матрица редактируема заявителем при новой заявке / черновике / возврате: useMatrixEditor(editable = !readOnly).
  • Условие IsManager в маршруте (если настроено в админке) резолвится по полю Position ЭТОЙ заявки, поэтому Position передаётся в fieldValues при планировании (getPlannedMatrix(..., [{ fieldName: 'Position', ... }])) — как в «Обходном листе».
  • Настроенный шаблон маршрута (approval-route, route id 17): шаг 1 — HeadType.Chief (непосредственный руководитель увольняемого, тот же Employee/Manager?managerType=chief, что резолвит «Лицо, принимающее ОС/НМА», см. ниже), шаг 2 — HeadType.CompanyDirectors (см. следующий раздел).

Особое поведение

  • По умолчанию заявка на себя, но подать можно и за другого: «Сотрудник» — People-picker (Employee), маршрут резолвится по e-mail выбранного сотрудника, reloadKey матрицы включает data.Employee и должность основной компании (см. ниже).

  • Автоподстановка при выборе сотрудника: вся таблица организаций — из ОДНОГО справочника Employee/ByEmail (shared/dictionary) — см. ниже.

  • Таблица организаций (OrganizationsTable, forms/Dismissal/sections/, без подписи над собой — у неё уже есть заголовки колонок «Компания» / «Структурное подразделение» / «Должность») — ЕДИНСТВЕННОЕ место для компании, подразделения и должности, и на форме подачи (редактируемая), и в режиме просмотра (read-only) — отдельных полей для них нет (были бы дублированием). data.Organization: IOrganizationRecord[] — массив {company, department, position}; поле называется department (тот же справочник Section, что и раньше — см. следующий абзац), колонка называется «Подразделение», и то и другое read-only. Должность редактируема В ЛЮБОЙ строке (справочник не всегда точен), но маршрут (условие «IsManager») и обязательность по валидации завязаны ТОЛЬКО на должность ПЕРВОЙ (основной) записи — primaryPosition(data.Organization); должности остальных компаний правятся, но ни на что не влияют. Справочник ничего не вернул (сотрудник не найден в api.tnsu.kz) — таблица всё равно рендерится одной строкой с прочерками вместо компании/подразделения и редактируемой должностью: иначе такому человеку нечем было бы заполнить обязательное поле.

    Хранение в SharePoint — три ПАРАЛЛЕЛЬНЫХ JSON-столбца, не один общий блок: Organization/Department/Position — каждый содержит JSON-массив строк (компании / департаменты / должности), один и тот же индекс = одна и та же запись во всех трёх. Сборка при чтении и разборка при записи — zipOrganization / organizationColumn в model.ts/submit.ts; не-JSON значение (заявка, сохранённая до перехода на этот формат) не роняет чтение — трактуется как одна компания (см. parseOrganizationColumn).

  • Последний рабочий день (LastWorkingDay) — обязателен, не раньше чем через MIN_NOTICE_MONTHS (1) календарный месяц от сегодня (isoDatePlusMonths, min у input type=date + проверка в validate на случай обхода UI) — срок предупреждения об увольнении по ТК. Более позднюю дату (по договорённости с работодателем) форма не ограничивает.

  • Контактный телефон (Phone) — обязателен, PhoneInput (маска).

  • Комментарий (Comment) — необязательный свободный текст. Причины увольнения отдельного поля нет: в шаблоне формулировка фиксированная («по собственному желанию»).

  • Заголовок элемента (Title): Заявление на увольнение — {ФИО} — дд.мм.гггг.

Совместители: организации таблицей, удаление синхронизировано с маршрутом

Сотрудник может числиться сразу в НЕСКОЛЬКИХ компаниях (совместительство). Approval-route резолвит маршрут через HeadType.CompanyDirectors — ОДИН шаг разворачивается в по одному согласующему НА КАЖДУЮ компанию сотрудника (директор, а где нет такой штатной единицы — должностной эквивалент: Главный механик для «ТОО KazPromService», Генеральный директор» для «ТОО СТ Global», иначе Директор, см. DirectorPositionByCompanyв approval-routeTemplateRouteService). Это уже работает БЕЗ участия портала — сам approval-route резолвит все компании сотрудника через Employee/ByEmail`.

OrganizationsTable (см. предыдущий раздел) на форме подачи показывает кнопку «убрать» у каждой строки, когда компаний больше одной. Если сотрудник реально не увольняется из ВСЕХ компаний сразу, HR убирает лишние: DismissalForm.handleRemoveOrganization

  1. снимает запись из data.Organization (массив всегда отражает ТЕКУЩИЙ, не первоначальный, набор — так же восстанавливается при повторном открытии черновика/возврата: loadDismissalRequest читает его из уже сохранённых Organization/Department/Position, без обращения к справочнику заново);
  2. независимо резолвит директора/эквивалента этой компании через getCompanyDirectorEmail (shared/dictionary/employeeLookup.service.ts — зеркало того же DirectorPositionByCompany, что и на бэкенде) и удаляет строку с этим email из уже загруженной ПЛАНИРУЕМОЙ матрицы (matrix.remove).

Убирать согласующего из матрицы вручную (кнопкой самой матрицы) при этом не нужно — это best-effort синхронизация: если матрица ещё не загрузилась или директор не нашёлся, компания всё равно снимается из заявления, а несовпадение в матрице HR поправит вручную через саму матрицу (она и так редактируема).

Согласовано → обходной лист создаётся сам

Как только заявление получает статус «Согласовано» (маршрут пройден целиком), портал заводит по нему обходной лист — ensureClearanceForDismissal (createClearance.ts), вызывается из DismissalView в onDecided последнего согласующего.

Форма обходного листа при этом не открывается: маршрут резолвится через approval-route (getPlannedMatrix для фаз «Согласование» и «Исполнение» — по Employee и Position заявления), элемент пишется в текущую партицию списка «Обходной лист … — данные» и сразу уходит на согласование (initialSubmitStatus + notifyActiveStep, как при обычной подаче). Поля целиком выводятся из заявления: сотрудник, должность, подразделение, организация, а последний рабочий день становится датой расторжения договора; отметки (парковка, СКУД, обучение) остаются пустыми — их заполняют профильные согласующие.

Пара связывается перекрёстными колонками-гиперссылками (тип Url): ClearanceUrl в заявлении → на обходной лист, DismissalUrl в обходном листе → на заявление. Не Lookup намеренно: ссылка ведёт на страницу портала (…?rpmode=approve&rptype=…&rpid=…&rplist=…, см. docs/routing.md), а не на элемент списка, и не ломается, когда парная заявка уезжает в другую партицию.

Ссылки видны на обеих формах и кликабельны: поле «Обходной лист» в заявлении и «Заявление на увольнение» в обходном листе (LinkField, shared/ui) — read-only, открывают парную заявку в ТОЙ ЖЕ вкладке и появляются только когда связь есть. Переход идёт без перезагрузки страницы: ссылка ведёт на ту же страницу портала с другими rp-параметрами, поэтому клик перехватывается и уходит в navigate() (navigateToRequestLink в shared/routing.ts) — а Ctrl/⌘+клик по-прежнему открывает заявку в отдельной вкладке, потому что href настоящий. У согласующего, только что одобрившего заявление, ссылка появляется сразу, без перезагрузки.

Что важно знать про это поведение:

  • Идемпотентность — по непустой ClearanceUrl. Заявление можно согласовать повторно (возврат на доработку → отправка → согласование), и без этой проверки на каждый цикл появлялся бы новый обходной лист.
  • Работает в браузере последнего согласующего. Если создание упало (или он закрыл вкладку в этот момент), заявление всё равно согласовано, а обходного листа нет — портал покажет предупреждение «заведите вручную». Серверного повтора нет: фонового обработчика у портала пока не существует, всё делает клиент.
  • Автором обходного листа в SharePoint станет этот согласующий (в «Мои заявки» лист попадёт к нему). На маршрут это не влияет — он резолвится по Employee.
  • Тип заявки clearance-form должен быть заведён в админке approval-route — иначе создание падает на резолве RequestTypeId.

Печать заблокирована до согласования обходного листа

DismissalView не показывает PrintButton, пока парный обходной лист не дошёл хотя бы до статуса «Согласовано» (или дальше — «Выполнение»/«Закрыта», если у него есть исполнители): печатать заявление раньше значит выдать документ до того, как процесс увольнения (карты, суммы к списанию) подтверждён целиком. Статус читается НАПРЯМУЮ по ClearanceUrl (parseRoute вытаскивает itemId/listId из самой ссылки, loadClearanceRequest — статус) — отдельного поля-дублёра нет. Пока ClearanceUrl пуст (обходной лист ещё не создан/не удалось создать) — печать тоже заблокирована, печатать нечего.

«Лицо, принимающее ОС/НМА»

Колонка AssetsAcceptor (People) — кто принимает основные средства/НМА увольняемого. Обязательна, но заполняет её не заявитель, а непосредственный руководитель увольняемого — тот, кого approval-route резолвит для HeadType.Chief (Employee/Manager?managerType=chief, см. «Маршрут» выше).

  • В форме подачи поле показывается read-only (обычно пустое) — просто информирует, что это будет заполнено позже.
  • В DismissalView поле редактируемо ТОЛЬКО когда одновременно: (а) у текущего пользователя сейчас активный шаг решения (getPendingRowForCurrentUser), и (б) его e-mail совпадает с результатом getManagerEmail(..., 'chief') для data.Employee — проверка по ИДЕНТИЧНОСТИ, а не по должности (в отличие от approverRights.ts «Обходного листа»: здесь должность руководителя может быть любой).
  • Обязательность применяется НЕ на уровне списка (иначе создание заявки падало бы — поле нечем заполнить до решения руководителя), а через DecisionPanel.onBeforeDecide: пустое поле бросает ошибку ДО применения решения (approve/return/reject — API не различает, какое именно) и сохраняет значение сразу перед ним, тем же приёмом, что «Обходной лист» использует для своих отметок.

Отличия от «Обходной лист» (clearance-form)

Обходной лист оформляет HR после решения об увольнении (обход подразделений, карты, обучение). Это заявление — вход в процесс: его подаёт сам работник, полей всего восемь, отметок подразделений нет.

Секции формы

RequestSection (сотрудник · организация/департамент/должность (таблица — редактируемая на форме подачи, read-only в просмотре) · последний рабочий день · телефон · комментарий · ссылка на обходной лист · лицо, принимающее ОС/НМА)

Точки входа

  • rptype: hr-dismissal
  • Режим согласующего (просмотр): есть (DismissalView)
  • «Создать на основе»: нет