Refactored row selection logic to leverage DevExpress's native `@bind-SelectedDataItem` for automatic updates to the `_selectedEnvelope` variable. Added `AllowSelectRowByClick` to enable row selection via single click. Updated event handlers for layout auto-loading and auto-saving to use specific methods for different grids. Simplified `OnRowClick` method by removing redundant manual selection logic and retaining toggle behavior for deselecting the same row. Updated comments to reflect these changes.
651 lines
38 KiB
Plaintext
651 lines
38 KiB
Plaintext
@page "/sender"
|
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
|
@using System.Text.Json
|
|
@using EnvelopeGenerator.Domain.Constants
|
|
@using EnvelopeGenerator.Server.Client.Models
|
|
@using DevExpress.Blazor
|
|
@using EnvelopeGenerator.Server.Client.Services
|
|
@using EnvelopeGenerator.Application.Common.Dto
|
|
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService
|
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
|
@inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService
|
|
@inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService
|
|
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
|
@inject NavigationManager Navigation
|
|
@inject IJSRuntime JSRuntime
|
|
@inject AppVersionService AppVersion
|
|
|
|
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
|
<link href="@AppVersion.GetVersionedUrl("css/sender-page.css")" rel="stylesheet" />
|
|
|
|
<div class="sender-dashboard-layout">
|
|
<div class="sender-action-bar">
|
|
<div class="sender-action-bar__inner">
|
|
<div class="sender-title-section">
|
|
<div class="sender-logo">
|
|
@* signFLOW logo from appsettings.json — fallback to envelope SVG if not loaded *@
|
|
@if (_appLogo is not null && !string.IsNullOrEmpty(_appLogo.Src))
|
|
{
|
|
<img src="@_appLogo.Src"
|
|
style="height: @_appLogo.Height"
|
|
class="@_appLogo.GetClass("Main")"
|
|
alt="signFLOW" />
|
|
}
|
|
else
|
|
{
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 16 16">
|
|
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z" />
|
|
</svg>
|
|
}
|
|
</div>
|
|
<div class="sender-title">Umschlag-Übersicht</div>
|
|
</div>
|
|
|
|
@* ── Kunden-Logo (Mitte) ───────────────────────────────────────── *@
|
|
<div class="sender-customer-logo-area">
|
|
@if (_companyLogo is not null && !string.IsNullOrEmpty(_companyLogo.Src))
|
|
{
|
|
<img src="@_companyLogo.Src"
|
|
style="height: @_companyLogo.Height"
|
|
class="@_companyLogo.GetClass("Show")"
|
|
alt="Kunden-Logo" />
|
|
}
|
|
else
|
|
{
|
|
<div class="sf-customer-logo-placeholder">Kunden-Logo</div>
|
|
}
|
|
</div>
|
|
|
|
@* ── Toolbar (rechts) ─────────────────────────────────────────── *@
|
|
<div class="sender-toolbar">
|
|
<button class="sender-btn sender-btn--primary" @onclick="CreateEnvelope" title="Neuen Umschlag erstellen">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
|
|
</svg>
|
|
Neuer Umschlag
|
|
</button>
|
|
|
|
<button class="sender-btn" @onclick="EditEnvelope" disabled="@(_selectedEnvelope == null || IsEnvelopeSent(_selectedEnvelope))" title="Ausgewählten Umschlag bearbeiten">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
|
|
</svg>
|
|
Bearbeiten
|
|
</button>
|
|
|
|
<button class="sender-btn sender-btn--danger" @onclick="DeleteEnvelope" disabled="@(_selectedEnvelope == null)" title="Ausgewählten Umschlag löschen">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
|
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z" />
|
|
</svg>
|
|
Löschen
|
|
</button>
|
|
|
|
<button class="sender-btn" @onclick="RefreshEnvelopes" disabled="@_isLoading" title="Aktualisieren">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
|
|
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
|
|
</svg>
|
|
@if (_isLoading)
|
|
{
|
|
<span class="spinner-border spinner-border-sm" style="width: 14px; height: 14px;"></span>
|
|
}
|
|
</button>
|
|
|
|
<button class="sender-btn" @onclick="ShowPreview" disabled="@(_selectedEnvelope == null)" title="Ausgewählten Umschlag als PDF anzeigen">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
<path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/>
|
|
<path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/>
|
|
</svg>
|
|
Vorschau
|
|
</button>
|
|
|
|
<button class="sender-btn sender-btn--logout" @onclick="LogoutAsync" disabled="@_isLoggingOut" title="Abmelden">
|
|
@if (_isLoggingOut)
|
|
{
|
|
<span class="spinner-border spinner-border-sm" style="width: 14px; height: 14px;"></span>
|
|
}
|
|
else
|
|
{
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z" />
|
|
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z" />
|
|
</svg>
|
|
}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sender-content">
|
|
@if (_isLoading && _allEnvelopes == null)
|
|
{
|
|
<div class="d-flex justify-content-center align-items-center h-100">
|
|
<div class="text-center">
|
|
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
|
|
<span class="visually-hidden">Lädt...</span>
|
|
</div>
|
|
<p class="text-white fw-semibold">Umschläge werden geladen...</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
else if (_errorMessage != null)
|
|
{
|
|
<div class="error-container">
|
|
<div class="alert alert-danger shadow-lg">
|
|
<div class="d-flex align-items-start">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3 flex-shrink-0" 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="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>
|
|
<div>
|
|
<h5 class="mb-2">Fehler beim Laden der Umschläge</h5>
|
|
<p class="mb-0">@_errorMessage</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="sender-grid-container">
|
|
<div class="sender-tabs">
|
|
<button class="sender-tab @(_activeTab == "active" ? "sender-tab--active" : "")" @onclick='() => _activeTab = "active"'>
|
|
<span>Aktive Umschläge</span>
|
|
@if (_activeEnvelopes != null)
|
|
{
|
|
<span style="opacity: 0.6; margin-left: 0.5rem;">(@_activeEnvelopes.Count())</span>
|
|
}
|
|
</button>
|
|
<button class="sender-tab @(_activeTab == "completed" ? "sender-tab--active" : "")" @onclick='() => _activeTab = "completed"'>
|
|
<span>Abgeschlossene Umschläge</span>
|
|
@if (_completedEnvelopes != null)
|
|
{
|
|
<span style="opacity: 0.6; margin-left: 0.5rem;">(@_completedEnvelopes.Count())</span>
|
|
}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="sender-grid-wrapper">
|
|
|
|
@* Grid persistence hint — informs users their layout is auto-saved *@
|
|
<div class="sf-grid-persistence-hint">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" 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.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z" />
|
|
</svg>
|
|
Die Größe und Position der Spalten wird automatisch für Sie gespeichert.
|
|
</div>
|
|
@if (_activeTab == "active")
|
|
{
|
|
<DxGrid Data="@_activeEnvelopes"
|
|
@ref="_gridActive"
|
|
ShowFilterRow="true"
|
|
ShowSearchBox="true"
|
|
AllowColumnReorder="true"
|
|
AllowSort=true
|
|
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
|
PageSize="20"
|
|
PagerVisible="true"
|
|
SelectionMode="GridSelectionMode.Single"
|
|
AllowSelectRowByClick="true"
|
|
SelectedDataItem="@_selectedEnvelope"
|
|
SelectedDataItemChanged="@((object o) => _selectedEnvelope = o as EnvelopeDto)"
|
|
RowClick="OnRowClick"
|
|
RowDoubleClick="OnRowDoubleClick"
|
|
CustomizeElement="OnCustomizeElement"
|
|
LayoutAutoLoading="OnActiveGridLayoutAutoLoading"
|
|
LayoutAutoSaving="OnActiveGridLayoutAutoSaving">
|
|
<Columns>
|
|
<DxGridDataColumn FieldName="Id" Caption="ID"
|
|
SortIndex="0"
|
|
SortOrder="GridColumnSortOrder.Descending">
|
|
<CellDisplayTemplate Context="cellContext">
|
|
@((cellContext.DataItem as EnvelopeDto)?.Id)
|
|
</CellDisplayTemplate>
|
|
</DxGridDataColumn>
|
|
<DxGridDataColumn FieldName="Title" Caption="Titel">
|
|
<CellDisplayTemplate Context="cellContext">
|
|
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
|
|
</CellDisplayTemplate>
|
|
</DxGridDataColumn>
|
|
<DxGridDataColumn FieldName="Status" Caption="Status">
|
|
<CellDisplayTemplate Context="cellContext">
|
|
@{
|
|
var envelope = cellContext.DataItem as EnvelopeDto;
|
|
if (envelope != null)
|
|
{
|
|
var statusInfo = GetStatusInfo(envelope.Status);
|
|
<div class="status-badge status-badge--@statusInfo.CssClass">
|
|
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
|
@statusInfo.Label
|
|
</div>
|
|
}
|
|
}
|
|
</CellDisplayTemplate>
|
|
</DxGridDataColumn>
|
|
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="Empfänger">
|
|
<CellDisplayTemplate Context="cellContext">
|
|
@{
|
|
var envelope = cellContext.DataItem as EnvelopeDto;
|
|
if (envelope != null)
|
|
{
|
|
var receivers = envelope.EnvelopeReceivers?.ToList() ?? [];
|
|
var signed = receivers.Count(r => r.Signed);
|
|
var total = receivers.Count;
|
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
<span style="font-size: 0.875rem; color: #6b7280;">
|
|
@signed / @total unterschrieben
|
|
</span>
|
|
@if (total > 0)
|
|
{
|
|
<div style="flex: 1; min-width: 60px; max-width: 120px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden;">
|
|
<div style="height: 100%; background: linear-gradient(90deg, #81c784 0%, #66bb6a 100%); width: @((signed * 100.0 / total).ToString("F0"))%;"></div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
</CellDisplayTemplate>
|
|
</DxGridDataColumn>
|
|
<DxGridDataColumn FieldName="EnvelopeTypeTitle" Caption="Typ"
|
|
Width="120px" AllowSort="true" />
|
|
<DxGridDataColumn FieldName="AddedWhen" Caption="Erstellt"
|
|
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
|
<DxGridDataColumn FieldName="ChangedWhen" Caption="Geändert am"
|
|
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
|
</Columns>
|
|
<DetailRowTemplate Context="detailContext">
|
|
<div style="padding: 1rem; background: #f9fafb;">
|
|
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">Empfänger</h6>
|
|
@{
|
|
var envelope = detailContext.DataItem as EnvelopeDto;
|
|
if (envelope?.EnvelopeReceivers?.Any() == true)
|
|
{
|
|
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
|
@foreach (var receiver in envelope.EnvelopeReceivers)
|
|
{
|
|
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
|
<span class="receiver-badge receiver-badge--@(receiver.Signed ? "signed" : "unsigned")" style="min-width: 100px;">
|
|
@if (receiver.Signed)
|
|
{
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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" />
|
|
</svg>
|
|
<span>Unterschrieben</span>
|
|
}
|
|
else
|
|
{
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
|
</svg>
|
|
<span>Ausstehend</span>
|
|
}
|
|
</span>
|
|
<div style="flex: 1; font-size: 0.875rem;">
|
|
<strong style="color: #1f2937;">@receiver.Name</strong>
|
|
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Receiver?.EmailAddress</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">Keine Empfänger</p>
|
|
}
|
|
}
|
|
</div>
|
|
</DetailRowTemplate>
|
|
</DxGrid>
|
|
}
|
|
else
|
|
{
|
|
<DxGrid Data="@_completedEnvelopes"
|
|
@ref="_gridCompleted"
|
|
ShowFilterRow="true"
|
|
ShowSearchBox="true"
|
|
PageSize="20"
|
|
PagerVisible="true"
|
|
SelectionMode="GridSelectionMode.Single"
|
|
AllowSelectRowByClick="true"
|
|
SelectedDataItem="@_selectedEnvelope"
|
|
SelectedDataItemChanged="@((object o) => _selectedEnvelope = o as EnvelopeDto)"
|
|
RowClick="OnRowClick"
|
|
RowDoubleClick="OnRowDoubleClick"
|
|
CustomizeElement="OnCustomizeElement"
|
|
LayoutAutoLoading="OnCompletedGridLayoutAutoLoading"
|
|
LayoutAutoSaving="OnCompletedGridLayoutAutoSaving">
|
|
<Columns>
|
|
<DxGridDataColumn FieldName="Id" Caption="ID"
|
|
SortIndex="0"
|
|
SortOrder="GridColumnSortOrder.Descending">
|
|
<CellDisplayTemplate Context="cellContext">
|
|
@((cellContext.DataItem as EnvelopeDto)?.Id)
|
|
</CellDisplayTemplate>
|
|
</DxGridDataColumn>
|
|
<DxGridDataColumn FieldName="Title" Caption="Titel">
|
|
<CellDisplayTemplate Context="cellContext">
|
|
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
|
|
</CellDisplayTemplate>
|
|
</DxGridDataColumn>
|
|
<DxGridDataColumn FieldName="Status" Caption="Status">
|
|
<CellDisplayTemplate Context="cellContext">
|
|
@{
|
|
var envelope = cellContext.DataItem as EnvelopeDto;
|
|
if (envelope != null)
|
|
{
|
|
var statusInfo = GetStatusInfo(envelope.Status);
|
|
<div class="status-badge status-badge--@statusInfo.CssClass">
|
|
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
|
@statusInfo.Label
|
|
</div>
|
|
}
|
|
}
|
|
</CellDisplayTemplate>
|
|
</DxGridDataColumn>
|
|
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="Empfänger">
|
|
<CellDisplayTemplate Context="cellContext">
|
|
@{
|
|
var envelope = cellContext.DataItem as EnvelopeDto;
|
|
if (envelope != null)
|
|
{
|
|
var receivers = envelope.EnvelopeReceivers?.ToList() ?? [];
|
|
var signed = receivers.Count(r => r.Signed);
|
|
var total = receivers.Count;
|
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
<span style="font-size: 0.875rem; color: #6b7280;">
|
|
@signed / @total unterschrieben
|
|
</span>
|
|
@if (total > 0)
|
|
{
|
|
<div style="flex: 1; min-width: 60px; max-width: 120px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden;">
|
|
<div style="height: 100%; background: linear-gradient(90deg, #81c784 0%, #66bb6a 100%); width: @((signed * 100.0 / total).ToString("F0"))%;"></div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
</CellDisplayTemplate>
|
|
</DxGridDataColumn>
|
|
<DxGridDataColumn FieldName="EnvelopeTypeTitle" Caption="Typ"
|
|
Width="120px" AllowSort="true" />
|
|
<DxGridDataColumn FieldName="AddedWhen" Caption="Erstellt"
|
|
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
|
<DxGridDataColumn FieldName="ChangedWhen" Caption="Geändert am"
|
|
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
|
</Columns>
|
|
<DetailRowTemplate Context="detailContext">
|
|
<div style="padding: 1rem; background: #f9fafb;">
|
|
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">Empfänger</h6>
|
|
@{
|
|
var envelope = detailContext.DataItem as EnvelopeDto;
|
|
if (envelope?.EnvelopeReceivers?.Any() == true)
|
|
{
|
|
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
|
@foreach (var receiver in envelope.EnvelopeReceivers)
|
|
{
|
|
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
|
<span class="receiver-badge receiver-badge--@(receiver.Signed ? "signed" : "unsigned")" style="min-width: 100px;">
|
|
@if (receiver.Signed)
|
|
{
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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" />
|
|
</svg>
|
|
<span>Unterschrieben</span>
|
|
}
|
|
else
|
|
{
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
|
</svg>
|
|
<span>Ausstehend</span>
|
|
}
|
|
</span>
|
|
<div style="flex: 1; font-size: 0.875rem;">
|
|
<strong style="color: #1f2937;">@receiver.Name</strong>
|
|
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Receiver?.EmailAddress</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">Keine Empfänger</p>
|
|
}
|
|
}
|
|
</div>
|
|
</DetailRowTemplate>
|
|
</DxGrid>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@
|
|
<DxPopup @bind-Visible="@_previewVisible"
|
|
HeaderText="@($"Vorschau — Umschlag #{_selectedEnvelope?.Id}")"
|
|
Width="480px"
|
|
CloseOnOutsideClick="true"
|
|
ShowFooter="true">
|
|
<BodyContentTemplate>
|
|
<div style="padding: 1rem;">
|
|
<p><strong>Umschlag-ID:</strong> @_selectedEnvelope?.Id</p>
|
|
<p><strong>Titel:</strong> @_selectedEnvelope?.Title</p>
|
|
</div>
|
|
</BodyContentTemplate>
|
|
<FooterContentTemplate>
|
|
<DxButton CssClass="popup-button my-1 ms-2"
|
|
RenderStyle="ButtonRenderStyle.Secondary"
|
|
Text="Schließen"
|
|
Click="@(() => _previewVisible = false)" />
|
|
</FooterContentTemplate>
|
|
</DxPopup>
|
|
|
|
@code {
|
|
private IEnumerable<EnvelopeDto>? _allEnvelopes;
|
|
private IEnumerable<EnvelopeDto>? _activeEnvelopes;
|
|
private IEnumerable<EnvelopeDto>? _completedEnvelopes;
|
|
private EnvelopeDto? _selectedEnvelope;
|
|
private string _activeTab = "active";
|
|
private bool _isLoading = true;
|
|
private bool _isLoggingOut = false;
|
|
private string? _errorMessage;
|
|
private DxGrid? _gridActive;
|
|
private DxGrid? _gridCompleted;
|
|
|
|
// Logo config loaded from /api/CustomImages after auth
|
|
private EnvelopeGenerator.Server.Client.Services.CustomImageDto? _appLogo;
|
|
private EnvelopeGenerator.Server.Client.Services.CustomImageDto? _companyLogo;
|
|
|
|
// Preview popup
|
|
private bool _previewVisible = false;
|
|
|
|
// Stable keys used to store/restore each grid's layout in UserPreferences.GridLayouts
|
|
private const string GridKeyActive = "sender.active-envelopes";
|
|
private const string GridKeyCompleted = "sender.completed-envelopes";
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
var hasAccess = await AuthService.CheckSenderAccessAsync();
|
|
if (!hasAccess)
|
|
{
|
|
Navigation.NavigateTo($"/sender/login");
|
|
return;
|
|
}
|
|
|
|
// Load theme + DisplayName from server after authentication.
|
|
// Non-fatal: if this fails the page still loads with default theme.
|
|
// ThemeService.ScheduleSaveAsync() already logs errors internally.
|
|
try { await ThemeService.LoadFromServerAsync(); }
|
|
catch { /* swallow — default theme remains active */ }
|
|
|
|
// Load logo config — authenticated, so this should succeed
|
|
var logos = await CustomImagesService.GetAsync();
|
|
_appLogo = logos.App;
|
|
_companyLogo = logos.Company;
|
|
|
|
await LoadEnvelopesAsync();
|
|
}
|
|
|
|
async Task LoadEnvelopesAsync()
|
|
{
|
|
_isLoading = true;
|
|
_errorMessage = null;
|
|
await InvokeAsync(StateHasChanged);
|
|
|
|
try
|
|
{
|
|
_allEnvelopes = await EnvelopeService.GetAsync() ?? [];
|
|
|
|
// Split into active and completed based on status
|
|
var envelopes = _allEnvelopes.ToList();
|
|
_activeEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsActive()).ToList();
|
|
_completedEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsCompleted()).ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_errorMessage = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
_isLoading = false;
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
|
|
async Task RefreshEnvelopes()
|
|
{
|
|
await LoadEnvelopesAsync();
|
|
}
|
|
|
|
void CreateEnvelope()
|
|
{
|
|
Navigation.NavigateTo("/sender/editor");
|
|
}
|
|
|
|
void EditEnvelope()
|
|
{
|
|
if (_selectedEnvelope == null) return;
|
|
// TODO: Navigate to envelope editor
|
|
}
|
|
|
|
void DeleteEnvelope()
|
|
{
|
|
if (_selectedEnvelope == null) return;
|
|
// TODO: Show delete confirmation dialog
|
|
}
|
|
|
|
async Task LogoutAsync()
|
|
{
|
|
_isLoggingOut = true;
|
|
await InvokeAsync(StateHasChanged);
|
|
UserPreferencesService.InvalidateCache();
|
|
await AuthService.LogoutSenderAsync();
|
|
Navigation.NavigateTo("/sender/login", forceLoad: true);
|
|
}
|
|
|
|
bool IsEnvelopeSent(EnvelopeDto envelope)
|
|
{
|
|
var status = (EnvelopeStatus)envelope.Status;
|
|
return status >= EnvelopeStatus.EnvelopeQueued;
|
|
}
|
|
|
|
(string Label, string CssClass, string DotColor) GetStatusInfo(EnvelopeStatus status)
|
|
{
|
|
return status switch
|
|
{
|
|
EnvelopeStatus.EnvelopePartlySigned => ("Teilweise unterschrieben", "partly-signed", "green"),
|
|
EnvelopeStatus.EnvelopeQueued => ("In Warteschlange", "queued", "orange"),
|
|
EnvelopeStatus.EnvelopeSent => ("Gesendet", "sent", "orange"),
|
|
EnvelopeStatus.EnvelopeCompletelySigned => ("Vollständig unterschrieben", "completed", "green"),
|
|
EnvelopeStatus.EnvelopeDeleted => ("Gelöscht", "deleted", "red"),
|
|
EnvelopeStatus.EnvelopeRejected => ("Abgelehnt", "rejected", "red"),
|
|
EnvelopeStatus.EnvelopeWithdrawn => ("Zurückgezogen", "withdrawn", "red"),
|
|
EnvelopeStatus.EnvelopeCreated => ("Erstellt", "created", "blue"),
|
|
EnvelopeStatus.EnvelopeSaved => ("Gespeichert", "saved", "blue"),
|
|
_ => ("Unbekannt", "unknown", "blue")
|
|
};
|
|
}
|
|
|
|
void OnCustomizeElement(GridCustomizeElementEventArgs e)
|
|
{
|
|
// Future: Add custom row coloring based on status if needed
|
|
}
|
|
|
|
// Single click: native DX selection handles the UI instantly.
|
|
// We only manage toggle: if the same row is clicked again, deselect.
|
|
void OnRowClick(GridRowClickEventArgs e)
|
|
{
|
|
var clicked = e.Grid.GetDataItem(e.VisibleIndex) as EnvelopeDto;
|
|
if (clicked == null) return;
|
|
|
|
if (_selectedEnvelope?.Id == clicked.Id)
|
|
{
|
|
// Same row clicked again → deselect
|
|
_selectedEnvelope = null;
|
|
e.Grid.DeselectDataItem(clicked);
|
|
}
|
|
// Different row: @bind-SelectedDataItem already updated _selectedEnvelope
|
|
}
|
|
|
|
// Double-click: select the row and open preview popup
|
|
void OnRowDoubleClick(GridRowClickEventArgs e)
|
|
{
|
|
var clicked = e.Grid.GetDataItem(e.VisibleIndex) as EnvelopeDto;
|
|
if (clicked == null) return;
|
|
_selectedEnvelope = clicked;
|
|
_previewVisible = true;
|
|
}
|
|
|
|
void ShowPreview()
|
|
{
|
|
if (_selectedEnvelope == null) return;
|
|
_previewVisible = true;
|
|
}
|
|
|
|
// ── Grid Layout Persistence ───────────────────────────────────────────────
|
|
// LayoutAutoLoading fires when the grid initializes — restore saved layout.
|
|
// LayoutAutoSaving fires when the user changes column order/width/sort — persist it.
|
|
// UserPreferencesService.GetAsync() uses the 1h in-memory cache → 0 HTTP on restore.
|
|
// UserPreferencesService.SaveGridLayoutAsync() merges the single key → 1 PUT.
|
|
|
|
async Task OnActiveGridLayoutAutoLoading(GridPersistentLayoutEventArgs e)
|
|
{
|
|
// Use cached preferences only — avoid HTTP during grid initialization.
|
|
// If cache is empty (LoadFromServerAsync failed), skip layout restore silently.
|
|
var prefs = UserPreferencesService.GetCached();
|
|
if (prefs is not null && prefs.GridLayouts.TryGetValue(GridKeyActive, out var json))
|
|
e.Layout = JsonSerializer.Deserialize<GridPersistentLayout>(json);
|
|
}
|
|
|
|
async Task OnActiveGridLayoutAutoSaving(GridPersistentLayoutEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
var json = JsonSerializer.Serialize(e.Layout);
|
|
await UserPreferencesService.SaveGridLayoutAsync(GridKeyActive, json);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await JSRuntime.InvokeVoidAsync("console.warn", $"[Sender] Grid layout save failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
async Task OnCompletedGridLayoutAutoLoading(GridPersistentLayoutEventArgs e)
|
|
{
|
|
var prefs = UserPreferencesService.GetCached();
|
|
if (prefs is not null && prefs.GridLayouts.TryGetValue(GridKeyCompleted, out var json))
|
|
e.Layout = JsonSerializer.Deserialize<GridPersistentLayout>(json);
|
|
}
|
|
|
|
async Task OnCompletedGridLayoutAutoSaving(GridPersistentLayoutEventArgs e)
|
|
{
|
|
var json = JsonSerializer.Serialize(e.Layout);
|
|
await UserPreferencesService.SaveGridLayoutAsync(GridKeyCompleted, json);
|
|
}
|
|
} |