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.
This commit is contained in:
@@ -6,6 +6,7 @@ namespace DXApp.TemplateKitProject.Data;
|
|||||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||||
{
|
{
|
||||||
public DbSet<ZugferdInvoice> ZugferdInvoices { get; set; }
|
public DbSet<ZugferdInvoice> ZugferdInvoices { get; set; }
|
||||||
|
public DbSet<InvoiceAttachment> InvoiceAttachments { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -27,6 +28,19 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
|||||||
|
|
||||||
entity.Property(e => e.TaxAmount)
|
entity.Property(e => e.TaxAmount)
|
||||||
.HasColumnType("decimal(18,2)");
|
.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");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
141
DXApp.TemplateKitProject/Migrations/20260602123528_AddInvoiceAttachments.Designer.cs
generated
Normal file
141
DXApp.TemplateKitProject/Migrations/20260602123528_AddInvoiceAttachments.Designer.cs
generated
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,40 @@ namespace DXApp.TemplateKitProject.Migrations
|
|||||||
|
|
||||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
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 =>
|
modelBuilder.Entity("DXApp.TemplateKitProject.Models.ZugferdInvoice", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -82,6 +116,22 @@ namespace DXApp.TemplateKitProject.Migrations
|
|||||||
|
|
||||||
b.ToTable("ZugferdInvoices");
|
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
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
DXApp.TemplateKitProject/Models/InvoiceAttachment.cs
Normal file
18
DXApp.TemplateKitProject/Models/InvoiceAttachment.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -17,5 +17,8 @@
|
|||||||
public string SourceType { get; set; } = string.Empty; // "Upload" oder "Email"
|
public string SourceType { get; set; } = string.Empty; // "Upload" oder "Email"
|
||||||
public string ResultFilePath { get; set; } = string.Empty; // Pfad der Result-PDF
|
public string ResultFilePath { get; set; } = string.Empty; // Pfad der Result-PDF
|
||||||
public string GuidelineId { get; set; } = string.Empty; // ZUGFeRD Guideline-ID aus XMP
|
public string GuidelineId { get; set; } = string.Empty; // ZUGFeRD Guideline-ID aus XMP
|
||||||
|
|
||||||
|
// Navigation Property
|
||||||
|
public List<InvoiceAttachment> Attachments { get; set; } = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,13 @@
|
|||||||
@model DXApp.TemplateKitProject.Pages.Invoices.DetailsModel
|
@model DXApp.TemplateKitProject.Pages.Invoices.DetailsModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Rechnungsdetails";
|
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>
|
<h2>📄 Rechnungsdetails</h2>
|
||||||
@@ -11,7 +18,7 @@
|
|||||||
{
|
{
|
||||||
<button class="btn btn-primary mb-3 ms-2"
|
<button class="btn btn-primary mb-3 ms-2"
|
||||||
onclick="openPdfViewer(@Model.Invoice.Id)">
|
onclick="openPdfViewer(@Model.Invoice.Id)">
|
||||||
📄 PDF anzeigen
|
📄 Ergebnis anzeigen
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +57,51 @@ else
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</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="/Invoices/ViewAttachment?filePath=@Uri.EscapeDataString(attachment.SavedFilePath)"
|
||||||
|
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
|
||||||
|
target="_blank">
|
||||||
|
<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()
|
@(Html.DevExtreme().Popup()
|
||||||
.ID("pdf-viewer-popup")
|
.ID("pdf-viewer-popup")
|
||||||
.Title("PDF Viewer")
|
.Title("PDF Viewer")
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ public class DetailsModel(AppDbContext db) : PageModel
|
|||||||
|
|
||||||
public async Task<IActionResult> OnGetAsync(int id)
|
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)
|
if (Invoice is null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|||||||
@@ -75,6 +75,29 @@ public class UploadModel(
|
|||||||
ImportedInvoice.ResultFilePath = ResultFilePath;
|
ImportedInvoice.ResultFilePath = ResultFilePath;
|
||||||
await db.SaveChangesAsync();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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 laden
|
||||||
|
if (ViewerType == AttachmentViewerType.Xml || ViewerType == AttachmentViewerType.Text)
|
||||||
|
{
|
||||||
|
TextContent = System.IO.File.ReadAllText(filePath, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ builder.Services.AddScoped<PdfResultPackageService>();
|
|||||||
builder.Services.AddScoped<ZugferdExtractorService>();
|
builder.Services.AddScoped<ZugferdExtractorService>();
|
||||||
builder.Services.AddScoped<ZugferdParserService>();
|
builder.Services.AddScoped<ZugferdParserService>();
|
||||||
builder.Services.AddScoped<ZugferdImportService>();
|
builder.Services.AddScoped<ZugferdImportService>();
|
||||||
|
builder.Services.AddScoped<AttachmentViewerService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|||||||
49
DXApp.TemplateKitProject/Services/AttachmentViewerService.cs
Normal file
49
DXApp.TemplateKitProject/Services/AttachmentViewerService.cs
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user