Compare commits

...

5 Commits

Author SHA1 Message Date
OlgunR
f7cac8c0a7 Improve popup layout and CodeMirror integration
Updated the `ContentTemplate` to enhance the layout of the
`#attachment-content` container by using `display: flex` and
ensuring full height utilization. Added CSS rules for consistent
styling of the `.CodeMirror` editor, including font size and
height adjustments.

Simplified CodeMirror initialization for XML files by removing
redundant `<textarea>` creation and directly embedding the
editor. Added `viewportMargin: Infinity` for better rendering
and used `setTimeout` to ensure proper sizing and refresh.

Refined plain text rendering for non-XML files to maintain a
clean and consistent display.
2026-06-02 15:32:42 +02:00
OlgunR
8065c589bc Enhance file attachment viewing experience
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.
2026-06-02 15:21:11 +02:00
OlgunR
c9ba7912fa Improve PDF Viewer popup styling and behavior
Added shading and overlay color to the PDF Viewer popup for better user experience. Adjusted z-index values in the `<style>` block to ensure the popup and its overlay appear above other elements. Updated the `openPdfViewer` function to explicitly set the container and position options, and ensured z-index values are reapplied after the popup is shown.
2026-06-02 15:13:54 +02:00
OlgunR
920dce13d5 Add support for invoice attachments
Introduced the `InvoiceAttachment` entity and its relationship with `ZugferdInvoice` to manage extracted invoice attachments. Updated `AppDbContext` and added a migration to create the `InvoiceAttachments` table with cascading delete behavior and an index for optimized queries.

Enhanced the UI to display attachments in `Details.cshtml`, including file type icons, file size, and extraction date. Added a new `ViewAttachment` page to render or download attachments based on their type, with support for XML, plain text, images, and downloads.

Implemented `AttachmentViewerService` to determine viewer types and MIME types for attachments. Registered the service in the DI container. Updated `Upload.cshtml.cs` to save extracted attachments to the database.

Improved user experience with syntax highlighting for XML files and appropriate messages for unsupported file types.
2026-06-02 15:03:37 +02:00
OlgunR
43d63e975d Enhance PDF processing and report integration
Refactor `PdfResultPackageService` to improve PDF handling:
- Load the main document into `PdfDocumentProcessor`.
- Remove original metadata to attribute the result to `DXApp`.
- Replace report file attachment with appending report pages.
- Use a temporary processor to determine report page count.
- Add logging for appended pages and total page count.
- Update save operation to include detailed logging.

Removed the functionality to attach the report file as a PDF attachment.
Improved overall usability, traceability, and document attribution.
2026-06-02 14:06:24 +02:00
14 changed files with 723 additions and 12 deletions

View File

@@ -6,6 +6,7 @@ namespace DXApp.TemplateKitProject.Data;
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<ZugferdInvoice> ZugferdInvoices { get; set; }
public DbSet<InvoiceAttachment> InvoiceAttachments { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -27,6 +28,19 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
entity.Property(e => e.TaxAmount)
.HasColumnType("decimal(18,2)");
// Relationship: One-to-Many mit InvoiceAttachments
entity.HasMany(e => e.Attachments)
.WithOne(a => a.ZugferdInvoice)
.HasForeignKey(a => a.ZugferdInvoiceId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<InvoiceAttachment>(entity =>
{
// Index für schnelleres Laden der Attachments einer Rechnung
entity.HasIndex(e => e.ZugferdInvoiceId)
.HasDatabaseName("IX_InvoiceAttachments_ZugferdInvoiceId");
});
}
}

View File

@@ -0,0 +1,141 @@
// <auto-generated />
using System;
using DXApp.TemplateKitProject.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace DXApp.TemplateKitProject.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260602123528_AddInvoiceAttachments")]
partial class AddInvoiceAttachments
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("DXApp.TemplateKitProject.Models.InvoiceAttachment", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("ExtractedAt")
.HasColumnType("datetime2");
b.Property<long>("FileSizeBytes")
.HasColumnType("bigint");
b.Property<bool>("IsZugferdXml")
.HasColumnType("bit");
b.Property<string>("OriginalFileName")
.HasColumnType("nvarchar(max)");
b.Property<string>("SavedFilePath")
.HasColumnType("nvarchar(max)");
b.Property<int>("ZugferdInvoiceId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ZugferdInvoiceId")
.HasDatabaseName("IX_InvoiceAttachments_ZugferdInvoiceId");
b.ToTable("InvoiceAttachments");
});
modelBuilder.Entity("DXApp.TemplateKitProject.Models.ZugferdInvoice", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("BuyerName")
.HasColumnType("nvarchar(max)");
b.Property<string>("CurrencyCode")
.HasColumnType("nvarchar(max)");
b.Property<string>("GuidelineId")
.HasColumnType("nvarchar(max)");
b.Property<string>("Iban")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("ImportedAt")
.HasColumnType("datetime2");
b.Property<DateTime>("InvoiceDate")
.HasColumnType("datetime2");
b.Property<string>("InvoiceNumber")
.HasColumnType("nvarchar(450)");
b.Property<string>("RawXml")
.HasColumnType("nvarchar(max)");
b.Property<string>("ResultFilePath")
.HasColumnType("nvarchar(max)");
b.Property<string>("SellerName")
.HasColumnType("nvarchar(max)");
b.Property<string>("SellerTaxId")
.HasColumnType("nvarchar(450)");
b.Property<string>("SourceType")
.HasColumnType("nvarchar(max)");
b.Property<decimal>("TaxAmount")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("TotalAmount")
.HasColumnType("decimal(18,2)");
b.HasKey("Id");
b.HasIndex("ImportedAt")
.HasDatabaseName("IX_ZugferdInvoices_ImportedAt");
b.HasIndex("InvoiceNumber", "SellerTaxId")
.HasDatabaseName("IX_ZugferdInvoices_InvoiceNumber_SellerTaxId");
b.ToTable("ZugferdInvoices");
});
modelBuilder.Entity("DXApp.TemplateKitProject.Models.InvoiceAttachment", b =>
{
b.HasOne("DXApp.TemplateKitProject.Models.ZugferdInvoice", "ZugferdInvoice")
.WithMany("Attachments")
.HasForeignKey("ZugferdInvoiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ZugferdInvoice");
});
modelBuilder.Entity("DXApp.TemplateKitProject.Models.ZugferdInvoice", b =>
{
b.Navigation("Attachments");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DXApp.TemplateKitProject.Migrations
{
/// <inheritdoc />
public partial class AddInvoiceAttachments : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "InvoiceAttachments",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
ZugferdInvoiceId = table.Column<int>(type: "int", nullable: false),
OriginalFileName = table.Column<string>(type: "nvarchar(max)", nullable: true),
SavedFilePath = table.Column<string>(type: "nvarchar(max)", nullable: true),
FileSizeBytes = table.Column<long>(type: "bigint", nullable: false),
IsZugferdXml = table.Column<bool>(type: "bit", nullable: false),
ExtractedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_InvoiceAttachments", x => x.Id);
table.ForeignKey(
name: "FK_InvoiceAttachments_ZugferdInvoices_ZugferdInvoiceId",
column: x => x.ZugferdInvoiceId,
principalTable: "ZugferdInvoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_InvoiceAttachments_ZugferdInvoiceId",
table: "InvoiceAttachments",
column: "ZugferdInvoiceId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "InvoiceAttachments");
}
}
}

View File

@@ -22,6 +22,40 @@ namespace DXApp.TemplateKitProject.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("DXApp.TemplateKitProject.Models.InvoiceAttachment", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("ExtractedAt")
.HasColumnType("datetime2");
b.Property<long>("FileSizeBytes")
.HasColumnType("bigint");
b.Property<bool>("IsZugferdXml")
.HasColumnType("bit");
b.Property<string>("OriginalFileName")
.HasColumnType("nvarchar(max)");
b.Property<string>("SavedFilePath")
.HasColumnType("nvarchar(max)");
b.Property<int>("ZugferdInvoiceId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ZugferdInvoiceId")
.HasDatabaseName("IX_InvoiceAttachments_ZugferdInvoiceId");
b.ToTable("InvoiceAttachments");
});
modelBuilder.Entity("DXApp.TemplateKitProject.Models.ZugferdInvoice", b =>
{
b.Property<int>("Id")
@@ -82,6 +116,22 @@ namespace DXApp.TemplateKitProject.Migrations
b.ToTable("ZugferdInvoices");
});
modelBuilder.Entity("DXApp.TemplateKitProject.Models.InvoiceAttachment", b =>
{
b.HasOne("DXApp.TemplateKitProject.Models.ZugferdInvoice", "ZugferdInvoice")
.WithMany("Attachments")
.HasForeignKey("ZugferdInvoiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ZugferdInvoice");
});
modelBuilder.Entity("DXApp.TemplateKitProject.Models.ZugferdInvoice", b =>
{
b.Navigation("Attachments");
});
#pragma warning restore 612, 618
}
}

View File

@@ -0,0 +1,18 @@
namespace DXApp.TemplateKitProject.Models;
/// <summary>
/// Extrahierte Anhänge einer ZUGFeRD-Rechnung (gespeichert in DB)
/// </summary>
public class InvoiceAttachment
{
public int Id { get; set; }
public int ZugferdInvoiceId { get; set; }
public string OriginalFileName { get; set; } = string.Empty;
public string SavedFilePath { get; set; } = string.Empty;
public long FileSizeBytes { get; set; }
public bool IsZugferdXml { get; set; }
public DateTime ExtractedAt { get; set; }
// Navigation Property
public ZugferdInvoice? ZugferdInvoice { get; set; }
}

View File

@@ -17,5 +17,8 @@
public string SourceType { get; set; } = string.Empty; // "Upload" oder "Email"
public string ResultFilePath { get; set; } = string.Empty; // Pfad der Result-PDF
public string GuidelineId { get; set; } = string.Empty; // ZUGFeRD Guideline-ID aus XMP
// Navigation Property
public List<InvoiceAttachment> Attachments { get; set; } = [];
}
}

View File

@@ -2,6 +2,13 @@
@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>
@@ -11,7 +18,7 @@
{
<button class="btn btn-primary mb-3 ms-2"
onclick="openPdfViewer(@Model.Invoice.Id)">
📄 PDF anzeigen
📄 Ergebnis anzeigen
</button>
}
@@ -50,6 +57,51 @@ else
</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")
@@ -57,12 +109,54 @@ else
.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:hidden;display:flex;flex-direction:column;""></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;
}
/* CodeMirror soll volle Höhe des Popups nutzen */
#attachment-content {
height: 100% !important;
}
#attachment-content .CodeMirror {
height: 100% !important;
font-size: 14px;
}
</style>
<script>
function openPdfViewer(invoiceId) {
var pdfUrl = window.location.origin + '/Invoices/ViewPdf?id=' + invoiceId;
@@ -70,9 +164,17 @@ else
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();
@@ -82,4 +184,111 @@ else
// 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 - Container leeren
$content.html('');
// 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 direkt in den Container einfügen
var editor = CodeMirror($content[0], {
value: content,
mode: 'xml',
lineNumbers: true,
readOnly: true,
theme: 'monokai',
lineWrapping: true,
viewportMargin: Infinity
});
// Höhe explizit setzen nach kurzer Verzögerung
setTimeout(function() {
editor.setSize('100%', '100%');
editor.refresh();
}, 100);
}
} 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>

View File

@@ -12,7 +12,9 @@ public class DetailsModel(AppDbContext db) : PageModel
public async Task<IActionResult> OnGetAsync(int id)
{
Invoice = await db.ZugferdInvoices.FirstOrDefaultAsync(i => i.Id == id);
Invoice = await db.ZugferdInvoices
.Include(i => i.Attachments)
.FirstOrDefaultAsync(i => i.Id == id);
if (Invoice is null)
return NotFound();

View File

@@ -75,6 +75,29 @@ public class UploadModel(
ImportedInvoice.ResultFilePath = ResultFilePath;
await db.SaveChangesAsync();
}
// 4. Attachments in DB speichern
if (Result.HasAttachments)
{
foreach (var attachment in Result.Attachments)
{
var invoiceAttachment = new InvoiceAttachment
{
ZugferdInvoiceId = ImportedInvoice.Id,
OriginalFileName = attachment.OriginalFileName,
SavedFilePath = attachment.SavedFilePath,
FileSizeBytes = attachment.FileSizeBytes,
IsZugferdXml = attachment.IsZugferdXml,
ExtractedAt = DateTime.UtcNow
};
db.InvoiceAttachments.Add(invoiceAttachment);
}
await db.SaveChangesAsync();
logger.LogInformation(
"Rechnung '{InvoiceNumber}': {Count} Anhang/Anhänge in DB gespeichert.",
ImportedInvoice.InvoiceNumber, Result.Attachments.Count);
}
}
}
}

View File

@@ -0,0 +1,88 @@
@page
@using DXApp.TemplateKitProject.Services
@model DXApp.TemplateKitProject.Pages.Invoices.ViewAttachmentModel
@{
ViewData["Title"] = $"Attachment: {Model.FileName}";
}
<h2>?? Attachment: @Model.FileName</h2>
<div class="mb-3">
<a href="/Invoices/Details" class="btn btn-secondary">? Zurück</a>
<a href="?handler=Download&filePath=@Request.Query["filePath"]"
class="btn btn-primary">
?? Download
</a>
</div>
@switch (Model.ViewerType)
{
case AttachmentViewerType.Pdf:
<div class="alert alert-info">
PDF-Dateien können Sie über die Result-PDF-Funktion anzeigen.
</div>
break;
case AttachmentViewerType.Xml:
<div class="card">
<div class="card-header bg-primary text-white">
<strong>XML-Inhalt</strong> (ZUGFeRD/Factur-X)
</div>
<div class="card-body p-0">
<textarea id="xml-viewer" style="display:none;">@Model.TextContent</textarea>
<div id="xml-rendered"></div>
</div>
</div>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/monokai.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/xml/xml.min.js"></script>
<script>
var editor = CodeMirror(document.getElementById('xml-rendered'), {
value: document.getElementById('xml-viewer').value,
mode: 'xml',
lineNumbers: true,
readOnly: true,
theme: 'monokai',
lineWrapping: true
});
</script>
break;
case AttachmentViewerType.Text:
<div class="card">
<div class="card-header bg-secondary text-white">
<strong>Text-Inhalt</strong>
</div>
<div class="card-body">
<pre style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; max-height: 600px; overflow: auto;">@Model.TextContent</pre>
</div>
</div>
break;
case AttachmentViewerType.Image:
<div class="card">
<div class="card-body text-center">
<img src="?handler=Download&filePath=@Request.Query["filePath"]"
alt="@Model.FileName"
class="img-fluid"
style="max-height: 800px;">
</div>
</div>
break;
case AttachmentViewerType.Word:
<div class="alert alert-warning">
<strong>Word-Dokumente können nicht direkt angezeigt werden.</strong>
<p>Bitte laden Sie die Datei herunter oder implementieren Sie eine Konvertierung zu PDF.</p>
</div>
break;
default:
<div class="alert alert-warning">
<strong>Dieser Dateityp kann nicht angezeigt werden.</strong>
<p>Bitte laden Sie die Datei herunter.</p>
</div>
break;
}

View File

@@ -0,0 +1,49 @@
using DXApp.TemplateKitProject.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Text;
namespace DXApp.TemplateKitProject.Pages.Invoices;
public class ViewAttachmentModel(AttachmentViewerService viewerService) : PageModel
{
public string FileName { get; private set; } = string.Empty;
public AttachmentViewerType ViewerType { get; private set; }
public string? TextContent { get; private set; }
public IActionResult OnGet(string filePath)
{
if (string.IsNullOrEmpty(filePath) || !System.IO.File.Exists(filePath))
return NotFound();
FileName = Path.GetFileName(filePath);
ViewerType = viewerService.DetermineViewerType(FileName);
// Für Text/XML: Inhalt direkt als Content zurückgeben (für AJAX)
if (ViewerType == AttachmentViewerType.Xml || ViewerType == AttachmentViewerType.Text)
{
TextContent = System.IO.File.ReadAllText(filePath, Encoding.UTF8);
// Wenn Request von AJAX kommt (Accept: */* oder text/plain)
if (Request.Headers.Accept.ToString().Contains("*/*") ||
Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
return Content(TextContent, "text/plain", Encoding.UTF8);
}
}
return Page();
}
public IActionResult OnGetDownload(string filePath)
{
if (string.IsNullOrEmpty(filePath) || !System.IO.File.Exists(filePath))
return NotFound();
var fileName = Path.GetFileName(filePath);
var mimeType = viewerService.GetMimeType(fileName);
var bytes = System.IO.File.ReadAllBytes(filePath);
return File(bytes, mimeType, fileName);
}
}

View File

@@ -18,6 +18,7 @@ builder.Services.AddScoped<PdfResultPackageService>();
builder.Services.AddScoped<ZugferdExtractorService>();
builder.Services.AddScoped<ZugferdParserService>();
builder.Services.AddScoped<ZugferdImportService>();
builder.Services.AddScoped<AttachmentViewerService>();
var app = builder.Build();

View File

@@ -0,0 +1,49 @@
using DXApp.TemplateKitProject.Models;
namespace DXApp.TemplateKitProject.Services;
public class AttachmentViewerService
{
public AttachmentViewerType DetermineViewerType(string fileName)
{
var extension = Path.GetExtension(fileName).ToLowerInvariant();
return extension switch
{
".pdf" => AttachmentViewerType.Pdf,
".xml" => AttachmentViewerType.Xml,
".txt" => AttachmentViewerType.Text,
".jpg" or ".jpeg" or ".png" or ".gif" => AttachmentViewerType.Image,
".docx" or ".doc" => AttachmentViewerType.Word,
_ => AttachmentViewerType.Download
};
}
public string GetMimeType(string fileName)
{
var extension = Path.GetExtension(fileName).ToLowerInvariant();
return extension switch
{
".pdf" => "application/pdf",
".xml" => "application/xml",
".txt" => "text/plain",
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".doc" => "application/msword",
_ => "application/octet-stream"
};
}
}
public enum AttachmentViewerType
{
Pdf, // PDF.js Viewer
Xml, // Syntax-highlighted XML
Text, // Plain Text
Image, // <img> Tag
Word, // Konvertierung zu PDF (optional)
Download // Nur Download-Button
}

View File

@@ -56,7 +56,7 @@ public class PdfResultPackageService(
converter.SaveDocument(convertedStream);
convertedStream.Position = 0;
// Schritt 3: Anhang einbetten
// Schritt 3: Hauptdokument laden
using var processor = new PdfDocumentProcessor();
processor.LoadDocument(convertedStream);
@@ -68,15 +68,23 @@ public class PdfResultPackageService(
processor.Document.Subject = string.Empty;
processor.Document.Keywords = string.Empty;
processor.AttachFile(new PdfFileAttachment
// Schritt 3a: Berichts-Seiten ans Ende anhängen
int reportPageCount;
using (var reportStream = new FileStream(reportPath, FileMode.Open, FileAccess.Read))
{
FileName = Path.GetFileName(reportPath),
Description = "Ergebnisbericht",
MimeType = "application/pdf",
Relationship = PdfAssociatedFileRelationship.Supplement,
CreationDate = DateTime.Now,
Data = File.ReadAllBytes(reportPath)
});
// Temporäres Dokument öffnen um Seitenanzahl zu ermitteln
using var tempProcessor = new PdfDocumentProcessor();
tempProcessor.LoadDocument(reportStream);
reportPageCount = tempProcessor.Document.Pages.Count;
logger.LogDebug(
"Hänge {PageCount} Seite(n) aus Bericht an das Dokument an.",
reportPageCount);
// Stream zurücksetzen und ans Hauptdokument anhängen
reportStream.Position = 0;
processor.AppendDocument(reportStream);
}
// Schritt 4: Stempel auf Seite 1 zeichnen
var firstPage = processor.Document.Pages[0];
@@ -126,10 +134,15 @@ public class PdfResultPackageService(
processor.Document.Pages[0], 96, 96);
}
// Schritt 4: Speichern
// Schritt 5: Speichern
try
{
processor.SaveDocument(outputPath);
logger.LogInformation(
"Result-PDF erstellt mit {TotalPages} Seiten (Original + {ReportPages} Berichtseiten).",
processor.Document.Pages.Count,
reportPageCount);
}
catch (Exception ex)
{