24d2cd7a6a
GET api/reports/vacancies?from=&to= отдаёт лист «Список вакансий (все)» по образцу ручного отчёта HR: те же шестнадцать колонок, разделители по объектам, двойная нумерация, итоги формулами. Данные — из двух форм портала: «Заявка на подбор» даёт вакансию, «Приём на работу» — её закрытие (связь по SelectionRequestId + SelectionRequestListId). Рекрутёр и дата закрытия берутся из фазы «Исполнение» парного списка «— согласование»: в самой заявке их нет. Период отбирает заявки по «Дате подачи заявки» с откатом на Created — поле заполняется руками и часто пустое. Заявки на приём периодом не ограничены: приём по вакансии из начала интервала мог оформляться уже после его конца. Колонка «Комментарии» остаётся пустой — в ручном отчёте это пометки рекрутёра, которым в заявках пока негде храниться. Генерация — ClosedXML (MIT): нужны стили, формулы и autofilter, а EPPlus с v5 под Polyform Noncommercial. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
53 lines
2.4 KiB
C#
53 lines
2.4 KiB
C#
using System.Globalization;
|
|
using System.Text.Json;
|
|
|
|
namespace Tnsu.ApprovalRoute.Application.Reports;
|
|
|
|
/// <summary>
|
|
/// Чтение полей из ответа SharePoint REST. Те же грабли, что разобраны в
|
|
/// Reminders/ApprovalRow: Number-колонка приходит дробной («1.0»), и обычный
|
|
/// TryGetInt32 на ней возвращает false, а часть полей SharePoint отдаёт строками.
|
|
/// </summary>
|
|
internal static class SharePointJson
|
|
{
|
|
public static string? String(JsonElement e, string name) =>
|
|
e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
|
|
? v.GetString() is { Length: > 0 } s ? s : null
|
|
: null;
|
|
|
|
public static int? Int(JsonElement e, string name)
|
|
{
|
|
if (!e.TryGetProperty(name, out var v)) return null;
|
|
|
|
if (v.ValueKind == JsonValueKind.Number)
|
|
{
|
|
if (v.TryGetInt32(out var i)) return i;
|
|
return v.TryGetDouble(out var d) && d is >= int.MinValue and <= int.MaxValue ? (int)Math.Round(d) : null;
|
|
}
|
|
|
|
if (v.ValueKind != JsonValueKind.String) return null;
|
|
|
|
return double.TryParse(v.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
|
|
&& parsed is >= int.MinValue and <= int.MaxValue
|
|
? (int)Math.Round(parsed)
|
|
: null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Дата БЕЗ приведения к другой зоне: колонки отчёта — dateOnly, SharePoint
|
|
/// отдаёт их полуночью, и любой сдвиг зоны увёл бы день на соседний.
|
|
/// </summary>
|
|
public static DateOnly? Date(JsonElement e, string name) =>
|
|
e.TryGetProperty(name, out var v)
|
|
&& v.ValueKind == JsonValueKind.String
|
|
&& DateTimeOffset.TryParse(v.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed)
|
|
? DateOnly.FromDateTime(parsed.DateTime)
|
|
: null;
|
|
|
|
/// <summary>Title развёрнутого персонального поля (…/Title через $expand). null — поле пустое.</summary>
|
|
public static string? PersonTitle(JsonElement e, string name) =>
|
|
e.TryGetProperty(name, out var person) && person.ValueKind == JsonValueKind.Object
|
|
? String(person, "Title")
|
|
: null;
|
|
}
|