using System.Globalization; using System.Text; using System.Text.RegularExpressions; namespace OpenNest.Data.MaterialLedger; /// How a nest's open ledger holds compare with the sheets it cuts. public enum LedgerHoldState { /// Every sheet has an open hold (possibly more holds than sheets). Held, /// Some sheets have an open hold and some do not. Short, /// No open holds, whether or not closed ones exist. None, /// The ledger did not answer, or rejected the API key. Unavailable, } /// /// A read-only summary of a server nest's MaterialLedger holds against its physical sheet /// count, for the desktop's status line and tooltip. Only open statuses /// () count as held. /// public sealed class LedgerHoldSummary { private const int MaxTooltipHolds = 20; private const double MillimetersPerInch = 25.4; private LedgerHoldSummary(LedgerHoldState state, int sheets, IReadOnlyList holds, string? reason) { State = state; Sheets = sheets; Holds = holds; Reason = reason; OpenHolds = holds.Count(hold => LedgerHoldStatuses.IsOpen(hold.Status)); ClosedHolds = holds.Count - OpenHolds; } public LedgerHoldState State { get; } /// The nest's physical sheets () when it was summarized. public int Sheets { get; } /// Every hold the ledger returned, open or not, in its order. public IReadOnlyList Holds { get; } public int OpenHolds { get; } /// Holds in any other status: released, closed, canceled, exception or unknown. public int ClosedHolds { get; } /// Sheets without an open hold. public int ShortSheets => System.Math.Max(0, Sheets - OpenHolds); /// Why the ledger is unavailable; null otherwise. public string? Reason { get; } /// The status line, e.g. "Ledger: 2 of 4 held, 2 short". public string Text => State switch { LedgerHoldState.Unavailable => "Ledger: unavailable", LedgerHoldState.None => ClosedHolds == 0 ? "Ledger: no holds" : $"Ledger: no open holds ({ClosedHolds} closed)", LedgerHoldState.Short => $"Ledger: {OpenHolds} of {Sheets} held, {ShortSheets} short", _ => OpenHolds > Sheets ? $"Ledger: {Sheets} of {Sheets} sheets held, {OpenHolds - Sheets} extra" : $"Ledger: {Sheets} of {Sheets} sheets held", }; /// /// Sheets the nest cuts: the sum of plate quantities, skipping a trailing plate with no /// material parts and no cut-offs as the server's /sheets does (it is the editor's /// new-plate slot). This is what MaterialLedger makes one hold per sheet from. /// public static int PhysicalSheetCount(Nest nest) { ArgumentNullException.ThrowIfNull(nest); var plates = nest.Plates; var count = plates.Count; if (count > 0 && plates[count - 1] is { } last && last.Parts.All(part => part.BaseDrawing.IsCutOff) && last.CutOffs.Count == 0) count--; var sheets = 0; for (var i = 0; i < count; i++) sheets += System.Math.Max(0, plates[i].Quantity); return sheets; } /// /// Summarizes against ; null when the /// ledger is not configured, so callers show nothing. /// public static LedgerHoldSummary? From(LedgerHoldsResult result, int sheets) { ArgumentNullException.ThrowIfNull(result); sheets = System.Math.Max(0, sheets); switch (result) { case LedgerHoldsResult.NotConfigured: return null; case LedgerHoldsResult.Unauthorized: return new(LedgerHoldState.Unavailable, sheets, Array.Empty(), "MaterialLedger rejected the API key. Check it in Options."); case LedgerHoldsResult.Unreachable unreachable: return new(LedgerHoldState.Unavailable, sheets, Array.Empty(), unreachable.Reason); case LedgerHoldsResult.Ok ok: var open = ok.Holds.Count(hold => LedgerHoldStatuses.IsOpen(hold.Status)); var state = open == 0 ? LedgerHoldState.None : open < sheets ? LedgerHoldState.Short : LedgerHoldState.Held; return new(state, sheets, ok.Holds, null); default: throw new ArgumentOutOfRangeException(nameof(result), result, null); } } /// The status line, then one line per hold with its size in . public string ToolTip(Units units) { var text = new StringBuilder(Text); if (Reason != null) text.AppendLine().Append(Reason); foreach (var hold in Holds.Take(MaxTooltipHolds)) text.AppendLine().Append(FormatHold(hold, units)); if (Holds.Count > MaxTooltipHolds) text.AppendLine().Append($"+{Holds.Count - MaxTooltipHolds} more"); return text.ToString(); } /// One hold, e.g. "DROP-1182 30 x 52.5 in B3 Approved". public static string FormatHold(LedgerHold hold, Units units) { ArgumentNullException.ThrowIfNull(hold); var parts = new List { hold.ItemCode, FormatSize(hold.WidthIn, hold.LengthIn, units) }; if (!string.IsNullOrWhiteSpace(hold.LocationCode)) parts.Add(hold.LocationCode.Trim()); parts.Add(DisplayStatus(hold.Status)); return string.Join(" ", parts); } /// A ledger size (inches) in nest units, e.g. "30 x 52.5 in" or "762 x 1333.5 mm". public static string FormatSize(decimal widthIn, decimal lengthIn, Units units) { var scale = units == Units.Millimeters ? MillimetersPerInch : 1.0; var format = units == Units.Millimeters ? "0.#" : "0.###"; return ((double)widthIn * scale).ToString(format, CultureInfo.CurrentCulture) + " x " + ((double)lengthIn * scale).ToString(format, CultureInfo.CurrentCulture) + " " + UnitsHelper.GetShortString(units); } /// An ML status name spaced for display ("AwaitingCloseout" → "Awaiting Closeout"). public static string DisplayStatus(string? status) => string.IsNullOrWhiteSpace(status) ? "(no status)" : Regex.Replace(status.Trim(), "(?<=[a-z])(?=[A-Z])", " "); }