Compare commits

..

8 Commits

Author SHA1 Message Date
5ef32a2b0c Adjust SenderEditor UI defaults and accordion styling
Changed the `DxAccordionItem` in `SenderEditorPage.razor` to be collapsed by default. Removed unused elements (`sender-settings-chip` and `sender-settings-grid`) from the accordion's content template.

Updated `envelope-viewer.css` to refine the accordion header's appearance: reduced font size, font weight, and padding, and added a `line-height` property for better spacing.
2026-10-02 15:23:57 +02:00
54d16eb3b8 Refactor SenderEditorPage for modernized UI design
Replaced inline styles with reusable CSS classes to improve maintainability, readability, and scalability. Updated the layout and styling of key components, including the action bar, title and message inputs, PDF metadata display, and buttons. Introduced a `DxAccordion` for envelope settings and redesigned the empty state and save result dialog. Added dynamic styling using CSS variables and centralized styles in `envelope-viewer.css`. Removed redundant inline styles and improved overall UI consistency.
2026-10-02 14:59:43 +02:00
ae3264253d Update ILogger type in SenderPage.razor
Changed the `ILogger` dependency injection type parameter from `EnvelopeSenderPage` to `SenderPage` to ensure alignment with the correct class name or intended logging context.
2026-10-02 14:49:38 +02:00
4cbfe52d5b Add localized strings and update localization logic
Added new localized strings to `Model.en.resx`, `Model.fr-FR.resx`,
and `Model.resx` for additional features/settings in English,
French, and German. Updated `SenderEditorPage.razor` to inject
`ModelLocalizer` and modified the `L` function to prioritize
localized strings from `ModelLocalizer` over `UiLocalizer`.
Included `@using EnvelopeGenerator.Domain.Resources` to ensure
access to the `Model` resource file. These changes improve
localization flexibility and maintainability.
2026-10-02 14:46:40 +02:00
27c6b017cd refactor (EnvelopeSenderPage): Rename to SenderPage.razor. 2026-10-02 14:30:11 +02:00
8378c33465 Refactor PDF rendering, UX, and security improvements
Refactored `PdfRenderController.cs` to replace JSON/base64 `byte[]` transport with binary streaming for PDF rendering, improving memory and latency performance for large files.

Introduced a DevExpress toast-based notification system using `DxToastProvider` and `IToastNotificationService` for consistent, actionable feedback.

Standardized request-to-domain mapping with AutoMapper/profile conventions or reflection-based mappers, enforced at the Core library level to reduce drift.

Addressed SQL injection risks by replacing dynamic SQL string composition with parameterized queries, conducted a repo-wide audit, and added CI guardrails.

Improved UX in `EnvelopeSenderPage.razor` by enforcing deterministic row double-click behavior and aligning row action buttons/tooltips.

Implemented SPA-style language switching for immediate UI culture updates without page reloads.

Updated the SQL Server connection string in `appsettings.json`.
2026-10-02 14:04:45 +02:00
c7c4e1194d Force full page reload on language change
Updated the `Navigation.NavigateTo` method in `LanguageControls.razor` to set the `forceLoad` parameter to `true`. This ensures that the application performs a full page reload when the language is changed, improving consistency in language updates.
2026-10-02 13:59:40 +02:00
3b367eff7f Remove EnvelopeSenderEditorPage.razor entirely
The `EnvelopeSenderEditorPage.razor` file was completely removed, including all Razor markup, Blazor components, JavaScript references, CSS styles, and C# code-behind logic.

Key changes:
- Removed the `/sender/editor_legacy` route and all associated imports (`@using`) and dependency injections (`@inject`).
- Deleted the entire HTML structure, including UI elements for PDF editing, signature placement, and receiver management.
- Removed all references to external CSS and JavaScript files.
- Deleted Blazor components like `DxPdfViewer`, `DxPopup`, and `DxTextBox`.
- Removed C# code for state management, lifecycle methods, event handlers, and helper methods.
- Eliminated localization, validation logic, session persistence, and cache management.
- Removed PDF manipulation logic (e.g., placeholder rendering with `PdfSharp`).
- Deleted receiver management logic, including email suggestions and validation.

This change removes the entire functionality of the `EnvelopeSenderEditorPage`, including its UI, backend logic, and external integrations.
2026-10-02 13:39:00 +02:00
9 changed files with 695 additions and 1472 deletions

View File

@@ -250,6 +250,7 @@ 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.
## Database ## Database

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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
@@ -22,6 +23,7 @@
@inject EnvelopeTypeService EnvelopeTypeService @inject EnvelopeTypeService EnvelopeTypeService
@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" />
@@ -32,64 +34,52 @@
@* ── 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 +87,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 +217,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 +227,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 +245,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 +254,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 +287,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 +308,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 +323,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 +342,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>.
&nbsp;<button @onclick="CancelPlacement" &nbsp;<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 +352,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 +398,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>
@@ -544,18 +522,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 +540,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 +559,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>
} }
@@ -1586,7 +1557,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;
} }
} }

View File

@@ -17,7 +17,7 @@
@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 ILogger<SenderPage> Logger
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
@inject AppVersionService AppVersion @inject AppVersionService AppVersion

View File

@@ -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;