Reopen estimate: carry signature strokes on the estimate payload
Reopening an already-validated estimate showed an empty pad: the
strokes were stored server-side but never sent back. Embed them in the
estimate payload and load them straight into the pad, in the same wire
format PostIt captured — no separate fetch.
- Server: Signature inverse navs get STJ [JsonIgnore] (cycle-safe
under System.Text.Json, matching ApplicationUser); the four estimate
read endpoints Include(e => e.Signatures) so strokes ride the payload
- Client: EstimateDto.Signatures with EstimateSignatureDto (Strokes,
Type=Pro/0|Client/1, CoordinateMax, CapturedAtUtc)
- PostIt: SignaturePadControl.Load(int[]) replaces the buffer and
raises RedrawRequested; EstimateValidationPageViewModel.LoadExisting
Signature() picks the perspective's side off the estimate and loads
it on Attach
- Tests: server asserts GET estimate/{id} carries the strokes; PostIt
VM tests cover load + side-by-perspective
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
827fe6b41f
commit
adc27a006b
7 changed files with 177 additions and 7 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using PostIt.Controls;
|
||||
using PostIt.ViewModels;
|
||||
|
|
@ -132,6 +133,51 @@ public class EstimateValidationPageViewModelTests
|
|||
Assert.Contains("client", cli.RoleLabel, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_existing_signature_renders_loaded_strokes_in_pad()
|
||||
{
|
||||
var strokes = new[] { 1, 5000, 5000 };
|
||||
var front = new FrontOfficeApiClient(new RecordingApi(), "https://business.example/api/v1/");
|
||||
var estimate = SampleEstimate();
|
||||
estimate.Signatures = new List<EstimateSignatureDto>
|
||||
{
|
||||
new() { Type = 0, Strokes = strokes, CoordinateMax = 10_000 },
|
||||
};
|
||||
var vm = new EstimateValidationPageViewModel(estimate, EstimateListPerspective.Provider, front);
|
||||
|
||||
var pad = new SignaturePadControl();
|
||||
vm.Attach(pad);
|
||||
|
||||
var snap = pad.Snapshot();
|
||||
Assert.False(snap.IsEmpty);
|
||||
Assert.Equal(strokes, snap.Strokes);
|
||||
Assert.True(vm.HasSignature);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_existing_signature_picks_side_by_perspective()
|
||||
{
|
||||
var proStrokes = new[] { 1, 1000, 1000 };
|
||||
var cliStrokes = new[] { 1, 9000, 9000 };
|
||||
var front = new FrontOfficeApiClient(new RecordingApi(), "https://business.example/api/v1/");
|
||||
var estimate = SampleEstimate();
|
||||
estimate.Signatures = new List<EstimateSignatureDto>
|
||||
{
|
||||
new() { Type = 0, Strokes = proStrokes },
|
||||
new() { Type = 1, Strokes = cliStrokes },
|
||||
};
|
||||
|
||||
var proPad = new SignaturePadControl();
|
||||
var proVm = new EstimateValidationPageViewModel(estimate, EstimateListPerspective.Provider, front);
|
||||
proVm.Attach(proPad);
|
||||
Assert.Equal(proStrokes, proPad.Snapshot().Strokes);
|
||||
|
||||
var cliPad = new SignaturePadControl();
|
||||
var cliVm = new EstimateValidationPageViewModel(estimate, EstimateListPerspective.Client, front);
|
||||
cliVm.Attach(cliPad);
|
||||
Assert.Equal(cliStrokes, cliPad.Snapshot().Strokes);
|
||||
}
|
||||
|
||||
private sealed class RecordingApi : IYavscApiClient
|
||||
{
|
||||
public HttpClient Http { get; } = new();
|
||||
|
|
|
|||
|
|
@ -181,6 +181,38 @@ public class SignaturePadControl : TemplatedControl
|
|||
RedrawRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace the buffer with the given wire-format strokes — e.g. a
|
||||
/// signature loaded back from the server — and raise
|
||||
/// <see cref="RedrawRequested"/> so the host view repaints the ink.
|
||||
/// Loaded strokes are all sealed (there is no pending stroke).
|
||||
/// Malformed input (a non-positive length prefix, or a stroke that
|
||||
/// runs past the end of the array) is truncated at the offending
|
||||
/// point so the buffer never holds an inconsistent state.
|
||||
/// </summary>
|
||||
public void Load(int[] strokes)
|
||||
{
|
||||
if (strokes is null) throw new ArgumentNullException(nameof(strokes));
|
||||
|
||||
_strokes.Clear();
|
||||
_pendingPoints = 0;
|
||||
_capturing = false;
|
||||
|
||||
int i = 0;
|
||||
while (i < strokes.Length)
|
||||
{
|
||||
int k = strokes[i];
|
||||
if (k <= 0) break;
|
||||
if (i + 1 + 2 * k > strokes.Length) break;
|
||||
_strokes.Add(k);
|
||||
for (int p = 0; p < 2 * k; p++)
|
||||
_strokes.Add(strokes[i + 1 + p]);
|
||||
i += 1 + 2 * k;
|
||||
}
|
||||
|
||||
RedrawRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defensive copy of the current buffer wrapped in a
|
||||
/// <see cref="SignaturePadData"/>. Cheap; call only when the
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
|
|
@ -111,7 +112,10 @@ public partial class EstimateValidationPageViewModel : ViewModelBase, IActionSta
|
|||
/// <summary>
|
||||
/// Bind the page's signature pad control (the inner control of the
|
||||
/// page's <see cref="SignaturePadView"/>). Called from the view's
|
||||
/// code-behind once the view is in the visual tree.
|
||||
/// code-behind once the view is in the visual tree. Also calls
|
||||
/// <see cref="LoadExistingSignature"/> so a previously drawn
|
||||
/// signature — carried on the estimate payload — is rendered back
|
||||
/// into the pad on reopen.
|
||||
/// </summary>
|
||||
public void Attach(SignaturePadControl pad)
|
||||
{
|
||||
|
|
@ -120,6 +124,7 @@ public partial class EstimateValidationPageViewModel : ViewModelBase, IActionSta
|
|||
_pad = pad;
|
||||
_pad.RedrawRequested += OnPadRedraw;
|
||||
RefreshFromPad();
|
||||
LoadExistingSignature();
|
||||
}
|
||||
|
||||
public void Detach()
|
||||
|
|
@ -131,6 +136,28 @@ public partial class EstimateValidationPageViewModel : ViewModelBase, IActionSta
|
|||
|
||||
private void OnPadRedraw(object? sender, EventArgs e) => RefreshFromPad();
|
||||
|
||||
/// <summary>
|
||||
/// Render the signature already stored for this side of the
|
||||
/// estimate back into the pad. The strokes are carried on the
|
||||
/// estimate payload (<see cref="EstimateDto.Signatures"/>); the
|
||||
/// side is chosen by the current <see cref="_perspective"/> — the
|
||||
/// provider reopens to see their "Pro" signature, the client their
|
||||
/// "Client" signature. No network round-trip: the payload is used
|
||||
/// as-is, in the same wire format PostIt captured.
|
||||
/// </summary>
|
||||
public void LoadExistingSignature()
|
||||
{
|
||||
if (_pad is null) return;
|
||||
// SignatureType.Pro = 0, SignatureType.Client = 1 (server enum).
|
||||
int wanted = _perspective == EstimateListPerspective.Provider ? 0 : 1;
|
||||
var sig = _estimate.Signatures?.FirstOrDefault(s => s.Type == wanted);
|
||||
if (sig?.Strokes is { Length: > 0 } strokes)
|
||||
{
|
||||
_pad.Load(strokes);
|
||||
RefreshFromPad();
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshFromPad()
|
||||
{
|
||||
if (_pad is null) return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue