Add AccessCode feature to EnvelopeSenderPage UI

Introduced a new `AccessCode` property in `EnvelopeReceiverDto` to support displaying and copying access codes in the envelope sender page.

- Updated `EnvelopeReceiverDto` to include the nullable `AccessCode` property.
- Enhanced `EnvelopeSenderPage.razor` to display the `AccessCode` for each receiver, with a copy-to-clipboard button.
- Implemented `CopyAccessCodeAsync` for clipboard functionality using JavaScript interop.
- Styled the `AccessCode` section in `sender-page.css` for a responsive and user-friendly design.
- Improved error handling for clipboard operations with console warnings.

These changes improve the user experience by allowing senders to view and copy access codes for receivers.
This commit is contained in:
2026-09-28 12:47:37 +02:00
parent a0e2492757
commit e960c02cd4
3 changed files with 110 additions and 1 deletions

View File

@@ -60,6 +60,11 @@ public record EnvelopeReceiverDto
/// </summary>
public string? PrivateMessage { get; init; }
/// <summary>
///
/// </summary>
public string? AccessCode { get; init; }
/// <summary>
///
/// </summary>
@@ -83,4 +88,4 @@ public record EnvelopeReceiverDto
h.Receiver?.Id == ReceiverId &&
h.Status == EnvelopeStatus.DocumentSigned
) ?? false;
}
}

View File

@@ -315,6 +315,8 @@
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
@{
var isSigned = IsReceiverSigned(envelope, receiver);
var accessCode = receiver.AccessCode;
var hasAccessCode = !string.IsNullOrWhiteSpace(accessCode);
}
<span class="receiver-badge receiver-badge--@(isSigned ? "signed" : "unsigned")" style="min-width: 100px;">
@if (isSigned)
@@ -337,6 +339,24 @@
<strong style="color: #1f2937;">@receiver.Name</strong>
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Receiver?.EmailAddress</span>
</div>
<div class="sender-access-code">
<span class="sender-access-code__label">@L("AccessCode", "Access code")</span>
<div class="sender-access-code__row">
<input class="sender-access-code__value"
value="@(hasAccessCode ? accessCode : "-")"
readonly
title="@L("AccessCode", "Access code")" />
<button type="button"
class="sender-access-code__copy"
disabled="@(!hasAccessCode)"
@onclick='() => CopyAccessCodeAsync(accessCode)'
title="@L("Copy", "Copy")">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
<path d="M10 1.5A1.5 1.5 0 0 1 11.5 3v1H13A1.5 1.5 0 0 1 14.5 5.5v8A1.5 1.5 0 0 1 13 15H5A1.5 1.5 0 0 1 3.5 13.5V12H3A1.5 1.5 0 0 1 1.5 10.5v-8A1.5 1.5 0 0 1 3 1h7zm.5 2A.5.5 0 0 0 10 3H3a.5.5 0 0 0-.5.5v7A.5.5 0 0 0 3 11h.5V5.5A1.5 1.5 0 0 1 5 4h5.5v-.5zM5 5a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-8A.5.5 0 0 0 13 5H5z"/>
</svg>
</button>
</div>
</div>
</div>
}
</div>
@@ -840,6 +860,21 @@
return value.ResourceNotFound ? fallback : value.Value;
}
async Task CopyAccessCodeAsync(string? accessCode)
{
if (string.IsNullOrWhiteSpace(accessCode))
return;
try
{
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", accessCode);
}
catch
{
await JSRuntime.InvokeVoidAsync("console.warn", "[Sender] Failed to copy access code to clipboard.");
}
}
void OnCustomizeElement(GridCustomizeElementEventArgs e)
{
// Future: Add custom row coloring based on status if needed

View File

@@ -382,6 +382,71 @@
color: #c62828;
}
.sender-access-code {
min-width: 120px;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.sender-access-code__label {
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.02em;
text-transform: uppercase;
color: #64748b;
}
.sender-access-code__row {
display: flex;
align-items: center;
gap: 0;
}
.sender-access-code__value {
width: 72px;
height: 28px;
border: 1px solid rgba(126, 34, 206, 0.22);
border-right: none;
border-radius: 6px 0 0 6px;
padding: 0 0.55rem;
font-size: 0.8rem;
font-weight: 600;
color: #1e293b;
background: #f8fafc;
}
.sender-access-code__value:focus {
outline: 2px solid rgba(126, 34, 206, 0.18);
border-color: rgba(126, 34, 206, 0.45);
}
.sender-access-code__copy {
height: 28px;
border: 1px solid rgba(126, 34, 206, 0.3);
border-radius: 0 6px 6px 0;
width: 30px;
min-width: 30px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
color: #4c1d95;
background: rgba(126, 34, 206, 0.08);
cursor: pointer;
transition: all 0.2s ease;
}
.sender-access-code__copy:hover:not(:disabled) {
border-color: rgba(126, 34, 206, 0.45);
background: rgba(126, 34, 206, 0.16);
}
.sender-access-code__copy:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.sender-history-list {
display: flex;
flex-direction: column;
@@ -521,6 +586,10 @@
padding: 0.875rem 1rem;
font-size: 0.813rem;
}
.sender-access-code {
min-width: 100%;
}
}
@media (max-width: 560px) {