Compare commits
7 Commits
db368b889a
...
4d069cdaa0
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d069cdaa0 | |||
| bbbfa4de01 | |||
| 3ff3373b27 | |||
| 2af8815cf6 | |||
| ca24a96084 | |||
| 6120e6062e | |||
| cc2aea90ed |
@@ -1,4 +1,6 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Query;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Query;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
||||||
@@ -29,5 +31,6 @@ public record ReceiverQueryBase
|
|||||||
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
||||||
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
[NotMapped]
|
||||||
|
public virtual bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ using EnvelopeGenerator.Application.Common.Query;
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Receivers.Queries;
|
namespace EnvelopeGenerator.Application.Receivers.Queries;
|
||||||
|
|
||||||
@@ -12,7 +13,24 @@ namespace EnvelopeGenerator.Application.Receivers.Queries;
|
|||||||
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
||||||
/// um spezifische Informationen über einen Empfänger abzurufen.
|
/// um spezifische Informationen über einen Empfänger abzurufen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>;
|
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Suchbegriff für eine teilweise Übereinstimmung in der E-Mail Adresse des Empfängers
|
||||||
|
/// </summary>
|
||||||
|
public virtual string? EmailAddressSearch { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether any of the specified query criteria have a value.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This property returns <c>true</c> if at least one of the fields
|
||||||
|
/// <see cref="ReceiverQueryBase.Id"/>, <see cref="ReceiverQueryBase.EmailAddress"/>, <see cref="EmailAddressSearch"/>, or <see cref="ReceiverQueryBase.Signature"/> is not null.
|
||||||
|
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
||||||
|
/// </remarks>
|
||||||
|
[NotMapped]
|
||||||
|
public override bool HasAnyCriteria => EmailAddressSearch is not null || base.HasAnyCriteria;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -53,6 +71,11 @@ public class ReadReceiverQueryHandler : IRequestHandler<ReadReceiverQuery, IEnum
|
|||||||
query = query.Where(r => r.EmailAddress == email);
|
query = query.Where(r => r.EmailAddress == email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.EmailAddressSearch))
|
||||||
|
{
|
||||||
|
query = query.Where(r => EF.Functions.Like(r.EmailAddress, $"%{request.EmailAddressSearch}%"));
|
||||||
|
}
|
||||||
|
|
||||||
if (request.Signature is string signature)
|
if (request.Signature is string signature)
|
||||||
{
|
{
|
||||||
query = query.Where(r => r.Signature == signature);
|
query = query.Where(r => r.Signature == signature);
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
@page "/envelope/editor"
|
@page "/sender/editor"
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
@using DevExpress.Blazor.PdfViewer
|
@using DevExpress.Blazor.PdfViewer
|
||||||
@using DevExpress.Blazor.Reporting.Models
|
@using DevExpress.Blazor.Reporting.Models
|
||||||
@using DevExpress.Blazor
|
@using DevExpress.Blazor
|
||||||
@using EnvelopeGenerator.Server.Client.Services
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
|
@using EnvelopeGenerator.Server.Services
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject AppVersionService AppVersion
|
@inject AppVersionService AppVersion
|
||||||
@inject ILogger<EnvelopeSenderEditorPage> Logger
|
@inject ILogger<EnvelopeSenderEditorPage> Logger
|
||||||
|
@inject EnvelopeReceiverPageDataService ReceiverPageDataService
|
||||||
|
|
||||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||||
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
||||||
@@ -21,22 +23,76 @@
|
|||||||
style="flex-direction: row; align-items: center; padding: 0.35rem 1.5rem; gap: 0.75rem;">
|
style="flex-direction: row; align-items: center; padding: 0.35rem 1.5rem; gap: 0.75rem;">
|
||||||
|
|
||||||
@* Left: Title *@
|
@* Left: Title *@
|
||||||
<div style="flex: 1; min-width: 0; display: flex; align-items: center; gap: 0.75rem;">
|
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: stretch; gap: 0.75rem;">
|
||||||
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937; white-space: nowrap;">
|
<div style="display: flex; align-items: center; gap: 0.75rem; min-width: 0; flex-wrap: wrap;">
|
||||||
Neues Dokument
|
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937; white-space: nowrap;">
|
||||||
</div>
|
Neues Dokument
|
||||||
@if (_pdfLoaded)
|
</div>
|
||||||
{
|
@if (_pdfLoaded)
|
||||||
<span style="font-size: 0.7rem; color: #6b7280;">@_fileName</span>
|
|
||||||
@if (_signatureFields.Count > 0)
|
|
||||||
{
|
{
|
||||||
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem;
|
<span style="font-size: 0.7rem; color: #6b7280;">@_fileName</span>
|
||||||
background: #ede9fe; border-radius: 0.25rem; color: #6d28d9;
|
@if (_signatureFields.Count > 0)
|
||||||
font-weight: 500; font-size: 0.7rem; white-space: nowrap;">
|
{
|
||||||
@_signatureFields.Count Signaturfeld@(_signatureFields.Count != 1 ? "er" : "")
|
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem;
|
||||||
</span>
|
background: #ede9fe; border-radius: 0.25rem; color: #6d28d9;
|
||||||
|
font-weight: 500; font-size: 0.7rem; white-space: nowrap;">
|
||||||
|
@_signatureFields.Count Signaturfeld@(_signatureFields.Count != 1 ? "er" : "")
|
||||||
|
</span>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-receivers-panel">
|
||||||
|
<div class="sender-receivers-panel__header">
|
||||||
|
<div class="sender-receivers-panel__title-wrap">
|
||||||
|
<span class="sender-receivers-panel__title">Empfänger</span>
|
||||||
|
<span class="sender-receivers-panel__count">@_receivers.Count</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn"
|
||||||
|
@onclick="OpenAddReceiverPopup"
|
||||||
|
title="Empfänger hinzufügen">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 1a.5.5 0 0 1 .5.5v6h6a.5.5 0 0 1 0 1h-6v6a.5.5 0 0 1-1 0v-6h-6a.5.5 0 0 1 0-1h6v-6A.5.5 0 0 1 8 1z" />
|
||||||
|
</svg>
|
||||||
|
<span class="pdf-toolbar__btn-text">Empfänger hinzufügen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (_receivers.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="sender-receivers-panel__empty">
|
||||||
|
Noch keine Empfänger hinzugefügt.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="sender-receivers-list">
|
||||||
|
@foreach (var receiver in _receivers)
|
||||||
|
{
|
||||||
|
<div class="sender-receiver-chip">
|
||||||
|
<div class="sender-receiver-chip__body">
|
||||||
|
<div class="sender-receiver-chip__name">@receiver.FullName</div>
|
||||||
|
<div class="sender-receiver-chip__email">@receiver.Email</div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(receiver.PhoneNumber))
|
||||||
|
{
|
||||||
|
<div class="sender-receiver-chip__phone">@receiver.PhoneNumber</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact"
|
||||||
|
@onclick="() => AddSignatureForReceiver(receiver)"
|
||||||
|
title="Signatur hinzufügen">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10z" />
|
||||||
|
</svg>
|
||||||
|
<span class="pdf-toolbar__btn-text">Signatur hinzufügen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* Right: Buttons *@
|
@* Right: Buttons *@
|
||||||
@@ -208,6 +264,90 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DxPopup @bind-Visible="_receiverPopupVisible"
|
||||||
|
HeaderText="Empfänger hinzufügen"
|
||||||
|
ShowFooter="true"
|
||||||
|
CloseOnEscape="true"
|
||||||
|
Width="720px"
|
||||||
|
CssClass="sender-receiver-popup">
|
||||||
|
<BodyContentTemplate Context="popupContext">
|
||||||
|
<div class="sender-receiver-popup__body">
|
||||||
|
<div class="sender-receiver-popup__form-grid">
|
||||||
|
<div class="sender-receiver-popup__field">
|
||||||
|
<div class="sender-receiver-popup__label">Vor- und Nachname</div>
|
||||||
|
<DxTextBox Text="@_receiverDraftName"
|
||||||
|
TextChanged="OnReceiverNameChanged"
|
||||||
|
NullText="Max Mustermann"
|
||||||
|
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto"
|
||||||
|
BindValueMode="BindValueMode.OnInput"
|
||||||
|
CssClass="w-100" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-receiver-popup__field">
|
||||||
|
<div class="sender-receiver-popup__label">Telefonnummer (optional)</div>
|
||||||
|
<DxTextBox Text="@_receiverDraftPhoneNumber"
|
||||||
|
TextChanged="OnReceiverPhoneNumberChanged"
|
||||||
|
NullText="+49 170 1234567"
|
||||||
|
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto"
|
||||||
|
BindValueMode="BindValueMode.OnInput"
|
||||||
|
CssClass="w-100" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-receiver-popup__field">
|
||||||
|
<div class="sender-receiver-popup__label">E-Mail-Adresse</div>
|
||||||
|
<div @onkeydown="OnReceiverEmailKeyDownAsync">
|
||||||
|
<DxTextBox Text="@_receiverDraftEmail"
|
||||||
|
TextChanged="OnReceiverEmailTextChangedAsync"
|
||||||
|
NullText="name@beispiel.de"
|
||||||
|
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto"
|
||||||
|
BindValueMode="BindValueMode.OnInput"
|
||||||
|
CssClass="w-100" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-receiver-popup__suggestions-shell">
|
||||||
|
@if (_receiverEmailSuggestions.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="sender-receiver-popup__suggestions">
|
||||||
|
<DxListBox TData="string"
|
||||||
|
TValue="string"
|
||||||
|
Data="@_receiverEmailSuggestions"
|
||||||
|
Value="@_selectedReceiverEmailSuggestion"
|
||||||
|
ValueChanged="OnReceiverEmailSuggestionSelectedAsync"
|
||||||
|
SelectionMode="ListBoxSelectionMode.Single"
|
||||||
|
CssClass="w-100" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-receiver-popup__hint">
|
||||||
|
Bereits verwendete E-Mail-Adressen werden bei der Eingabe vorgeschlagen.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (_isReceiverEmailSearchRunning)
|
||||||
|
{
|
||||||
|
<div class="sender-receiver-popup__loading">E-Mail-Vorschläge werden geladen…</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_receiverPopupValidationMessage))
|
||||||
|
{
|
||||||
|
<div class="sender-receiver-popup__validation">@_receiverPopupValidationMessage</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</BodyContentTemplate>
|
||||||
|
<FooterContentTemplate>
|
||||||
|
<div class="sender-receiver-popup__footer">
|
||||||
|
<DxButton Text="Abbrechen"
|
||||||
|
RenderStyle="ButtonRenderStyle.Secondary"
|
||||||
|
Click="CloseAddReceiverPopup" />
|
||||||
|
<DxButton Text="Empfänger hinzufügen"
|
||||||
|
RenderStyle="ButtonRenderStyle.Primary"
|
||||||
|
Click="SaveReceiverAsync" />
|
||||||
|
</div>
|
||||||
|
</FooterContentTemplate>
|
||||||
|
</DxPopup>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
// ── Constants ──
|
// ── Constants ──
|
||||||
// Signature field size in PDF points (fixed): 1.77" × 1.96" × 72 pt/inch
|
// Signature field size in PDF points (fixed): 1.77" × 1.96" × 72 pt/inch
|
||||||
@@ -227,6 +367,17 @@
|
|||||||
string? _errorMessage;
|
string? _errorMessage;
|
||||||
bool _placementMode = false;
|
bool _placementMode = false;
|
||||||
List<SignatureFieldDraft> _signatureFields = [];
|
List<SignatureFieldDraft> _signatureFields = [];
|
||||||
|
List<ReceiverDraft> _receivers = [];
|
||||||
|
bool _receiverPopupVisible;
|
||||||
|
string _receiverDraftName = string.Empty;
|
||||||
|
string _receiverDraftEmail = string.Empty;
|
||||||
|
string _receiverDraftPhoneNumber = string.Empty;
|
||||||
|
string? _selectedReceiverEmailSuggestion;
|
||||||
|
string? _receiverPopupValidationMessage;
|
||||||
|
bool _isReceiverEmailSearchRunning;
|
||||||
|
List<string> _receiverEmailSuggestions = [];
|
||||||
|
int _receiverEmailSearchVersion;
|
||||||
|
static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new();
|
||||||
|
|
||||||
// ── PDF upload ──
|
// ── PDF upload ──
|
||||||
async Task OnPdfFileSelectedAsync(InputFileChangeEventArgs e)
|
async Task OnPdfFileSelectedAsync(InputFileChangeEventArgs e)
|
||||||
@@ -336,6 +487,182 @@
|
|||||||
$"[SenderEditor] Total fields: {_signatureFields.Count}");
|
$"[SenderEditor] Total fields: {_signatureFields.Count}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OpenAddReceiverPopup()
|
||||||
|
{
|
||||||
|
_receiverDraftName = string.Empty;
|
||||||
|
_receiverDraftEmail = string.Empty;
|
||||||
|
_receiverDraftPhoneNumber = string.Empty;
|
||||||
|
_selectedReceiverEmailSuggestion = null;
|
||||||
|
_receiverPopupValidationMessage = null;
|
||||||
|
_receiverEmailSuggestions.Clear();
|
||||||
|
_receiverPopupVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CloseAddReceiverPopup()
|
||||||
|
{
|
||||||
|
_receiverPopupVisible = false;
|
||||||
|
_receiverPopupValidationMessage = null;
|
||||||
|
_selectedReceiverEmailSuggestion = null;
|
||||||
|
_isReceiverEmailSearchRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnReceiverNameChanged(string? value)
|
||||||
|
{
|
||||||
|
_receiverDraftName = value ?? string.Empty;
|
||||||
|
_receiverPopupValidationMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnReceiverPhoneNumberChanged(string? value)
|
||||||
|
{
|
||||||
|
_receiverDraftPhoneNumber = value ?? string.Empty;
|
||||||
|
_receiverPopupValidationMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task OnReceiverEmailKeyDownAsync(KeyboardEventArgs e)
|
||||||
|
{
|
||||||
|
if (_receiverEmailSuggestions.Count == 0)
|
||||||
|
{
|
||||||
|
if (e.Key == "Escape")
|
||||||
|
_selectedReceiverEmailSuggestion = null;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentIndex = _selectedReceiverEmailSuggestion is null
|
||||||
|
? -1
|
||||||
|
: _receiverEmailSuggestions.FindIndex(email => string.Equals(email, _selectedReceiverEmailSuggestion, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (e.Key == "ArrowDown")
|
||||||
|
{
|
||||||
|
var nextIndex = currentIndex < _receiverEmailSuggestions.Count - 1 ? currentIndex + 1 : 0;
|
||||||
|
await OnReceiverEmailSuggestionSelectedAsync(_receiverEmailSuggestions[nextIndex]);
|
||||||
|
}
|
||||||
|
else if (e.Key == "ArrowUp")
|
||||||
|
{
|
||||||
|
var nextIndex = currentIndex > 0 ? currentIndex - 1 : _receiverEmailSuggestions.Count - 1;
|
||||||
|
await OnReceiverEmailSuggestionSelectedAsync(_receiverEmailSuggestions[nextIndex]);
|
||||||
|
}
|
||||||
|
else if (e.Key == "Enter")
|
||||||
|
{
|
||||||
|
var selectedValue = currentIndex >= 0 && currentIndex < _receiverEmailSuggestions.Count
|
||||||
|
? _receiverEmailSuggestions[currentIndex]
|
||||||
|
: _receiverEmailSuggestions.FirstOrDefault();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(selectedValue))
|
||||||
|
{
|
||||||
|
await OnReceiverEmailSuggestionSelectedAsync(selectedValue);
|
||||||
|
_receiverEmailSuggestions.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (e.Key == "Escape")
|
||||||
|
{
|
||||||
|
_receiverEmailSuggestions.Clear();
|
||||||
|
_selectedReceiverEmailSuggestion = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Task OnReceiverEmailSuggestionSelectedAsync(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
_selectedReceiverEmailSuggestion = value.Trim();
|
||||||
|
_receiverDraftEmail = _selectedReceiverEmailSuggestion;
|
||||||
|
_receiverPopupValidationMessage = null;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task OnReceiverEmailTextChangedAsync(string? value)
|
||||||
|
{
|
||||||
|
_receiverDraftEmail = value?.Trim() ?? string.Empty;
|
||||||
|
_selectedReceiverEmailSuggestion = _receiverDraftEmail;
|
||||||
|
_receiverPopupValidationMessage = null;
|
||||||
|
|
||||||
|
var searchVersion = ++_receiverEmailSearchVersion;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_receiverDraftEmail) || _receiverDraftEmail.Length < 2)
|
||||||
|
{
|
||||||
|
_receiverEmailSuggestions.Clear();
|
||||||
|
_selectedReceiverEmailSuggestion = null;
|
||||||
|
_isReceiverEmailSearchRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isReceiverEmailSearchRunning = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var results = await ReceiverPageDataService.SearchReceiverEMailsAsync(_receiverDraftEmail);
|
||||||
|
|
||||||
|
if (searchVersion != _receiverEmailSearchVersion)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_receiverEmailSuggestions = results
|
||||||
|
.Where(email => !string.IsNullOrWhiteSpace(email))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(email => email)
|
||||||
|
.Take(12)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_selectedReceiverEmailSuggestion = _receiverEmailSuggestions.FirstOrDefault(email =>
|
||||||
|
string.Equals(email, _receiverDraftEmail, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to load receiver email suggestions for {SearchTerm}", _receiverDraftEmail);
|
||||||
|
if (searchVersion == _receiverEmailSearchVersion)
|
||||||
|
{
|
||||||
|
_receiverEmailSuggestions.Clear();
|
||||||
|
_selectedReceiverEmailSuggestion = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (searchVersion == _receiverEmailSearchVersion)
|
||||||
|
_isReceiverEmailSearchRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Task SaveReceiverAsync()
|
||||||
|
{
|
||||||
|
var fullName = _receiverDraftName.Trim();
|
||||||
|
var email = _receiverDraftEmail.Trim();
|
||||||
|
var phoneNumber = _receiverDraftPhoneNumber.Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(fullName))
|
||||||
|
{
|
||||||
|
_receiverPopupValidationMessage = "Bitte geben Sie einen Vor- und Nachnamen ein.";
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(email))
|
||||||
|
{
|
||||||
|
_receiverPopupValidationMessage = "Bitte geben Sie eine E-Mail-Adresse ein.";
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ReceiverEmailValidator.IsValid(email))
|
||||||
|
{
|
||||||
|
_receiverPopupValidationMessage = "Bitte geben Sie eine gültige E-Mail-Adresse ein.";
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_receivers.Any(receiver => string.Equals(receiver.Email, email, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
_receiverPopupValidationMessage = "Diese E-Mail-Adresse wurde bereits hinzugefügt.";
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
_receivers.Add(new ReceiverDraft(Guid.NewGuid(), fullName, email, phoneNumber));
|
||||||
|
CloseAddReceiverPopup();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddSignatureForReceiver(ReceiverDraft receiver)
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Signature placement requested for receiver {Email}", receiver.Email);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (!_pdfLoaded || _errorMessage is not null)
|
if (!_pdfLoaded || _errorMessage is not null)
|
||||||
@@ -351,4 +678,6 @@
|
|||||||
record SignatureFieldDraft(double XPt, double YPt, int Page, double DisplayX, double DisplayY);
|
record SignatureFieldDraft(double XPt, double YPt, int Page, double DisplayX, double DisplayY);
|
||||||
|
|
||||||
record OverlayCoords(double RelX, double RelY, double ContainerW, double ContainerH);
|
record OverlayCoords(double RelX, double RelY, double ContainerW, double ContainerH);
|
||||||
|
|
||||||
|
record ReceiverDraft(Guid Id, string FullName, string Email, string PhoneNumber);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using EnvelopeGenerator.Application.Receivers.Queries;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
namespace EnvelopeGenerator.Server.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Controller für die Verwaltung von Empfängern.
|
/// Controller für die Verwaltung von Empfängern.
|
||||||
@@ -33,15 +33,16 @@ public class ReceiverController : ControllerBase
|
|||||||
/// <param name="receiver">Die Abfrageparameter, einschließlich E-Mail-Adresse und Signatur.</param>
|
/// <param name="receiver">Die Abfrageparameter, einschließlich E-Mail-Adresse und Signatur.</param>
|
||||||
/// <returns>Eine Liste von Empfängern oder ein Fehlerstatus.</returns>
|
/// <returns>Eine Liste von Empfängern oder ein Fehlerstatus.</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery receiver)
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery? receiver = null, [FromQuery] bool onlyEmailAddress = false)
|
||||||
{
|
{
|
||||||
if (!receiver.HasAnyCriteria)
|
var result = await _mediator.Send(receiver ?? new ReadReceiverQuery());
|
||||||
{
|
|
||||||
var all = await _mediator.Send(new ReadReceiverQuery());
|
|
||||||
return Ok(all);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _mediator.Send(receiver);
|
if (result is null)
|
||||||
return result is null ? NotFound() : Ok(result);
|
return NotFound();
|
||||||
|
else if (onlyEmailAddress)
|
||||||
|
return Ok(result.Select(r => r.EmailAddress).ToList());
|
||||||
|
else
|
||||||
|
return Ok(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,12 +4,14 @@ using EnvelopeGenerator.Application.Common.Dto;
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
using EnvelopeGenerator.Application.Documents.Queries;
|
using EnvelopeGenerator.Application.Documents.Queries;
|
||||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||||
|
using EnvelopeGenerator.Application.Receivers.Queries;
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
using EnvelopeGenerator.Server.Client.Models.Constants;
|
using EnvelopeGenerator.Server.Client.Models.Constants;
|
||||||
using EnvelopeGenerator.Server.Extensions;
|
using EnvelopeGenerator.Server.Extensions;
|
||||||
using EnvelopeGenerator.Server.Options;
|
using EnvelopeGenerator.Server.Options;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using ApplicationEnvelopeReceiverDto = EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto;
|
using ApplicationEnvelopeReceiverDto = EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto;
|
||||||
|
|
||||||
@@ -21,7 +23,8 @@ namespace EnvelopeGenerator.Server.Services;
|
|||||||
public class EnvelopeReceiverPageDataService(
|
public class EnvelopeReceiverPageDataService(
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IDistributedCache cache,
|
IDistributedCache cache,
|
||||||
IOptions<CacheOptions> cacheOptions)
|
IOptions<CacheOptions> cacheOptions,
|
||||||
|
IMemoryCache memoryCache)
|
||||||
{
|
{
|
||||||
private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
|
private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
|
||||||
|
|
||||||
@@ -101,4 +104,23 @@ public class EnvelopeReceiverPageDataService(
|
|||||||
Page = element.Page,
|
Page = element.Page,
|
||||||
SenderAppType = (EnvelopeGenerator.Server.Client.Models.Constants.SenderAppType)element.SenderAppType
|
SenderAppType = (EnvelopeGenerator.Server.Client.Models.Constants.SenderAppType)element.SenderAppType
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly string ReceiverEmailSearchCacheKey = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
public async Task<IEnumerable<string>> SearchReceiverEMailsAsync(string emailSearchTerm, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
|
||||||
|
return await memoryCache.GetOrCreateAsync(ReceiverEmailSearchCacheKey + emailSearchTerm, async entry =>
|
||||||
|
{
|
||||||
|
var query = new ReadReceiverQuery { EmailAddressSearch = emailSearchTerm };
|
||||||
|
var receivers = await mediator.Send(query, cancellationToken);
|
||||||
|
|
||||||
|
if(receivers.Any())
|
||||||
|
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30);
|
||||||
|
else
|
||||||
|
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
return receivers.Select(r => r.EmailAddress);
|
||||||
|
}) ?? [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -573,6 +573,285 @@ body.resizing {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sender-toolbar-action-btn {
|
||||||
|
min-width: auto;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-toolbar-action-btn--compact {
|
||||||
|
padding: 0.45rem 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.625rem;
|
||||||
|
padding: 0.75rem 0.9rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%);
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel__title-wrap {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel__title {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #4c1d95;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel__count {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 1.5rem;
|
||||||
|
min-height: 1.5rem;
|
||||||
|
padding: 0 0.45rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(79, 70, 229, 0.12);
|
||||||
|
color: #5b21b6;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel__add-btn .dxbl-btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn .dxbl-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 0.45rem 0.85rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.2);
|
||||||
|
background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%);
|
||||||
|
color: #1e293b;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn .dxbl-btn:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%);
|
||||||
|
border-color: rgba(126, 34, 206, 0.4);
|
||||||
|
color: #1e293b;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(126, 34, 206, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn .dxbl-btn:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 6px rgba(126, 34, 206, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn--add .dxbl-btn::before,
|
||||||
|
.pdf-toolbar-like-btn--signature .dxbl-btn::before {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn--add .dxbl-btn::before {
|
||||||
|
content: '+';
|
||||||
|
color: #7e22ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn--signature .dxbl-btn {
|
||||||
|
color: #7e22ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn--signature .dxbl-btn::before {
|
||||||
|
content: '?';
|
||||||
|
color: #7e22ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn--signature .dxbl-btn:hover:not(:disabled) {
|
||||||
|
color: #7e22ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel__empty {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-width: 220px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.88);
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.12);
|
||||||
|
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__body {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__name {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__email {
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #64748b;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__phone {
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #64748b;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup .dxbl-modal {
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup .dxbl-popup {
|
||||||
|
max-width: min(720px, calc(100vw - 2rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup .dxbl-popup-content {
|
||||||
|
padding: 1rem 1.25rem 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup .dxbl-popup-header {
|
||||||
|
padding: 0.95rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup .dxbl-popup-footer {
|
||||||
|
padding: 0.75rem 1.25rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 1rem 1.25rem;
|
||||||
|
align-items: start;
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__field {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__label {
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__field .dxbl-text-edit,
|
||||||
|
.sender-receiver-popup__field .dxbl-dropdown-edit {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__field .dxbl-input-editor,
|
||||||
|
.sender-receiver-popup__field .dxbl-text-edit-input {
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__suggestions-shell {
|
||||||
|
min-height: 188px;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__suggestions {
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.12);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__suggestions .dxbl-listbox {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__suggestions .dxbl-listbox-scroll-viewer {
|
||||||
|
max-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__hint {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__loading {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #4f46e5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__validation {
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(239, 68, 68, 0.08);
|
||||||
|
color: #b91c1c;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__footer .dxbl-btn {
|
||||||
|
min-width: 148px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.pdf-frame {
|
.pdf-frame {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@@ -775,6 +1054,30 @@ body.resizing {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel {
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__action {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__action {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.85rem;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.envelope-title {
|
.envelope-title {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user