Compare commits
13 Commits
feat/migr-
...
36025fad7b
| Author | SHA1 | Date | |
|---|---|---|---|
| 36025fad7b | |||
| 0606efabf9 | |||
| 2cb1e266f7 | |||
| 35c1374b9c | |||
| feaea1626b | |||
| babd96200a | |||
| f9cfcf1f19 | |||
| ffb9e07466 | |||
| 5384a08905 | |||
| dda5ff28e1 | |||
| 043349a47f | |||
| c36f7ccf98 | |||
| cf3e85bcd7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -369,3 +369,4 @@ FodyWeavers.xsd
|
|||||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
||||||
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||||
|
/ignore/EnvelopeGenerator.Form
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||||
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
||||||
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\API\net8\$(Version)\EnvelopeGenerator.API.zip</DesktopBuildPackageLocation>
|
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\PreRelease\API\net8\$(Version)\EnvelopeGenerator.API.zip</DesktopBuildPackageLocation>
|
||||||
<PackageAsSingleFile>true</PackageAsSingleFile>
|
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||||
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
||||||
<_TargetId>IISWebDeployPackage</_TargetId>
|
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
Imports System.IO
|
Imports System.IO
|
||||||
Imports DigitalData.Modules.Database
|
Imports DigitalData.Modules.Database
|
||||||
Imports DigitalData.Modules.Logging
|
Imports DigitalData.Modules.Logging
|
||||||
|
Imports EnvelopeGenerator.CommonServices
|
||||||
Imports EnvelopeGenerator.CommonServices.Jobs
|
Imports EnvelopeGenerator.CommonServices.Jobs
|
||||||
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument
|
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument
|
||||||
Imports GdPicture14
|
Imports GdPicture14
|
||||||
@@ -145,25 +146,23 @@ Public Class frmFinalizePDF
|
|||||||
log.AppendLine($"Document bytes: {oBuffer.Length}")
|
log.AppendLine($"Document bytes: {oBuffer.Length}")
|
||||||
|
|
||||||
' 3. Check what BurnAnnotsToPDF will do internally
|
' 3. Check what BurnAnnotsToPDF will do internally
|
||||||
|
Dim oEnvelopeEntity As Envelope = Nothing
|
||||||
Using scope = Factory.Shared.ScopeFactory.CreateScope()
|
Using scope = Factory.Shared.ScopeFactory.CreateScope()
|
||||||
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
|
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
|
||||||
Dim envelope = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
|
oEnvelopeEntity = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
|
||||||
|
|
||||||
If envelope Is Nothing Then
|
If oEnvelopeEntity Is Nothing Then
|
||||||
log.AppendLine("ERROR: Envelope not found in EF Core!")
|
log.AppendLine("ERROR: Envelope not found in EF Core!")
|
||||||
txtResult.Text = log.ToString()
|
txtResult.Text = log.ToString()
|
||||||
Return
|
Return
|
||||||
End If
|
End If
|
||||||
|
|
||||||
log.AppendLine($"Envelope found: Id={envelope.Id}, EnvelopeTypeId={envelope.EnvelopeTypeId}")
|
log.AppendLine($"Envelope found: Id={oEnvelopeEntity.Id}, EnvelopeTypeId={oEnvelopeEntity.EnvelopeTypeId}")
|
||||||
log.AppendLine($"ReadOnly (IsReadAndConfirm): {envelope.ReadOnly}")
|
log.AppendLine($"ReadOnly (IsReadAndConfirm): {oEnvelopeEntity.ReadOnly}")
|
||||||
|
|
||||||
If envelope.ReadOnly Then
|
|
||||||
log.AppendLine(">>> EARLY RETURN: ReadOnly=True, original PDF returned without burning")
|
|
||||||
txtResult.Text = log.ToString()
|
|
||||||
Return
|
|
||||||
End If
|
|
||||||
|
|
||||||
|
If oEnvelopeEntity.ReadOnly Then
|
||||||
|
log.AppendLine(">>> ReadOnly=True: original PDF will be used without burning (annotations skipped)")
|
||||||
|
Else
|
||||||
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
|
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
|
||||||
Dim elements = sigRepo _
|
Dim elements = sigRepo _
|
||||||
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
||||||
@@ -186,23 +185,52 @@ Public Class frmFinalizePDF
|
|||||||
Else
|
Else
|
||||||
log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)")
|
log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)")
|
||||||
End If
|
End If
|
||||||
|
End If
|
||||||
End Using
|
End Using
|
||||||
|
|
||||||
' 4. Actually call BurnAnnotsToPDF (same as Service)
|
' 4. Burn annotations (ReadOnly envelopes return original PDF unchanged)
|
||||||
log.AppendLine("")
|
log.AppendLine("")
|
||||||
log.AppendLine("Calling BurnAnnotsToPDF...")
|
log.AppendLine("Calling BurnAnnotsToPDF...")
|
||||||
Dim oNewBuffer = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
|
Dim oBurnedBuffer As Byte() = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
|
||||||
log.AppendLine($"Result bytes: {oNewBuffer.Length}")
|
log.AppendLine($"Burned bytes: {oBurnedBuffer.Length}")
|
||||||
log.AppendLine($"Same as input: {oBuffer.Length = oNewBuffer.Length AndAlso oBuffer.SequenceEqual(oNewBuffer)}")
|
log.AppendLine($"Unchanged (ReadOnly): {oBuffer.Length = oBurnedBuffer.Length AndAlso oBuffer.SequenceEqual(oBurnedBuffer)}")
|
||||||
|
|
||||||
' 5. Write output
|
' 5. Build State and load Envelope domain model for ReportCreator
|
||||||
|
log.AppendLine("")
|
||||||
|
log.AppendLine("Loading envelope domain model for report...")
|
||||||
|
Dim oState As New State() With {
|
||||||
|
.Database = Database,
|
||||||
|
.LogConfig = LogConfig
|
||||||
|
}
|
||||||
|
Dim oEnvelopeModel As New EnvelopeModel(oState)
|
||||||
|
Dim oEnvelope = oEnvelopeModel.GetById(envelopeId)
|
||||||
|
If oEnvelope Is Nothing Then
|
||||||
|
log.AppendLine("ERROR: Envelope not found via EnvelopeModel!")
|
||||||
|
txtResult.Text = log.ToString()
|
||||||
|
Return
|
||||||
|
End If
|
||||||
|
log.AppendLine($"Envelope loaded: {oEnvelope.Id}")
|
||||||
|
|
||||||
|
' 6. Create signing certificate report
|
||||||
|
log.AppendLine("Creating signing certificate report...")
|
||||||
|
Dim oReportCreator As New ReportCreator(LogConfig, oState)
|
||||||
|
Dim oReportBuffer As Byte() = oReportCreator.CreateReport(oEnvelope)
|
||||||
|
log.AppendLine($"Report bytes: {oReportBuffer.Length}")
|
||||||
|
|
||||||
|
' 7. Merge burned document + report (last page)
|
||||||
|
log.AppendLine("Merging documents...")
|
||||||
|
Dim oMerger As New PDFMerger(LogConfig, pGDPictureLicenseKey)
|
||||||
|
Dim oMergedBuffer As Byte() = oMerger.MergeDocuments(oBurnedBuffer, oReportBuffer)
|
||||||
|
log.AppendLine($"Merged bytes: {oMergedBuffer.Length}")
|
||||||
|
|
||||||
|
' 8. Save and open
|
||||||
Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
|
Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
|
||||||
Dim oNewPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.burned.pdf")
|
Dim oMergedPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.merged.pdf")
|
||||||
File.WriteAllBytes(oNewPath, oNewBuffer)
|
File.WriteAllBytes(oMergedPath, oMergedBuffer)
|
||||||
log.AppendLine($"Output: {oNewPath}")
|
log.AppendLine($"Output: {oMergedPath}")
|
||||||
|
|
||||||
txtResult.Text = log.ToString()
|
txtResult.Text = log.ToString()
|
||||||
Process.Start(oNewPath)
|
Process.Start(oMergedPath)
|
||||||
|
|
||||||
Catch ex As Exception
|
Catch ex As Exception
|
||||||
txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}"
|
txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}"
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Dim y = frameY + yOffsetofFF
|
Dim y = frameY + yOffsetofFF
|
||||||
|
|
||||||
If annot.Type = AnnotationType.FormField Then
|
If annot.Type = AnnotationType.FormField Then
|
||||||
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value)
|
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value, annot.Name)
|
||||||
ElseIf annot.Type = AnnotationType.Image Then
|
ElseIf annot.Type = AnnotationType.Image Then
|
||||||
AddImageAnnotation(
|
AddImageAnnotation(
|
||||||
annot.X / inchFactor,
|
annot.X / inchFactor,
|
||||||
@@ -269,11 +269,14 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Next
|
Next
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String)
|
Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String, Optional egName As String = "")
|
||||||
Manager.SelectPage(page)
|
Manager.SelectPage(page)
|
||||||
|
|
||||||
|
' Truncate if needed
|
||||||
|
Dim displayValue As String = TruncateText(value, egName)
|
||||||
|
|
||||||
' Add the text annotation
|
' Add the text annotation
|
||||||
Dim ant = Manager.AddTextAnnot(x, y, width, height, value)
|
Dim ant = Manager.AddTextAnnot(x, y, width, height, displayValue)
|
||||||
|
|
||||||
' Set the font properties
|
' Set the font properties
|
||||||
ant.FontName = _pdfBurnerParams.FontName
|
ant.FontName = _pdfBurnerParams.FontName
|
||||||
@@ -294,8 +297,9 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Dim oHeight = oBounds.Item(3)
|
Dim oHeight = oBounds.Item(3)
|
||||||
|
|
||||||
Manager.SelectPage(pAnnotation.pageIndex + 1)
|
Manager.SelectPage(pAnnotation.pageIndex + 1)
|
||||||
' Add the text annotation
|
' Add the text annotation (truncate if needed)
|
||||||
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, formFieldValue.value)
|
Dim displayValue As String = TruncateText(formFieldValue.value, pAnnotation.egName)
|
||||||
|
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, displayValue)
|
||||||
|
|
||||||
' Set the font properties
|
' Set the font properties
|
||||||
ant.FontName = _pdfBurnerParams.FontName
|
ant.FontName = _pdfBurnerParams.FontName
|
||||||
@@ -306,6 +310,48 @@ Namespace Jobs.FinalizeDocument
|
|||||||
#End Region
|
#End Region
|
||||||
|
|
||||||
#Region "Helpers"
|
#Region "Helpers"
|
||||||
|
''' <summary>
|
||||||
|
''' Truncates a field value so its rendered width fits within the configured max width (inches).
|
||||||
|
''' Uses Graphics.MeasureString with the configured font for pixel-accurate measurement.
|
||||||
|
''' Appends TruncationSuffix when the value is cut.
|
||||||
|
''' </summary>
|
||||||
|
Private Function TruncateText(value As String, fieldEgName As String) As String
|
||||||
|
If String.IsNullOrEmpty(value) Then Return value
|
||||||
|
If _pdfBurnerParams.TextMaxWidths Is Nothing Then Return value
|
||||||
|
|
||||||
|
Dim maxWidthInches As Single = 0
|
||||||
|
If Not _pdfBurnerParams.TextMaxWidths.TryGetValue(fieldEgName, maxWidthInches) Then Return value
|
||||||
|
If maxWidthInches <= 0 Then Return value
|
||||||
|
|
||||||
|
Dim suffix As String = If(_pdfBurnerParams.TruncationSuffix, "")
|
||||||
|
|
||||||
|
Using bmp As New Bitmap(1, 1)
|
||||||
|
Using g As Graphics = Graphics.FromImage(bmp)
|
||||||
|
Dim font As New Font(_pdfBurnerParams.FontName, _pdfBurnerParams.FontSize, _pdfBurnerParams.FontStyle, GraphicsUnit.Point)
|
||||||
|
Dim maxWidthPx As Single = maxWidthInches * g.DpiX
|
||||||
|
|
||||||
|
' Already fits — no truncation needed
|
||||||
|
If g.MeasureString(value, font).Width <= maxWidthPx Then Return value
|
||||||
|
|
||||||
|
' Binary search for the longest prefix that fits (with suffix)
|
||||||
|
Dim lo As Integer = 0
|
||||||
|
Dim hi As Integer = value.Length
|
||||||
|
While lo < hi
|
||||||
|
Dim mid As Integer = (lo + hi + 1) \ 2
|
||||||
|
Dim candidate As String = value.Substring(0, mid) & suffix
|
||||||
|
If g.MeasureString(candidate, font).Width <= maxWidthPx Then
|
||||||
|
lo = mid
|
||||||
|
Else
|
||||||
|
hi = mid - 1
|
||||||
|
End If
|
||||||
|
End While
|
||||||
|
|
||||||
|
Return value.Substring(0, lo) & suffix
|
||||||
|
End Using
|
||||||
|
End Using
|
||||||
|
End Function
|
||||||
|
|
||||||
|
|
||||||
Private Function ToPointF(pPoints As List(Of Single)) As PointF
|
Private Function ToPointF(pPoints As List(Of Single)) As PointF
|
||||||
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
|
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
|
||||||
Return New PointF(oPoints.Item(0), oPoints.Item(1))
|
Return New PointF(oPoints.Item(0), oPoints.Item(1))
|
||||||
|
|||||||
@@ -12,5 +12,24 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Public Property FontSize As Integer = 8
|
Public Property FontSize As Integer = 8
|
||||||
|
|
||||||
Public Property FontStyle As FontStyle = FontStyle.Italic
|
Public Property FontStyle As FontStyle = FontStyle.Italic
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' The suffix appended to a truncated field value. Defaults to "..".
|
||||||
|
''' Can be changed to any string (e.g. "…", "...", "").
|
||||||
|
''' </summary>
|
||||||
|
Public Property TruncationSuffix As String = ".."
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Per-field maximum render width in inches.
|
||||||
|
''' Text is measured using the configured Font/FontSize via Graphics.MeasureString (pixel-accurate).
|
||||||
|
''' If the rendered width exceeds the limit, the text is cut and TruncationSuffix is appended.
|
||||||
|
''' Keys must match egName values used in PDFBurner (e.g. "name", "position", "city", "date").
|
||||||
|
''' Set a value to 0 to disable truncation for that field.
|
||||||
|
''' </summary>
|
||||||
|
Public Property TextMaxWidths As New Dictionary(Of String, Single) From {
|
||||||
|
{"position", 1.41F},
|
||||||
|
{"city", 1.41F},
|
||||||
|
{"date", 2.0F}
|
||||||
|
}
|
||||||
End Class
|
End Class
|
||||||
End Namespace
|
End Namespace
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
Imports EnvelopeGenerator.Domain.Constants
|
Imports EnvelopeGenerator.Domain.Constants
|
||||||
Imports EnvelopeGenerator.Domain.Entities
|
Imports EnvelopeGenerator.Domain.Entities
|
||||||
|
Imports EnvelopeGenerator.Domain.Interfaces
|
||||||
|
|
||||||
Public Class ReportItem
|
Public Class ReportItem
|
||||||
|
|
||||||
@@ -11,8 +12,15 @@ Public Class ReportItem
|
|||||||
Public Property ItemStatus As EnvelopeStatus
|
Public Property ItemStatus As EnvelopeStatus
|
||||||
Public ReadOnly Property ItemStatusTranslated As String
|
Public ReadOnly Property ItemStatusTranslated As String
|
||||||
Get
|
Get
|
||||||
Dim oStatus = ItemStatus.ToString()
|
Dim oStatusKey = ItemStatus.ToString()
|
||||||
Return My.Resources.Model.ResourceManager.GetString(oStatus)
|
|
||||||
|
If ItemStatus = EnvelopeStatus.DocumentSigned AndAlso
|
||||||
|
Envelope IsNot Nothing AndAlso
|
||||||
|
Envelope.IsReadAndConfirm() Then
|
||||||
|
oStatusKey = "DocumentSignedRaC"
|
||||||
|
End If
|
||||||
|
|
||||||
|
Return My.Resources.Model.ResourceManager.GetString(oStatusKey)
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,9 @@
|
|||||||
<data name="DocumentSigned" xml:space="preserve">
|
<data name="DocumentSigned" xml:space="preserve">
|
||||||
<value>Document signed</value>
|
<value>Document signed</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||||
|
<value>Read and confirmed</value>
|
||||||
|
</data>
|
||||||
<data name="Draft" xml:space="preserve">
|
<data name="Draft" xml:space="preserve">
|
||||||
<value>Draft</value>
|
<value>Draft</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -168,6 +168,9 @@
|
|||||||
<data name="DocumentSigned" xml:space="preserve">
|
<data name="DocumentSigned" xml:space="preserve">
|
||||||
<value>Dokument unterzeichnet</value>
|
<value>Dokument unterzeichnet</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||||
|
<value>Gelesen und bestätigt</value>
|
||||||
|
</data>
|
||||||
<data name="Draft" xml:space="preserve">
|
<data name="Draft" xml:space="preserve">
|
||||||
<value>Entwurf</value>
|
<value>Entwurf</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -213,6 +213,15 @@ namespace My.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Gelesen und bestätigt.
|
||||||
|
/// </summary>
|
||||||
|
public static string DocumentSignedRaC {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DocumentSignedRaC", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Entwurf.
|
/// Looks up a localized string similar to Entwurf.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -168,6 +168,9 @@
|
|||||||
<data name="DocumentSigned" xml:space="preserve">
|
<data name="DocumentSigned" xml:space="preserve">
|
||||||
<value>Dokument unterzeichnet</value>
|
<value>Dokument unterzeichnet</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||||
|
<value>Gelesen und bestätigt</value>
|
||||||
|
</data>
|
||||||
<data name="Draft" xml:space="preserve">
|
<data name="Draft" xml:space="preserve">
|
||||||
<value>Entwurf</value>
|
<value>Entwurf</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||||
<ProjectGuid>fb2d306b-1042-4a70-31ed-f991a1599371</ProjectGuid>
|
<ProjectGuid>fb2d306b-1042-4a70-31ed-f991a1599371</ProjectGuid>
|
||||||
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\ReceiverUI\net8\$(Version)\EnvelopeGenerator.ReceiverUI.zip</DesktopBuildPackageLocation>
|
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\PreRelease\ReceiverUI\net8\$(Version)\EnvelopeGenerator.ReceiverUI.zip</DesktopBuildPackageLocation>
|
||||||
<PackageAsSingleFile>true</PackageAsSingleFile>
|
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||||
<DeployIisAppPath>EnvelopeGenerator.ReceiverUI</DeployIisAppPath>
|
<DeployIisAppPath>EnvelopeGenerator.ReceiverUI</DeployIisAppPath>
|
||||||
<_TargetId>IISWebDeployPackage</_TargetId>
|
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||||
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
||||||
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\API\net8\$(Version)\EnvelopeGenerator.Server.zip</DesktopBuildPackageLocation>
|
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\PreRelease\API\net8\$(Version)\EnvelopeGenerator.Server.zip</DesktopBuildPackageLocation>
|
||||||
<PackageAsSingleFile>true</PackageAsSingleFile>
|
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||||
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
||||||
<_TargetId>IISWebDeployPackage</_TargetId>
|
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ dotnet publish EnvelopeGenerator.Server\EnvelopeGenerator.Server\EnvelopeGenerat
|
|||||||
| `--runtime win-x64` | Zielplattform: Windows 64-Bit |
|
| `--runtime win-x64` | Zielplattform: Windows 64-Bit |
|
||||||
| `-o .\publish-output` | Ausgabeverzeichnis |
|
| `-o .\publish-output` | Ausgabeverzeichnis |
|
||||||
|
|
||||||
### Doğrulama nach dem Publish
|
### Überprüfung nach dem Publish
|
||||||
|
|
||||||
Nach erfolgreichem Publish folgende Dateien im Ausgabeverzeichnis prüfen:
|
Nach erfolgreichem Publish folgende Dateien im Ausgabeverzeichnis prüfen:
|
||||||
|
|
||||||
|
|||||||
@@ -23,19 +23,20 @@
|
|||||||
"Annotations": [
|
"Annotations": [
|
||||||
{
|
{
|
||||||
"Name": "Signature",
|
"Name": "Signature",
|
||||||
"MarginTop": 0
|
"MarginTop": 0,
|
||||||
|
"MarginLeft": 0.20
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "PositionLabel",
|
"Name": "PositionLabel",
|
||||||
"VerBoundAnnotName": "Signature",
|
"VerBoundAnnotName": "Signature",
|
||||||
"WidthRatio": 1.2,
|
"WidthRatio": 1.2,
|
||||||
"HeightRatio": 0.5,
|
"HeightRatio": 0.5,
|
||||||
"MarginTopRatio": 0.22
|
"MarginTopRatio": 0.30
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "Position",
|
"Name": "Position",
|
||||||
"VerBoundAnnotName": "PositionLabel",
|
"VerBoundAnnotName": "PositionLabel",
|
||||||
"WidthRatio": 1.2,
|
"WidthRatio": 1.4,
|
||||||
"HeightRatio": 0.5,
|
"HeightRatio": 0.5,
|
||||||
"MarginTopRatio": -0.05
|
"MarginTopRatio": -0.05
|
||||||
},
|
},
|
||||||
@@ -49,7 +50,7 @@
|
|||||||
{
|
{
|
||||||
"Name": "City",
|
"Name": "City",
|
||||||
"VerBoundAnnotName": "CityLabel",
|
"VerBoundAnnotName": "CityLabel",
|
||||||
"WidthRatio": 1.2,
|
"WidthRatio": 1.4,
|
||||||
"HeightRatio": 0.5,
|
"HeightRatio": 0.5,
|
||||||
"MarginTopRatio": -0.05
|
"MarginTopRatio": -0.05
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,13 +11,16 @@ async function createAnnotations(document, envelopeId, receiverId) {
|
|||||||
|
|
||||||
//#region signatures
|
//#region signatures
|
||||||
const id = generateId(envelopeId, receiverId, element.id, 'signature');
|
const id = generateId(envelopeId, receiverId, element.id, 'signature');
|
||||||
|
|
||||||
|
const boundingBox = new PSPDFKit.Geometry.Rect(annotParams.signature);
|
||||||
|
|
||||||
const annotation = new PSPDFKit.Annotations.WidgetAnnotation({
|
const annotation = new PSPDFKit.Annotations.WidgetAnnotation({
|
||||||
id: id,
|
id: id,
|
||||||
pageIndex: page,
|
pageIndex: page,
|
||||||
formFieldName: id,
|
formFieldName: id,
|
||||||
backgroundColor: PSPDFKit.Color.LIGHT_YELLOW,
|
backgroundColor: null,
|
||||||
blendMode: 'normal',
|
blendMode: 'normal',
|
||||||
boundingBox: new PSPDFKit.Geometry.Rect(annotParams.signature),
|
boundingBox: boundingBox,
|
||||||
})
|
})
|
||||||
|
|
||||||
const formField = new PSPDFKit.FormFields.SignatureFormField({
|
const formField = new PSPDFKit.FormFields.SignatureFormField({
|
||||||
@@ -237,7 +240,7 @@ async function createAnnotationFrameBlob(receiverName, receiverSignature, timest
|
|||||||
Comp.SignatureProgress.SignedCount += 1;
|
Comp.SignatureProgress.SignedCount += 1;
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
const scale = 4
|
const scale = 4
|
||||||
const fontSize = 10
|
const fontSize = 8
|
||||||
|
|
||||||
canvas.width = width * scale
|
canvas.width = width * scale
|
||||||
canvas.height = height * scale
|
canvas.height = height * scale
|
||||||
@@ -270,21 +273,17 @@ async function createAnnotationFrameBlob(receiverName, receiverSignature, timest
|
|||||||
ctx.moveTo(30 * scale, 50 * scale)
|
ctx.moveTo(30 * scale, 50 * scale)
|
||||||
ctx.lineTo(signatureLength, 50 * scale)
|
ctx.lineTo(signatureLength, 50 * scale)
|
||||||
|
|
||||||
ctx.strokeStyle = 'darkblue'
|
ctx.strokeStyle = 'darkgray'
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
ctx.fillStyle = 'black'
|
ctx.fillStyle = 'black'
|
||||||
ctx.font = `${fontSize * scale}px sans-serif`
|
ctx.font = `${fontSize * scale}px sans-serif`
|
||||||
ctx.fillText((READ_AND_CONFIRM ? localized.confirmedBy : localized.signedBy) ?? 'Signed by', 15 * scale, 10 * scale)
|
ctx.fillText((READ_AND_CONFIRM ? localized.confirmedBy : localized.signedBy) ?? 'Signed by', 15 * scale, 10 * scale)
|
||||||
ctx.fillText(receiverName, 15 * scale, 60 * scale)
|
ctx.fillText(receiverName, 15 * scale, 58.5 * scale)
|
||||||
ctx.fillText(signatureString, 15 * scale, 70 * scale)
|
ctx.fillText(signatureString, 15 * scale, 69 * scale)
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
const blob = await new Promise((resolve) => canvas.toBlob(resolve))
|
||||||
canvas.toBlob((blob) => {
|
return URL.createObjectURL(blob)
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
resolve(url)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//required
|
//required
|
||||||
|
|||||||
@@ -37,10 +37,15 @@ class App {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Load annotations into PSPDFKit
|
// Load annotations into PSPDFKit
|
||||||
|
this.signatureWidgets = [] // { pageIndex, boundingBox, id }
|
||||||
if (!READ_AND_CONFIRM)
|
if (!READ_AND_CONFIRM)
|
||||||
try {
|
try {
|
||||||
let signatures = await createAnnotations(this.currentDocument, this.envelopeReceiver.envelopeId, this.envelopeReceiver.receiverId);
|
let signatures = await createAnnotations(this.currentDocument, this.envelopeReceiver.envelopeId, this.envelopeReceiver.receiverId);
|
||||||
await this.pdfKit.create(signatures);
|
await this.pdfKit.create(signatures);
|
||||||
|
// Cache signature widget bounding boxes for stable frame placement
|
||||||
|
this.signatureWidgets = signatures
|
||||||
|
.filter(s => s instanceof PSPDFKit.Annotations.WidgetAnnotation && s.formFieldName?.includes('signature'))
|
||||||
|
.map(s => ({ id: s.id, pageIndex: s.pageIndex, boundingBox: s.boundingBox.toJS() }))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error loading annotations:", e);
|
console.error("Error loading annotations:", e);
|
||||||
}
|
}
|
||||||
@@ -66,8 +71,27 @@ class App {
|
|||||||
|
|
||||||
if (isFormField === false && isSignature === true) {
|
if (isFormField === false && isSignature === true) {
|
||||||
|
|
||||||
const left = annotation.boundingBox.left - 20
|
// Find the nearest signature widget on the same page to use its stable bounding box
|
||||||
const top = annotation.boundingBox.top - 20
|
const inkBBox = annotation.boundingBox
|
||||||
|
const samePageWidgets = this.signatureWidgets.filter(w => w.pageIndex === annotation.pageIndex)
|
||||||
|
const nearestWidget = samePageWidgets.reduce((nearest, w) => {
|
||||||
|
const dx = w.boundingBox.left - inkBBox.left
|
||||||
|
const dy = w.boundingBox.top - inkBBox.top
|
||||||
|
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
return dist < nearest.dist ? { dist, widget: w } : nearest
|
||||||
|
}, { dist: Infinity, widget: null }).widget
|
||||||
|
|
||||||
|
// 1. Remove widget background after signing
|
||||||
|
if (nearestWidget) {
|
||||||
|
const pageAnnots = await this.pdfKit.getAnnotations(annotation.pageIndex)
|
||||||
|
const widgetAnnot = pageAnnots.find(a => a.id === nearestWidget.id)
|
||||||
|
if (widgetAnnot)
|
||||||
|
await this.pdfKit.update(widgetAnnot.set('backgroundColor', PSPDFKit.Color.TRANSPARENT))
|
||||||
|
}
|
||||||
|
|
||||||
|
const bbox = nearestWidget?.boundingBox ?? inkBBox
|
||||||
|
const left = bbox.left - 20
|
||||||
|
const top = bbox.top - 12
|
||||||
const width = 150
|
const width = 150
|
||||||
const height = 75
|
const height = 75
|
||||||
const timestamp = new Date()
|
const timestamp = new Date()
|
||||||
@@ -330,7 +354,7 @@ class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleReset(event) {
|
async handleReset(event) {
|
||||||
const result = Swal.fire({
|
const result = await Swal.fire({
|
||||||
title: localized.resetConfirmTitle,
|
title: localized.resetConfirmTitle,
|
||||||
text: localized.resetConfirmText,
|
text: localized.resetConfirmText,
|
||||||
icon: 'question',
|
icon: 'question',
|
||||||
@@ -338,7 +362,24 @@ class App {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (result.isConfirmed) {
|
if (result.isConfirmed) {
|
||||||
const result = await deleteAnnotations(this.pdfKit)
|
await deleteAnnotations(this.pdfKit)
|
||||||
|
|
||||||
|
// Reset editable text form field values (Position, City)
|
||||||
|
const allFormFields = await this.pdfKit.getFormFields()
|
||||||
|
const editableFields = allFormFields.filter(f =>
|
||||||
|
f instanceof PSPDFKit.FormFields.TextFormField && !f.readOnly
|
||||||
|
)
|
||||||
|
for (const field of editableFields) {
|
||||||
|
await this.pdfKit.setFormFieldValues({ [field.name]: '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore widget backgrounds that were made transparent after signing
|
||||||
|
for (const w of this.signatureWidgets) {
|
||||||
|
const pageAnnots = await this.pdfKit.getAnnotations(w.pageIndex)
|
||||||
|
const widgetAnnot = pageAnnots.find(a => a.id === w.id)
|
||||||
|
if (widgetAnnot)
|
||||||
|
await this.pdfKit.update(widgetAnnot.set('backgroundColor', null))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
4
EnvelopeGenerator.Web/wwwroot/js/app.min.js
vendored
4
EnvelopeGenerator.Web/wwwroot/js/app.min.js
vendored
@@ -1,3 +1,3 @@
|
|||||||
const formatLocalized=(n,...t)=>typeof n=="string"?n.replace(/\{(\d+)\}/g,(n,i)=>t[i]??""):"";class App{constructor(n,t,i,r,u,f){this.container=f??`#${this.constructor.name.toLowerCase()}`;this.envelopeKey=n;this.pdfKit=null;this.currentDocument=t.envelope.documents[0];this.currentReceiver=t.receiver;this.signatureCount=t.envelope.documents[0].elements.length;this.envelopeReceiver=t;this.documentBytes=i;this.licenseKey=r;this.locale=u}async init(){if(this.pdfKit=await loadPSPDFKit(this.documentBytes,this.container,this.licenseKey,this.locale),addToolbarItems(this.pdfKit,this.handleClick.bind(this)),this.pdfKit.addEventListener("annotations.load",this.handleAnnotationsLoad.bind(this)),this.pdfKit.addEventListener("annotations.change",this.handleAnnotationsChange.bind(this)),this.pdfKit.addEventListener("annotations.create",this.handleAnnotationsCreate.bind(this)),this.pdfKit.addEventListener("annotations.willChange",()=>{Comp.ActPanel.Toggle()}),!READ_AND_CONFIRM)try{let n=await createAnnotations(this.currentDocument,this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId);await this.pdfKit.create(n)}catch(n){console.error("Error loading annotations:",n)}READ_AND_CONFIRM||[...document.getElementsByClassName("btn_refresh")].forEach(n=>n.addEventListener("click",()=>this.handleClick("RESET")));[...document.getElementsByClassName("btn_complete")].forEach(n=>n.addEventListener("click",()=>this.handleClick("FINISH")));[...document.getElementsByClassName("btn_reject")].forEach(n=>n.addEventListener("click",()=>this.handleClick("REJECT")))}handleAnnotationsLoad(n){n.toJS()}handleAnnotationsChange(){}async handleAnnotationsCreate(n){const t=n.toJS()[0],i=!!t.formFieldName,r=!!t.isSignature;if(i===!1&&r===!0){const r=t.boundingBox.left-20,u=t.boundingBox.top-20,n=150,i=75,f=new Date,e=await createAnnotationFrameBlob(this.envelopeReceiver.name,this.currentReceiver.signature,f,n,i),o=await fetch(e),s=await o.blob(),h=await this.pdfKit.createAttachment(s),c=createImageAnnotation(new PSPDFKit.Geometry.Rect({left:r,top:u,width:n,height:i}),t.pageIndex,h,generateId(this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId,this.fakeElementId--,"signed"));this.pdfKit.create(c)}}async handleClick(n){let t=!1;switch(n){case"RESET":t=await this.handleReset(null);Comp.SignatureProgress.SignedCount=0;t.isConfirmed&&Swal.fire({title:localized.success,text:localized.documentReset,icon:"info"});break;case"FINISH":t=await this.handleFinish(null);t==!0&&(window.location.href=`/Envelope/${this.envelopeKey}`);break;case"REJECT":Swal.fire({title:localized.rejection,html:`<div class="text-start fs-6 p-0 m-0">${localized.rejectionReasonQ}</div>`,icon:"question",input:"text",inputAttributes:{autocapitalize:"off"},showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.complete,cancelButtonText:localized.back,showLoaderOnConfirm:!0,preConfirm:async n=>{try{return await rejectEnvelope(n)}catch(t){Swal.showValidationMessage(`
|
const formatLocalized=(n,...t)=>typeof n=="string"?n.replace(/\{(\d+)\}/g,(n,i)=>t[i]??""):"";class App{constructor(n,t,i,r,u,f){this.container=f??`#${this.constructor.name.toLowerCase()}`;this.envelopeKey=n;this.pdfKit=null;this.currentDocument=t.envelope.documents[0];this.currentReceiver=t.receiver;this.signatureCount=t.envelope.documents[0].elements.length;this.envelopeReceiver=t;this.documentBytes=i;this.licenseKey=r;this.locale=u}async init(){if(this.pdfKit=await loadPSPDFKit(this.documentBytes,this.container,this.licenseKey,this.locale),addToolbarItems(this.pdfKit,this.handleClick.bind(this)),this.pdfKit.addEventListener("annotations.load",this.handleAnnotationsLoad.bind(this)),this.pdfKit.addEventListener("annotations.change",this.handleAnnotationsChange.bind(this)),this.pdfKit.addEventListener("annotations.create",this.handleAnnotationsCreate.bind(this)),this.pdfKit.addEventListener("annotations.willChange",()=>{Comp.ActPanel.Toggle()}),this.signatureWidgets=[],!READ_AND_CONFIRM)try{let n=await createAnnotations(this.currentDocument,this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId);await this.pdfKit.create(n);this.signatureWidgets=n.filter(n=>n instanceof PSPDFKit.Annotations.WidgetAnnotation&&n.formFieldName?.includes("signature")).map(n=>({id:n.id,pageIndex:n.pageIndex,boundingBox:n.boundingBox.toJS()}))}catch(n){console.error("Error loading annotations:",n)}READ_AND_CONFIRM||[...document.getElementsByClassName("btn_refresh")].forEach(n=>n.addEventListener("click",()=>this.handleClick("RESET")));[...document.getElementsByClassName("btn_complete")].forEach(n=>n.addEventListener("click",()=>this.handleClick("FINISH")));[...document.getElementsByClassName("btn_reject")].forEach(n=>n.addEventListener("click",()=>this.handleClick("REJECT")))}handleAnnotationsLoad(n){n.toJS()}handleAnnotationsChange(){}async handleAnnotationsCreate(n){const t=n.toJS()[0],i=!!t.formFieldName,r=!!t.isSignature;if(i===!1&&r===!0){const n=t.boundingBox,e=this.signatureWidgets.filter(n=>n.pageIndex===t.pageIndex),i=e.reduce((t,i)=>{const r=i.boundingBox.left-n.left,u=i.boundingBox.top-n.top,f=Math.sqrt(r*r+u*u);return f<t.dist?{dist:f,widget:i}:t},{dist:Infinity,widget:null}).widget;if(i){const r=await this.pdfKit.getAnnotations(t.pageIndex),n=r.find(n=>n.id===i.id);n&&await this.pdfKit.update(n.set("backgroundColor",PSPDFKit.Color.TRANSPARENT))}const r=i?.boundingBox??n,o=r.left-20,s=r.top-12,u=150,f=75,h=new Date,c=await createAnnotationFrameBlob(this.envelopeReceiver.name,this.currentReceiver.signature,h,u,f),l=await fetch(c),a=await l.blob(),v=await this.pdfKit.createAttachment(a),y=createImageAnnotation(new PSPDFKit.Geometry.Rect({left:o,top:s,width:u,height:f}),t.pageIndex,v,generateId(this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId,this.fakeElementId--,"signed"));this.pdfKit.create(y)}}async handleClick(n){let t=!1;switch(n){case"RESET":t=await this.handleReset(null);Comp.SignatureProgress.SignedCount=0;t.isConfirmed&&Swal.fire({title:localized.success,text:localized.documentReset,icon:"info"});break;case"FINISH":t=await this.handleFinish(null);t==!0&&(window.location.href=`/Envelope/${this.envelopeKey}`);break;case"REJECT":Swal.fire({title:localized.rejection,html:`<div class="text-start fs-6 p-0 m-0">${localized.rejectionReasonQ}</div>`,icon:"question",input:"text",inputAttributes:{autocapitalize:"off"},showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.complete,cancelButtonText:localized.back,showLoaderOnConfirm:!0,preConfirm:async n=>{try{return await rejectEnvelope(n)}catch(t){Swal.showValidationMessage(`
|
||||||
Request failed: ${t}
|
Request failed: ${t}
|
||||||
`)}},allowOutsideClick:()=>!Swal.isLoading()}).then(n=>{if(n.isConfirmed){const t=n.value;t.ok?reload():Swal.showValidationMessage(`Request failed: ${t.message}`)}});break;case"COPY_URL":const n=window.location.href.replace(/\/readonly/gi,"");navigator.clipboard.writeText(n).then(function(){bsNotify(localized.copyLinkSuccess,{alert_type:"success",delay:4,icon_name:"check_circle"})}).catch(function(){bsNotify(localized.copyLinkFailure??localized.unexpectedErrorTitle,{alert_type:"danger",delay:4,icon_name:"error"})});break;case"SHARE":Comp.ShareBackdrop.show();break;case"LOGOUT":await logout()}}async handleFinish(){let n=undefined;if(READ_AND_CONFIRM){const n=JSON.parse(sessionStorage.getItem("pspdf_all_pages_rendered")||"false")===!0;if(!n){const n=JSON.parse(sessionStorage.getItem("pspdf_unviewed_pages")||"[]"),t=n.length?formatLocalized(localized.viewRemainingPages,n.join(", ")):localized.viewAllPages;return await Swal.fire({title:localized.warning,text:t,icon:"warning"}),!1}}else{const i=await this.pdfKit.exportInstantJSON(),r=i.formFieldValues,u=r.filter(n=>isFieldRequired(n)),f=u.some(n=>n.value===undefined||n.value===null||n.value==="");if(f)return Swal.fire({title:localized.warning,text:localized.locationFieldsRequired,icon:"warning"}),!1;const e=new RegExp("^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$"),o=r.filter(n=>isCityField(n));for(var t of o)if(!IS_MOBILE_DEVICE&&!e.test(t.value))return Swal.fire({title:localized.warning,text:formatLocalized(localized.cityFormatInvalid,t.value),icon:"warning"}),!1;const s=await this.validateAnnotations(this.signatureCount);if(s===!1)return Swal.fire({title:localized.warning,text:localized.missingSignatures,icon:"warning"}),!1;n={instant:i,structured:mapSignature(i)}}return Swal.fire({title:localized.confirmation,html:`<div class="text-start fs-6 p-0 m-0">${READ_AND_CONFIRM?localized.confirmAgree:localized.sigAgree}</div>`,icon:"question",showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.finalize,cancelButtonText:localized.back}).then(async t=>{if(t.isConfirmed){try{await this.pdfKit.save()}catch(i){return Swal.fire({title:localized.warning,text:localized.envelopeSignError,icon:"error"}),!1}try{const t=READ_AND_CONFIRM?await signEnvelope():await signEnvelope(n);if(t.ok)return!0;if(t.status===409)return Swal.fire({title:localized.warning,text:localized.envelopeUnavailable,icon:"warning"}),!1;if(t.status===423)Swal.fire({title:localized.info??localized.warning,text:localized.envelopeRejectedRedirect,icon:"info",timer:2e3,showConfirmButton:!1}).then(()=>{location.reload()});else throw new Error;}catch(i){return Swal.fire({title:localized.warning,text:localized.envelopeSignError,icon:"error"}),!1}}else return!1})}async validateAnnotations(n){const t=await getAnnotations(this.pdfKit),i=t.map(n=>n.toJS()).filter(n=>n.isSignature);return n<=i.length}async handleReset(){const n=Swal.fire({title:localized.resetConfirmTitle,text:localized.resetConfirmText,icon:"question",showCancelButton:!0});if(n.isConfirmed){const n=await deleteAnnotations(this.pdfKit)}return n}fakeElementId=0;}
|
`)}},allowOutsideClick:()=>!Swal.isLoading()}).then(n=>{if(n.isConfirmed){const t=n.value;t.ok?reload():Swal.showValidationMessage(`Request failed: ${t.message}`)}});break;case"COPY_URL":const n=window.location.href.replace(/\/readonly/gi,"");navigator.clipboard.writeText(n).then(function(){bsNotify(localized.copyLinkSuccess,{alert_type:"success",delay:4,icon_name:"check_circle"})}).catch(function(){bsNotify(localized.copyLinkFailure??localized.unexpectedErrorTitle,{alert_type:"danger",delay:4,icon_name:"error"})});break;case"SHARE":Comp.ShareBackdrop.show();break;case"LOGOUT":await logout()}}async handleFinish(){let n=undefined;if(READ_AND_CONFIRM){const n=JSON.parse(sessionStorage.getItem("pspdf_all_pages_rendered")||"false")===!0;if(!n){const n=JSON.parse(sessionStorage.getItem("pspdf_unviewed_pages")||"[]"),t=n.length?formatLocalized(localized.viewRemainingPages,n.join(", ")):localized.viewAllPages;return await Swal.fire({title:localized.warning,text:t,icon:"warning"}),!1}}else{const i=await this.pdfKit.exportInstantJSON(),r=i.formFieldValues,u=r.filter(n=>isFieldRequired(n)),f=u.some(n=>n.value===undefined||n.value===null||n.value==="");if(f)return Swal.fire({title:localized.warning,text:localized.locationFieldsRequired,icon:"warning"}),!1;const e=new RegExp("^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$"),o=r.filter(n=>isCityField(n));for(var t of o)if(!IS_MOBILE_DEVICE&&!e.test(t.value))return Swal.fire({title:localized.warning,text:formatLocalized(localized.cityFormatInvalid,t.value),icon:"warning"}),!1;const s=await this.validateAnnotations(this.signatureCount);if(s===!1)return Swal.fire({title:localized.warning,text:localized.missingSignatures,icon:"warning"}),!1;n={instant:i,structured:mapSignature(i)}}return Swal.fire({title:localized.confirmation,html:`<div class="text-start fs-6 p-0 m-0">${READ_AND_CONFIRM?localized.confirmAgree:localized.sigAgree}</div>`,icon:"question",showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.finalize,cancelButtonText:localized.back}).then(async t=>{if(t.isConfirmed){try{await this.pdfKit.save()}catch(i){return Swal.fire({title:localized.warning,text:localized.envelopeSignError,icon:"error"}),!1}try{const t=READ_AND_CONFIRM?await signEnvelope():await signEnvelope(n);if(t.ok)return!0;if(t.status===409)return Swal.fire({title:localized.warning,text:localized.envelopeUnavailable,icon:"warning"}),!1;if(t.status===423)Swal.fire({title:localized.info??localized.warning,text:localized.envelopeRejectedRedirect,icon:"info",timer:2e3,showConfirmButton:!1}).then(()=>{location.reload()});else throw new Error;}catch(i){return Swal.fire({title:localized.warning,text:localized.envelopeSignError,icon:"error"}),!1}}else return!1})}async validateAnnotations(n){const t=await getAnnotations(this.pdfKit),i=t.map(n=>n.toJS()).filter(n=>n.isSignature);return n<=i.length}async handleReset(){const n=await Swal.fire({title:localized.resetConfirmTitle,text:localized.resetConfirmText,icon:"question",showCancelButton:!0});if(n.isConfirmed){await deleteAnnotations(this.pdfKit);const n=await this.pdfKit.getFormFields(),t=n.filter(n=>n instanceof PSPDFKit.FormFields.TextFormField&&!n.readOnly);for(const n of t)await this.pdfKit.setFormFieldValues({[n.name]:""});for(const n of this.signatureWidgets){const i=await this.pdfKit.getAnnotations(n.pageIndex),t=i.find(t=>t.id===n.id);t&&await this.pdfKit.update(t.set("backgroundColor",null))}}return n}fakeElementId=0;}
|
||||||
@@ -22,6 +22,8 @@ EndProject
|
|||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B291-4E19-99B9-E4FA3AFAB62C}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B291-4E19-99B9-E4FA3AFAB62C}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
|
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
|
||||||
|
fix-report-label-read-and-confirmed.md = fix-report-label-read-and-confirmed.md
|
||||||
|
fix-signature-field-formatting.md = fix-signature-field-formatting.md
|
||||||
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
|
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
|
||||||
RECEIVER_PDF_VIEWER_CONTEXT.md = RECEIVER_PDF_VIEWER_CONTEXT.md
|
RECEIVER_PDF_VIEWER_CONTEXT.md = RECEIVER_PDF_VIEWER_CONTEXT.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
|
|||||||
135
fix-report-label-read-and-confirmed.md
Normal file
135
fix-report-label-read-and-confirmed.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# Fix: Report Label "Read and confirmed" for Read and Sign Envelopes
|
||||||
|
|
||||||
|
## Status
|
||||||
|
✅ **COMPLETED**
|
||||||
|
|
||||||
|
## Git Branch
|
||||||
|
```
|
||||||
|
fix/report-label-read-and-confirmed
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
In the **Signature Certificate report** (`Signierungszertifikat`), the history event
|
||||||
|
list always showed **"Document signed"** for status `DocumentSigned` (code `2005`) <20>
|
||||||
|
even when the envelope type was **"Read and Sign"** (`EnvelopeTypeId = 2`).
|
||||||
|
|
||||||
|
For "Read and Sign" envelopes the correct label must be **"Read and confirmed"**
|
||||||
|
(DE: **"Gelesen und best<73>tigt"**).
|
||||||
|
|
||||||
|
### Screenshot reference
|
||||||
|
The report shows a table with columns *Ereignis | Benutzer | Zeitstempel*.
|
||||||
|
The row with status 2005 displayed "Document signed" where "Read and confirmed"
|
||||||
|
was expected.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
|
||||||
|
`ReportItem.ItemStatusTranslated` in
|
||||||
|
`EnvelopeGenerator.CommonServices\Jobs\FinalizeDocument\ReportItem.vb`
|
||||||
|
resolved the display label by calling:
|
||||||
|
|
||||||
|
```vb
|
||||||
|
My.Resources.Model.ResourceManager.GetString(ItemStatus.ToString())
|
||||||
|
```
|
||||||
|
|
||||||
|
There was **no branching logic** to handle the Read and Sign envelope type.
|
||||||
|
The same `DocumentSigned` key was always used regardless of envelope type.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
### Convention followed
|
||||||
|
The project already uses a `RaC` (**R**ead **a**nd **C**onfirm) suffix for
|
||||||
|
Read-and-Sign label variants across all resource files:
|
||||||
|
|
||||||
|
| Existing RaC keys |
|
||||||
|
|---|
|
||||||
|
| `DocumentRejectedRaC` |
|
||||||
|
| `EnvelopeReportCreatedRaC` |
|
||||||
|
| `MessageConfirmationSentRaC` |
|
||||||
|
|
||||||
|
### New resource key added: `DocumentSignedRaC`
|
||||||
|
|
||||||
|
| Resource file | Value |
|
||||||
|
|---|---|
|
||||||
|
| `Model.resx` (DE, default) | `Gelesen und best<73>tigt` |
|
||||||
|
| `Model.en.resx` (EN) | `Read and confirmed` |
|
||||||
|
|
||||||
|
### Logic change in `ReportItem.vb`
|
||||||
|
|
||||||
|
```vb
|
||||||
|
Public ReadOnly Property ItemStatusTranslated As String
|
||||||
|
Get
|
||||||
|
Dim oStatusKey = ItemStatus.ToString()
|
||||||
|
|
||||||
|
If ItemStatus = EnvelopeStatus.DocumentSigned AndAlso
|
||||||
|
Envelope IsNot Nothing AndAlso
|
||||||
|
Envelope.IsReadAndConfirm() Then
|
||||||
|
oStatusKey = "DocumentSignedRaC"
|
||||||
|
End If
|
||||||
|
|
||||||
|
Return My.Resources.Model.ResourceManager.GetString(oStatusKey)
|
||||||
|
End Get
|
||||||
|
End Property
|
||||||
|
```
|
||||||
|
|
||||||
|
`Envelope.IsReadAndConfirm()` is defined in
|
||||||
|
`EnvelopeGenerator.Domain\Interfaces\IEnvelope.cs` and returns `true`
|
||||||
|
when `EnvelopeTypeId == 2`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
| # | File | Change | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | `EnvelopeGenerator.CommonServices\Jobs\FinalizeDocument\ReportItem.vb` | Added `RaC` branching logic to `ItemStatusTranslated` | ? Done |
|
||||||
|
| 2 | `EnvelopeGenerator.Domain\Resources\Model.resx` | Added `DocumentSignedRaC` = `Gelesen und bestätigt` | ✅ Done |
|
||||||
|
| 3 | `EnvelopeGenerator.CommonServices\Strings\Model.resx` | Added `DocumentSignedRaC` = `Gelesen und bestätigt` | ✅ Done |
|
||||||
|
| 4 | `EnvelopeGenerator.CommonServices\Strings\Model.en.resx` | Added `DocumentSignedRaC` = `Read and confirmed` | ✅ Done |
|
||||||
|
| 5 | `EnvelopeGenerator.Domain\Resources\Model.Designer.cs` | Added `DocumentSignedRaC` strongly-typed property | ✅ Done |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to resume (for next agent)
|
||||||
|
|
||||||
|
All pending items are **resource file additions only**.
|
||||||
|
Each file needs one new `<data>` block inserted **directly after** the existing
|
||||||
|
`DocumentSigned` entry:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||||
|
<value>Gelesen und best<73>tigt</value> <!-- DE files -->
|
||||||
|
<!-- OR -->
|
||||||
|
<value>Read and confirmed</value> <!-- EN file -->
|
||||||
|
</data>
|
||||||
|
```
|
||||||
|
|
||||||
|
`Model.Designer.cs` also needs a new strongly-typed property added after
|
||||||
|
`DocumentSigned`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Gelesen und best<73>tigt.
|
||||||
|
/// </summary>
|
||||||
|
public static string DocumentSignedRaC {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DocumentSignedRaC", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
After changes, generate a Signature Certificate report for a **Read and Sign**
|
||||||
|
envelope and verify:
|
||||||
|
- History row with status 2005 shows **"Read and confirmed"** (EN)
|
||||||
|
- History row with status 2005 shows **"Gelesen und best<73>tigt"** (DE)
|
||||||
|
- Regular (non-RaC) envelopes still show **"Document signed"** / **"Dokument unterzeichnet"**
|
||||||
111
fix-signature-field-formatting.md
Normal file
111
fix-signature-field-formatting.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# Signature Field Formatting — WISAG Feedback
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Customer feedback received from the new WISAG signFLOW project lead (email dated 3/7/2026).
|
||||||
|
The signature stamp rendered inside the finalized PDF has layout issues that need to be addressed.
|
||||||
|
|
||||||
|
### Current State (screenshot from customer email)
|
||||||
|
|
||||||
|
```
|
||||||
|
Signiert von ___________
|
||||||
|
[ signature image ]
|
||||||
|
Armin Schneider
|
||||||
|
C6B85E96A520C60... ← certificate hash, too close to name
|
||||||
|
Bereichsleiter Kern- ← position text wraps, no spacing to date
|
||||||
|
Anwendungen
|
||||||
|
Frankfurt am Main
|
||||||
|
23.06.2026, 17:13:42 GMT+2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issues Reported
|
||||||
|
|
||||||
|
1. **Spacing between name and certificate number** is too small — needs more vertical gap.
|
||||||
|
2. **Spacing between position/title and date/time** is too small — should be dynamic (expand with content).
|
||||||
|
3. *(Secondary, out of scope here)* Multiple signature fields side-by-side scale individually, causing uneven heights.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agreed Solution
|
||||||
|
|
||||||
|
> **"Height stays fixed. Text stretches horizontally. Beyond a certain character limit, truncate with '...'"**
|
||||||
|
> *(Original: "Höhe bleibt fix. Text dehnt sich horizontal aus. Ab einem bestimmten Limit wird mit '...' abgekürzt.")*
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
- The signature field dimensions are **fixed** (`1.77" × 1.96"`). Allowing text to wrap causes position and date rows to collide vertically.
|
||||||
|
- Truncating long strings with ellipsis (`...`) keeps the layout deterministic and prevents overflow.
|
||||||
|
- Per-field max-length limits allow tuning each text row independently (name, certificate, position, city, date).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### 1. `PDFBurnerParams.vb` — Add per-field max-length dictionary
|
||||||
|
|
||||||
|
Add a new property `TextMaxLengths` that maps each field name (matching `EGName` keys) to a maximum character count.
|
||||||
|
|
||||||
|
```vbnet
|
||||||
|
' EnvelopeGenerator.CommonServices/Jobs/FinalizeDocument/PDFBurnerParams.vb
|
||||||
|
|
||||||
|
Public Property TextMaxLengths As New Dictionary(Of String, Integer) From {
|
||||||
|
{"name", 28},
|
||||||
|
{"position", 32},
|
||||||
|
{"city", 24},
|
||||||
|
{"date", 28}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Default values are starting points and should be tuned against real customer data.
|
||||||
|
|
||||||
|
### 2. `PDFBurner.vb` — Add truncation helper and apply in `AddFormFieldValue`
|
||||||
|
|
||||||
|
#### 2a. Add a private helper function
|
||||||
|
|
||||||
|
```vbnet
|
||||||
|
Private Function TruncateText(value As String, fieldName As String) As String
|
||||||
|
If String.IsNullOrEmpty(value) Then Return value
|
||||||
|
|
||||||
|
Dim maxLen As Integer = 0
|
||||||
|
If _pdfBurnerParams.TextMaxLengths IsNot Nothing AndAlso
|
||||||
|
_pdfBurnerParams.TextMaxLengths.TryGetValue(fieldName, maxLen) AndAlso
|
||||||
|
maxLen > 0 AndAlso value.Length > maxLen Then
|
||||||
|
Return value.Substring(0, maxLen - 3) & "..."
|
||||||
|
End If
|
||||||
|
|
||||||
|
Return value
|
||||||
|
End Function
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2b. Apply truncation in the `egName`-aware overload
|
||||||
|
|
||||||
|
In `AddFormFieldValue(pAnnotation As Annotation, formFieldValue As FormFieldValue)`:
|
||||||
|
|
||||||
|
```vbnet
|
||||||
|
' Before passing value to AddTextAnnot, truncate:
|
||||||
|
Dim displayValue As String = TruncateText(formFieldValue.value, pAnnotation.egName)
|
||||||
|
|
||||||
|
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, displayValue)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2c. Apply truncation in the element-based overload (optional)
|
||||||
|
|
||||||
|
In `AddFormFieldValue(x, y, width, height, page, value)` — if a field name is available in calling context, pass it through; otherwise this path does not use `egName` and truncation can be skipped or applied with a generic fallback key.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Affected Files
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `EnvelopeGenerator.CommonServices/Jobs/FinalizeDocument/PDFBurnerParams.vb` | Add `TextMaxLengths As Dictionary(Of String, Integer)` |
|
||||||
|
| `EnvelopeGenerator.CommonServices/Jobs/FinalizeDocument/PDFBurner.vb` | Add `TruncateText()` helper; apply in `AddFormFieldValue` overloads |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Field names (`"name"`, `"position"`, `"city"`, `"date"`) must match the keys used in `EGName.Index` dictionary (`PDFBurner.vb:443–450`).
|
||||||
|
- The certificate hash field (e.g. `C6B85E96A520C60...`) is already truncated upstream — verify its `egName` mapping if it needs its own limit.
|
||||||
|
- The `YOffset` / `TopMargin` values in `PDFBurnerParams` may also need adjustment to increase vertical spacing between rows. That is a separate (simpler) change.
|
||||||
|
- This issue is **not** related to the Blazor WebUI rendering — it is purely in the GdPicture14 PDF burning pipeline.
|
||||||
Reference in New Issue
Block a user