Replaced direct file links with a JavaScript-based viewer (`openAttachmentViewer`) and added a DevExtreme Popup (`attachment-viewer-popup`) for displaying attachments in a modal dialog. The viewer supports PDFs (via PDF.js), XML (with CodeMirror syntax highlighting), plain text, images, and provides a fallback for unsupported file types. Dynamically load CodeMirror for XML files and handle errors gracefully when loading file content. Added `onAttachmentPopupHiding` to clear popup content on close. Updated `ViewAttachmentModel` to return text/XML content directly for AJAX requests, improving frontend performance and enabling dynamic content loading.
277 lines
12 KiB
Plaintext
277 lines
12 KiB
Plaintext
@page
|
||
@model DXApp.TemplateKitProject.Pages.Invoices.DetailsModel
|
||
@{
|
||
ViewData["Title"] = "Rechnungsdetails";
|
||
|
||
string FormatFileSize(long bytes)
|
||
{
|
||
if (bytes < 1024) return $"{bytes} Bytes";
|
||
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:N2} KB";
|
||
return $"{bytes / (1024.0 * 1024.0):N2} MB";
|
||
}
|
||
}
|
||
|
||
<h2>📄 Rechnungsdetails</h2>
|
||
<a href="/Invoices" class="btn btn-secondary mb-3">← Zurück zur Liste</a>
|
||
|
||
@if (!string.IsNullOrEmpty(Model.Invoice?.ResultFilePath))
|
||
{
|
||
<button class="btn btn-primary mb-3 ms-2"
|
||
onclick="openPdfViewer(@Model.Invoice.Id)">
|
||
📄 Ergebnis anzeigen
|
||
</button>
|
||
}
|
||
|
||
@if (Model.Invoice is null)
|
||
{
|
||
<div class="alert alert-danger">Rechnung nicht gefunden.</div>
|
||
}
|
||
else
|
||
{
|
||
<table class="table table-sm table-bordered w-auto">
|
||
<tr><th>ID</th><td>@Model.Invoice.Id</td></tr>
|
||
<tr><th>Rechnungsnummer</th><td>@Model.Invoice.InvoiceNumber</td></tr>
|
||
<tr><th>Rechnungsdatum</th><td>@Model.Invoice.InvoiceDate.ToString("dd.MM.yyyy")</td></tr>
|
||
<tr><th>Verkäufer</th><td>@Model.Invoice.SellerName</td></tr>
|
||
<tr><th>USt-ID Verkäufer</th><td>@Model.Invoice.SellerTaxId</td></tr>
|
||
<tr><th>Käufer</th><td>@Model.Invoice.BuyerName</td></tr>
|
||
<tr><th>Währung</th><td>@Model.Invoice.CurrencyCode</td></tr>
|
||
<tr><th>Steuerbetrag</th><td>@Model.Invoice.TaxAmount.ToString("N2")</td></tr>
|
||
<tr><th>Gesamtbetrag</th><td><strong>@Model.Invoice.TotalAmount.ToString("N2")</strong></td></tr>
|
||
<tr><th>IBAN</th><td>@Model.Invoice.Iban</td></tr>
|
||
<tr><th>Quelle</th><td>@Model.Invoice.SourceType</td></tr>
|
||
<tr><th>Guideline-ID</th><td><code>@Model.Invoice.GuidelineId</code></td></tr>
|
||
<tr><th>Importiert am</th><td>@Model.Invoice.ImportedAt.ToString("dd.MM.yyyy HH:mm")</td></tr>
|
||
<tr>
|
||
<th>Result-PDF</th>
|
||
<td>
|
||
@if (!string.IsNullOrEmpty(Model.Invoice.ResultFilePath))
|
||
{
|
||
<small class="text-muted">@Model.Invoice.ResultFilePath</small>
|
||
}
|
||
else
|
||
{
|
||
<span class="text-muted">– nicht vorhanden –</span>
|
||
}
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
|
||
@* Anhänge-Sektion *@
|
||
@if (Model.Invoice.Attachments.Any())
|
||
{
|
||
<h4 class="mt-4">📎 Anhänge (@Model.Invoice.Attachments.Count)</h4>
|
||
<div class="list-group">
|
||
@foreach (var attachment in Model.Invoice.Attachments)
|
||
{
|
||
var icon = attachment.IsZugferdXml ? "📋" : "📄";
|
||
var extension = System.IO.Path.GetExtension(attachment.OriginalFileName).ToLowerInvariant();
|
||
icon = extension switch
|
||
{
|
||
".xml" => "📋",
|
||
".pdf" => "📄",
|
||
".jpg" or ".jpeg" or ".png" or ".gif" => "🖼️",
|
||
".txt" => "📝",
|
||
_ => "📎"
|
||
};
|
||
|
||
<a href="javascript:void(0);"
|
||
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
|
||
onclick="openAttachmentViewer('@attachment.OriginalFileName', '@Uri.EscapeDataString(attachment.SavedFilePath)', '@extension')">
|
||
<div>
|
||
<span class="me-2">@icon</span>
|
||
<strong>@attachment.OriginalFileName</strong>
|
||
@if (attachment.IsZugferdXml)
|
||
{
|
||
<span class="badge bg-success ms-2">ZUGFeRD-XML</span>
|
||
}
|
||
<br />
|
||
<small class="text-muted">
|
||
Größe: @FormatFileSize(attachment.FileSizeBytes) ·
|
||
Extrahiert: @attachment.ExtractedAt.ToString("dd.MM.yyyy HH:mm")
|
||
</small>
|
||
</div>
|
||
<span class="badge bg-primary">Öffnen</span>
|
||
</a>
|
||
}
|
||
</div>
|
||
}
|
||
else
|
||
{
|
||
<h4 class="mt-4">📎 Anhänge</h4>
|
||
<div class="alert alert-info">Keine Anhänge extrahiert.</div>
|
||
}
|
||
|
||
@(Html.DevExtreme().Popup()
|
||
.ID("pdf-viewer-popup")
|
||
.Title("PDF Viewer")
|
||
.Width("90%")
|
||
.Height("90%")
|
||
.ShowCloseButton(true)
|
||
.OnHiding("onPdfPopupHiding")
|
||
.Shading(true)
|
||
.ShadingColor("rgba(0, 0, 0, 0.5)")
|
||
.ContentTemplate(new JS(@"function() {
|
||
return '<iframe id=""pdf-iframe"" style=""width:100%;height:100%;border:none;""></iframe>';
|
||
}"))
|
||
)
|
||
|
||
@(Html.DevExtreme().Popup()
|
||
.ID("attachment-viewer-popup")
|
||
.Title("Anhang")
|
||
.Width("90%")
|
||
.Height("90%")
|
||
.ShowCloseButton(true)
|
||
.OnHiding("onAttachmentPopupHiding")
|
||
.Shading(true)
|
||
.ShadingColor("rgba(0, 0, 0, 0.5)")
|
||
.ContentTemplate(new JS(@"function() {
|
||
return '<div id=""attachment-content"" style=""width:100%;height:100%;overflow:auto;""></div>';
|
||
}"))
|
||
)
|
||
}
|
||
|
||
<style>
|
||
/* Z-Index für PDF-Viewer Popup erhöhen - muss höher sein als layout-header (1505) */
|
||
.dx-popup-wrapper.dx-overlay-wrapper {
|
||
z-index: 10500 !important;
|
||
}
|
||
|
||
.dx-overlay-shader {
|
||
z-index: 10499 !important;
|
||
}
|
||
|
||
/* Spezifisch für unser PDF-Popup */
|
||
#pdf-viewer-popup .dx-overlay-content {
|
||
z-index: 10500 !important;
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
function openPdfViewer(invoiceId) {
|
||
var pdfUrl = window.location.origin + '/Invoices/ViewPdf?id=' + invoiceId;
|
||
var viewerUrl = '/js/pdfjs/web/viewer.html?file=' + encodeURIComponent(pdfUrl);
|
||
|
||
var popup = $('#pdf-viewer-popup').dxPopup('instance');
|
||
|
||
// Z-Index explizit setzen (höher als layout-header mit 1505)
|
||
popup.option('container', undefined); // Default container verwenden
|
||
popup.option('position', { my: 'center', at: 'center', of: window });
|
||
|
||
// onShown sicherstellen dass iframe im DOM ist
|
||
popup.option('onShown', function () {
|
||
$('#pdf-iframe').attr('src', viewerUrl);
|
||
|
||
// Z-Index nach dem Öffnen nochmal sicherstellen
|
||
$('.dx-popup-wrapper').css('z-index', '10500');
|
||
$('.dx-overlay-shader').css('z-index', '10499');
|
||
});
|
||
|
||
popup.show();
|
||
}
|
||
|
||
function onPdfPopupHiding() {
|
||
// iframe src leeren beim Schließen → verhindert dass PDF im Hintergrund weiter läuft
|
||
$('#pdf-iframe').attr('src', '');
|
||
}
|
||
|
||
function openAttachmentViewer(fileName, encodedFilePath, extension) {
|
||
var filePath = decodeURIComponent(encodedFilePath);
|
||
var popup = $('#attachment-viewer-popup').dxPopup('instance');
|
||
var $content = $('#attachment-content');
|
||
|
||
// Popup-Titel setzen
|
||
popup.option('title', fileName);
|
||
|
||
// Z-Index setzen
|
||
popup.option('container', undefined);
|
||
popup.option('position', { my: 'center', at: 'center', of: window });
|
||
|
||
// Content basierend auf Dateityp laden
|
||
popup.option('onShown', function () {
|
||
$content.html('<div class="text-center p-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Laden...</span></div></div>');
|
||
|
||
// Z-Index sicherstellen
|
||
$('.dx-popup-wrapper').css('z-index', '10500');
|
||
$('.dx-overlay-shader').css('z-index', '10499');
|
||
|
||
if (extension === '.pdf') {
|
||
// PDF mit PDF.js anzeigen
|
||
var pdfUrl = window.location.origin + '/Invoices/ViewAttachment?handler=Download&filePath=' + encodedFilePath;
|
||
var viewerUrl = '/js/pdfjs/web/viewer.html?file=' + encodeURIComponent(pdfUrl);
|
||
$content.html('<iframe style="width:100%;height:100%;border:none;" src="' + viewerUrl + '"></iframe>');
|
||
}
|
||
else if (extension === '.xml' || extension === '.txt') {
|
||
// Text/XML laden und mit Syntax-Highlighting anzeigen
|
||
$.get('/Invoices/ViewAttachment?filePath=' + encodedFilePath, function(data) {
|
||
if (extension === '.xml') {
|
||
// CodeMirror für XML
|
||
var textarea = $('<textarea id="code-viewer"></textarea>');
|
||
$content.html('').append(textarea);
|
||
|
||
// CodeMirror laden (falls noch nicht geladen)
|
||
if (typeof CodeMirror === 'undefined') {
|
||
$('<link>')
|
||
.attr('rel', 'stylesheet')
|
||
.attr('href', 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css')
|
||
.appendTo('head');
|
||
$('<link>')
|
||
.attr('rel', 'stylesheet')
|
||
.attr('href', 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/monokai.min.css')
|
||
.appendTo('head');
|
||
|
||
$.getScript('https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js', function() {
|
||
$.getScript('https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/xml/xml.min.js', function() {
|
||
initCodeMirror(data);
|
||
});
|
||
});
|
||
} else {
|
||
initCodeMirror(data);
|
||
}
|
||
|
||
function initCodeMirror(content) {
|
||
CodeMirror(document.getElementById('attachment-content'), {
|
||
value: content,
|
||
mode: 'xml',
|
||
lineNumbers: true,
|
||
readOnly: true,
|
||
theme: 'monokai',
|
||
lineWrapping: true
|
||
});
|
||
}
|
||
} else {
|
||
// Plain Text
|
||
$content.html('<pre style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; height: 100%; overflow: auto; margin: 0;">' +
|
||
$('<div>').text(data).html() + '</pre>');
|
||
}
|
||
}).fail(function() {
|
||
$content.html('<div class="alert alert-danger m-3">Fehler beim Laden der Datei.</div>');
|
||
});
|
||
}
|
||
else if (extension === '.jpg' || extension === '.jpeg' || extension === '.png' || extension === '.gif') {
|
||
// Bild anzeigen
|
||
var imageUrl = window.location.origin + '/Invoices/ViewAttachment?handler=Download&filePath=' + encodedFilePath;
|
||
$content.html('<div class="text-center p-3" style="height: 100%; display: flex; align-items: center; justify-content: center;">' +
|
||
'<img src="' + imageUrl + '" alt="' + fileName + '" class="img-fluid" style="max-height: 100%; max-width: 100%; object-fit: contain;">' +
|
||
'</div>');
|
||
}
|
||
else {
|
||
// Nicht unterstützter Typ → Download anbieten
|
||
var downloadUrl = window.location.origin + '/Invoices/ViewAttachment?handler=Download&filePath=' + encodedFilePath;
|
||
$content.html('<div class="alert alert-info m-3">' +
|
||
'<h5>Dieser Dateityp kann nicht angezeigt werden.</h5>' +
|
||
'<p>Bitte laden Sie die Datei herunter:</p>' +
|
||
'<a href="' + downloadUrl + '" class="btn btn-primary" download="' + fileName + '">' +
|
||
'<i class="dx-icon-download"></i> Datei herunterladen' +
|
||
'</a></div>');
|
||
}
|
||
});
|
||
|
||
popup.show();
|
||
}
|
||
|
||
function onAttachmentPopupHiding() {
|
||
// Content leeren
|
||
$('#attachment-content').html('');
|
||
}
|
||
</script> |