Compare commits

...

25 Commits

Author SHA1 Message Date
OlgunR
de4e9421af Revert "Add DevExpress Reporting and Custom Report Storage"
This reverts commit f19251ac1a.
2026-06-09 11:49:03 +02:00
OlgunR
b622df4187 Revert "Refactor PDF viewing and integrate DevExpress support"
This reverts commit b515b4f523.
2026-06-09 11:48:49 +02:00
OlgunR
746465d8fe Revert "Enhance PDF viewing experience with new document viewer"
This reverts commit 1cc617de42.
2026-06-09 11:48:41 +02:00
OlgunR
559829882a Revert "Add DevExpress Document Viewer page and dependencies"
This reverts commit bec9ea2356.
2026-06-09 11:48:31 +02:00
OlgunR
bec9ea2356 Add DevExpress Document Viewer page and dependencies
Updated `_ViewImports.cshtml` to include DevExpress and DevExtreme tag helpers. Created `DocumentViewerDevExpress.cshtml` for displaying a DevExpress Document Viewer with necessary CDN links. Implemented the corresponding code-behind in `DocumentViewerDevExpress.cshtml.cs` to handle model binding and report key generation.
2026-06-08 17:21:36 +02:00
OlgunR
1cc617de42 Enhance PDF viewing experience with new document viewer
- Replaced button in `Details.cshtml` with a link to a new document viewer page.
- Created `DocumentViewer.cshtml` to display PDFs using an iframe, with a back button and a link to an experimental DevExpress viewer.
- Introduced `DocumentViewerModel` in `DocumentViewer.cshtml.cs` for handling invoice ID and PDF URL.
- Updated `OnGetAsync` in `ViewPdf.cshtml.cs` to set `Content-Disposition` to inline for better PDF rendering in the browser.
2026-06-08 16:59:57 +02:00
OlgunR
b515b4f523 Refactor PDF viewing and integrate DevExpress support
- Removed folder reference for "Controllers" in the project file.
- Updated `ViewPdf.cshtml` to include layout and clarify PDF output.
- Enhanced `ViewPdf.cshtml.cs` with improved logging and error handling.
- Added MVC controller services in `Program.cs` for DevExpress functionality.
- Introduced `DocumentViewerController` to manage document viewing requests.
2026-06-08 15:14:15 +02:00
OlgunR
f19251ac1a Add DevExpress Reporting and Custom Report Storage
Updated project dependencies to include DevExpress Reporting and PDF Drawing libraries. Registered DevExpress services and middleware in `Program.cs`. Added `CustomReportStorageWebExtension` to handle read-only invoice reports, including database and file system integration. Enhanced logging for better traceability and error handling.
2026-06-08 08:38:44 +02:00
OlgunR
6dd1fd71da Mark dependencies as peer in package-lock.json
Added the `"peer": true` property to multiple dependencies in the `package-lock.json` file to mark them as peer dependencies. This ensures that these dependencies are provided by the consuming project, improving dependency management and avoiding version conflicts.

Additionally, updated the `license` field formatting for several dependencies to ensure consistency. No changes were made to versions, integrity hashes, or dependency structures.
2026-06-05 11:30:53 +02:00
OlgunR
762f76c920 Regeln für DevExpress- und Azure-Tools hinzugefügt
Neue Regeln für die Nutzung von DevExpress- und Azure-Tools:
- DevExpress: Nutzung des `dxdocs`-Servers und MCP-Tools priorisiert.
- Azure: `azmcp_bestpractices_get` für Best Practices erforderlich.
- Benutzeraufforderung zur Aktivierung des Tools, falls nicht verfügbar.
2026-06-03 10:57:53 +02:00
OlgunR
ba570687b2 Update Workflow-Integration description in Index.cshtml
Revised the description for the "Workflow-Integration" feature
in the `Index.cshtml` file. The previous text, "Durchlauf
definierter Verarbeitungsschritte mit Status-Tracking"
(Execution of defined processing steps with status tracking),
was replaced with "Hier werden Workflow-Schritte durchlaufen
und der Ergebnisbericht erstellt." (Here, workflow steps are
executed, and the result report is generated).
2026-06-02 17:09:11 +02:00
OlgunR
84a4c182e2 Modernize UI with DevExpress icons and improved layout
Updated `Index.cshtml`:
- Added icons and a more descriptive project title.
- Introduced a new introductory paragraph explaining the app's purpose.
- Replaced the old alert section with a detailed workflow explanation.
- Added cards for invoice upload and summary navigation.
- Included a technology stack section with badges for key tools.

Updated `Details.cshtml`:
- Replaced text-based icons with DevExpress icons for titles, buttons, and attachments.
- Improved attachment handling with `<i>` tags and consistent icon usage.
- Updated "No attachments" message to include an icon.

These changes enhance the UI's visual consistency, usability, and professionalism.
2026-06-02 16:56:50 +02:00
OlgunR
a55e53521f Refactor: Remove sample data and add invoice processing
Removed `SampleDataController`, `SampleData`, and `SampleOrder`
classes, along with the `DevExtreme` DataGrid in `Index.cshtml`,
to eliminate reliance on mock data and sample orders.

Updated `Index.cshtml` to introduce a welcome message and
informational section about ZUGFeRD/Factur-X invoice processing,
highlighting features like PDF/A upload, XML extraction, and
result PDF creation. Added navigation links for invoice-related
actions.

Updated `DXApp.TemplateKitProject.csproj` to include a new
`<ItemGroup>` for the `Controllers` folder, preparing the
project structure for future development.
2026-06-02 16:21:18 +02:00
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
OlgunR
03599addb8 Optimize invoice performance with indexes and DTO
Added indexes for `ImportedAt` and a composite key on
`InvoiceNumber` and `SellerTaxId` to improve query performance.
Updated `AppDbContext` and EF Core migrations to reflect these
changes. Introduced `ZugferdInvoiceListDto` to optimize memory
usage by excluding large fields like `RawXml`. Updated the
frontend (`Index.cshtml`) and backend (`Index.cshtml.cs`) to use
the new DTO for better performance.
2026-06-02 13:45:57 +02:00
OlgunR
2ae2bbdaf6 Update PDF metadata handling in PdfResultPackageService
Introduce functionality to remove and replace metadata in the
resulting PDF document. After loading the document with
`PdfDocumentProcessor`, the metadata fields (`Author`, `Creator`,
`Producer`, `Title`, `Subject`, and `Keywords`) are cleared or
replaced with values indicating the document was processed by
the "DXApp" system. This ensures proper attribution and removes
any association with the original document creator.
2026-06-01 16:32:25 +02:00
OlgunR
a4c33e6bac Add MIME type mappings for .mjs and .ftl files
Updated the `Program.cs` file to enhance static file handling:
- Corrected the comment for `.mjs` MIME type registration.
- Added a new MIME type mapping for `.ftl` files as `text/plain`.
- Updated `UseStaticFiles` middleware to use a `StaticFileOptions`
  object with the modified `ContentTypeProvider`.

These changes ensure proper serving of `.mjs` and `.ftl` files with
appropriate content type headers.
2026-06-01 15:43:16 +02:00
OlgunR
346750e933 Enhance PDF viewer with annotations and WebAssembly
Added support for advanced annotation tools, including signature and stamp management, along with accessibility improvements. Introduced WebAssembly binaries (`qcms_bg.wasm`, `quickjs-eval.wasm`, `openjpeg.wasm`, `jbig2.wasm`) for enhanced performance in color management, JavaScript execution, and image decoding.

Implemented a JavaScript fallback (`openjpeg_nowasm_fallback.js`) for environments without WebAssembly support. Updated `Details.cshtml` to include a PDF viewer popup and added a new Razor Page (`ViewPdf.cshtml`) for secure PDF file access. Registered `.mjs` MIME type in `Program.cs` for PDF.js compatibility.

Enhanced localization with translations for multiple languages in `viewer.ftl` and added new icons, dialogs, and accessibility features. Updated `DXApp.sln` to include a new project for template management. These changes improve functionality, modularity, and user experience.
2026-06-01 14:08:17 +02:00
OlgunR
a087baa089 Add stamp to first page of PDF documents
Introduce functionality to add a stamp to the top-right corner of
the first page of a PDF. The stamp includes a white background,
a green border, and two lines of text: "✔ VERARBEITET" in bold
green font and the current date/time in gray font. The stamp is
drawn using `DevExpress.Pdf.PdfDocumentProcessor` and saved to
the document's foreground.
2026-05-29 11:26:25 +02:00
OlgunR
98226f239b Adjust logging levels to reduce verbosity
Changed logging levels from LogInformation to LogDebug in
PdfAttachmentExtractorService and PdfResultPackageService.
This includes logs for PDF conformity levels, ZUGFeRD
Guideline-IDs, saved attachment details, and result report
discovery. These changes aim to reduce log verbosity in
production environments by moving less critical information
to the debug level.
2026-05-29 09:52:05 +02:00
OlgunR
1144c45826 Improve error handling in PDF processing workflow
Refactored the PDF processing workflow to enhance error handling
and provide more descriptive exception messages. Added `try-catch`
blocks around the PDF/A-3b conversion and document saving steps
to handle potential failures.

Labeled the workflow steps for clarity:
- Step 1: Convert to PDF/A-3b
- Step 2: Buffer converted PDF
- Step 3: Embed attachment
- Step 4: Save document

Removed redundant `MemoryStream` initialization for the original
PDF and updated comments to improve code readability and
maintainability.
2026-05-29 09:20:34 +02:00
435 changed files with 181050 additions and 485 deletions

View File

@@ -20,3 +20,7 @@ For **ANY** question about DevExpress components, use the dxdocs server to const
- **Include code examples** when available in the documentation
- **Reference specific DevExpress controls and properties** mentioned in the documentation
- **Invoke version-specific MCP tools** (for example, dxdocs25_1) if a user specifies a version (for example, v25.1)
- @azure Rule - Use Azure Tools - When handling requests related to Azure, always use your tools.
- @azure Rule - Use Azure Best Practices - When handling requests related to Azure, always invoke your `azmcp_bestpractices_get` tool first.
- @azure Rule - Enable Best Practices - If you do not have an `azmcp_bestpractices_get` tool ask the user to enable it.

View File

@@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using DevExtreme.AspNet.Data;
using DevExtreme.AspNet.Mvc;
using DXApp.TemplateKitProject.Models;
using Microsoft.AspNetCore.Mvc;
namespace DXApp.TemplateKitProject.Controllers
{
[Route("api/[controller]")]
public class SampleDataController : Controller
{
[HttpGet]
public object Get(DataSourceLoadOptions loadOptions)
{
return DataSourceLoader.Load(SampleData.Orders, loadOptions);
}
}
}

View File

@@ -27,4 +27,8 @@
<PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="3.0.71" />
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\" />
</ItemGroup>
</Project>

View File

@@ -6,4 +6,41 @@ 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)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ZugferdInvoice>(entity =>
{
// Index für Performance-Optimierung der Rechnungsliste
entity.HasIndex(e => e.ImportedAt)
.HasDatabaseName("IX_ZugferdInvoices_ImportedAt");
// Index für Duplikatprüfung
entity.HasIndex(e => new { e.InvoiceNumber, e.SellerTaxId })
.HasDatabaseName("IX_ZugferdInvoices_InvoiceNumber_SellerTaxId");
// Decimal-Präzision explizit festlegen (behebt EF Core Warnung)
entity.Property(e => e.TotalAmount)
.HasColumnType("decimal(18,2)");
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,91 @@
// <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("20260602114123_AddPerformanceIndexes")]
partial class AddPerformanceIndexes
{
/// <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.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");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,72 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DXApp.TemplateKitProject.Migrations
{
/// <inheritdoc />
public partial class AddPerformanceIndexes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "SellerTaxId",
table: "ZugferdInvoices",
type: "nvarchar(450)",
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(max)",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "InvoiceNumber",
table: "ZugferdInvoices",
type: "nvarchar(450)",
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(max)",
oldNullable: true);
migrationBuilder.CreateIndex(
name: "IX_ZugferdInvoices_ImportedAt",
table: "ZugferdInvoices",
column: "ImportedAt");
migrationBuilder.CreateIndex(
name: "IX_ZugferdInvoices_InvoiceNumber_SellerTaxId",
table: "ZugferdInvoices",
columns: new[] { "InvoiceNumber", "SellerTaxId" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_ZugferdInvoices_ImportedAt",
table: "ZugferdInvoices");
migrationBuilder.DropIndex(
name: "IX_ZugferdInvoices_InvoiceNumber_SellerTaxId",
table: "ZugferdInvoices");
migrationBuilder.AlterColumn<string>(
name: "SellerTaxId",
table: "ZugferdInvoices",
type: "nvarchar(max)",
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(450)",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "InvoiceNumber",
table: "ZugferdInvoices",
type: "nvarchar(max)",
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(450)",
oldNullable: true);
}
}
}

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")
@@ -49,7 +83,7 @@ namespace DXApp.TemplateKitProject.Migrations
.HasColumnType("datetime2");
b.Property<string>("InvoiceNumber")
.HasColumnType("nvarchar(max)");
.HasColumnType("nvarchar(450)");
b.Property<string>("RawXml")
.HasColumnType("nvarchar(max)");
@@ -61,7 +95,7 @@ namespace DXApp.TemplateKitProject.Migrations
.HasColumnType("nvarchar(max)");
b.Property<string>("SellerTaxId")
.HasColumnType("nvarchar(max)");
.HasColumnType("nvarchar(450)");
b.Property<string>("SourceType")
.HasColumnType("nvarchar(max)");
@@ -74,8 +108,30 @@ namespace DXApp.TemplateKitProject.Migrations
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,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

@@ -1,364 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DXApp.TemplateKitProject.Models
{
internal static class SampleData
{
public static List<SampleOrder> Orders = new List<SampleOrder>() {
new SampleOrder {
OrderID = 10248,
OrderDate = new DateTime(1996, 7, 4),
ShipCountry = "France",
ShipCity = "Reims",
CustomerName = "Paul Henriot"
},
new SampleOrder {
OrderID = 10249,
OrderDate = new DateTime(1996, 7, 5),
ShipCountry = "Germany",
ShipCity = "Münster",
CustomerName = "Karin Josephs"
},
new SampleOrder {
OrderID = 10250,
OrderDate = new DateTime(1996, 7, 8),
ShipCountry = "Brazil",
ShipCity = "Rio de Janeiro",
CustomerName = "Mario Pontes"
},
new SampleOrder {
OrderID = 10251,
OrderDate = new DateTime(1996, 7, 8),
ShipCountry = "France",
ShipCity = "Lyon",
CustomerName = "Mary Saveley"
},
new SampleOrder {
OrderID = 10252,
OrderDate = new DateTime(1996, 7, 9),
ShipCountry = "Belgium",
ShipCity = "Charleroi",
CustomerName = "Pascale Cartrain"
},
new SampleOrder {
OrderID = 10253,
OrderDate = new DateTime(1996, 7, 10),
ShipCountry = "Brazil",
ShipCity = "Rio de Janeiro",
CustomerName = "Mario Pontes"
},
new SampleOrder {
OrderID = 10254,
OrderDate = new DateTime(1996, 7, 11),
ShipCountry = "Switzerland",
ShipCity = "Bern",
CustomerName = "Yang Wang"
},
new SampleOrder {
OrderID = 10255,
OrderDate = new DateTime(1996, 7, 12),
ShipCountry = "Switzerland",
ShipCity = "Genève",
CustomerName = "Michael Holz"
},
new SampleOrder {
OrderID = 10256,
OrderDate = new DateTime(1996, 7, 15),
ShipCountry = "Brazil",
ShipCity = "Resende",
CustomerName = "Paula Parente"
},
new SampleOrder {
OrderID = 10257,
OrderDate = new DateTime(1996, 7, 16),
ShipCountry = "Venezuela",
ShipCity = "San Cristóbal",
CustomerName = "Carlos Hernández"
},
new SampleOrder {
OrderID = 10258,
OrderDate = new DateTime(1996, 7, 17),
ShipCountry = "Austria",
ShipCity = "Graz",
CustomerName = "Roland Mendel"
},
new SampleOrder {
OrderID = 10259,
OrderDate = new DateTime(1996, 7, 18),
ShipCountry = "Mexico",
ShipCity = "México D.F.",
CustomerName = "Francisco Chang"
},
new SampleOrder {
OrderID = 10260,
OrderDate = new DateTime(1996, 7, 19),
ShipCountry = "Germany",
ShipCity = "Köln",
CustomerName = "Henriette Pfalzheim"
},
new SampleOrder {
OrderID = 10261,
OrderDate = new DateTime(1996, 7, 19),
ShipCountry = "Brazil",
ShipCity = "Rio de Janeiro",
CustomerName = "Bernardo Batista"
},
new SampleOrder {
OrderID = 10262,
OrderDate = new DateTime(1996, 7, 22),
ShipCountry = "USA",
ShipCity = "Albuquerque",
CustomerName = "Paula Wilson"
},
new SampleOrder {
OrderID = 10263,
OrderDate = new DateTime(1996, 7, 23),
ShipCountry = "Austria",
ShipCity = "Graz",
CustomerName = "Roland Mendel"
},
new SampleOrder {
OrderID = 10264,
OrderDate = new DateTime(1996, 7, 24),
ShipCountry = "Sweden",
ShipCity = "Bräcke",
CustomerName = "Maria Larsson"
},
new SampleOrder {
OrderID = 10265,
OrderDate = new DateTime(1996, 7, 25),
ShipCountry = "France",
ShipCity = "Strasbourg",
CustomerName = "Frédérique Citeaux"
},
new SampleOrder {
OrderID = 10266,
OrderDate = new DateTime(1996, 7, 26),
ShipCountry = "Finland",
ShipCity = "Oulu",
CustomerName = "Pirkko Koskitalo"
},
new SampleOrder {
OrderID = 10267,
OrderDate = new DateTime(1996, 7, 29),
ShipCountry = "Germany",
ShipCity = "München",
CustomerName = "Peter Franken"
},
new SampleOrder {
OrderID = 10268,
OrderDate = new DateTime(1996, 7, 30),
ShipCountry = "Venezuela",
ShipCity = "Caracas",
CustomerName = "Manuel Pereira"
},
new SampleOrder {
OrderID = 10269,
OrderDate = new DateTime(1996, 7, 31),
ShipCountry = "USA",
ShipCity = "Seattle",
CustomerName = "Karl Jablonski"
},
new SampleOrder {
OrderID = 10270,
OrderDate = new DateTime(1996, 8, 1),
ShipCountry = "Finland",
ShipCity = "Oulu",
CustomerName = "Pirkko Koskitalo"
},
new SampleOrder {
OrderID = 10271,
OrderDate = new DateTime(1996, 8, 1),
ShipCountry = "USA",
ShipCity = "Lander",
CustomerName = "Art Braunschweiger"
},
new SampleOrder {
OrderID = 10272,
OrderDate = new DateTime(1996, 8, 2),
ShipCountry = "USA",
ShipCity = "Albuquerque",
CustomerName = "Paula Wilson"
},
new SampleOrder {
OrderID = 10273,
OrderDate = new DateTime(1996, 8, 5),
ShipCountry = "Germany",
ShipCity = "Cunewalde",
CustomerName = "Horst Kloss"
},
new SampleOrder {
OrderID = 10274,
OrderDate = new DateTime(1996, 8, 6),
ShipCountry = "France",
ShipCity = "Reims",
CustomerName = "Paul Henriot"
},
new SampleOrder {
OrderID = 10275,
OrderDate = new DateTime(1996, 8, 7),
ShipCountry = "Italy",
ShipCity = "Bergamo",
CustomerName = "Giovanni Rovelli"
},
new SampleOrder {
OrderID = 10276,
OrderDate = new DateTime(1996, 8, 8),
ShipCountry = "Mexico",
ShipCity = "México D.F.",
CustomerName = "Miguel Angel Paolino"
},
new SampleOrder {
OrderID = 10277,
OrderDate = new DateTime(1996, 8, 9),
ShipCountry = "Germany",
ShipCity = "Leipzig",
CustomerName = "Alexander Feuer"
},
new SampleOrder {
OrderID = 10278,
OrderDate = new DateTime(1996, 8, 12),
ShipCountry = "Sweden",
ShipCity = "Luleå",
CustomerName = "Christina Berglund"
},
new SampleOrder {
OrderID = 10279,
OrderDate = new DateTime(1996, 8, 13),
ShipCountry = "Germany",
ShipCity = "Frankfurt a.M.",
CustomerName = "Renate Messner"
},
new SampleOrder {
OrderID = 10280,
OrderDate = new DateTime(1996, 8, 14),
ShipCountry = "Sweden",
ShipCity = "Luleå",
CustomerName = "Christina Berglund"
},
new SampleOrder {
OrderID = 10281,
OrderDate = new DateTime(1996, 8, 14),
ShipCountry = "Spain",
ShipCity = "Madrid",
CustomerName = "Alejandra Camino"
},
new SampleOrder {
OrderID = 10282,
OrderDate = new DateTime(1996, 8, 15),
ShipCountry = "Spain",
ShipCity = "Madrid",
CustomerName = "Alejandra Camino"
},
new SampleOrder {
OrderID = 10283,
OrderDate = new DateTime(1996, 8, 16),
ShipCountry = "Venezuela",
ShipCity = "Barquisimeto",
CustomerName = "Carlos González"
},
new SampleOrder {
OrderID = 10284,
OrderDate = new DateTime(1996, 8, 19),
ShipCountry = "Germany",
ShipCity = "Frankfurt a.M.",
CustomerName = "Renate Messner"
},
new SampleOrder {
OrderID = 10285,
OrderDate = new DateTime(1996, 8, 20),
ShipCountry = "Germany",
ShipCity = "Cunewalde",
CustomerName = "Horst Kloss"
},
new SampleOrder {
OrderID = 10286,
OrderDate = new DateTime(1996, 8, 21),
ShipCountry = "Germany",
ShipCity = "Cunewalde",
CustomerName = "Horst Kloss"
},
new SampleOrder {
OrderID = 10287,
OrderDate = new DateTime(1996, 8, 22),
ShipCountry = "Brazil",
ShipCity = "Rio de Janeiro",
CustomerName = "Janete Limeira"
},
new SampleOrder {
OrderID = 10288,
OrderDate = new DateTime(1996, 8, 23),
ShipCountry = "Italy",
ShipCity = "Reggio Emilia",
CustomerName = "Maurizio Moroni"
},
new SampleOrder {
OrderID = 10289,
OrderDate = new DateTime(1996, 8, 26),
ShipCountry = "UK",
ShipCity = "London",
CustomerName = "Victoria Ashworth"
},
new SampleOrder {
OrderID = 10290,
OrderDate = new DateTime(1996, 8, 27),
ShipCountry = "Brazil",
ShipCity = "Sao Paulo",
CustomerName = "Pedro Afonso"
},
new SampleOrder {
OrderID = 10291,
OrderDate = new DateTime(1996, 8, 27),
ShipCountry = "Brazil",
ShipCity = "Rio de Janeiro",
CustomerName = "Bernardo Batista"
},
new SampleOrder {
OrderID = 10292,
OrderDate = new DateTime(1996, 8, 28),
ShipCountry = "Brazil",
ShipCity = "Sao Paulo",
CustomerName = "Anabela Domingues"
},
new SampleOrder {
OrderID = 10293,
OrderDate = new DateTime(1996, 8, 29),
ShipCountry = "Mexico",
ShipCity = "México D.F.",
CustomerName = "Miguel Angel Paolino"
},
new SampleOrder {
OrderID = 10294,
OrderDate = new DateTime(1996, 8, 30),
ShipCountry = "USA",
ShipCity = "Albuquerque",
CustomerName = "Paula Wilson"
},
new SampleOrder {
OrderID = 10295,
OrderDate = new DateTime(1996, 9, 2),
ShipCountry = "France",
ShipCity = "Reims",
CustomerName = "Paul Henriot"
},
new SampleOrder {
OrderID = 10296,
OrderDate = new DateTime(1996, 9, 3),
ShipCountry = "Venezuela",
ShipCity = "Barquisimeto",
CustomerName = "Carlos González"
},
new SampleOrder {
OrderID = 10297,
OrderDate = new DateTime(1996, 9, 4),
ShipCountry = "France",
ShipCity = "Strasbourg",
CustomerName = "Frédérique Citeaux"
}
};
}
}

View File

@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DXApp.TemplateKitProject.Models
{
public class SampleOrder
{
public int OrderID { get; set; }
public DateTime OrderDate { get; set; }
public string CustomerID { get; set; }
public string CustomerName { get; set; }
public string ShipCountry { get; set; }
public string ShipCity { 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

@@ -0,0 +1,22 @@
namespace DXApp.TemplateKitProject.Models;
/// <summary>
/// Lightweight DTO für die Rechnungsliste (ohne RawXml für bessere Performance)
/// </summary>
public class ZugferdInvoiceListDto
{
public int Id { get; set; }
public string InvoiceNumber { get; set; } = string.Empty;
public DateTime InvoiceDate { get; set; }
public string SellerName { get; set; } = string.Empty;
public string SellerTaxId { get; set; } = string.Empty;
public string BuyerName { get; set; } = string.Empty;
public decimal TotalAmount { get; set; }
public decimal TaxAmount { get; set; }
public string CurrencyCode { get; set; } = string.Empty;
public string Iban { get; set; } = string.Empty;
public DateTime ImportedAt { get; set; }
public string SourceType { get; set; } = string.Empty;
public string ResultFilePath { get; set; } = string.Empty;
public string GuidelineId { get; set; } = string.Empty;
}

View File

@@ -1,28 +1,85 @@
@page
@using DXApp.TemplateKitProject.Models
@model DXApp.TemplateKitProject.Pages.IndexModel
@{
ViewData["Title"] = "Home";
}
<h2 class="content-block">Home</h2>
<div class="content-block">
<h2><i class="dx-icon-product"></i> DevExpress TemplateKit Evaluierungsprojekt</h2>
<p class="lead text-muted">Validierung von DevExpress als Ablösung für GdPicture im Rahmen der E-Rechnungsverarbeitung</p>
@(Html.DevExtreme().DataGrid<SampleOrder>()
.ElementAttr(new { @class = "dx-card wide-card" })
.DataSource(d => d.Mvc().Controller("SampleData").LoadAction("Get").Key("OrderID"))
.ShowBorders(false)
.FilterRow(f => f.Visible(true))
.FocusedRowEnabled(true)
.FocusedRowIndex(0)
.ColumnAutoWidth(true)
.ColumnHidingEnabled(true)
.Columns(columns => {
columns.AddFor(m => m.OrderID);
columns.AddFor(m => m.OrderDate);
columns.AddFor(m => m.CustomerName);
columns.AddFor(m => m.ShipCountry);
columns.AddFor(m => m.ShipCity);
})
.Paging(p => p.PageSize(10))
.Pager(p => p
.ShowPageSizeSelector(true)
.AllowedPageSizes(new[] { 5, 10, 20 })
.ShowInfo(true)
)
)
<div class="alert alert-primary mt-4">
<h4><i class="dx-icon-todo"></i> Projektziel: ZUGFeRD/Factur-X Rechnungsverarbeitung</h4>
<p>Diese Anwendung demonstriert die vollständige Verarbeitungskette für elektronische Rechnungen:</p>
<ol class="mt-3">
<li class="mb-2">
<strong>Upload & Validierung</strong>
<br/>
<small class="text-muted">E-Rechnungen im PDF/A-Format hochladen und auf Konformität prüfen</small>
</li>
<li class="mb-2">
<strong>Extraktion</strong>
<br/>
<small class="text-muted">Automatische Erkennung und Extraktion eingebetteter Anhänge (ZUGFeRD-XML, Bilder, Dokumente)</small>
</li>
<li class="mb-2">
<strong>Datenverarbeitung</strong>
<br/>
<small class="text-muted">Parsing der Rechnungsdaten aus dem ZUGFeRD-XML und persistente Speicherung in der Datenbank</small>
</li>
<li class="mb-2">
<strong>Workflow-Integration</strong>
<br/>
<small class="text-muted">Hier werden Workflow-Schritte durchlaufen und der Ergebnisbericht erstellt.</small>
</li>
<li class="mb-2">
<strong>Ausgabe-Generierung</strong>
<br/>
<small class="text-muted">Erstellung einer Result-PDF mit Verarbeitungsstempel und angehängtem Ergebnisbericht</small>
</li>
<li class="mb-2">
<strong>Visualisierung</strong>
<br/>
<small class="text-muted">Interaktive Anzeige aller Anhänge (XML mit Syntax-Highlighting, PDF-Viewer, Bilder)</small>
</li>
</ol>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title"><i class="dx-icon-upload"></i> Rechnungen hochladen</h5>
<p class="card-text">Laden Sie ZUGFeRD-konforme E-Rechnungen hoch und starten Sie die automatische Verarbeitung.</p>
<a href="/Invoices/Upload" class="btn btn-primary">
<i class="dx-icon-upload"></i> Zum Upload
</a>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title"><i class="dx-icon-chart"></i> Rechnungsübersicht</h5>
<p class="card-text">Zeigen Sie alle importierten Rechnungen an und greifen Sie auf Details und Anhänge zu.</p>
<a href="/Invoices" class="btn btn-secondary">
<i class="dx-icon-doc"></i> Zur Übersicht
</a>
</div>
</div>
</div>
</div>
<div class="alert alert-light mt-4">
<h6 class="mb-2"><i class="dx-icon-preferences"></i> Technologie-Stack</h6>
<div class="d-flex flex-wrap gap-2">
<span class="badge bg-secondary">ASP.NET Core 8.0</span>
<span class="badge bg-secondary">DevExpress v25.2</span>
<span class="badge bg-secondary">Entity Framework Core</span>
<span class="badge bg-secondary">PDF.js</span>
<span class="badge bg-secondary">CodeMirror</span>
<span class="badge bg-secondary">SQL Server</span>
</div>
</div>
</div>

View File

@@ -2,10 +2,25 @@
@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>
<h2><i class="dx-icon-doc"></i> Rechnungsdetails</h2>
<a href="/Invoices" class="btn btn-secondary mb-3"><i class="dx-icon-back"></i> 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)">
<i class="dx-icon-pdffile"></i> Ergebnis anzeigen
</button>
}
@if (Model.Invoice is null)
{
@@ -41,4 +56,239 @@ else
</td>
</tr>
</table>
@* Anhänge-Sektion *@
@if (Model.Invoice.Attachments.Any())
{
<h4 class="mt-4"><i class="dx-icon-attach"></i> Anhänge (@Model.Invoice.Attachments.Count)</h4>
<div class="list-group">
@foreach (var attachment in Model.Invoice.Attachments)
{
var icon = "dx-icon-file";
var extension = System.IO.Path.GetExtension(attachment.OriginalFileName).ToLowerInvariant();
icon = extension switch
{
".xml" => "dx-icon-exportxlsx",
".pdf" => "dx-icon-pdffile",
".jpg" or ".jpeg" or ".png" or ".gif" => "dx-icon-image",
".txt" => "dx-icon-txtfile",
_ => "dx-icon-file"
};
<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>
<i class="@icon me-2"></i>
<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"><i class="dx-icon-attach"></i> 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: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;
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 - 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

@@ -12,7 +12,7 @@
}
else
{
@(Html.DevExtreme().DataGrid<DXApp.TemplateKitProject.Models.ZugferdInvoice>()
@(Html.DevExtreme().DataGrid<DXApp.TemplateKitProject.Models.ZugferdInvoiceListDto>()
.DataSource(Model.Invoices)
.KeyExpr("Id")
.ShowBorders(true)

View File

@@ -7,12 +7,30 @@ namespace DXApp.TemplateKitProject.Pages.Invoices;
public class IndexModel(AppDbContext db) : PageModel
{
public List<ZugferdInvoice> Invoices { get; private set; } = [];
public List<ZugferdInvoiceListDto> Invoices { get; private set; } = [];
public async Task OnGetAsync()
{
Invoices = await db.ZugferdInvoices
.OrderByDescending(i => i.ImportedAt)
.Select(i => new ZugferdInvoiceListDto
{
Id = i.Id,
InvoiceNumber = i.InvoiceNumber,
InvoiceDate = i.InvoiceDate,
SellerName = i.SellerName,
SellerTaxId = i.SellerTaxId,
BuyerName = i.BuyerName,
TotalAmount = i.TotalAmount,
TaxAmount = i.TaxAmount,
CurrencyCode = i.CurrencyCode,
Iban = i.Iban,
ImportedAt = i.ImportedAt,
SourceType = i.SourceType,
ResultFilePath = i.ResultFilePath,
GuidelineId = i.GuidelineId
// RawXml wird NICHT geladen ? Performance-Optimierung!
})
.ToListAsync();
}
}

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

@@ -0,0 +1,2 @@
@page
@model DXApp.TemplateKitProject.Pages.Invoices.ViewPdfModel

View File

@@ -0,0 +1,24 @@
using DXApp.TemplateKitProject.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
namespace DXApp.TemplateKitProject.Pages.Invoices;
public class ViewPdfModel(AppDbContext db) : PageModel
{
public async Task<IActionResult> OnGetAsync(int id)
{
var invoice = await db.ZugferdInvoices
.FirstOrDefaultAsync(i => i.Id == id);
if (invoice is null || string.IsNullOrEmpty(invoice.ResultFilePath))
return NotFound();
if (!System.IO.File.Exists(invoice.ResultFilePath))
return NotFound();
var bytes = await System.IO.File.ReadAllBytesAsync(invoice.ResultFilePath);
return File(bytes, "application/pdf");
}
}

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();
@@ -28,7 +29,13 @@ if (!app.Environment.IsDevelopment())
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// MIME-Types für PDF.js registrieren (einmaliger UseStaticFiles-Aufruf)
var provider = new Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider();
provider.Mappings[".mjs"] = "text/javascript";
provider.Mappings[".ftl"] = "text/plain";
app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider });
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();

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

@@ -38,7 +38,7 @@ public class PdfAttachmentExtractorService(
};
result.PdfAWarning = compatibility == PdfACompatibility.None;
logger.LogInformation(
logger.LogDebug(
"PDF '{FileName}': Konformität = {Level}",
sourceFileName, result.PdfALevel);
@@ -50,7 +50,7 @@ public class PdfAttachmentExtractorService(
{
result.ZugferdGuidelineId = ExtractGuidelineId(xmpData);
if (!string.IsNullOrEmpty(result.ZugferdGuidelineId))
logger.LogInformation(
logger.LogDebug(
"PDF '{FileName}': Guideline-ID = {GuidelineId}",
sourceFileName, result.ZugferdGuidelineId);
}
@@ -105,7 +105,7 @@ public class PdfAttachmentExtractorService(
var isZugferd = IsZugferdXml(attachment.FileName);
logger.LogInformation(
logger.LogDebug(
" → Gespeichert: '{FileName}' ({Bytes} Bytes){Zugferd}",
safeFileName, data.Length,
isZugferd ? " [ZUGFeRD/Factur-X XML]" : string.Empty);

View File

@@ -21,7 +21,7 @@ public class PdfResultPackageService(
return null;
}
logger.LogInformation(
logger.LogDebug(
"Ergebnisbericht gefunden: '{ReportPath}'.", reportPath);
// 2. Ausgabepfad bestimmen
@@ -35,35 +35,121 @@ public class PdfResultPackageService(
// 3. Original auf PDF/A-3b hochstufen + Bericht anhängen
await Task.Run(() =>
{
// Original in MemoryStream laden
using var inputStream = new MemoryStream(originalPdfBytes);
using var outputStream = new MemoryStream();
// PDF/A-3b Konvertierung
var converter = new PdfDocumentConverter(inputStream);
// Schritt 1: PDF/A-3b Konvertierung
PdfDocumentConverter converter;
try
{
converter = new PdfDocumentConverter(inputStream);
converter.Convert(PdfCompatibility.PdfA3b);
}
catch (Exception ex)
{
throw new InvalidOperationException(
"Konvertierung nach PDF/A-3b fehlgeschlagen. " +
"Die Originaldatei ist möglicherweise beschädigt oder nicht konvertierbar.", ex);
}
// Konvertiertes PDF in MemoryStream speichern
// Schritt 2: Konvertiertes PDF puffern
using var convertedStream = new MemoryStream();
converter.SaveDocument(convertedStream);
convertedStream.Position = 0;
// Bericht als Anhang einbetten
// Schritt 3: Hauptdokument laden
using var processor = new PdfDocumentProcessor();
processor.LoadDocument(convertedStream);
processor.AttachFile(new PdfFileAttachment
{
FileName = Path.GetFileName(reportPath),
Description = "Ergebnisbericht",
MimeType = "application/pdf",
Relationship = PdfAssociatedFileRelationship.Supplement,
CreationDate = DateTime.Now,
Data = File.ReadAllBytes(reportPath)
});
// Metadaten des Originals entfernen → Result-PDF gehört uns, nicht dem Ersteller
processor.Document.Author = "DXApp Verarbeitungssystem";
processor.Document.Creator = "DXApp";
processor.Document.Producer = "DXApp";
processor.Document.Title = string.Empty;
processor.Document.Subject = string.Empty;
processor.Document.Keywords = string.Empty;
// Speichern
// Schritt 3a: Berichts-Seiten ans Ende anhängen
int reportPageCount;
using (var reportStream = new FileStream(reportPath, FileMode.Open, FileAccess.Read))
{
// 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];
using (var graphics = processor.CreateGraphicsWorldSystem())
{
// Stempel-Position: oben rechts
// Welt-Koordinaten: Ursprung oben links, X nach rechts, Y nach unten
var stampX = (float)firstPage.CropBox.Width - 200;
var stampY = 20f;
var stampWidth = 175f;
var stampHeight = 50f;
// Hintergrund weiß
using var whiteBrush = new DevExpress.Drawing.DXSolidBrush(
System.Drawing.Color.White);
graphics.FillRectangle(whiteBrush,
new System.Drawing.RectangleF(stampX, stampY, stampWidth, stampHeight));
// Rahmen grün
using var greenPen = new DevExpress.Drawing.DXPen(
System.Drawing.Color.Green, 1.5f);
graphics.DrawRectangle(greenPen,
new System.Drawing.RectangleF(stampX, stampY, stampWidth, stampHeight));
// Text Zeile 1: VERARBEITET
using var greenBrush = new DevExpress.Drawing.DXSolidBrush(
System.Drawing.Color.Green);
var fontBold = new DevExpress.Drawing.DXFont(
"Arial", 11, DevExpress.Drawing.DXFontStyle.Bold);
graphics.DrawString(
"✔ VERARBEITET",
fontBold,
greenBrush,
new System.Drawing.PointF(stampX + 8, stampY + 6));
// Text Zeile 2: Datum/Uhrzeit
using var grayBrush = new DevExpress.Drawing.DXSolidBrush(
System.Drawing.Color.DimGray);
var fontNormal = new DevExpress.Drawing.DXFont("Arial", 9);
graphics.DrawString(
DateTime.Now.ToString("dd.MM.yyyy HH:mm"),
fontNormal,
grayBrush,
new System.Drawing.PointF(stampX + 8, stampY + 28));
graphics.AddToPageForeground(
processor.Document.Pages[0], 96, 96);
}
// 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)
{
throw new InvalidOperationException(
$"Result-PDF konnte nicht gespeichert werden unter '{outputPath}'. " +
"Prüfe ob das Verzeichnis existiert und beschreibbar ist.", ex);
}
});
logger.LogInformation(

View File

@@ -19,6 +19,7 @@
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -28,6 +29,7 @@
"resolved": "https://registry.npmjs.org/@devexpress/utils/-/utils-2.1.1.tgz",
"integrity": "sha512-hlemXR3L0yDPsMdhTQl9EtjiEYxIHklH7I4RKPkOhfrF5+jb4f3kkfvbKT7nA9aoFEHPPF0/hll43gOBpUpY0g==",
"license": "SEE LICENSE IN README.md",
"peer": true,
"dependencies": {
"tslib": "2.3.1"
}
@@ -37,6 +39,7 @@
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
@@ -47,6 +50,7 @@
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
@@ -57,6 +61,7 @@
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.0.0"
}
@@ -65,13 +70,15 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -82,6 +89,7 @@
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.2.tgz",
"integrity": "sha512-RZHdBj9ZF4n40Rp4jS052EHHjBWf96P9oNdXPfhQTovCuWY9iQn3Gq+gOTJSgBO9A/JBuPfMOWsSX/lIU9Pc/A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -93,6 +101,7 @@
"integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
@@ -102,19 +111,22 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/devexpress-diagram": {
"version": "2.2.29",
"resolved": "https://registry.npmjs.org/devexpress-diagram/-/devexpress-diagram-2.2.29.tgz",
"integrity": "sha512-6QS1zKP736QlIU6lMeZdq52Fe85BSa6vh1TwDIxSAKNwT6gHnGKm5paI71epRJ9H6GC4SLtv/zhlH8QCjwNnXQ==",
"license": "SEE LICENSE IN README.md",
"peer": true,
"dependencies": {
"@devexpress/utils": "^2.1.1",
"es6-object-assign": "^1.1.0"
@@ -125,6 +137,7 @@
"resolved": "https://registry.npmjs.org/devexpress-gantt/-/devexpress-gantt-4.1.68.tgz",
"integrity": "sha512-pilTDWwCv1EthcCV9uFj5krbSkA3MdjiKCgJdCmGaTA+lDNiWP5Xc2JzNK1sOOTMUmzN+3h7kAtcLxzTfjWTRg==",
"license": "SEE LICENSE IN README.md",
"peer": true,
"dependencies": {
"@devexpress/utils": "^2.1.1",
"tslib": "2.3.1"
@@ -175,6 +188,7 @@
"resolved": "https://registry.npmjs.org/devextreme-quill/-/devextreme-quill-1.7.9.tgz",
"integrity": "sha512-UEkR16+I/7P/4+7dUmc65lv+VRdRGk0kFvZXgHnM2UFcEBKqm1kVDdVPBolGRFwuOFuDttJxp2SXUAyiuSC8wA==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"core-js": "^3.34.0",
"eventemitter3": "^4.0.7",
@@ -188,25 +202,29 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz",
"integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/fast-diff": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
"license": "Apache-2.0"
"license": "Apache-2.0",
"peer": true
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/inferno": {
"version": "8.2.3",
@@ -214,6 +232,7 @@
"integrity": "sha512-LMeRlCe+RlXw8kHCLyOWRk2PsZ3Fo4jkESyAR1g4FfPT48N78i11YhTVXW2ukCx5MFjv+qrfa73JzJWU9sg4CQ==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.1.2",
"inferno-vnode-flags": "8.2.3",
@@ -229,6 +248,7 @@
"resolved": "https://registry.npmjs.org/inferno-create-element/-/inferno-create-element-8.2.3.tgz",
"integrity": "sha512-YEwX4OiFlgeTutvE16uCGxkaSVwZ1DklpAPX8okjVsGaLIWQrM8QIQFxn3mTLWSu70Uea+afQfKL5wE4hxn39Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"inferno": "8.2.3"
}
@@ -238,6 +258,7 @@
"resolved": "https://registry.npmjs.org/inferno-hydrate/-/inferno-hydrate-8.2.3.tgz",
"integrity": "sha512-AyCiswnjYg7D9veJdjiQg06Npp0/iXKhwOm2hjoY3cjadT3fIdz2XtDElLB7imU4icuJ3xOmXA8FgIfnSJfHrQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"inferno": "8.2.3"
}
@@ -246,19 +267,22 @@
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/inferno-vnode-flags/-/inferno-vnode-flags-8.2.3.tgz",
"integrity": "sha512-dfC0MIwFv9PCbZCUsuk9ISejFS3fKJODC0rZ/LjxxzE+OrCk+PMwPLsUnGU6O9/jbBnPACVz1BkACDf5LWgU5Q==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
"license": "ISC",
"peer": true
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/jquery": {
"version": "3.7.1",
@@ -271,6 +295,7 @@
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"peer": true,
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
@@ -283,6 +308,7 @@
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"immediate": "~3.0.5"
}
@@ -291,26 +317,30 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/opencollective-postinstall": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
"license": "MIT",
"peer": true,
"bin": {
"opencollective-postinstall": "index.js"
}
@@ -319,19 +349,22 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
"license": "(MIT AND Zlib)",
"peer": true
},
"node_modules/parchment": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-2.0.1.tgz",
"integrity": "sha512-VBKrlEoZCBD+iwoeag0QTtY1Cti+Ma4nLpVYcc/uus/wHhMsPOi5InH3RL1s4aekahPZpabcS2ToKyGf7RMH/g==",
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -343,13 +376,15 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/quill-delta": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
"integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-diff": "^1.3.0",
"lodash.clonedeep": "^4.5.0",
@@ -364,6 +399,7 @@
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"peer": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -379,6 +415,7 @@
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.8.1.tgz",
"integrity": "sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -387,25 +424,29 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
"license": "0BSD",
"peer": true
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
@@ -414,13 +455,15 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"license": "0BSD"
"license": "0BSD",
"peer": true
},
"node_modules/unplugin": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz",
"integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"picomatch": "^4.0.3",
@@ -434,13 +477,15 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"license": "MIT"
"license": "MIT",
"peer": true
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
àRCopyright 1990-2009 Adobe Systems Incorporated.
All rights reserved.
See ./LICENSEáCNS2-H

View File

@@ -0,0 +1,3 @@
àRCopyright 1990-2009 Adobe Systems Incorporated.
All rights reserved.
See ./LICENSEá ETen-B5-H` ^

Some files were not shown because too many files have changed in this diff Show More