Files
OpenNest/OpenNest.Data/MaterialLedger/LedgerHolds.cs
T
ajandClaude Opus 5.5 f847b216f2 feat(desktop): show a server nest's MaterialLedger holds and closeout note
Server nests in Database mode get a read-only status bar label comparing their
open MaterialLedger holds with the sheets they cut, e.g. "Ledger: 2 of 4 held,
2 short". Nothing writes to the ledger and nothing waits on it.

- MaterialLedgerClient.GetHoldsAsync reads GET /api/stock/holds?openNestNestId=
  with the closed LedgerHoldsResult set; unknown status strings are kept. The
  request plumbing is shared with GetStockAsync.
- LedgerHoldSummary (OpenNest.Data) counts Proposed/Approved/Released/
  AwaitingCloseout holds against the plate quantities, skipping the trailing
  empty plate as /sheets does.
- NestHoldStatus refreshes on server open, after save, after storage or
  Options changes and on label click; it never polls. The tooltip lists holds
  in nest units. File-mode, unsaved and ledger-less nests show nothing.
- Setting Has Been Cut in Nest Info with holds still open shows a non-modal
  note linking to {ledgerUrl}/Nests/{id}. Posting is not gated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 17:40:45 -04:00

60 lines
2.5 KiB
C#

namespace OpenNest.Data.MaterialLedger;
/// <summary>The body of MaterialLedger's <c>GET /api/stock/holds?openNestNestId=</c>.</summary>
public sealed record LedgerHoldsResponse(Guid OpenNestNestId, IReadOnlyList<LedgerHold>? Holds);
/// <summary>
/// One ML <c>MaterialReservation</c> for a sheet of the nest. Sizes are inches with
/// <see cref="WidthIn"/> the short side. <see cref="Status"/> stays a string so a new ML
/// <c>ReservationStatus</c> value still deserializes.
/// </summary>
public sealed record LedgerHold(
long MaterialReservationId,
string ItemCode,
decimal WidthIn,
decimal LengthIn,
string Status,
string? LocationCode,
DateTime CreatedAtUtc);
/// <summary>ML <c>ReservationStatus</c> names.</summary>
public static class LedgerHoldStatuses
{
public const string Proposed = "Proposed";
public const string Approved = "Approved";
public const string Released = "Released";
public const string AwaitingCloseout = "AwaitingCloseout";
public const string ClosedConsumed = "ClosedConsumed";
public const string ClosedReturned = "ClosedReturned";
public const string Cancelled = "Cancelled";
public const string Exception = "Exception";
/// <summary>
/// True for holds that still keep a sheet for the nest and need closing out. Closed,
/// cancelled, exception and unknown statuses are not open.
/// </summary>
public static bool IsOpen(string? status) =>
string.Equals(status, Proposed, StringComparison.OrdinalIgnoreCase)
|| string.Equals(status, Approved, StringComparison.OrdinalIgnoreCase)
|| string.Equals(status, Released, StringComparison.OrdinalIgnoreCase)
|| string.Equals(status, AwaitingCloseout, StringComparison.OrdinalIgnoreCase);
}
/// <summary>What a ledger hold request came to. Expected failures are results, never exceptions.</summary>
public abstract record LedgerHoldsResult
{
private LedgerHoldsResult() { }
/// <summary>No ledger URL is set on this PC.</summary>
public sealed record NotConfigured : LedgerHoldsResult;
/// <summary>The ledger did not answer usefully: network error, timeout, bad status or body.</summary>
public sealed record Unreachable(string Reason) : LedgerHoldsResult;
/// <summary>The ledger rejected the API key (401/403).</summary>
public sealed record Unauthorized : LedgerHoldsResult;
/// <summary>The nest's holds in every status; empty when ML has none for it.</summary>
public sealed record Ok(IReadOnlyList<LedgerHold> Holds) : LedgerHoldsResult;
}