Enhance EnvelopeSenderPage with new UI and features
Integrated DevExpress Blazor components and added a responsive, modern UI for the sender dashboard. Replaced placeholder content with a functional layout, including a grid-based envelope viewer with filtering, pagination, and detailed row templates. Added status badges, progress indicators, and a sender action bar with buttons for creating, editing, deleting, refreshing envelopes, and logging out. Introduced loading and error handling states for better user experience. Refactored data loading with `LoadEnvelopesAsync` to fetch and categorize envelopes. Added methods for envelope management and logout functionality. Improved state management and removed unused code. These changes lay the groundwork for future enhancements.
This commit is contained in:
@@ -3,35 +3,715 @@
|
|||||||
|
|
||||||
@using System.Text.Json
|
@using System.Text.Json
|
||||||
@using EnvelopeGenerator.ReceiverUI.Models
|
@using EnvelopeGenerator.ReceiverUI.Models
|
||||||
|
@using DevExpress.Blazor
|
||||||
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeService EnvelopeService
|
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeService EnvelopeService
|
||||||
|
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
|
|
||||||
<h3>Umschläge</h3>
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||||
|
<link href="css/envelope-viewer.css" rel="stylesheet" />
|
||||||
|
|
||||||
<p><em>Daten werden geladen und in der Konsole ausgegeben...</em></p>
|
<style>
|
||||||
|
.sender-dashboard-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #7e22ce 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-action-bar {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-bottom: 3px solid rgba(126, 34, 206, 0.3);
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-action-bar__inner {
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-title-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-logo svg {
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(126, 34, 206, 0.3));
|
||||||
|
color: #7e22ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.625rem 1.125rem;
|
||||||
|
background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%);
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%);
|
||||||
|
border-color: rgba(126, 34, 206, 0.4);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(126, 34, 206, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
border-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn--primary {
|
||||||
|
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
|
||||||
|
border-color: transparent;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn--primary:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, #6b1cb0 0%, #1e3a72 100%);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 16px rgba(126, 34, 206, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn--danger {
|
||||||
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(220, 38, 38, 0.08) 100%);
|
||||||
|
border-color: rgba(239, 68, 68, 0.3);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn--danger:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
border-color: transparent;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-btn--logout {
|
||||||
|
padding: 0.5rem;
|
||||||
|
min-width: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-grid-container {
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow:
|
||||||
|
0 25px 50px -12px rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-grid-container::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #7e22ce 0%, #2a5298 100%);
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 2px solid rgba(126, 34, 206, 0.1);
|
||||||
|
padding: 0 2rem;
|
||||||
|
background: rgba(126, 34, 206, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-tab {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-tab:hover {
|
||||||
|
color: #7e22ce;
|
||||||
|
background: rgba(126, 34, 206, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-tab--active {
|
||||||
|
color: #7e22ce;
|
||||||
|
border-bottom-color: #7e22ce;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-grid-wrapper {
|
||||||
|
padding: 1.5rem 2rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.25rem 0.625rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--partly-signed,
|
||||||
|
.status-badge--completed {
|
||||||
|
background: rgba(129, 199, 132, 0.15);
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--queued,
|
||||||
|
.status-badge--sent {
|
||||||
|
background: rgba(255, 183, 77, 0.15);
|
||||||
|
color: #e65100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--deleted,
|
||||||
|
.status-badge--rejected,
|
||||||
|
.status-badge--withdrawn {
|
||||||
|
background: rgba(229, 115, 115, 0.15);
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--created,
|
||||||
|
.status-badge--saved {
|
||||||
|
background: rgba(100, 181, 246, 0.15);
|
||||||
|
color: #1565c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot--green {
|
||||||
|
background: #81c784;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot--orange {
|
||||||
|
background: #ffb74d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot--red {
|
||||||
|
background: #e57373;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot--blue {
|
||||||
|
background: #64b5f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #374151;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-badge--signed {
|
||||||
|
background: rgba(129, 199, 132, 0.15);
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-badge--unsigned {
|
||||||
|
background: rgba(229, 115, 115, 0.15);
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@media (max-width: 768px) {
|
||||||
|
.sender-action-bar {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-action-bar__inner {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-toolbar {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-content {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-grid-wrapper {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-tabs {
|
||||||
|
padding: 0 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-tab {
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
font-size: 0.813rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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 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">
|
||||||
|
@if (_activeTab == "active") {
|
||||||
|
<DxGrid Data="@_activeEnvelopes"
|
||||||
|
@ref="_gridActive"
|
||||||
|
ShowFilterRow="true"
|
||||||
|
ShowSearchBox="true"
|
||||||
|
PageSize="20"
|
||||||
|
PagerVisible="true"
|
||||||
|
SelectionMode="GridSelectionMode.Single"
|
||||||
|
SelectedDataItem="@_selectedEnvelope"
|
||||||
|
SelectedDataItemChanged="@OnSelectedEnvelopeChanged"
|
||||||
|
CustomizeElement="OnCustomizeElement">
|
||||||
|
<Columns>
|
||||||
|
<DxGridDataColumn FieldName="Id" Caption="ID" Width="80px" />
|
||||||
|
<DxGridDataColumn FieldName="Title" Caption="Titel" Width="300px">
|
||||||
|
<CellDisplayTemplate Context="cellContext">
|
||||||
|
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
|
||||||
|
</CellDisplayTemplate>
|
||||||
|
</DxGridDataColumn>
|
||||||
|
<DxGridDataColumn FieldName="Status" Caption="Status" Width="180px">
|
||||||
|
<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" Width="250px">
|
||||||
|
<CellDisplayTemplate Context="cellContext">
|
||||||
|
@{
|
||||||
|
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||||
|
if (envelope != null) {
|
||||||
|
var receivers = envelope.EnvelopeReceivers ?? new List<EnvelopeReceiverSimpleDto>();
|
||||||
|
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>
|
||||||
|
</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.Email</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"
|
||||||
|
SelectedDataItem="@_selectedEnvelope"
|
||||||
|
SelectedDataItemChanged="@OnSelectedEnvelopeChanged"
|
||||||
|
CustomizeElement="OnCustomizeElement">
|
||||||
|
<Columns>
|
||||||
|
<DxGridDataColumn FieldName="Id" Caption="ID" Width="80px" />
|
||||||
|
<DxGridDataColumn FieldName="Title" Caption="Titel" Width="300px">
|
||||||
|
<CellDisplayTemplate Context="cellContext">
|
||||||
|
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
|
||||||
|
</CellDisplayTemplate>
|
||||||
|
</DxGridDataColumn>
|
||||||
|
<DxGridDataColumn FieldName="Status" Caption="Status" Width="180px">
|
||||||
|
<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" Width="250px">
|
||||||
|
<CellDisplayTemplate Context="cellContext">
|
||||||
|
@{
|
||||||
|
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||||
|
if (envelope != null) {
|
||||||
|
var receivers = envelope.EnvelopeReceivers ?? new List<EnvelopeReceiverSimpleDto>();
|
||||||
|
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(90px, #81c784 0%, #66bb6a 100%); width: @((signed * 100.0 / total).ToString("F0"))%;"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</CellDisplayTemplate>
|
||||||
|
</DxGridDataColumn>
|
||||||
|
</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.Email</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>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
private IEnumerable<EnvelopeDto>? _allEnvelopes;
|
||||||
private IEnumerable<EnvelopeDto>? _activeEnvelopes;
|
private IEnumerable<EnvelopeDto>? _activeEnvelopes;
|
||||||
private IEnumerable<EnvelopeDto>? _completedEnvelopes;
|
private IEnumerable<EnvelopeDto>? _completedEnvelopes;
|
||||||
|
private EnvelopeDto? _selectedEnvelope;
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
private string _activeTab = "active";
|
||||||
|
private bool _isLoading = true;
|
||||||
|
private bool _isLoggingOut = false;
|
||||||
|
private string? _errorMessage;
|
||||||
|
private DxGrid? _gridActive;
|
||||||
|
private DxGrid? _gridCompleted;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
await LoadEnvelopesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task LoadEnvelopesAsync()
|
||||||
|
{
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_activeEnvelopes = await EnvelopeService.GetAsync(onlyActive: true);
|
_allEnvelopes = await EnvelopeService.GetAsync();
|
||||||
_completedEnvelopes = await EnvelopeService.GetAsync(onlyCompleted: true);
|
|
||||||
|
|
||||||
await JSRuntime.InvokeVoidAsync("console.log", "----- Aktive Umschläge -----");
|
// Split into active and completed based on status
|
||||||
await JSRuntime.InvokeVoidAsync("console.log", _activeEnvelopes);
|
var envelopes = _allEnvelopes.ToList();
|
||||||
|
_activeEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsActive()).ToList();
|
||||||
|
_completedEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsCompleted()).ToList();
|
||||||
|
|
||||||
await JSRuntime.InvokeVoidAsync("console.log", "----- Abgeschlossene Umschläge -----");
|
await JSRuntime.InvokeVoidAsync("console.log", $"Loaded {_activeEnvelopes.Count()} active and {_completedEnvelopes.Count()} completed envelopes");
|
||||||
await JSRuntime.InvokeVoidAsync("console.log", _completedEnvelopes);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
_errorMessage = ex.Message;
|
||||||
await JSRuntime.InvokeVoidAsync("console.error", "Fehler beim Laden der Umschläge:", ex.ToString());
|
await JSRuntime.InvokeVoidAsync("console.error", "Fehler beim Laden der Umschläge:", ex.ToString());
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isLoading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task RefreshEnvelopes()
|
||||||
|
{
|
||||||
|
await LoadEnvelopesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CreateEnvelope()
|
||||||
|
{
|
||||||
|
// TODO: Navigate to envelope creation page
|
||||||
|
JSRuntime.InvokeVoidAsync("console.log", "Create envelope clicked - not yet implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditEnvelope()
|
||||||
|
{
|
||||||
|
if (_selectedEnvelope == null) return;
|
||||||
|
// TODO: Navigate to envelope editor
|
||||||
|
JSRuntime.InvokeVoidAsync("console.log", $"Edit envelope {_selectedEnvelope.Id} clicked - not yet implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeleteEnvelope()
|
||||||
|
{
|
||||||
|
if (_selectedEnvelope == null) return;
|
||||||
|
// TODO: Show delete confirmation dialog
|
||||||
|
JSRuntime.InvokeVoidAsync("console.log", $"Delete envelope {_selectedEnvelope.Id} clicked - not yet implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task LogoutAsync()
|
||||||
|
{
|
||||||
|
_isLoggingOut = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
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(int statusCode)
|
||||||
|
{
|
||||||
|
var status = (EnvelopeStatus)statusCode;
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnSelectedEnvelopeChanged(object envelope)
|
||||||
|
{
|
||||||
|
_selectedEnvelope = envelope as EnvelopeDto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user