using System.Globalization; using System.Text.Json; namespace Tnsu.ApprovalRoute.Application.Reminders; /// /// Строка списка «… — согласование», разобранная из ответа SharePoint. /// Имена колонок — как в request-portal (FIELDS в approvalList.service.ts): /// именно он эти списки и создаёт. /// internal sealed record ApprovalRow( int Id, int TicketId, string Status, string Phase, DateTimeOffset? Date, string? ApproverEmail, string? ApproverName, int? SlaHours, bool? AskOverdueReason) { public static ApprovalRow? TryParse(JsonElement item) { // TicketId — Lookup на заявку; без него строку не с чем связать. if (!TryGetInt(item, "TicketId", out var ticketId)) return null; var approverObject = item.TryGetProperty("TansuSiteApprover", out var a) && a.ValueKind == JsonValueKind.Object ? a : (JsonElement?)null; return new ApprovalRow( TryGetInt(item, "Id", out var id) ? id : 0, ticketId, GetString(item, "ApprovalStatus") ?? string.Empty, // Пустая фаза — старые строки, заведённые до появления колонки; // трактуем как «Согласование» (так же поступает request-portal). GetString(item, "Phase") is { Length: > 0 } phase ? phase : ApprovalPhases.Approval, TryGetDate(item, "Date"), approverObject is { } ao ? GetString(ao, "EMail") : null, // Title у персонального поля — не всегда заполнен (например, у // системных/сервисных учёток) — тогда ReceiverName в DTO остаётся null. approverObject is { } an ? GetString(an, "Title") : null, TryGetInt(item, "SLAhours", out var sla) ? sla : null, TryGetBool(item, "AskOverdueReason", out var ask) ? ask : null); } private static string? GetString(JsonElement e, string name) => e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null; /// /// Целое из JSON — с поправкой на то, КАК SharePoint отдаёт Number-колонки. /// /// Одного TryGetInt32 здесь мало: у колонки типа Number значение приходит /// дробным («1.0», а не «1»), а TryGetInt32 на числе с десятичной точкой /// возвращает false — и поле молча становилось null. На фронтенде этой /// проблемы нет вовсе: в JavaScript 1.0 и 1 — одно и то же число, поэтому /// там те же данные читались верно, а на бэкенде терялись. /// /// Строку тоже принимаем: с odata=verbose (и у части полей) SharePoint /// отдаёт числа строками. /// private static bool TryGetInt(JsonElement e, string name, out int value) { value = 0; if (!e.TryGetProperty(name, out var v)) return false; if (v.ValueKind == JsonValueKind.Number) { if (v.TryGetInt32(out value)) return true; if (!v.TryGetDouble(out var d) || d is < int.MinValue or > int.MaxValue) return false; value = (int)Math.Round(d); return true; } if (v.ValueKind != JsonValueKind.String) return false; if (!double.TryParse(v.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)) return false; if (parsed is < int.MinValue or > int.MaxValue) return false; value = (int)Math.Round(parsed); return true; } private static bool TryGetBool(JsonElement e, string name, out bool value) { value = false; if (!e.TryGetProperty(name, out var v)) return false; if (v.ValueKind is JsonValueKind.True or JsonValueKind.False) { value = v.GetBoolean(); return true; } // Да/Нет-колонка тоже может прийти строкой («true»/«false») — см. TryGetInt. return v.ValueKind == JsonValueKind.String && bool.TryParse(v.GetString(), out value); } private static DateTimeOffset? TryGetDate(JsonElement e, string name) => e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String && DateTimeOffset.TryParse(v.GetString(), out var parsed) ? parsed : null; } internal static class ApprovalPhases { public const string Approval = "Согласование"; public const string Execution = "Исполнение"; } internal static class ApprovalStatuses { public const string OnApproval = "На согласовании"; public const string OnExecution = "На исполнении"; }