Compare commits
28 Commits
bugfix/sav
...
feat/blazo
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f7519941a | |||
| 66e58b4a1a | |||
| de9d8c6f37 | |||
| 47f48ba12a | |||
| 60241c9c6d | |||
| 2a939b4cad | |||
| e89d471c6f | |||
| ece676fbcf | |||
| fac4ec28a4 | |||
| a3b11ed63f | |||
| 5627483589 | |||
| bf23e88cf8 | |||
| 9435561e78 | |||
| db1e91a3f6 | |||
| 0c664c0995 | |||
| 5db2f6f1e3 | |||
| a535e20b0e | |||
| bb35a68b1f | |||
| 715add7f51 | |||
| 621e22701a | |||
| 5ef32a2b0c | |||
| 54d16eb3b8 | |||
| ae3264253d | |||
| 4cbfe52d5b | |||
| 27c6b017cd | |||
| 8378c33465 | |||
| c7c4e1194d | |||
| 3b367eff7f |
32
AGENTS.md
32
AGENTS.md
@@ -63,6 +63,36 @@ This section explains every markdown file in the repo: what it contains, why it
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Markdown Audit Snapshot (2026-10-05)
|
||||||
|
|
||||||
|
The following table summarizes repository markdown files by practical status: completed, partially complete, or removable candidates.
|
||||||
|
|
||||||
|
| File | Type | Status | Missing / Gap | Removal Recommendation |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `AGENTS.md` | Agent onboarding | Partially complete | Route/migration statements can drift from real code over time. | Keep |
|
||||||
|
| `COPILOT_CONTEXT.md` | Architecture reference | Partially complete | Some route/render details may be out of sync with other docs; needs regular reconciliation. | Keep |
|
||||||
|
| `FORM_APPLICATION_CONTEXT.md` | Legacy-to-modern feature spec | Partially complete | Large sections still marked not implemented / needs implementation. | Keep |
|
||||||
|
| `RECEIVER_PDF_VIEWER_CONTEXT.md` | Receiver PDF viewer deep-dive | Partially complete | Long migration plan needs periodic sync with active implementation. | Keep |
|
||||||
|
| `SENDER_SAVE_EDIT_MIGRATION_PLAN.md` | Sender workflow implementation plan | Partially complete | No explicit implementation progress markers per phase. | Keep (until migration closes) |
|
||||||
|
| `SENDER_PDF_TOOLBAR_PLAN_EN.md` | Sender PDF toolbar plan | Incomplete | Explicit pending item remains (`Toolbar visual polish`). | Keep (until done) |
|
||||||
|
| `SENDER_EDITOR_WASM_PDF_EDITOR_IMPLEMENTATION_PLAN_en.md` | Sender editor WASM migration plan | Incomplete | Plan exists but completion/progress tracking is minimal. | Keep (until done) |
|
||||||
|
| `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md` | Deployment guide | Complete (operational) | No critical gap identified. | Keep |
|
||||||
|
| `envelope-generator-ui/README.md` | Angular scaffold README | Complete but generic | Template text only; low project-specific value. | Removable or replace with project-specific summary |
|
||||||
|
| `EnvelopeGenerator.Web/wwwroot/lib/typed.js@2.1.0/README.md` | Third-party vendor README | Complete (vendor) | Not project workflow documentation. | Keep with vendor assets |
|
||||||
|
| `EnvelopeGenerator.Web/wwwroot/lib/jquery-validation/LICENSE.md` | Third-party license | Complete (legal) | No gap. | Keep |
|
||||||
|
| `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/README.md` | Third-party vendor README | Complete (vendor) | Not project-specific guidance. | Keep with vendor assets |
|
||||||
|
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/README.md` | Legacy PSPDFKit note | Outdated | Mentions deprecated PSPDFKit-based flow; may mislead if reused. | Removable/archive |
|
||||||
|
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/typed.js@2.1.0/README.md` | Duplicate vendor README | Redundant | Duplicate copy under `ignore/`. | Removable/archive |
|
||||||
|
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/bootstrap-icons/README.md` | Vendor README in archive area | Secondary | Archive-only utility, not active docs. | Removable/archive |
|
||||||
|
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/jquery-validation/LICENSE.md` | Duplicate license in archive area | Redundant | Duplicate copy under `ignore/`. | Removable/archive (if legal policy allows) |
|
||||||
|
| `ignore/EnvelopeGenerator/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/README.md` | Duplicate Angular scaffold README | Redundant | Generic template + duplicate in archive path. | Removable/archive |
|
||||||
|
|
||||||
|
### Documentation Audit TODO
|
||||||
|
|
||||||
|
- [ ] Validate this markdown audit table against the current codebase and update statuses (`complete`, `partial`, `incomplete`, `removable`) when implementation changes land.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Active Architecture (Post-Migration)
|
## Active Architecture (Post-Migration)
|
||||||
|
|
||||||
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
||||||
@@ -250,6 +280,8 @@ Manual testing workflow:
|
|||||||
- Eliminate manual property-by-property request→domain mapping drift (example: `CreateEnvelopeCommandHandler`) by introducing a standardized mapping policy (AutoMapper/profile conventions or reflection/source-generator based mapper) and enforcing it across envelope workflows; this needs to be solved in the shared company Core library level so all services get the same guardrails by default.
|
- Eliminate manual property-by-property request→domain mapping drift (example: `CreateEnvelopeCommandHandler`) by introducing a standardized mapping policy (AutoMapper/profile conventions or reflection/source-generator based mapper) and enforcing it across envelope workflows; this needs to be solved in the shared company Core library level so all services get the same guardrails by default.
|
||||||
- P1 security workstream: remove dynamic SQL string composition/interpolation from active save/create/update paths (`string.Format`, `$"...{input}..."`, `ToSqlParam`) and migrate to parameterized queries only; treat this as a potential SQL injection vulnerability (stability + security risk), execute repo-wide audit, and add CI guardrails to block reintroduction.
|
- P1 security workstream: remove dynamic SQL string composition/interpolation from active save/create/update paths (`string.Format`, `$"...{input}..."`, `ToSqlParam`) and migrate to parameterized queries only; treat this as a potential SQL injection vulnerability (stability + security risk), execute repo-wide audit, and add CI guardrails to block reintroduction.
|
||||||
- Alternative UX design (Server component): in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`, enforce deterministic row double-click behavior as exactly one action (preview OR edit), and keep row action buttons/tooltips aligned with the same rule to eliminate ambiguous navigation.
|
- Alternative UX design (Server component): in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`, enforce deterministic row double-click behavior as exactly one action (preview OR edit), and keep row action buttons/tooltips aligned with the same rule to eliminate ambiguous navigation.
|
||||||
|
- Implement SPA-style language switching so the UI culture updates immediately without a full page reload.
|
||||||
|
- Implement sender-scoped caching for "recent receiver suggestions" used during envelope creation (wizard/editor flows): cache key should include sender identity, support short TTL + invalidation hooks after successful envelope receiver writes, and prevent cross-sender suggestion leakage.
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
|
|
||||||
|
public class RecentReceiverSuggestionDto
|
||||||
|
{
|
||||||
|
public required string FullName { get; init; }
|
||||||
|
|
||||||
|
public required string EmailAddress { get; init; }
|
||||||
|
|
||||||
|
public required DateTime LastUsedAt { get; init; }
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -16,5 +16,9 @@ public class MappingProfile : Profile
|
|||||||
public MappingProfile()
|
public MappingProfile()
|
||||||
{
|
{
|
||||||
CreateMap<Receiver, ReceiverDto>();
|
CreateMap<Receiver, ReceiverDto>();
|
||||||
|
|
||||||
|
CreateMap<EnvelopeReceiver, RecentReceiverSuggestionDto>()
|
||||||
|
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.Name))
|
||||||
|
.ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => src.Receiver.EmailAddress));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads recent receiver suggestions for envelope creation.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Sender scope is assigned via chain method to avoid exposing username mutation from API query binding.
|
||||||
|
/// </remarks>
|
||||||
|
public record ReadRecentReceiverSuggestionsQuery : IRequest<IReadOnlyList<RecentReceiverSuggestionDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sender username used for scoping suggestion results.
|
||||||
|
/// </summary>
|
||||||
|
internal string? Username { get; private init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desired number of results.
|
||||||
|
/// </summary>
|
||||||
|
public int? Take { get; init; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a new query instance scoped to the specified sender username.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">Sender username from authenticated claims.</param>
|
||||||
|
/// <returns>Scoped query instance.</returns>
|
||||||
|
public ReadRecentReceiverSuggestionsQuery ForSender(string username)
|
||||||
|
{
|
||||||
|
return this with { Username = username };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles <see cref="ReadRecentReceiverSuggestionsQuery"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class ReadRecentReceiverSuggestionsQueryHandler(IRepository<EnvelopeReceiver> repository)
|
||||||
|
: IRequestHandler<ReadRecentReceiverSuggestionsQuery, IReadOnlyList<RecentReceiverSuggestionDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves distinct recent receiver suggestions grouped by receiver email.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Suggestion query.</param>
|
||||||
|
/// <param name="cancel">Cancellation token.</param>
|
||||||
|
/// <returns>Ordered list of recent receiver suggestions.</returns>
|
||||||
|
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> Handle(ReadRecentReceiverSuggestionsQuery request, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var query = repository.Query
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(er => er.Receiver != null);
|
||||||
|
|
||||||
|
if (request.Username is not null)
|
||||||
|
query = query.Where(er => er.Envelope != null && er.Envelope.User.Username == request.Username);
|
||||||
|
|
||||||
|
// 1) For each email, compute the latest usage timestamp.
|
||||||
|
var latestPerEmail = query
|
||||||
|
.GroupBy(er => er.Receiver!.EmailAddress)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
EmailAddress = g.Key,
|
||||||
|
LastUsedAt = g.Max(er => er.AddedWhen),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2) Join back to rows that match the latest timestamp per email.
|
||||||
|
var latestRows =
|
||||||
|
from er in query
|
||||||
|
join latest in latestPerEmail
|
||||||
|
on new { EmailAddress = er.Receiver!.EmailAddress, LastUsedAt = er.AddedWhen }
|
||||||
|
equals new { latest.EmailAddress, latest.LastUsedAt }
|
||||||
|
select new
|
||||||
|
{
|
||||||
|
EmailAddress = er.Receiver!.EmailAddress,
|
||||||
|
FullName = er.Name,
|
||||||
|
LastUsedAt = er.AddedWhen,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3) Tie-break: if multiple rows share the same latest timestamp for an email,
|
||||||
|
// pick a deterministic name via Max(FullName), then order by most recent usage.
|
||||||
|
var suggestions = latestRows
|
||||||
|
.GroupBy(x => new { x.EmailAddress, x.LastUsedAt })
|
||||||
|
.Select(g => new RecentReceiverSuggestionDto
|
||||||
|
{
|
||||||
|
EmailAddress = g.Key.EmailAddress,
|
||||||
|
FullName = g.Max(x => x.FullName) ?? string.Empty,
|
||||||
|
LastUsedAt = g.Key.LastUsedAt,
|
||||||
|
})
|
||||||
|
.OrderByDescending(x => x.LastUsedAt)
|
||||||
|
.ThenBy(x => x.EmailAddress);
|
||||||
|
|
||||||
|
return request.Take is int take
|
||||||
|
? await suggestions.Take(take).ToListAsync(cancel)
|
||||||
|
: await suggestions.ToListAsync(cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -123,6 +123,9 @@
|
|||||||
<data name="Back" xml:space="preserve">
|
<data name="Back" xml:space="preserve">
|
||||||
<value>Zurück</value>
|
<value>Zurück</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Next" xml:space="preserve">
|
||||||
|
<value>Weiter</value>
|
||||||
|
</data>
|
||||||
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
||||||
<value>Klicken Sie auf den</value>
|
<value>Klicken Sie auf den</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -564,6 +567,12 @@
|
|||||||
<data name="AddReceiver" xml:space="preserve">
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
<value>Empfänger hinzufügen</value>
|
<value>Empfänger hinzufügen</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SenderWizard_RecentReceivers" xml:space="preserve">
|
||||||
|
<value>Zuletzt verwendete Empfänger</value>
|
||||||
|
</data>
|
||||||
|
<data name="Skip" xml:space="preserve">
|
||||||
|
<value>Überspringen</value>
|
||||||
|
</data>
|
||||||
<data name="DeleteReceiver" xml:space="preserve">
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
<value>Empfänger löschen</value>
|
<value>Empfänger löschen</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -123,6 +123,9 @@
|
|||||||
<data name="Back" xml:space="preserve">
|
<data name="Back" xml:space="preserve">
|
||||||
<value>Back</value>
|
<value>Back</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Next" xml:space="preserve">
|
||||||
|
<value>Next</value>
|
||||||
|
</data>
|
||||||
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
||||||
<value>Click the</value>
|
<value>Click the</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -564,6 +567,12 @@
|
|||||||
<data name="AddReceiver" xml:space="preserve">
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
<value>Add Receiver</value>
|
<value>Add Receiver</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SenderWizard_RecentReceivers" xml:space="preserve">
|
||||||
|
<value>Recently used recipients</value>
|
||||||
|
</data>
|
||||||
|
<data name="Skip" xml:space="preserve">
|
||||||
|
<value>Skip</value>
|
||||||
|
</data>
|
||||||
<data name="DeleteReceiver" xml:space="preserve">
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
<value>Delete Receiver</value>
|
<value>Delete Receiver</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -123,6 +123,9 @@
|
|||||||
<data name="Back" xml:space="preserve">
|
<data name="Back" xml:space="preserve">
|
||||||
<value>Retour</value>
|
<value>Retour</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Next" xml:space="preserve">
|
||||||
|
<value>Suivant</value>
|
||||||
|
</data>
|
||||||
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
<data name="AuthenticatorSetup_Prefix" xml:space="preserve">
|
||||||
<value>Cliquez sur</value>
|
<value>Cliquez sur</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -564,6 +567,12 @@
|
|||||||
<data name="AddReceiver" xml:space="preserve">
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
<value>Ajouter un destinataire</value>
|
<value>Ajouter un destinataire</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SenderWizard_RecentReceivers" xml:space="preserve">
|
||||||
|
<value>Destinataires recents</value>
|
||||||
|
</data>
|
||||||
|
<data name="Skip" xml:space="preserve">
|
||||||
|
<value>Passer</value>
|
||||||
|
</data>
|
||||||
<data name="DeleteReceiver" xml:space="preserve">
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
<value>Supprimer le destinataire</value>
|
<value>Supprimer le destinataire</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -276,4 +276,61 @@
|
|||||||
<data name="YesWithAttachment" xml:space="preserve">
|
<data name="YesWithAttachment" xml:space="preserve">
|
||||||
<value>Yes, with Attachment</value>
|
<value>Yes, with Attachment</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
<data name="ElectronicSignature" xml:space="preserve">
|
||||||
|
<value>Electronic Signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Envelope Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Language</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Certification</value>
|
||||||
|
</data>
|
||||||
|
<data name="TFAEnabled" xml:space="preserve">
|
||||||
|
<value>2FA enabled</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Use Access Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>Final Email to Creator</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>Final Email to Receivers</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Send Reminder Emails</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>First Reminder (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Reminder Interval (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Expires After (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningWhenDays" xml:space="preserve">
|
||||||
|
<value>Expiry Warning (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_EnvelopeSettings" xml:space="preserve">
|
||||||
|
<value>Envelope Settings</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_AdvancedOptional" xml:space="preserve">
|
||||||
|
<value>Advanced</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_SelectTemplate" xml:space="preserve">
|
||||||
|
<value>Select Template</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_Validation_ReminderInterval" xml:space="preserve">
|
||||||
|
<value>Please enter a reminder interval greater than 0 days.</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_Validation_FirstReminder" xml:space="preserve">
|
||||||
|
<value>Please enter valid days for the first reminder.</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_Validation_ExpiryWarning" xml:space="preserve">
|
||||||
|
<value>The expiry warning cannot be after the expiry date.</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
|
|||||||
@@ -113,4 +113,61 @@
|
|||||||
<data name="MessageInvitationSent" xml:space="preserve">
|
<data name="MessageInvitationSent" xml:space="preserve">
|
||||||
<value>Invitation envoyee</value>
|
<value>Invitation envoyee</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ElectronicSignature" xml:space="preserve">
|
||||||
|
<value>Signature electronique</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Type d'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Langue</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Certification</value>
|
||||||
|
</data>
|
||||||
|
<data name="TFAEnabled" xml:space="preserve">
|
||||||
|
<value>2FA active</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Utiliser un code d'acces</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>E-mail final au createur</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>E-mail final aux destinataires</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Envoyer des e-mails de rappel</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>Premier rappel (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Intervalle de rappel (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Expire apres (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningWhenDays" xml:space="preserve">
|
||||||
|
<value>Avertissement d'expiration (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_EnvelopeSettings" xml:space="preserve">
|
||||||
|
<value>Parametres de l'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_AdvancedOptional" xml:space="preserve">
|
||||||
|
<value>Avance</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_SelectTemplate" xml:space="preserve">
|
||||||
|
<value>Selectionner un modele</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_Validation_ReminderInterval" xml:space="preserve">
|
||||||
|
<value>Veuillez saisir un intervalle de rappel superieur a 0 jour.</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_Validation_FirstReminder" xml:space="preserve">
|
||||||
|
<value>Veuillez saisir des jours valides pour le premier rappel.</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_Validation_ExpiryWarning" xml:space="preserve">
|
||||||
|
<value>L'avertissement d'expiration ne peut pas etre apres la date d'expiration.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -279,4 +279,61 @@
|
|||||||
<data name="YesWithAttachment" xml:space="preserve">
|
<data name="YesWithAttachment" xml:space="preserve">
|
||||||
<value>Ja, mit Anhang</value>
|
<value>Ja, mit Anhang</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
<data name="ElectronicSignature" xml:space="preserve">
|
||||||
|
<value>Elektronische Signatur</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Umschlagtyp</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Sprache</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Zertifizierung</value>
|
||||||
|
</data>
|
||||||
|
<data name="TFAEnabled" xml:space="preserve">
|
||||||
|
<value>2FA aktiviert</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Zugriffscode verwenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>Finale E-Mail an Ersteller</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>Finale E-Mail an Empfänger</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Erinnerungs-E-Mails senden</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>Erste Erinnerung (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Erinnerungsintervall (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Läuft ab nach (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningWhenDays" xml:space="preserve">
|
||||||
|
<value>Ablaufwarnung (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_EnvelopeSettings" xml:space="preserve">
|
||||||
|
<value>Envelope-Einstellungen</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_AdvancedOptional" xml:space="preserve">
|
||||||
|
<value>Erweitert</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_SelectTemplate" xml:space="preserve">
|
||||||
|
<value>Vorlage auswählen</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_Validation_ReminderInterval" xml:space="preserve">
|
||||||
|
<value>Bitte geben Sie ein Erinnerungsintervall größer als 0 Tage ein.</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_Validation_FirstReminder" xml:space="preserve">
|
||||||
|
<value>Bitte geben Sie gültige Tage für die erste Erinnerung ein.</value>
|
||||||
|
</data>
|
||||||
|
<data name="SenderEditor_Validation_ExpiryWarning" xml:space="preserve">
|
||||||
|
<value>Die Ablaufwarnung darf nicht nach dem Ablaufdatum liegen.</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
|||||||
|
namespace EnvelopeGenerator.Server.Client.Components.Sender;
|
||||||
|
|
||||||
|
public sealed record CreateEnvelopeWizardResult(
|
||||||
|
string Title,
|
||||||
|
string Message,
|
||||||
|
int? EnvelopeTypeId,
|
||||||
|
string Language,
|
||||||
|
bool UseAccessCode,
|
||||||
|
bool TFAEnabled,
|
||||||
|
int CertificationType,
|
||||||
|
int FinalEmailToCreator,
|
||||||
|
int FinalEmailToReceivers,
|
||||||
|
bool SendReminderEmails,
|
||||||
|
int FirstReminderDays,
|
||||||
|
int ReminderIntervalDays,
|
||||||
|
int ExpiresWhenDays,
|
||||||
|
int ExpiresWarningWhenDays,
|
||||||
|
IReadOnlyList<CreateEnvelopeWizardReceiver> Receivers,
|
||||||
|
byte[]? DocumentBytes,
|
||||||
|
string? DocumentFileName);
|
||||||
|
|
||||||
|
public sealed record CreateEnvelopeWizardReceiver(string FullName, string EmailAddress);
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
|
||||||
|
<div class="sender-pdf-dropzone @DropZoneClass">
|
||||||
|
<div class="sender-pdf-dropzone__content">
|
||||||
|
<div class="sender-pdf-dropzone__title">@CurrentActionText</div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(DragDropHintText))
|
||||||
|
{
|
||||||
|
<div class="sender-pdf-dropzone__hint">@DragDropHintText</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InputFile OnChange="OnFileSelectedAsync"
|
||||||
|
accept="@Accept"
|
||||||
|
disabled="@Disabled"
|
||||||
|
@ondragenter="OnDragEnter"
|
||||||
|
@ondragleave="OnDragLeave"
|
||||||
|
@ondragover="OnDragOver"
|
||||||
|
@ondrop="OnDrop"
|
||||||
|
class="sender-pdf-dropzone__input" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public bool Disabled { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool HasDocument { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Accept { get; set; } = ".pdf";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string UploadText { get; set; } = "PDF hochladen";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string ReplaceText { get; set; } = "PDF ersetzen";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string DragDropHintText { get; set; } = "Datei hierhin ziehen oder klicken";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<InputFileChangeEventArgs> FileSelected { get; set; }
|
||||||
|
|
||||||
|
private int _dragDepth;
|
||||||
|
private bool _isDragActive;
|
||||||
|
|
||||||
|
private string CurrentActionText => HasDocument ? ReplaceText : UploadText;
|
||||||
|
|
||||||
|
private string DropZoneClass
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Disabled)
|
||||||
|
return "sender-pdf-dropzone--disabled";
|
||||||
|
|
||||||
|
return _isDragActive ? "sender-pdf-dropzone--drag-active" : string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragEnter(DragEventArgs _)
|
||||||
|
{
|
||||||
|
if (Disabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_dragDepth++;
|
||||||
|
_isDragActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragLeave(DragEventArgs _)
|
||||||
|
{
|
||||||
|
if (Disabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_dragDepth = Math.Max(0, _dragDepth - 1);
|
||||||
|
if (_dragDepth == 0)
|
||||||
|
_isDragActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragOver(DragEventArgs _)
|
||||||
|
{
|
||||||
|
if (Disabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isDragActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDrop(DragEventArgs _)
|
||||||
|
{
|
||||||
|
_dragDepth = 0;
|
||||||
|
_isDragActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnFileSelectedAsync(InputFileChangeEventArgs args)
|
||||||
|
{
|
||||||
|
_dragDepth = 0;
|
||||||
|
_isDragActive = false;
|
||||||
|
|
||||||
|
if (Disabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (FileSelected.HasDelegate)
|
||||||
|
await FileSelected.InvokeAsync(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
namespace EnvelopeGenerator.Server.Client.Components.Sender.ReceiverSuggestionFilter;
|
||||||
|
|
||||||
|
public readonly record struct ReceiverFilterCriteria
|
||||||
|
{
|
||||||
|
public readonly string FullName;
|
||||||
|
public readonly string EmailAddress;
|
||||||
|
|
||||||
|
public ReceiverFilterCriteria(string? fullName = "", string? emailAddress = "")
|
||||||
|
{
|
||||||
|
FullName = Normalize(fullName);
|
||||||
|
EmailAddress = Normalize(emailAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly bool Matches(ReceiverFilterCriteria criteria)
|
||||||
|
=> FullName.Contains(criteria.FullName, StringComparison.Ordinal)
|
||||||
|
&& EmailAddress.Contains(criteria.EmailAddress, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static string Normalize(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return string.Concat(text.Where(c => !char.IsWhiteSpace(c))).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Client.Components.Sender.ReceiverSuggestionFilter;
|
||||||
|
|
||||||
|
public class ReceiverSuggestionFilterHandler(IEnumerable<RecentReceiverSuggestionDto> suggestions)
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyList<Entry> _entries = suggestions
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.FullName) && !string.IsNullOrWhiteSpace(x.EmailAddress))
|
||||||
|
.Select(x => new Entry(new ReceiverFilterCriteria(x.FullName, x.EmailAddress), x))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private readonly record struct Entry(ReceiverFilterCriteria Criteria, RecentReceiverSuggestionDto Suggestion);
|
||||||
|
|
||||||
|
public IEnumerable<RecentReceiverSuggestionDto> Filter(ReceiverFilterCriteria criteria)
|
||||||
|
{
|
||||||
|
return _entries
|
||||||
|
.Where(x => x.Criteria.Matches(criteria))
|
||||||
|
.Select(x => x.Suggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<RecentReceiverSuggestionDto> Filter(string fullName, string emailAddress)
|
||||||
|
{
|
||||||
|
var criteria = new ReceiverFilterCriteria(fullName, emailAddress);
|
||||||
|
return Filter(criteria);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
_selected = option;
|
_selected = option;
|
||||||
_isOpen = false;
|
_isOpen = false;
|
||||||
await LocalizationService.SetLanguageAsync(option.Code);
|
await LocalizationService.SetLanguageAsync(option.Code);
|
||||||
Navigation.NavigateTo(Navigation.Uri, forceLoad: false);
|
Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record LanguageOption(string Code, string FlagClass, string ShortName);
|
private sealed record LanguageOption(string Code, string FlagClass, string ShortName);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
@using EnvelopeGenerator.Application.Envelopes.Commands
|
@using EnvelopeGenerator.Application.Envelopes.Commands
|
||||||
@using EnvelopeGenerator.Application.Resources
|
@using EnvelopeGenerator.Application.Resources
|
||||||
@using EnvelopeGenerator.Domain.Constants
|
@using EnvelopeGenerator.Domain.Constants
|
||||||
|
@using EnvelopeGenerator.Domain.Resources
|
||||||
@using EnvelopeGenerator.Server.Client.Components.PdfEditor
|
@using EnvelopeGenerator.Server.Client.Components.PdfEditor
|
||||||
@using EnvelopeGenerator.Server.Client.Services
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
@using Microsoft.AspNetCore.WebUtilities
|
@using Microsoft.AspNetCore.WebUtilities
|
||||||
@@ -20,76 +21,71 @@
|
|||||||
@inject ReceiverService ReceiverService
|
@inject ReceiverService ReceiverService
|
||||||
@inject EnvelopeService EnvelopeService
|
@inject EnvelopeService EnvelopeService
|
||||||
@inject EnvelopeTypeService EnvelopeTypeService
|
@inject EnvelopeTypeService EnvelopeTypeService
|
||||||
|
@inject CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService
|
||||||
@inject AuthService AuthService
|
@inject AuthService AuthService
|
||||||
@inject IStringLocalizer<Resource> UiLocalizer
|
@inject IStringLocalizer<Resource> UiLocalizer
|
||||||
|
@inject IStringLocalizer<Model> ModelLocalizer
|
||||||
|
|
||||||
<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="_content/DevExpress.Blazor.Viewer/css/dx-blazor-viewer-components.bs5.css" rel="stylesheet" />
|
<link href="_content/DevExpress.Blazor.Viewer/css/dx-blazor-viewer-components.bs5.css" rel="stylesheet" />
|
||||||
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
||||||
<script src="@AppVersion.GetVersionedUrl("js/envelope-editor.js")"></script>
|
<script src="@AppVersion.GetVersionedUrl("js/envelope-editor.js")"></script>
|
||||||
|
|
||||||
|
<DxLoadingPanel Visible="@IsBootstrapLoading"
|
||||||
|
IsContentBlocked="true"
|
||||||
|
ApplyBackgroundShading="true"
|
||||||
|
IndicatorAreaVisible="true"
|
||||||
|
Text='@L("SenderEditor_BootstrapLoadingHint", "Der Umschlag wird vorbereitet und die Daten werden uebernommen.")'>
|
||||||
<div class="envelope-viewer-layout">
|
<div class="envelope-viewer-layout">
|
||||||
|
|
||||||
@* ── Action Bar ── *@
|
@* ── Action Bar ── *@
|
||||||
<div class="envelope-action-bar">
|
<div class="envelope-action-bar">
|
||||||
<div class="envelope-action-bar__inner"
|
<div class="envelope-action-bar__inner sender-editor-action-inner">
|
||||||
style="flex-direction: row; align-items: center; padding: 0.35rem 1.5rem; gap: 0.75rem;">
|
|
||||||
|
|
||||||
@* Left: Title + meta inputs + receivers *@
|
@* Left: Title + meta inputs + receivers *@
|
||||||
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: stretch; gap: 0.5rem;">
|
<div class="sender-editor-main-column">
|
||||||
|
|
||||||
@* ── Title & Message inputs ── *@
|
@* ── Title & Message inputs ── *@
|
||||||
<div style="display: flex; align-items: flex-start; gap: 0.5rem; flex-wrap: wrap;">
|
<div class="sender-editor-meta-row">
|
||||||
|
|
||||||
@* Titel *@
|
@* Titel *@
|
||||||
<div style="display: flex; flex-direction: column; gap: 0.2rem; flex: 1 1 180px; min-width: 140px;">
|
<div class="sender-editor-input-field sender-editor-input-field--title">
|
||||||
<label style="font-size: 0.65rem; font-weight: 600; color: #6b7280;
|
<label class="sender-editor-label">
|
||||||
text-transform: uppercase; letter-spacing: 0.05em;">
|
@L("Title", "Titel") <span class="sender-editor-required-asterisk">*</span>
|
||||||
@L("Title", "Titel") <span style="color:#dc2626;">*</span>
|
|
||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
value="@_envelopeTitle"
|
value="@_envelopeTitle"
|
||||||
@oninput="e => _envelopeTitle = e.Value?.ToString() ?? string.Empty"
|
@oninput="e => _envelopeTitle = e.Value?.ToString() ?? string.Empty"
|
||||||
placeholder='@L("SenderEditor_Title_Placeholder", "z.B. Arbeitsvertrag 2025")'
|
placeholder='@L("SenderEditor_Title_Placeholder", "z.B. Arbeitsvertrag 2025")'
|
||||||
maxlength="120"
|
maxlength="120"
|
||||||
style="font-size: 0.78rem; font-weight: 500; color: #1f2937;
|
class="sender-editor-text-input @(_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle) ? "sender-editor-text-input--invalid" : string.Empty)" />
|
||||||
border: 1px solid #d1d5db; border-radius: 5px;
|
|
||||||
padding: 0.3rem 0.55rem; outline: none; width: 100%;
|
|
||||||
background: #fff;
|
|
||||||
@(_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle) ? "border-color:#dc2626;" : "")" />
|
|
||||||
@if (_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle))
|
@if (_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle))
|
||||||
{
|
{
|
||||||
<span style="font-size:0.6rem;color:#dc2626;">@L("RequiredField", "Pflichtfeld")</span>
|
<span class="sender-editor-required-text">@L("RequiredField", "Pflichtfeld")</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* Nachricht *@
|
@* Nachricht *@
|
||||||
<div style="display: flex; flex-direction: column; gap: 0.2rem; flex: 2 1 260px; min-width: 180px;">
|
<div class="sender-editor-input-field sender-editor-input-field--message">
|
||||||
<label style="font-size: 0.65rem; font-weight: 600; color: #6b7280;
|
<label class="sender-editor-label">
|
||||||
text-transform: uppercase; letter-spacing: 0.05em;">
|
@L("Message", "Nachricht") <span class="sender-editor-optional">(@L("Optional", "optional"))</span>
|
||||||
@L("Message", "Nachricht") <span style="color:#6b7280;font-weight:400;">(@L("Optional", "optional"))</span>
|
|
||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
value="@_envelopeMessage"
|
value="@_envelopeMessage"
|
||||||
@oninput="e => _envelopeMessage = e.Value?.ToString() ?? string.Empty"
|
@oninput="e => _envelopeMessage = e.Value?.ToString() ?? string.Empty"
|
||||||
placeholder='@L("SenderEditor_Message_Placeholder", "Bitte unterzeichnen Sie das beigefügte Dokument.")'
|
placeholder='@L("SenderEditor_Message_Placeholder", "Bitte unterzeichnen Sie das beigefügte Dokument.")'
|
||||||
maxlength="500"
|
maxlength="500"
|
||||||
style="font-size: 0.78rem; color: #1f2937;
|
class="sender-editor-text-input" />
|
||||||
border: 1px solid #d1d5db; border-radius: 5px;
|
|
||||||
padding: 0.3rem 0.55rem; outline: none; width: 100%;
|
|
||||||
background: #fff;" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* PDF filename badge *@
|
@* PDF filename badge *@
|
||||||
@if (_pdfLoaded)
|
@if (_pdfLoaded)
|
||||||
{
|
{
|
||||||
<div style="display:flex;align-items:flex-end;gap:0.4rem;padding-bottom:0.05rem;flex-shrink:0;">
|
<div class="sender-editor-file-meta">
|
||||||
<span style="font-size: 0.7rem; color: #6b7280;">@_fileName</span>
|
<span class="sender-editor-file-name">@_fileName</span>
|
||||||
@if (_signatureFields.Count > 0)
|
@if (_signatureFields.Count > 0)
|
||||||
{
|
{
|
||||||
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem;
|
<span class="sender-editor-signature-count">
|
||||||
background: #ede9fe; border-radius: 0.25rem; color: #6d28d9;
|
|
||||||
font-weight: 500; font-size: 0.7rem; white-space: nowrap;">
|
|
||||||
@_signatureFields.Count @L("SenderEditor_SignatureField", "Signaturfeld")@(_signatureFields.Count != 1 ? "er" : "")
|
@_signatureFields.Count @L("SenderEditor_SignatureField", "Signaturfeld")@(_signatureFields.Count != 1 ? "er" : "")
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -97,106 +93,107 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details style="border:1px solid #dbe5ef;border-radius:8px;background:#f9fbfd;">
|
<div class="sender-settings-shell">
|
||||||
<summary style="cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between;padding:0.55rem 0.7rem;font-size:0.74rem;font-weight:700;color:#1f2937;text-transform:uppercase;letter-spacing:0.04em;">
|
<DxAccordion ExpandMode="AccordionExpandMode.SingleOrNone" CssClass="sender-settings-accordion">
|
||||||
<span>@L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen")</span>
|
<Items>
|
||||||
<span style="font-size:0.66rem;color:#64748b;font-weight:600;">@L("SenderEditor_AdvancedOptional", "Erweitert")</span>
|
<DxAccordionItem Text='@L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen")' Expanded="false">
|
||||||
</summary>
|
<ContentTemplate>
|
||||||
|
<div class="sender-settings-body">
|
||||||
|
<div class="sender-settings-grid">
|
||||||
|
<div class="sender-settings-field">
|
||||||
|
<label class="sender-settings-label">@L("EnvelopeType", "Umschlagtyp")</label>
|
||||||
|
<select value="@(_selectedEnvelopeTypeId?.ToString() ?? string.Empty)"
|
||||||
|
@onchange="OnEnvelopeTypeChanged"
|
||||||
|
class="sender-settings-select">
|
||||||
|
<option value="">@L("SenderEditor_SelectTemplate", "Vorlage auswählen")</option>
|
||||||
|
@foreach (var type in _envelopeTypes)
|
||||||
|
{
|
||||||
|
<option value="@type.Id">@type.Title</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="padding:0 0.7rem 0.7rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:0.45rem;">
|
<div class="sender-settings-field">
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<label class="sender-settings-label">@L("Language", "Sprache")</label>
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("EnvelopeType", "Umschlagtyp")</label>
|
<select @bind="_language" class="sender-settings-select">
|
||||||
<select value="@(_selectedEnvelopeTypeId?.ToString() ?? string.Empty)"
|
<option value="de">Deutsch (de)</option>
|
||||||
@onchange="OnEnvelopeTypeChanged"
|
<option value="en">English (en)</option>
|
||||||
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
<option value="de-DE">Deutsch (de-DE)</option>
|
||||||
<option value="">@L("SenderEditor_SelectTemplate", "Vorlage auswählen")</option>
|
<option value="en-US">English (en-US)</option>
|
||||||
@foreach (var type in _envelopeTypes)
|
</select>
|
||||||
{
|
</div>
|
||||||
<option value="@type.Id">@type.Title</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div class="sender-settings-field">
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("Language", "Sprache")</label>
|
<label class="sender-settings-label">@L("CertificationType", "Zertifizierung")</label>
|
||||||
<select @bind="_language"
|
<select @bind="_certificationType" class="sender-settings-select">
|
||||||
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
<option value="1">@L("AdvancedElectronicSignature", "Advanced Electronic Signature")</option>
|
||||||
<option value="de">Deutsch (de)</option>
|
<option value="2">@L("ElectronicSignature", "Electronic Signature")</option>
|
||||||
<option value="en">English (en)</option>
|
<option value="3">@L("QualifiedSignature", "Qualified Signature")</option>
|
||||||
<option value="de-DE">Deutsch (de-DE)</option>
|
</select>
|
||||||
<option value="en-US">English (en-US)</option>
|
</div>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div class="sender-settings-toggles">
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("CertificationType", "Zertifizierung")</label>
|
<label class="sender-settings-toggle">
|
||||||
<select @bind="_certificationType"
|
<input type="checkbox" checked="@_tfaEnabled" @onchange="OnTfaChanged" />
|
||||||
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
@L("TFAEnabled", "2FA aktiviert")
|
||||||
<option value="1">@L("AdvancedElectronicSignature", "Advanced Electronic Signature")</option>
|
</label>
|
||||||
<option value="2">@L("ElectronicSignature", "Electronic Signature")</option>
|
<label class="sender-settings-toggle">
|
||||||
<option value="3">@L("QualifiedSignature", "Qualified Signature")</option>
|
<input type="checkbox" checked="@_useAccessCode" @onchange="OnUseAccessCodeChanged" disabled="@_tfaEnabled" />
|
||||||
</select>
|
@L("UseAccessCode", "Zugriffscode verwenden")
|
||||||
</div>
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.3rem;justify-content:flex-end;padding-bottom:0.25rem;">
|
<div class="sender-settings-field">
|
||||||
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
|
<label class="sender-settings-label">@L("FinalEmailToCreator", "Finale E-Mail an Ersteller")</label>
|
||||||
<input type="checkbox" checked="@_tfaEnabled" @onchange="OnTfaChanged" />
|
<select @bind="_finalEmailToCreator" class="sender-settings-select">
|
||||||
@L("TFAEnabled", "2FA aktiviert")
|
<option value="0">@L("No", "Nein")</option>
|
||||||
</label>
|
<option value="1">@L("Yes", "Ja")</option>
|
||||||
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
|
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
|
||||||
<input type="checkbox" checked="@_useAccessCode" @onchange="OnUseAccessCodeChanged" disabled="@_tfaEnabled" />
|
</select>
|
||||||
@L("UseAccessCode", "Zugriffscode verwenden")
|
</div>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div class="sender-settings-field">
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FinalEmailToCreator", "Finale E-Mail an Ersteller")</label>
|
<label class="sender-settings-label">@L("FinalEmailToReceivers", "Finale E-Mail an Empfänger")</label>
|
||||||
<select @bind="_finalEmailToCreator"
|
<select @bind="_finalEmailToReceivers" class="sender-settings-select">
|
||||||
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
<option value="0">@L("No", "Nein")</option>
|
||||||
<option value="0">@L("No", "Nein")</option>
|
<option value="1">@L("Yes", "Ja")</option>
|
||||||
<option value="1">@L("Yes", "Ja")</option>
|
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
|
||||||
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
|
</select>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div class="sender-settings-toggles sender-settings-toggles--single">
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FinalEmailToReceivers", "Finale E-Mail an Empfänger")</label>
|
<label class="sender-settings-toggle">
|
||||||
<select @bind="_finalEmailToReceivers"
|
<input type="checkbox" checked="@_sendReminderEmails" @onchange="OnSendReminderEmailsChanged" />
|
||||||
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
|
||||||
<option value="0">@L("No", "Nein")</option>
|
</label>
|
||||||
<option value="1">@L("Yes", "Ja")</option>
|
</div>
|
||||||
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.3rem;justify-content:flex-end;padding-bottom:0.25rem;">
|
<div class="sender-settings-field">
|
||||||
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
|
<label class="sender-settings-label">@L("FirstReminderDays", "Erste Erinnerung (Tage)")</label>
|
||||||
<input type="checkbox" checked="@_sendReminderEmails" @onchange="OnSendReminderEmailsChanged" />
|
<InputNumber @bind-Value="_firstReminderDays" class="form-control form-control-sm sender-settings-number" disabled="@(!_sendReminderEmails)" />
|
||||||
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
|
</div>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div class="sender-settings-field">
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FirstReminderDays", "Erste Erinnerung (Tage)")</label>
|
<label class="sender-settings-label">@L("ReminderIntervalDays", "Erinnerungsintervall (Tage)")</label>
|
||||||
<InputNumber @bind-Value="_firstReminderDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
|
<InputNumber @bind-Value="_reminderIntervalDays" class="form-control form-control-sm sender-settings-number" disabled="@(!_sendReminderEmails)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div class="sender-settings-field">
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ReminderIntervalDays", "Erinnerungsintervall (Tage)")</label>
|
<label class="sender-settings-label">@L("ExpiresWhenDays", "Läuft ab nach (Tage)")</label>
|
||||||
<InputNumber @bind-Value="_reminderIntervalDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
|
<InputNumber @bind-Value="_expiresWhenDays" class="form-control form-control-sm sender-settings-number" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div class="sender-settings-field">
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ExpiresWhenDays", "Läuft ab nach (Tage)")</label>
|
<label class="sender-settings-label">@L("ExpiresWarningWhenDays", "Ablaufwarnung (Tage)")</label>
|
||||||
<InputNumber @bind-Value="_expiresWhenDays" class="form-control form-control-sm" />
|
<InputNumber @bind-Value="_expiresWarningWhenDays" class="form-control form-control-sm sender-settings-number" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
</div>
|
||||||
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ExpiresWarningWhenDays", "Ablaufwarnung (Tage)")</label>
|
</ContentTemplate>
|
||||||
<InputNumber @bind-Value="_expiresWarningWhenDays" class="form-control form-control-sm" />
|
</DxAccordionItem>
|
||||||
</div>
|
</Items>
|
||||||
</div>
|
</DxAccordion>
|
||||||
</details>
|
</div>
|
||||||
|
|
||||||
<div class="sender-receivers-panel">
|
<div class="sender-receivers-panel">
|
||||||
<div class="sender-receivers-panel__header">
|
<div class="sender-receivers-panel__header">
|
||||||
@@ -226,8 +223,7 @@
|
|||||||
<div class="sender-receivers-list">
|
<div class="sender-receivers-list">
|
||||||
@foreach (var receiver in _receivers)
|
@foreach (var receiver in _receivers)
|
||||||
{
|
{
|
||||||
<div class="sender-receiver-chip"
|
<div class="sender-receiver-chip" style="--receiver-color:@receiver.Color;">
|
||||||
style="border-left: 3px solid @receiver.Color;">
|
|
||||||
<div class="sender-receiver-chip__body">
|
<div class="sender-receiver-chip__body">
|
||||||
<div class="sender-receiver-chip__name">@receiver.FullName</div>
|
<div class="sender-receiver-chip__name">@receiver.FullName</div>
|
||||||
<div class="sender-receiver-chip__email">@receiver.Email</div>
|
<div class="sender-receiver-chip__email">@receiver.Email</div>
|
||||||
@@ -237,11 +233,11 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact sender-receiver-chip__place-btn
|
||||||
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "pdf-toolbar__btn--signature-change-active" : "")"
|
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "sender-receiver-chip__place-btn--active" : "sender-receiver-chip__place-btn--idle")"
|
||||||
@onclick="() => ActivatePlacementForReceiver(receiver)"
|
@onclick="() => ActivatePlacementForReceiver(receiver)"
|
||||||
style="@(_pendingReceiverForPlacement?.Id == receiver.Id ? $"background:{receiver.Color};color:#fff;border-color:{receiver.Color};" : $"color:{receiver.Color};border-color:{receiver.Color};")"
|
style="--receiver-color:@receiver.Color;"
|
||||||
title='@L("SenderEditor_PlaceSignatureField", "Signaturfeld platzieren")'>
|
title='@L("SenderEditor_PlaceSignatureField", "Signaturfeld platzieren")'>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
<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" />
|
<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>
|
</svg>
|
||||||
@@ -255,7 +251,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* Right: Buttons *@
|
@* Right: Buttons *@
|
||||||
<div style="display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0;">
|
<div class="sender-editor-actions">
|
||||||
|
|
||||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
|
||||||
@onclick="Cancel"
|
@onclick="Cancel"
|
||||||
@@ -264,14 +260,12 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
@* PDF Upload *@
|
@* PDF Upload *@
|
||||||
<label class="pdf-toolbar__btn"
|
<label class="pdf-toolbar__btn sender-editor-upload-btn @((!_isPageReady || _isPdfUploadInProgress) ? "sender-editor-upload-btn--disabled" : string.Empty)"
|
||||||
title='@L("UploadPdf", "PDF hochladen")'
|
title='@L("UploadPdf", "PDF hochladen")'
|
||||||
style="cursor: @((_isPageReady && !_isPdfUploadInProgress) ? "pointer" : "not-allowed");
|
>
|
||||||
opacity:@((_isPageReady && !_isPdfUploadInProgress) ? "1" : "0.6");
|
|
||||||
display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.3rem 0.75rem; font-size: 0.75rem; font-weight: 500;">
|
|
||||||
@if (_isPdfUploadInProgress)
|
@if (_isPdfUploadInProgress)
|
||||||
{
|
{
|
||||||
<span class="spinner-border spinner-border-sm" role="status" style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
|
<span class="spinner-border spinner-border-sm sender-editor-spinner" role="status"></span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -299,7 +293,7 @@
|
|||||||
<InputFile OnChange="OnPdfFileSelectedAsync"
|
<InputFile OnChange="OnPdfFileSelectedAsync"
|
||||||
accept=".pdf"
|
accept=".pdf"
|
||||||
disabled="@(!_isPageReady || _isPdfUploadInProgress)"
|
disabled="@(!_isPageReady || _isPdfUploadInProgress)"
|
||||||
style="display: none;" />
|
class="sender-editor-hidden-input" />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@if (_pdfLoaded && _signatureFields.Count > 0)
|
@if (_pdfLoaded && _signatureFields.Count > 0)
|
||||||
@@ -320,8 +314,7 @@
|
|||||||
title='@L("Save", "Speichern")'>
|
title='@L("Save", "Speichern")'>
|
||||||
@if (_isSaving)
|
@if (_isSaving)
|
||||||
{
|
{
|
||||||
<span class="spinner-border spinner-border-sm me-1" role="status"
|
<span class="spinner-border spinner-border-sm me-1 sender-editor-spinner" role="status"></span>
|
||||||
style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -336,12 +329,10 @@
|
|||||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--success"
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--success"
|
||||||
@onclick="SendAsync"
|
@onclick="SendAsync"
|
||||||
disabled="@_isSaving"
|
disabled="@_isSaving"
|
||||||
title='@L("SendEnvelope", "Umschlag senden")'
|
title='@L("SendEnvelope", "Umschlag senden")'>
|
||||||
style="background: linear-gradient(135deg,#059669,#047857); color:#fff; border-radius:6px; padding:0.3rem 0.75rem; font-size:0.75rem; font-weight:600; border:none;">
|
|
||||||
@if (_isSaving)
|
@if (_isSaving)
|
||||||
{
|
{
|
||||||
<span class="spinner-border spinner-border-sm me-1" role="status"
|
<span class="spinner-border spinner-border-sm me-1 sender-editor-spinner" role="status"></span>
|
||||||
style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -357,11 +348,9 @@
|
|||||||
@* Placement mode hint bar *@
|
@* Placement mode hint bar *@
|
||||||
@if (_pendingReceiverForPlacement is not null)
|
@if (_pendingReceiverForPlacement is not null)
|
||||||
{
|
{
|
||||||
<div style="background: @_pendingReceiverForPlacement.Color; color: white; font-size: 0.75rem; font-weight: 500;
|
<div class="sender-editor-placement-hint" style="--placement-color:@_pendingReceiverForPlacement.Color;">
|
||||||
padding: 0.3rem 1.5rem; text-align: center; letter-spacing: 0.01em;">
|
|
||||||
📌 @L("SenderEditor_ClickDocumentFor", "Klicken Sie auf die Stelle im Dokument für") <strong>@_pendingReceiverForPlacement.FullName</strong>.
|
📌 @L("SenderEditor_ClickDocumentFor", "Klicken Sie auf die Stelle im Dokument für") <strong>@_pendingReceiverForPlacement.FullName</strong>.
|
||||||
<button @onclick="CancelPlacement"
|
<button @onclick="CancelPlacement" class="sender-editor-placement-cancel">
|
||||||
style="background:none;border:none;color:white;cursor:pointer;font-size:0.75rem;text-decoration:underline;padding:0;">
|
|
||||||
@L("Cancel", "Abbrechen")
|
@L("Cancel", "Abbrechen")
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -369,30 +358,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* ── Content ── *@
|
@* ── Content ── *@
|
||||||
<div class="envelope-content" style="padding: 0; overflow: hidden;">
|
<div class="envelope-content sender-editor-content">
|
||||||
@if (!_pdfLoaded)
|
@if (!_pdfLoaded)
|
||||||
{
|
{
|
||||||
@* Empty state *@
|
@* Empty state *@
|
||||||
<div class="d-flex justify-content-center align-items-center h-100">
|
<div class="d-flex justify-content-center align-items-center h-100">
|
||||||
<div class="text-center" style="max-width: 420px;">
|
<div class="text-center sender-editor-empty-state">
|
||||||
<div style="width: 72px; height: 72px; background: rgba(255,255,255,0.15);
|
<div class="sender-editor-empty-state__icon-wrap">
|
||||||
border-radius: 50%; display: flex; align-items: center;
|
|
||||||
justify-content: center; margin: 0 auto 1.25rem;">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="white" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="white" viewBox="0 0 16 16">
|
||||||
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z" />
|
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="text-white mb-2">@L("SenderEditor_NoDocumentLoaded", "Kein Dokument geladen")</h5>
|
<h5 class="text-white mb-2">@L("SenderEditor_NoDocumentLoaded", "Kein Dokument geladen")</h5>
|
||||||
<p class="text-white mb-4" style="opacity: 0.75; font-size: 0.85rem;">
|
<p class="text-white mb-4 sender-editor-empty-state__description">
|
||||||
@L("SenderEditor_UploadPdfToPlaceFields", "Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.")
|
@L("SenderEditor_UploadPdfToPlaceFields", "Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.")
|
||||||
</p>
|
</p>
|
||||||
<label class="btn btn-light btn-sm"
|
<label class="btn btn-light btn-sm sender-editor-empty-state__upload @((!_isPageReady || _isPdfUploadInProgress) ? "sender-editor-empty-state__upload--disabled" : string.Empty)">
|
||||||
style="cursor: @((_isPageReady && !_isPdfUploadInProgress) ? "pointer" : "not-allowed");
|
|
||||||
opacity:@((_isPageReady && !_isPdfUploadInProgress) ? "1" : "0.8");
|
|
||||||
font-weight: 500;">
|
|
||||||
@if (_isPdfUploadInProgress)
|
@if (_isPdfUploadInProgress)
|
||||||
{
|
{
|
||||||
<span class="spinner-border spinner-border-sm me-1" role="status" style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
|
<span class="spinner-border spinner-border-sm me-1 sender-editor-spinner" role="status"></span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -420,7 +404,7 @@
|
|||||||
<InputFile OnChange="OnPdfFileSelectedAsync"
|
<InputFile OnChange="OnPdfFileSelectedAsync"
|
||||||
accept=".pdf"
|
accept=".pdf"
|
||||||
disabled="@(!_isPageReady || _isPdfUploadInProgress)"
|
disabled="@(!_isPageReady || _isPdfUploadInProgress)"
|
||||||
style="display: none;" />
|
class="sender-editor-hidden-input" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -447,6 +431,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</DxLoadingPanel>
|
||||||
|
|
||||||
<DxPopup @bind-Visible="_receiverPopupVisible"
|
<DxPopup @bind-Visible="_receiverPopupVisible"
|
||||||
HeaderText='@L("AddReceiver", "Empfänger hinzufügen")'
|
HeaderText='@L("AddReceiver", "Empfänger hinzufügen")'
|
||||||
@@ -544,18 +529,17 @@
|
|||||||
<BodyContentTemplate>
|
<BodyContentTemplate>
|
||||||
@if (_saveErrorMessage is null)
|
@if (_saveErrorMessage is null)
|
||||||
{
|
{
|
||||||
<div style="display:flex;align-items:flex-start;gap:1rem;padding:0.5rem 0;">
|
<div class="sender-editor-save-result">
|
||||||
<div style="flex-shrink:0;width:40px;height:40px;background:#d1fae5;border-radius:50%;
|
<div class="sender-editor-save-result__icon sender-editor-save-result__icon--success">
|
||||||
display:flex;align-items:center;justify-content:center;">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#065f46" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#065f46" viewBox="0 0 16 16">
|
||||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
|
<p class="sender-editor-save-result__title">
|
||||||
@_saveSuccessTitle
|
@_saveSuccessTitle
|
||||||
</p>
|
</p>
|
||||||
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;">
|
<p class="sender-editor-save-result__message">
|
||||||
@_saveSuccessBody
|
@_saveSuccessBody
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -563,19 +547,18 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div style="display:flex;align-items:flex-start;gap:1rem;padding:0.5rem 0;">
|
<div class="sender-editor-save-result">
|
||||||
<div style="flex-shrink:0;width:40px;height:40px;background:#fee2e2;border-radius:50%;
|
<div class="sender-editor-save-result__icon sender-editor-save-result__icon--error">
|
||||||
display:flex;align-items:center;justify-content:center;">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#991b1b" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#991b1b" viewBox="0 0 16 16">
|
||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
|
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
|
<p class="sender-editor-save-result__title">
|
||||||
@L("SenderEditor_ErrorCreatingEnvelope", "Fehler beim Erstellen des Umschlags")
|
@L("SenderEditor_ErrorCreatingEnvelope", "Fehler beim Erstellen des Umschlags")
|
||||||
</p>
|
</p>
|
||||||
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;word-break:break-word;">
|
<p class="sender-editor-save-result__message sender-editor-save-result__message--break">
|
||||||
@_saveErrorMessage
|
@_saveErrorMessage
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -583,30 +566,25 @@
|
|||||||
}
|
}
|
||||||
</BodyContentTemplate>
|
</BodyContentTemplate>
|
||||||
<FooterContentTemplate>
|
<FooterContentTemplate>
|
||||||
<div style="display:flex;gap:0.5rem;justify-content:flex-end;width:100%;padding:0.5rem 0;">
|
<div class="sender-editor-save-actions">
|
||||||
@if (_saveErrorMessage is null)
|
@if (_saveErrorMessage is null)
|
||||||
{
|
{
|
||||||
<button class="btn btn-outline-secondary"
|
<button class="btn btn-outline-secondary sender-editor-save-actions__secondary"
|
||||||
@onclick="() => _savePopupVisible = false"
|
@onclick="() => _savePopupVisible = false">
|
||||||
style="border-radius:6px;padding:0.5rem 1.25rem;font-weight:500;">
|
|
||||||
@L("Close", "Schließen")
|
@L("Close", "Schließen")
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@if (_saveNavigateToDashboard)
|
@if (_saveNavigateToDashboard)
|
||||||
{
|
{
|
||||||
<button class="btn btn-primary"
|
<button class="btn btn-primary sender-editor-save-actions__primary" @onclick="GoToDashboard">
|
||||||
@onclick="GoToDashboard"
|
|
||||||
style="background:linear-gradient(135deg,#059669,#047857);border:none;border-radius:6px;
|
|
||||||
padding:0.5rem 1.5rem;font-weight:600;color:#fff;">
|
|
||||||
@L("SenderEditor_ToOverview", "Zur Übersicht")
|
@L("SenderEditor_ToOverview", "Zur Übersicht")
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<button class="btn btn-outline-secondary"
|
<button class="btn btn-outline-secondary sender-editor-save-actions__secondary"
|
||||||
@onclick="() => _savePopupVisible = false"
|
@onclick="() => _savePopupVisible = false">
|
||||||
style="border-radius:6px;padding:0.5rem 1.25rem;font-weight:500;">
|
|
||||||
@L("Close", "Schließen")
|
@L("Close", "Schließen")
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -618,6 +596,9 @@
|
|||||||
[SupplyParameterFromQuery(Name = "envelopeId")]
|
[SupplyParameterFromQuery(Name = "envelopeId")]
|
||||||
public int? EnvelopeId { get; set; }
|
public int? EnvelopeId { get; set; }
|
||||||
|
|
||||||
|
[SupplyParameterFromQuery(Name = "bootstrapToken")]
|
||||||
|
public string? BootstrapToken { get; set; }
|
||||||
|
|
||||||
// ── Constants ──
|
// ── Constants ──
|
||||||
// Signature field size in PDF points (fixed): 1.77" × 1.96"
|
// Signature field size in PDF points (fixed): 1.77" × 1.96"
|
||||||
const double SigWidthPt = 1.77 * 72; // 127.44 pt
|
const double SigWidthPt = 1.77 * 72; // 127.44 pt
|
||||||
@@ -638,6 +619,8 @@
|
|||||||
int? _loadedEnvelopeId;
|
int? _loadedEnvelopeId;
|
||||||
int? _editingEnvelopeId;
|
int? _editingEnvelopeId;
|
||||||
bool _isLoadingEnvelopeForEdit;
|
bool _isLoadingEnvelopeForEdit;
|
||||||
|
bool _bootstrapApplied;
|
||||||
|
string? _pendingBootstrapToken;
|
||||||
|
|
||||||
List<SignatureFieldDraft> _signatureFields = [];
|
List<SignatureFieldDraft> _signatureFields = [];
|
||||||
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
|
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
|
||||||
@@ -658,6 +641,7 @@
|
|||||||
string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen.";
|
string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen.";
|
||||||
string _saveSuccessBody = string.Empty;
|
string _saveSuccessBody = string.Empty;
|
||||||
bool _saveNavigateToDashboard = false;
|
bool _saveNavigateToDashboard = false;
|
||||||
|
bool _saveDraftAfterBootstrap;
|
||||||
|
|
||||||
// ── Envelope metadata ──
|
// ── Envelope metadata ──
|
||||||
string _envelopeTitle = string.Empty;
|
string _envelopeTitle = string.Empty;
|
||||||
@@ -692,6 +676,7 @@
|
|||||||
static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new();
|
static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new();
|
||||||
|
|
||||||
bool IsEditMode => EnvelopeId.HasValue && EnvelopeId.Value > 0;
|
bool IsEditMode => EnvelopeId.HasValue && EnvelopeId.Value > 0;
|
||||||
|
bool IsBootstrapLoading => !IsEditMode && !_bootstrapApplied;
|
||||||
|
|
||||||
// ── Lifecycle ──
|
// ── Lifecycle ──
|
||||||
|
|
||||||
@@ -732,19 +717,115 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsEditMode)
|
if (IsEditMode || _bootstrapApplied)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(BootstrapToken))
|
||||||
|
{
|
||||||
|
NavigationManager.NavigateTo("/sender", replace: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingBootstrapToken = BootstrapToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
async Task TryApplyBootstrapAsync(string token)
|
||||||
|
{
|
||||||
|
var payload = await CreateEnvelopeBootstrapStorageService.ConsumeAsync(token);
|
||||||
|
_bootstrapApplied = true;
|
||||||
|
|
||||||
|
if (payload is null)
|
||||||
|
{
|
||||||
|
NavigationManager.NavigateTo("/sender", replace: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_envelopeTitle = payload.Title ?? string.Empty;
|
||||||
|
_envelopeMessage = payload.Message ?? string.Empty;
|
||||||
|
_selectedEnvelopeTypeId = payload.EnvelopeTypeId;
|
||||||
|
_language = string.IsNullOrWhiteSpace(payload.Language) ? "de-DE" : payload.Language;
|
||||||
|
_useAccessCode = payload.UseAccessCode;
|
||||||
|
_tfaEnabled = payload.TFAEnabled;
|
||||||
|
_certificationType = payload.CertificationType;
|
||||||
|
_finalEmailToCreator = payload.FinalEmailToCreator;
|
||||||
|
_finalEmailToReceivers = payload.FinalEmailToReceivers;
|
||||||
|
_sendReminderEmails = payload.SendReminderEmails;
|
||||||
|
_firstReminderDays = payload.FirstReminderDays;
|
||||||
|
_reminderIntervalDays = payload.ReminderIntervalDays;
|
||||||
|
_expiresWhenDays = payload.ExpiresWhenDays;
|
||||||
|
_expiresWarningWhenDays = payload.ExpiresWarningWhenDays;
|
||||||
|
NormalizeSettings();
|
||||||
|
|
||||||
|
_receivers = payload.Receivers
|
||||||
|
.Select((receiver, index) => new ReceiverDraft(
|
||||||
|
Guid.NewGuid(),
|
||||||
|
receiver.FullName,
|
||||||
|
receiver.EmailAddress,
|
||||||
|
string.Empty,
|
||||||
|
ReceiverPalette[index % ReceiverPalette.Length]))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_originalPdfBytes = payload.DocumentBytes;
|
||||||
|
_fileName = string.IsNullOrWhiteSpace(payload.DocumentFileName) ? "document.pdf" : payload.DocumentFileName;
|
||||||
|
_pdfLoaded = _originalPdfBytes is { Length: > 0 };
|
||||||
|
|
||||||
|
_signatureFields.Clear();
|
||||||
|
_pendingReceiverForPlacement = null;
|
||||||
|
|
||||||
|
_saveDraftAfterBootstrap = true;
|
||||||
|
|
||||||
|
if (_pdfLoaded && _originalPdfBytes is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_pagePreviews = (await PdfRenderService.RenderPagesAsync(_originalPdfBytes))
|
||||||
|
.OrderBy(p => p.PageNumber)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to render bootstrap PDF previews.");
|
||||||
|
_pagePreviews = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_pageCount = _pagePreviews.Count > 0
|
||||||
|
? _pagePreviews.Count
|
||||||
|
: EstimatePdfPageCount(_originalPdfBytes) ?? 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pagePreviews = [];
|
||||||
|
_pageCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
_isPageReady = true;
|
_isPageReady = true;
|
||||||
StateHasChanged();
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!OperatingSystem.IsBrowser())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!IsEditMode && !_bootstrapApplied && !string.IsNullOrWhiteSpace(_pendingBootstrapToken))
|
||||||
|
{
|
||||||
|
var token = _pendingBootstrapToken;
|
||||||
|
_pendingBootstrapToken = null;
|
||||||
|
await TryApplyBootstrapAsync(token);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_saveDraftAfterBootstrap && !_isSaving)
|
||||||
|
{
|
||||||
|
_saveDraftAfterBootstrap = false;
|
||||||
|
await SaveDraftAndStayOnEditorAsync();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task LoadEnvelopeForEditAsync(int envelopeId)
|
async Task LoadEnvelopeForEditAsync(int envelopeId)
|
||||||
@@ -1137,12 +1218,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async Task SaveDraftAsync()
|
async Task SaveDraftAsync()
|
||||||
|
{
|
||||||
|
await SaveDraftCoreAsync(navigateToDashboard: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<int?> SaveDraftCoreAsync(bool navigateToDashboard)
|
||||||
{
|
{
|
||||||
if (!ValidateSettings(out var settingsError))
|
if (!ValidateSettings(out var settingsError))
|
||||||
{
|
{
|
||||||
_saveErrorMessage = settingsError;
|
_saveErrorMessage = settingsError;
|
||||||
_savePopupVisible = true;
|
_savePopupVisible = true;
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_isSaving = true;
|
_isSaving = true;
|
||||||
@@ -1163,25 +1249,37 @@
|
|||||||
_loadedEnvelopeId = result.Id;
|
_loadedEnvelopeId = result.Id;
|
||||||
_editingEnvelopeId = result.Id;
|
_editingEnvelopeId = result.Id;
|
||||||
|
|
||||||
if (previousEnvelopeId != result.Id)
|
if (navigateToDashboard)
|
||||||
|
{
|
||||||
|
NavigationManager.NavigateTo("/sender", forceLoad: false);
|
||||||
|
}
|
||||||
|
else if (previousEnvelopeId != result.Id)
|
||||||
{
|
{
|
||||||
NavigationManager.NavigateTo($"/sender/editor?envelopeId={result.Id}", forceLoad: false);
|
NavigationManager.NavigateTo($"/sender/editor?envelopeId={result.Id}", forceLoad: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationManager.NavigateTo("/sender", forceLoad: false);
|
return result.Id;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "[SenderEditor] Failed to save draft");
|
Logger.LogError(ex, "[SenderEditor] Failed to save draft");
|
||||||
_saveErrorMessage = ex.Message;
|
_saveErrorMessage = ex.Message;
|
||||||
_savePopupVisible = true;
|
_savePopupVisible = true;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_isSaving = false;
|
_isSaving = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task SaveDraftAndStayOnEditorAsync()
|
||||||
|
{
|
||||||
|
Logger.LogInformation("[SenderEditor] Trigger bootstrap draft save.");
|
||||||
|
await SaveDraftCoreAsync(navigateToDashboard: false);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Send envelope ──
|
// ── Send envelope ──
|
||||||
async Task SendAsync()
|
async Task SendAsync()
|
||||||
{
|
{
|
||||||
@@ -1586,7 +1684,11 @@
|
|||||||
|
|
||||||
string L(string key, string fallback)
|
string L(string key, string fallback)
|
||||||
{
|
{
|
||||||
var value = UiLocalizer[key];
|
var modelValue = ModelLocalizer[key];
|
||||||
return value.ResourceNotFound ? fallback : value.Value;
|
if (!modelValue.ResourceNotFound)
|
||||||
|
return modelValue.Value;
|
||||||
|
|
||||||
|
var uiValue = UiLocalizer[key];
|
||||||
|
return uiValue.ResourceNotFound ? fallback : uiValue.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,18 @@
|
|||||||
@using EnvelopeGenerator.Application.Common.Dto.History
|
@using EnvelopeGenerator.Application.Common.Dto.History
|
||||||
@using EnvelopeGenerator.Application.Resources
|
@using EnvelopeGenerator.Application.Resources
|
||||||
@using EnvelopeGenerator.Domain.Resources
|
@using EnvelopeGenerator.Domain.Resources
|
||||||
|
@using EnvelopeGenerator.Server.Client.Components.Sender
|
||||||
@using Microsoft.Extensions.Localization
|
@using Microsoft.Extensions.Localization
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService
|
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.DocumentService DocumentService
|
@inject EnvelopeGenerator.Server.Client.Services.DocumentService DocumentService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.PdfRenderService PdfRenderService
|
@inject EnvelopeGenerator.Server.Client.Services.PdfRenderService PdfRenderService
|
||||||
|
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeTypeService EnvelopeTypeService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService
|
@inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService
|
@inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
||||||
@inject ILogger<EnvelopeSenderPage> Logger
|
@inject EnvelopeGenerator.Server.Client.Services.CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService
|
||||||
|
@inject ILogger<SenderPage> Logger
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject AppVersionService AppVersion
|
@inject AppVersionService AppVersion
|
||||||
@@ -414,6 +417,11 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CreateEnvelopeWizardPopup Visible="@_createWizardVisible"
|
||||||
|
VisibleChanged="OnCreateWizardVisibleChangedAsync"
|
||||||
|
EnvelopeTypes="@_envelopeTypes"
|
||||||
|
StartEditorRequested="StartEditorFromWizardAsync" />
|
||||||
|
|
||||||
@* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@
|
@* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@
|
||||||
<DxPopup @bind-Visible="@_previewVisible"
|
<DxPopup @bind-Visible="@_previewVisible"
|
||||||
HeaderText="@($"{L("Preview", "Vorschau")} — {_selectedEnvelope?.Title} (#{_selectedEnvelope?.Id})")"
|
HeaderText="@($"{L("Preview", "Vorschau")} — {_selectedEnvelope?.Title} (#{_selectedEnvelope?.Id})")"
|
||||||
@@ -567,6 +575,9 @@
|
|||||||
private string _deleteReason = string.Empty;
|
private string _deleteReason = string.Empty;
|
||||||
private string? _deleteErrorMessage;
|
private string? _deleteErrorMessage;
|
||||||
|
|
||||||
|
private bool _createWizardVisible = false;
|
||||||
|
private IReadOnlyList<EnvelopeTypeDto> _envelopeTypes = [];
|
||||||
|
|
||||||
// Stable keys used to store/restore each tab's layout in UserPreferences.GridLayouts
|
// Stable keys used to store/restore each tab's layout in UserPreferences.GridLayouts
|
||||||
private const string GridKeyActive = "sender.active-envelopes";
|
private const string GridKeyActive = "sender.active-envelopes";
|
||||||
private const string GridKeyCompleted = "sender.completed-envelopes";
|
private const string GridKeyCompleted = "sender.completed-envelopes";
|
||||||
@@ -596,6 +607,16 @@
|
|||||||
_appLogo = logos.App;
|
_appLogo = logos.App;
|
||||||
_companyLogo = logos.Company;
|
_companyLogo = logos.Company;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to load envelope types for create wizard.");
|
||||||
|
_envelopeTypes = [];
|
||||||
|
}
|
||||||
|
|
||||||
await LoadEnvelopesAsync();
|
await LoadEnvelopesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,7 +714,40 @@
|
|||||||
|
|
||||||
void CreateEnvelope()
|
void CreateEnvelope()
|
||||||
{
|
{
|
||||||
Navigation.NavigateTo("/sender/editor");
|
_createWizardVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task OnCreateWizardVisibleChangedAsync(bool visible)
|
||||||
|
{
|
||||||
|
_createWizardVisible = visible;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task StartEditorFromWizardAsync(CreateEnvelopeWizardResult result)
|
||||||
|
{
|
||||||
|
var payload = new CreateEnvelopeBootstrapPayload(
|
||||||
|
Title: result.Title,
|
||||||
|
Message: result.Message,
|
||||||
|
EnvelopeTypeId: result.EnvelopeTypeId,
|
||||||
|
Language: result.Language,
|
||||||
|
UseAccessCode: result.UseAccessCode,
|
||||||
|
TFAEnabled: result.TFAEnabled,
|
||||||
|
CertificationType: result.CertificationType,
|
||||||
|
FinalEmailToCreator: result.FinalEmailToCreator,
|
||||||
|
FinalEmailToReceivers: result.FinalEmailToReceivers,
|
||||||
|
SendReminderEmails: result.SendReminderEmails,
|
||||||
|
FirstReminderDays: result.FirstReminderDays,
|
||||||
|
ReminderIntervalDays: result.ReminderIntervalDays,
|
||||||
|
ExpiresWhenDays: result.ExpiresWhenDays,
|
||||||
|
ExpiresWarningWhenDays: result.ExpiresWarningWhenDays,
|
||||||
|
Receivers: result.Receivers
|
||||||
|
.Select(x => new CreateEnvelopeBootstrapReceiver(x.FullName, x.EmailAddress))
|
||||||
|
.ToList(),
|
||||||
|
DocumentBytes: result.DocumentBytes,
|
||||||
|
DocumentFileName: result.DocumentFileName);
|
||||||
|
|
||||||
|
var token = await CreateEnvelopeBootstrapStorageService.StoreAsync(payload);
|
||||||
|
Navigation.NavigateTo($"/sender/editor?bootstrapToken={token}");
|
||||||
}
|
}
|
||||||
|
|
||||||
void EditEnvelope()
|
void EditEnvelope()
|
||||||
@@ -45,6 +45,7 @@ builder.Services.AddScoped<EnvelopeTypeService>();
|
|||||||
builder.Services.AddScoped<LocalizationService>();
|
builder.Services.AddScoped<LocalizationService>();
|
||||||
builder.Services.AddScoped<ReceiverService>();
|
builder.Services.AddScoped<ReceiverService>();
|
||||||
builder.Services.AddScoped<PdfRenderService>();
|
builder.Services.AddScoped<PdfRenderService>();
|
||||||
|
builder.Services.AddScoped<CreateEnvelopeBootstrapStorageService>();
|
||||||
|
|
||||||
// UI Preferences & Theme
|
// UI Preferences & Theme
|
||||||
builder.Services.AddScoped<UserPreferencesService>();
|
builder.Services.AddScoped<UserPreferencesService>();
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
|
public sealed class CreateEnvelopeBootstrapStorageService(IJSRuntime jsRuntime)
|
||||||
|
{
|
||||||
|
private const string StorageKeyPrefix = "sender.createEnvelope.bootstrap.";
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
public async Task<string> StoreAsync(CreateEnvelopeBootstrapPayload payload)
|
||||||
|
{
|
||||||
|
var token = Guid.NewGuid().ToString("N");
|
||||||
|
var key = BuildStorageKey(token);
|
||||||
|
var json = JsonSerializer.Serialize(payload, JsonOptions);
|
||||||
|
await jsRuntime.InvokeVoidAsync("sessionStorage.setItem", key, json);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CreateEnvelopeBootstrapPayload?> ConsumeAsync(string? token)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var key = BuildStorageKey(token);
|
||||||
|
var raw = await jsRuntime.InvokeAsync<string?>("sessionStorage.getItem", key);
|
||||||
|
await jsRuntime.InvokeVoidAsync("sessionStorage.removeItem", key);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<CreateEnvelopeBootstrapPayload>(raw, JsonOptions);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildStorageKey(string token) => $"{StorageKeyPrefix}{token}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record CreateEnvelopeBootstrapPayload(
|
||||||
|
string Title,
|
||||||
|
string Message,
|
||||||
|
int? EnvelopeTypeId,
|
||||||
|
string Language,
|
||||||
|
bool UseAccessCode,
|
||||||
|
bool TFAEnabled,
|
||||||
|
int CertificationType,
|
||||||
|
int FinalEmailToCreator,
|
||||||
|
int FinalEmailToReceivers,
|
||||||
|
bool SendReminderEmails,
|
||||||
|
int FirstReminderDays,
|
||||||
|
int ReminderIntervalDays,
|
||||||
|
int ExpiresWhenDays,
|
||||||
|
int ExpiresWarningWhenDays,
|
||||||
|
IReadOnlyList<CreateEnvelopeBootstrapReceiver> Receivers,
|
||||||
|
byte[]? DocumentBytes,
|
||||||
|
string? DocumentFileName);
|
||||||
|
|
||||||
|
public sealed record CreateEnvelopeBootstrapReceiver(string FullName, string EmailAddress);
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
@@ -26,4 +27,21 @@ public class ReceiverService(IHttpClientFactory httpClientFactory)
|
|||||||
var emails = await response.Content.ReadFromJsonAsync<List<string>>(JsonOptions, cancel);
|
var emails = await response.Content.ReadFromJsonAsync<List<string>>(JsonOptions, cancel);
|
||||||
return emails ?? [];
|
return emails ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches recent receiver suggestions for the authenticated sender.
|
||||||
|
/// Calls GET /api/EnvelopeReceiver/recent-suggestions?take={take}.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> GetRecentReceiverSuggestionsAsync(int take = 20, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
var url = $"/api/EnvelopeReceiver/recent-suggestions?take={take}";
|
||||||
|
var response = await http.GetAsync(url, cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var suggestions = await response.Content.ReadFromJsonAsync<List<RecentReceiverSuggestionDto>>(JsonOptions, cancel);
|
||||||
|
return suggestions ?? [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -112,6 +112,20 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
return name is null ? NotFound() : Ok(name);
|
return name is null ? NotFound() : Ok(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns recent receiver suggestions for the authenticated sender.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">Suggestion query options (take 3..5 is applied in handler).</param>
|
||||||
|
/// <param name="cancel">Cancellation token.</param>
|
||||||
|
/// <returns>List of recent distinct receiver suggestions.</returns>
|
||||||
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
[HttpGet("recent-suggestions")]
|
||||||
|
public async Task<IActionResult> GetRecentSuggestions([FromQuery] ReadRecentReceiverSuggestionsQuery query, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(query.ForSender(User.GetUsername()), cancel);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften.
|
/// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -278,4 +292,4 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -346,6 +346,7 @@ try
|
|||||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.ReceiverService>();
|
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.ReceiverService>();
|
||||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.PdfRenderService>();
|
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.PdfRenderService>();
|
||||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.EnvelopeTypeService>();
|
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.EnvelopeTypeService>();
|
||||||
|
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.CreateEnvelopeBootstrapStorageService>();
|
||||||
|
|
||||||
// Business Services (Server specific)
|
// Business Services (Server specific)
|
||||||
builder.Services.AddScoped<DocumentService>();
|
builder.Services.AddScoped<DocumentService>();
|
||||||
|
|||||||
@@ -1002,6 +1002,108 @@ body.resizing {
|
|||||||
border: 1px solid rgba(126, 34, 206, 0.12);
|
border: 1px solid rgba(126, 34, 206, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sender-settings-shell {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-settings-accordion {
|
||||||
|
border: 1px solid #dbe5ef;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f8fbff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-settings-accordion .dxbl-accordion-item {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-settings-accordion .dxbl-accordion-item-header {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
padding: 0.38rem 0.62rem;
|
||||||
|
background: rgba(124, 58, 237, 0.04);
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-settings-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.65rem;
|
||||||
|
padding: 0.25rem 0.7rem 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-settings-chip {
|
||||||
|
align-self: flex-start;
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #5b21b6;
|
||||||
|
background: rgba(126, 34, 206, 0.1);
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.15);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.12rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-settings-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-settings-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-settings-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #64748b;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-settings-select,
|
||||||
|
.sender-settings-number {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.3rem 0.45rem;
|
||||||
|
background: #fff;
|
||||||
|
min-height: 32px;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-settings-number:focus,
|
||||||
|
.sender-settings-select:focus {
|
||||||
|
border-color: rgba(126, 34, 206, 0.45);
|
||||||
|
box-shadow: 0 0 0 2px rgba(126, 34, 206, 0.12);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-settings-toggles {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.3rem;
|
||||||
|
padding-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-settings-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: #1f2937;
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-settings-toggles--single {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.sender-receivers-panel__header {
|
.sender-receivers-panel__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1119,9 +1221,275 @@ body.resizing {
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: rgba(255, 255, 255, 0.88);
|
background: rgba(255, 255, 255, 0.88);
|
||||||
border: 1px solid rgba(126, 34, 206, 0.12);
|
border: 1px solid rgba(126, 34, 206, 0.12);
|
||||||
|
border-left: 3px solid var(--receiver-color, #7c3aed);
|
||||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06);
|
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__place-btn--idle {
|
||||||
|
color: var(--receiver-color, #7c3aed);
|
||||||
|
border-color: var(--receiver-color, #7c3aed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__place-btn--active {
|
||||||
|
background: var(--receiver-color, #7c3aed);
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: var(--receiver-color, #7c3aed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-action-inner {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.35rem 1.5rem;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-main-column {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-input-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-input-field--title {
|
||||||
|
flex: 1 1 180px;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-input-field--message {
|
||||||
|
flex: 2 1 260px;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-required-asterisk {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-optional {
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-text-input {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #1f2937;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0.3rem 0.55rem;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-text-input--invalid {
|
||||||
|
border-color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-required-text {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-file-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding-bottom: 0.05rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-file-name {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-signature-count {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.125rem 0.4rem;
|
||||||
|
background: #ede9fe;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: #6d28d9;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-upload-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
padding: 0.3rem 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-upload-btn--disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-hidden-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-spinner {
|
||||||
|
width: 0.8rem;
|
||||||
|
height: 0.8rem;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-placement-hint {
|
||||||
|
background: var(--placement-color, #7c3aed);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.3rem 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-placement-cancel {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-decoration: underline;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-content {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-empty-state {
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-empty-state__icon-wrap {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-empty-state__description {
|
||||||
|
opacity: 0.75;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-empty-state__upload {
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-empty-state__upload--disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-save-result {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-save-result__icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-save-result__icon--success {
|
||||||
|
background: #d1fae5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-save-result__icon--error {
|
||||||
|
background: #fee2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-save-result__title {
|
||||||
|
margin: 0 0 0.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-save-result__message {
|
||||||
|
margin: 0;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-save-result__message--break {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-save-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-save-actions__secondary {
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-save-actions__primary {
|
||||||
|
background: linear-gradient(135deg, #059669, #047857);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.sender-receiver-chip__body {
|
.sender-receiver-chip__body {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -1094,3 +1094,557 @@
|
|||||||
[data-sf-theme="blazing-dark"] .sender-preview-error-msg {
|
[data-sf-theme="blazing-dark"] .sender-preview-error-msg {
|
||||||
color: rgba(255, 255, 255, 0.50);
|
color: rgba(255, 255, 255, 0.50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard-popup .dxbl-popup-content {
|
||||||
|
max-height: min(78vh, 760px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard-popup .dxbl-popup-footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__progress {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__progress-track {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
height: 2px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: rgba(126, 34, 206, 0.18);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__step {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__step-index {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #64748b;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__step--active .sender-create-wizard__step-index {
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: 0 0 0 3px rgba(126, 34, 206, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__step--done .sender-create-wizard__step-index {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #2a5298;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__description {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__stage {
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.12);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(126, 34, 206, 0.04);
|
||||||
|
padding: 0.85rem 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__stage p {
|
||||||
|
margin: 0;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__footer {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.sender-create-wizard__footer {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__footer .dxbl-btn {
|
||||||
|
flex: 1 1 calc(50% - 0.5rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__validation {
|
||||||
|
border: 1px solid rgba(220, 38, 38, 0.24);
|
||||||
|
background: rgba(254, 226, 226, 0.9);
|
||||||
|
color: #991b1b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__field--full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__settings-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__settings-group {
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.34);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(248, 250, 252, 0.86);
|
||||||
|
padding: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__settings-title {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #334155;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__settings-toggles {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.55rem 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__settings-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__label {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__required {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__optional {
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__select {
|
||||||
|
min-height: 38px;
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.25);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.45rem 0.55rem;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__add-receiver-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
min-height: 36px;
|
||||||
|
min-width: 220px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
border: 1px solid rgba(14, 116, 144, 0.28);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(236, 254, 255, 0.95);
|
||||||
|
color: #0f766e;
|
||||||
|
box-shadow: 0 6px 14px rgba(14, 116, 144, 0.15);
|
||||||
|
padding: 0.45rem 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__add-receiver-btn:hover:not(:disabled) {
|
||||||
|
background: rgba(207, 250, 254, 1);
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__add-receiver-btn:disabled {
|
||||||
|
opacity: 0.58;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__suggestions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__suggestions-title {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__suggestion-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
gap: 0.45rem;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__suggestion-item {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.12rem;
|
||||||
|
border: 1px solid rgba(14, 116, 144, 0.22);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(236, 254, 255, 0.85);
|
||||||
|
color: #0f172a;
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-tagbox-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-tagbox {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.18rem 0.45rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(236, 254, 255, 0.95);
|
||||||
|
border: 1px solid rgba(14, 116, 144, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-tag-name {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-tag-email {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-tag-remove {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #0f766e;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__tagbox-empty {
|
||||||
|
height: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__suggestion-item:disabled {
|
||||||
|
opacity: 0.58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__suggestion-name {
|
||||||
|
font-size: 0.77rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__suggestion-email {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #155e75;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.18);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.55rem 0.7rem;
|
||||||
|
background: rgba(248, 250, 252, 0.95);
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-name {
|
||||||
|
font-size: 0.86rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-email {
|
||||||
|
font-size: 0.79rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-remove {
|
||||||
|
border: 1px solid rgba(220, 38, 38, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(220, 38, 38, 0.08);
|
||||||
|
color: #b91c1c;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0.25rem 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__upload-stage {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__upload-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.28);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.55rem 0.9rem;
|
||||||
|
background: rgba(126, 34, 206, 0.08);
|
||||||
|
color: #4c1d95;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
font-weight: 700;
|
||||||
|
width: fit-content;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__upload-btn--disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__hidden-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-pdf-dropzone {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 92px;
|
||||||
|
border: 1px dashed rgba(100, 116, 139, 0.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(248, 250, 252, 0.9);
|
||||||
|
color: #334155;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 120ms ease, background-color 120ms ease, box-shadow 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-pdf-dropzone:hover {
|
||||||
|
border-color: rgba(126, 34, 206, 0.45);
|
||||||
|
background: rgba(241, 245, 249, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-pdf-dropzone--drag-active {
|
||||||
|
border-color: rgba(16, 185, 129, 0.75);
|
||||||
|
background: linear-gradient(180deg, rgba(16, 185, 129, 0.14), rgba(16, 185, 129, 0.08));
|
||||||
|
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-pdf-dropzone--disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-pdf-dropzone__content {
|
||||||
|
text-align: center;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.2rem;
|
||||||
|
padding: 0.75rem 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-pdf-dropzone__title {
|
||||||
|
font-size: 0.86rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-pdf-dropzone__hint {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-pdf-dropzone__input {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-pdf-dropzone--disabled .sender-pdf-dropzone__input {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__document-meta {
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(16, 185, 129, 0.08);
|
||||||
|
padding: 0.55rem 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__document-preview-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__document-preview-wrap {
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.38);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(248, 250, 252, 0.96);
|
||||||
|
height: 320px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__document-preview-wrap .sender-image-editor-shell {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__document-preview-wrap .sender-image-editor {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__document-name {
|
||||||
|
color: #064e3b;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__document-size {
|
||||||
|
color: #065f46;
|
||||||
|
font-size: 0.77rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__empty-list {
|
||||||
|
border: 1px dashed rgba(100, 116, 139, 0.4);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 0.83rem;
|
||||||
|
background: rgba(248, 250, 252, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.sender-create-wizard-popup .dxbl-popup {
|
||||||
|
width: 96vw !important;
|
||||||
|
max-width: 96vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__progress {
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__title {
|
||||||
|
font-size: 0.93rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__description {
|
||||||
|
font-size: 0.81rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__receiver-remove {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-pdf-dropzone {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__upload-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user