| | | 1 | | namespace Services.BitrixStockServices; |
| | | 2 | | |
| | | 3 | | /// <summary>Форматирование ячеек строк отчёта BitrixStock (Excel + HTML).</summary> |
| | | 4 | | public static class BitrixStockSyncReportRowPresenter |
| | | 5 | | { |
| | 1 | 6 | | public static readonly Dictionary<string, (string BgHex, string FgHex, string Label)> IndexStyles = |
| | 1 | 7 | | new(StringComparer.OrdinalIgnoreCase) |
| | 1 | 8 | | { |
| | 1 | 9 | | ["SUPER"] = ("#EDE9FE", "#7C3AED", "Супервыгодный"), |
| | 1 | 10 | | ["GREEN"] = ("#D1E7DD", "#0A3C22", "Выгодный"), |
| | 1 | 11 | | ["YELLOW"] = ("#FFF3CD", "#664200", "Умеренный"), |
| | 1 | 12 | | ["RED"] = ("#F8D7DA", "#841F27", "Невыгодный"), |
| | 1 | 13 | | ["WITHOUT_INDEX"] = ("#E9ECEF", "#6C757D", "Нет индекса"), |
| | 1 | 14 | | ["BENEFIT"] = ("#D1E7DD", "#0A3C22", "Выгодный"), |
| | 1 | 15 | | ["AVERAGE"] = ("#FFF3CD", "#664200", "Умеренный"), |
| | 1 | 16 | | ["OVERPRICED"] = ("#F8D7DA", "#841F27", "Невыгодный"), |
| | 1 | 17 | | }; |
| | | 18 | | |
| | | 19 | | public static string GetSkipLabel(string? error) |
| | | 20 | | { |
| | 33 | 21 | | if (string.IsNullOrEmpty(error)) return "Пропущено"; |
| | 29 | 22 | | if (error.Contains("Не найден", StringComparison.OrdinalIgnoreCase) || |
| | 29 | 23 | | error.Contains("product_id", StringComparison.OrdinalIgnoreCase) || |
| | 34 | 24 | | error.Contains("склада Ozon", StringComparison.OrdinalIgnoreCase)) return "Не участвовал в обработке"; |
| | 27 | 25 | | if (error.Contains("совпадает", StringComparison.OrdinalIgnoreCase)) return "Уже совпадает"; |
| | 21 | 26 | | if (error.StartsWith("К2", StringComparison.OrdinalIgnoreCase) && |
| | 25 | 27 | | error.Contains("приоритет", StringComparison.OrdinalIgnoreCase)) return "⓪ К2 обнулено (К1)"; |
| | 17 | 28 | | if (error.StartsWith("К1", StringComparison.OrdinalIgnoreCase) && |
| | 20 | 29 | | error.Contains("приоритет", StringComparison.OrdinalIgnoreCase)) return "К1 → К2 обнулён"; |
| | 14 | 30 | | if (error.Contains("связь отключена", StringComparison.OrdinalIgnoreCase) || |
| | 19 | 31 | | error.Contains("обнулено", StringComparison.OrdinalIgnoreCase)) return "⓪₂ Обнулено (связь)"; |
| | 11 | 32 | | if (error.Contains("К1 не продается", StringComparison.OrdinalIgnoreCase)) return "К2: К1 не продается"; |
| | 10 | 33 | | if (error.Contains("архив", StringComparison.OrdinalIgnoreCase)) return "Архивный товар"; |
| | 6 | 34 | | if (error.Contains("уже 0", StringComparison.OrdinalIgnoreCase)) return "Уже 0"; |
| | 3 | 35 | | if (error.StartsWith("К2 пропущен: ", StringComparison.OrdinalIgnoreCase)) return error; |
| | 1 | 36 | | return $"Пропущено: {error}"; |
| | | 37 | | } |
| | | 38 | | |
| | 173 | 39 | | public static int? GetTargetQty(BitrixStockSyncReportWarehouseLineDto w) => w.Action switch |
| | 173 | 40 | | { |
| | 2 | 41 | | BitrixStockSyncReportActions.Clear => 0, |
| | 3 | 42 | | BitrixStockSyncReportActions.ClearDisabledLink => 0, |
| | 162 | 43 | | BitrixStockSyncReportActions.Push => w.OneCQty, |
| | 1 | 44 | | BitrixStockSyncReportActions.Decrease => w.OneCQty, |
| | 1 | 45 | | BitrixStockSyncReportActions.FbsNoWarehouse => w.OneCQty, |
| | 1 | 46 | | BitrixStockSyncReportActions.CleanupExtra => 0, |
| | 3 | 47 | | _ => null |
| | 173 | 48 | | }; |
| | | 49 | | |
| | | 50 | | public static string FormatDeltaC0(decimal? oneCPrice, decimal? prevOneCPrice) |
| | | 51 | | { |
| | 137 | 52 | | if (!oneCPrice.HasValue || !prevOneCPrice.HasValue) return ""; |
| | 7 | 53 | | var diff = (long)Math.Round(oneCPrice.Value) - (long)Math.Round(prevOneCPrice.Value); |
| | 7 | 54 | | var prevFmt = ((long)Math.Round(prevOneCPrice.Value)).ToString("N0"); |
| | 9 | 55 | | if (diff == 0) return "="; |
| | 5 | 56 | | return diff > 0 ? $"↑ +{diff:N0} (было {prevFmt})" : $"↓ {diff:N0} (было {prevFmt})"; |
| | | 57 | | } |
| | | 58 | | |
| | | 59 | | public static string DeltaC0CssClass(decimal? oneCPrice, decimal? prevOneCPrice) |
| | | 60 | | { |
| | 127 | 61 | | if (!oneCPrice.HasValue || !prevOneCPrice.HasValue) return ""; |
| | 3 | 62 | | var diff = (long)Math.Round(oneCPrice.Value) - (long)Math.Round(prevOneCPrice.Value); |
| | 4 | 63 | | if (diff == 0) return "text-muted"; |
| | 2 | 64 | | return diff > 0 ? "text-success" : "text-danger"; |
| | | 65 | | } |
| | | 66 | | |
| | | 67 | | public static string FormatOzonApplyStatus(BitrixStockSyncReportWarehouseLineDto w) |
| | | 68 | | { |
| | 219 | 69 | | if (string.Equals(w.Action, BitrixStockSyncReportActions.ClearDisabledLink, StringComparison.OrdinalIgnoreCase)) |
| | | 70 | | { |
| | 4 | 71 | | return w.OzonApplyStatus switch |
| | 4 | 72 | | { |
| | 3 | 73 | | "Success" => $"⓪₂ Связь снята | {w.OzonWarehouseName}", |
| | 1 | 74 | | "Failed" => $"✗ {w.OzonApplyError}", |
| | 0 | 75 | | _ => "⓪₂ Связь снята" |
| | 4 | 76 | | }; |
| | | 77 | | } |
| | | 78 | | |
| | | 79 | | // Строки этапа 5: OzonApplyStatus у них всегда NULL, поэтому колонка «Статус Ozon» |
| | | 80 | | // оставалась пустой и провалившееся обнуление сироты выглядело как «ничего не произошло». |
| | 215 | 81 | | if (w.IsOzonOrphan) |
| | | 82 | | { |
| | 4 | 83 | | return w.OzonOrphanStatus switch |
| | 4 | 84 | | { |
| | 2 | 85 | | "Success" => "✓ Обнулено (сирота)", |
| | 2 | 86 | | "Failed" => "✗ Не обнулено — см. примечание", |
| | 0 | 87 | | _ => "" |
| | 4 | 88 | | }; |
| | | 89 | | } |
| | | 90 | | |
| | 211 | 91 | | return w.OzonApplyStatus switch |
| | 211 | 92 | | { |
| | 155 | 93 | | "Success" when !string.IsNullOrEmpty(w.OzonApplyError) => GetSkipLabel(w.OzonApplyError), |
| | 155 | 94 | | "Success" => "✓ Применено", |
| | 2 | 95 | | "Failed" when (w.OzonApplyError ?? "").Contains("Не продается", StringComparison.OrdinalIgnoreCase) |
| | 1 | 96 | | => "⊘ Не продается", |
| | 1 | 97 | | "Failed" => $"✗ Ошибка: {w.OzonApplyError}", |
| | 1 | 98 | | "Pending" => "Ожидает", |
| | 1 | 99 | | "Skipped" => GetSkipLabel(w.OzonApplyError), |
| | 52 | 100 | | _ => w.Action is BitrixStockSyncReportActions.NotFound |
| | 52 | 101 | | or BitrixStockSyncReportActions.FbsNoWarehouse |
| | 52 | 102 | | or BitrixStockSyncReportActions.NotForSale |
| | 52 | 103 | | ? "Не участвовал в обработке" |
| | 52 | 104 | | : "" |
| | 211 | 105 | | }; |
| | | 106 | | } |
| | | 107 | | |
| | | 108 | | public static string FormatOzonCleanupStatus(BitrixStockSyncReportWarehouseLineDto w) |
| | | 109 | | { |
| | 101 | 110 | | return w.OzonCleanupStatus switch |
| | 101 | 111 | | { |
| | 0 | 112 | | "Success" when (w.OzonCleanupError ?? "").Contains("Сохранён", StringComparison.OrdinalIgnoreCase) => "✓ Сох |
| | 0 | 113 | | "Success" when (w.OzonCleanupError ?? "").Contains("⓪₃", StringComparison.OrdinalIgnoreCase) => "⓪₃ Этап 3: |
| | 0 | 114 | | "Success" when w.Action == BitrixStockSyncReportActions.CleanupExtra => "⓪₃ Этап 3: обнулён", |
| | 0 | 115 | | "Success" when (w.OzonCleanupError ?? "").Contains("Обнулён", StringComparison.OrdinalIgnoreCase) => "⓪ Обну |
| | 0 | 116 | | "Success" => "✓", |
| | 0 | 117 | | "Failed" => $"✗ {w.OzonCleanupError}", |
| | 0 | 118 | | "Skipped" => w.OzonCleanupError ?? "пропуск", |
| | 101 | 119 | | _ => "" |
| | 101 | 120 | | }; |
| | | 121 | | } |
| | | 122 | | |
| | | 123 | | public static string FormatDeltaItog(BitrixStockSyncReportWarehouseLineDto w, out string cssClass) |
| | | 124 | | { |
| | 131 | 125 | | cssClass = "text-muted"; |
| | 131 | 126 | | if (w.OzonQtyAfter.HasValue) |
| | | 127 | | { |
| | 9 | 128 | | var actualDelta = w.OzonQtyAfter.Value - w.OzonQty; |
| | 9 | 129 | | cssClass = actualDelta > 0 ? "text-success fw-bold" : actualDelta < 0 ? "text-danger fw-bold" : "text-muted |
| | 9 | 130 | | return actualDelta > 0 ? $"+{actualDelta}" : actualDelta.ToString(); |
| | | 131 | | } |
| | 122 | 132 | | return w.Delta > 0 ? $"+{w.Delta}" : w.Delta == 0 ? "0" : w.Delta.ToString(); |
| | | 133 | | } |
| | | 134 | | |
| | | 135 | | public static string FormatPriceIndex(string? colorIndex) |
| | | 136 | | { |
| | 3 | 137 | | if (string.IsNullOrEmpty(colorIndex) || !IndexStyles.TryGetValue(colorIndex, out var s)) |
| | 0 | 138 | | return ""; |
| | 3 | 139 | | return s.Label; |
| | | 140 | | } |
| | | 141 | | |
| | | 142 | | public static string PriceIndexCssClass(string? colorIndex) |
| | | 143 | | { |
| | 0 | 144 | | if (string.IsNullOrEmpty(colorIndex)) return ""; |
| | 0 | 145 | | var key = colorIndex.ToUpperInvariant(); |
| | 0 | 146 | | return IndexStyles.ContainsKey(key) ? $"bsx-price-index-{key}" : ""; |
| | | 147 | | } |
| | | 148 | | |
| | | 149 | | public static string FormatPushedNew(OzonPriceInfoDto? pi) |
| | | 150 | | { |
| | 3 | 151 | | if (pi?.PricePushedNew is { } pn) |
| | 1 | 152 | | return ((long)Math.Round(pn)).ToString("N0"); |
| | 2 | 153 | | if (pi?.PricePushStatus == "Unchanged") |
| | | 154 | | { |
| | 1 | 155 | | var cur = pi.SellingPrice ?? pi.TruePrice; |
| | 1 | 156 | | return cur.HasValue ? $"без изменений · {(long)Math.Round(cur.Value):N0}" : "без изменений"; |
| | | 157 | | } |
| | 1 | 158 | | if (pi?.PricePushStatus == "Error") |
| | | 159 | | { |
| | 1 | 160 | | var cur = pi.SellingPrice ?? pi.TruePrice; |
| | 1 | 161 | | var err = string.IsNullOrEmpty(pi.PricePushError) ? "Ошибка" : $"Ошибка: {pi.PricePushError}"; |
| | 1 | 162 | | return cur.HasValue ? $"{err} · {(long)Math.Round(cur.Value):N0}" : err; |
| | | 163 | | } |
| | 0 | 164 | | return ""; |
| | | 165 | | } |
| | | 166 | | |
| | | 167 | | public static string PushedNewCssClass(OzonPriceInfoDto? pi) |
| | | 168 | | { |
| | 8 | 169 | | if (pi?.PricePushedNew.HasValue == true) return "text-success fw-bold"; |
| | 5 | 170 | | if (pi?.PricePushStatus == "Error") return "text-danger"; |
| | 4 | 171 | | if (pi?.PricePushStatus == "Unchanged") return "text-muted fst-italic"; |
| | 2 | 172 | | return ""; |
| | | 173 | | } |
| | | 174 | | } |