using System.Globalization; using System.Text.Json; namespace Tnsu.ApprovalRoute.Application.Approvals; /// /// Строка списка «… — согласование», разобранная из ответа SharePoint. /// Имена колонок — как в request-portal (FIELDS в approvalList.service.ts): /// именно он эти списки и создаёт. /// /// Общая для напоминаний (ReminderService) и автосогласования /// (AutoApproveService) — оба читают ОДИН и тот же список теми же колонками, и /// расхождение в разборе (например, в TryGetInt, см. ниже) означало бы, что /// просроченным шаг считается в одном месте и не считается в другом. /// internal sealed record ApprovalRow( int Id, int TicketId, string Status, string Phase, DateTimeOffset? Date, string? ApproverEmail, string? ApproverName, int? SlaHours, bool? AskOverdueReason, // StepOrder и AutoApprove нужны только автосогласованию: по первому оно // двигает очередь (активирует следующий шаг), по второму — вообще отбирает // строки. Напоминаниям они не нужны, но читаются одним разбором на оба // сценария — см. RowSelect у каждого из них. double Order = 0, bool AutoApprove = false) { 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, // StepOrder — Number, и он бывает ДРОБНЫМ: делегат вклинивается между // шагами долей (см. ApproverService.delegate в request-portal), // поэтому здесь double, а не int. TryGetDouble(item, "StepOrder", out var order) ? order : 0, TryGetBool(item, "AutoApprove", out var auto) && auto); } 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; } /// Дробное число из JSON — с теми же оговорками, что и . private static bool TryGetDouble(JsonElement e, string name, out double value) { value = 0; if (!e.TryGetProperty(name, out var v)) return false; if (v.ValueKind == JsonValueKind.Number) return v.TryGetDouble(out value); return v.ValueKind == JsonValueKind.String && double.TryParse(v.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out value); } 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 = "Исполнение"; } /// /// Значения колонки ApprovalStatus — статус СТРОКИ маршрута (не заявки). /// Один в один с REQUEST_STATUS в request-portal /// (shared/approval/models/statuses.ts) — он эти значения и пишет. /// internal static class ApprovalStatuses { public const string Queued = "Ждёт очереди"; public const string OnApproval = "На согласовании"; public const string OnExecution = "На исполнении"; public const string Approved = "Согласовано"; public const string Executed = "Выполнено"; public const string Rejected = "Отклонено"; public const string Returned = "Возвращена на доработку"; } /// /// Значения колонки RequestStatus — статус ЗАЯВКИ целиком (список «… — данные»). /// Набор ДРУГОЙ, чем у строки маршрута: «Ждёт очереди»/«На исполнении» у заявки /// не бывает, а «Выполнение»/«Закрыта» не бывает у строки (см. тот же /// statuses.ts, где оба набора сведены). /// internal static class RequestStatuses { public const string OnApproval = "На согласовании"; public const string Approved = "Согласовано"; public const string Rejected = "Отклонено"; public const string Returned = "Возвращена на доработку"; public const string Execution = "Выполнение"; public const string Closed = "Закрыта"; }