Estimate validation polish + PDF/TeX generation in Yavsc.Api
- List removal: GetOngoingEstimatesAsProvider filters on ProviderValidationDate so a provider's signed estimate leaves their list; PostIt refreshes the list on navigation pop-back. - Title renames: "Devis en attente de signature fournisseur/client" on both the provider and client sides (HomePage, VM, tests). - Move the estimate/bill TeX+PDF generation flow from Yavsc.Org into Yavsc.Api (TeXHelpers, PdfGenerationViewModel, BillViewComponent, Bill/Estimate templates, _ViewImports). Program.cs wires AddControllersWithViews + SiteSettings; FrontOfficeApiController injects IRazorViewEngine (cast to IViewEngine) with optional deps and exposes estimate.tex/estimate.pdf endpoints keyed by query id. - PDF generation drives through the Estimate_pdf view (side-effect render) and compiles via the system lualatex, fed TeX on stdin (no intermediate .tex), PDF to the bills dir, aux artifacts cleaned so only the .pdf persists. Resolve the bills dir to an absolute path so lualatex's -output-directory and working directory agree regardless of the host cwd (fixes "I can't write on file estimate-1.log"). - PostIt: DownloadAsync on the client + GetEstimateTex/PdfAsync; Télécharger PDF/TeX buttons on the validation and command-detail pages via a StorageProvider save helper. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
70a123b840
commit
97d0584292
41 changed files with 1036 additions and 290 deletions
|
|
@ -41,32 +41,34 @@ public sealed class EstimateDto
|
|||
public DateTime ClientValidationDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Signatures already captured against this estimate (Pro and/or
|
||||
/// Client), as returned with the estimate payload. Each carries the
|
||||
/// PostIt wire-format <see cref="EstimateSignatureDto.Strokes"/>
|
||||
/// so the pad can repaint a previously drawn signature on reopen
|
||||
/// without a second round-trip. Null/empty when nobody has signed.
|
||||
/// The provider's signature on this estimate, or null when the
|
||||
/// provider has not signed. Carries the PostIt wire-format
|
||||
/// <see cref="EstimateSignatureDto.Strokes"/> so the pad can
|
||||
/// repaint a previously drawn signature on reopen without a second
|
||||
/// round-trip.
|
||||
/// </summary>
|
||||
public List<EstimateSignatureDto>? Signatures { get; set; }
|
||||
public EstimateSignatureDto? SignaturePro { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The client's signature on this estimate, or null when the
|
||||
/// client has not signed.
|
||||
/// </summary>
|
||||
public EstimateSignatureDto? SignatureClient { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One stored signature on an estimate, mirroring the JSON shape of
|
||||
/// the server-side <c>Yavsc.Models.Billing.Signature</c> entity. The
|
||||
/// <see cref="Strokes"/> are the same PostIt wire-format payload that
|
||||
/// was captured and are loaded back into the pad unchanged.
|
||||
/// was captured and are loaded back into the pad unchanged. The side
|
||||
/// (Pro/Client) is implicit in which property of
|
||||
/// <see cref="EstimateDto"/> carries this DTO, so no <c>Type</c>
|
||||
/// field is needed.
|
||||
/// </summary>
|
||||
public sealed class EstimateSignatureDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long EstimateId { get; set; }
|
||||
/// <summary>
|
||||
/// Who signed, as the integer value of the server
|
||||
/// <c>SignatureType</c> enum: <c>0</c> = Pro (provider),
|
||||
/// <c>1</c> = Client. Matched by value on the client so PostIt
|
||||
/// need not reference the server enum.
|
||||
/// </summary>
|
||||
public int Type { get; set; }
|
||||
public int CoordinateMax { get; set; }
|
||||
public DateTime CapturedAtUtc { get; set; }
|
||||
public int[]? Strokes { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using Yavsc.Models.Billing;
|
||||
|
||||
namespace Yavsc.Api.Client;
|
||||
|
||||
|
|
@ -17,6 +18,17 @@ public sealed class QueryAcceptanceRequestDto
|
|||
public int CoordinateMax { get; set; } = 10_000;
|
||||
|
||||
public DateTime? CapturedAtUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Which signature is being submitted:
|
||||
/// <see cref="SignatureType.Pro"/> (the provider) or
|
||||
/// <see cref="SignatureType.Client"/>. The PostIt client sets this
|
||||
/// from the perspective the page was opened in (provider signs Pro,
|
||||
/// client signs Client), so the server stores the signature under
|
||||
/// the right author even when one user is both parties. Null lets
|
||||
/// the server infer the role from the caller's identity.
|
||||
/// </summary>
|
||||
public SignatureType? SignatureType { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -95,6 +95,33 @@ public sealed class FrontOfficeApiClient
|
|||
return response ?? new QueryAcceptanceResponseDto { QueryId = queryId };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download the estimate linked to query <paramref name="queryId"/>
|
||||
/// as a LaTeX (<c>.tex</c>) source document
|
||||
/// (<c>GET api/v1/front/query/{queryId}/estimate.tex</c>). Returns
|
||||
/// the raw TeX bytes (UTF-8 text).
|
||||
/// </summary>
|
||||
public Task<byte[]> GetEstimateTexAsync(long queryId, CancellationToken ct = default)
|
||||
{
|
||||
if (queryId <= 0) throw new ArgumentOutOfRangeException(nameof(queryId));
|
||||
var path = $"{PathPrefix}/query/{queryId}/estimate.tex";
|
||||
return _api.DownloadAsync(HttpMethod.Get, Absolute(path), ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download the estimate linked to query <paramref name="queryId"/>
|
||||
/// as a compiled PDF document
|
||||
/// (<c>GET api/v1/front/query/{queryId}/estimate.pdf</c>). Returns
|
||||
/// the raw PDF bytes. Requires <c>texi2pdf</c> on the server host;
|
||||
/// a non-2xx response throws <see cref="HttpRequestException"/>.
|
||||
/// </summary>
|
||||
public Task<byte[]> GetEstimatePdfAsync(long queryId, CancellationToken ct = default)
|
||||
{
|
||||
if (queryId <= 0) throw new ArgumentOutOfRangeException(nameof(queryId));
|
||||
var path = $"{PathPrefix}/query/{queryId}/estimate.pdf";
|
||||
return _api.DownloadAsync(HttpMethod.Get, Absolute(path), ct);
|
||||
}
|
||||
|
||||
private string Absolute(string relativePath) => new Uri(ResolveBusinessBaseAddress(), relativePath).ToString();
|
||||
|
||||
private Uri ResolveBusinessBaseAddress()
|
||||
|
|
|
|||
|
|
@ -73,4 +73,23 @@ public interface IYavscApiClient : IAsyncDisposable
|
|||
string path,
|
||||
Func<HttpContent> contentFactory,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Download a binary resource (PDF, TeX, image, …). Unlike
|
||||
/// <see cref="CallAsync{T}(HttpMethod,string,object?,CancellationToken)"/>,
|
||||
/// which JSON-decodes the response, this returns the raw response
|
||||
/// bytes. The same Bearer-token attach + 401-refresh-retry-once
|
||||
/// contract applies.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A default interface implementation is provided so that test
|
||||
/// fakes that do not exercise binary download still compile without
|
||||
/// declaring the member; the real <see cref="YavscApiClient"/>
|
||||
/// overrides it with the authenticated transport.
|
||||
/// </remarks>
|
||||
Task<byte[]> DownloadAsync(
|
||||
HttpMethod method,
|
||||
string path,
|
||||
CancellationToken ct = default)
|
||||
=> Task.FromResult(Array.Empty<byte>());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue