Files
tnsu-approval-route/backend/Tnsu.ApprovalRoute.Application/Reports/VacancyReport.cs
T
gorbenko.pavel 240e5984b1
Tests / frontend-test (pull_request) Successful in 53s
Tests / backend-test (pull_request) Successful in 5m34s
feat(отчёт): строка на каждый приём + столбец «Ф.И.О. нанимаемого сотрудника»
На одну заявку на подбор приходится несколько заявок на приём («Разнорабочий,
4 вакансии» → 4 кандидата), и раньше они схлопывались в одну строку: ФИО не
было вовсе, а «Дата выхода» показывала только САМУЮ ПОЗДНЮЮ из четырёх. Кто
именно вышел и когда — из отчёта было не узнать.

Теперь вакансия занимает столько строк, сколько по ней приёмов. Построчные
колонки — «Дата выхода», новая «Ф.И.О. нанимаемого сотрудника» и «Вместо
сотрудника Ф.И.О.» (у каждого приёма своё «взамен кого»); все остальные общие
и объединяются по вертикали.

Модель: VacancyReportRow.Lines — именно СТРОКИ ЛИСТА, а не «приёмы». У вакансии
без приёмов строка всё равно одна (иначе она пропала бы из отчёта), с пустыми
ФИО и датой, но с «Вместо сотрудника» из причины открытия вакансии — прежнее
поведение сохранено. Реальное число приёмов осталось в ClosedCount.

Отклонённые приёмы теперь отбрасываются целиком, а не только из счётчика:
отдельная строка с ФИО отклонённого кандидата утверждала бы «вот кого приняли».

Объединение заводится только при lineCount > 1 — область 1×1 Excel считает
некорректной и портит файл. Значение общих колонок пишется ТОЛЬКО в первую
строку: у объединённой области Excel хранит его в верхней левой ячейке, а дубли
сломали бы SUM в итогах (посчитал бы вакансию по разу на каждый приём) — на это
есть отдельный тест.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 14:29:13 +05:00

73 lines
4.6 KiB
C#

namespace Tnsu.ApprovalRoute.Application.Reports;
/// <summary>
/// Отчёт по вакансиям за период — ровно то, что ложится на лист «Список вакансий
/// (все)» в xlsx: интервал плюс вакансии, разбитые по объектам/проектам.
/// </summary>
public sealed record VacancyReport(DateOnly From, DateOnly To, IReadOnlyList<VacancyReportGroup> Groups);
/// <summary>
/// Объект/проект и его вакансии. Имя группы становится строкой-разделителем на
/// листе (как «КЭС КУРЧАТОВ» в образце отчёта); пустое имя — вакансии, у которых
/// объект в заявке не указан, они идут первым блоком без разделителя.
/// </summary>
public sealed record VacancyReportGroup(string Name, IReadOnlyList<VacancyReportRow> Rows);
/// <summary>
/// Одна ВАКАНСИЯ (заявка на подбор). Поля этой записи — ОБЩИЕ для всех её строк
/// на листе, там они объединяются по вертикали; то, что различается от приёма к
/// приёму, лежит в Lines.
///
/// Колонки нумерации, «в работе», «срок отработки» и итоги здесь не хранятся:
/// их считает сам xlsx формулами, чтобы цифры пересчитывались, если строку в
/// файле поправят руками.
/// </summary>
public sealed record VacancyReportRow(
// «Вакансия» — должность из заявки на подбор.
string Position,
// «Подразделение» ответственного за подачу заявки.
string? Department,
// «Кол-во вакансий».
int VacanciesCount,
// «количество закрытых вакансий»: сколько заявок на приём завели по этой заявке на подбор.
int ClosedCount,
// «Дата подачи заявки».
DateOnly? SubmitDate,
// «Дата закрытия вакансии»: дата решения по последнему шагу исполнения.
DateOnly? ClosingDate,
// «Инициатор заявки».
string? Initiator,
// «Закрыта вакансия»: да / нет / частично.
string ClosedFlag,
// «Рекрутер»: исполнитель фазы исполнения заявки на подбор.
string? Recruiter,
// Строки листа по этой вакансии — ВСЕГДА не меньше одной (см. VacancyReportLine).
IReadOnlyList<VacancyReportLine> Lines);
/// <summary>
/// Одна СТРОКА ЛИСТА в пределах вакансии — то, что различается между её строками.
/// Обычно это один приём: на заявку на подбор «Разнорабочий, 4 вакансии»
/// приходится четыре заявки на приём, у каждой свой кандидат, своя дата выхода и
/// свой «взамен кого».
///
/// Именно СТРОКА, а не «приём»: у вакансии, по которой приёмов ещё нет (или все
/// отклонены), строка всё равно одна — с пустыми ФИО и датой выхода, но с
/// заполненным «Вместо сотрудника» из причины открытия вакансии. Реальное число
/// приёмов лежит в VacancyReportRow.ClosedCount, так что информация не теряется.
/// </summary>
public sealed record VacancyReportLine(
// «Ф.И.О. нанимаемого сотрудника» — кандидат из заявки на приём.
string? CandidateName,
// «Дата выхода» — дата приёма этого кандидата.
DateOnly? StartDate,
// «Вместо сотрудника Ф.И.О.».
string? ReplacedEmployee);
/// <summary>Значения колонки O. «Пауза» из образца отчёта не выводится — такого статуса в заявке нет.</summary>
public static class VacancyClosedFlags
{
public const string Yes = "да";
public const string No = "нет";
public const string Partial = "частично";
}