From 79169cf9fdb75a968eb2e12c7fe375a988cdf092 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Thu, 18 Sep 2025 21:40:51 +0200 Subject: [PATCH] Erweiterungen --- AGENTS.md | 19 ++ .../votianlt/service/PdfBoxService.java | 83 +++++++-- .../resources/templates/invoice-template.html | 163 ++++++++++-------- 3 files changed, 172 insertions(+), 93 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8e1b90e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,19 @@ +# Repository Guidelines + +## Project Structure & Module Organization +Backend Java sits in `src/main/java/de/assecutor/votianlt`; domain models stay in `model`, persistence logic in `repository`, services in `service`, MQTT integration under `mqtt`, and access control in `security`. Vaadin views and UI helpers live in `pages/view` and `pages/base`. TypeScript, styles, and theming live in `src/main/frontend` (leave `generated/` untouched). Shared configs and templates live in `src/main/resources`, while Vaadin bundle descriptors reside in `src/main/bundles`. Maven output lands in `target/`. + +## Build, Test, and Development Commands +Use `./mvnw` for the Spring Boot dev server with frontend hot reload. Build production bits with `./mvnw -Pproduction package`. Run `./mvnw test` for unit checks and `./mvnw -Pintegration-test verify` when integration coverage is needed. After dependency changes, refresh Vaadin assets with `./mvnw vaadin:prepare-frontend`. Apply formatting via `./mvnw spotless:apply`. + +## Coding Style & Naming Conventions +Spotless enforces Java 21 formatting using `eclipse-formatter.xml`; keep imports ordered and rely on Lombok already present. Classes remain PascalCase, Spring stereotypes end with `Service`, `Repository`, or `Config`, and Vaadin views retain the `*View` naming within `pages/view`. Frontend code follows the repo’s Prettier rules (`.prettierrc.json`); keep TypeScript modules co-located with their views, prefer camelCase for variables, and avoid checking in generated `.class` files. + +## Testing Guidelines +Create tests under `src/test/java` mirroring the production package path. Name unit classes `*Test` and integration suites `*IT` so the failsafe profile picks them up. Lean on Spring Boot’s testing annotations for wiring, Mockito for isolates, and add Testcontainers when MongoDB or MQTT brokers are involved. Run `./mvnw test` before any push; trigger `./mvnw -Pintegration-test verify` for messaging, persistence, or security changes. + +## Commit & Pull Request Guidelines +History currently uses brief German titles; shift to imperative, scoped summaries such as `feat: add PDF mailer` or `fix: guard MQTT reconnects`. Keep unrelated updates out of the same commit and exclude artifacts like `node_modules/` or `target/`. Pull requests should explain the motivation, link issues, note config or data-seed impacts, and attach screenshots or screencasts when Vaadin views change. List manual verification steps and flag any migrations or bundle adjustments for reviewers. + +## Security & Configuration Tips +External service credentials for MongoDB, SMTP, and MQTT belong in environment variables or a developer-specific `application-local.properties` kept out of version control. Document default ports and topics when touching `MqttConfig` or Zeroconf publishers so ops can replicate environments. For two-factor flows, keep shared secrets in secure storage and avoid logging codes during development. diff --git a/src/main/java/de/assecutor/votianlt/service/PdfBoxService.java b/src/main/java/de/assecutor/votianlt/service/PdfBoxService.java index e49393e..8d9ff6b 100644 --- a/src/main/java/de/assecutor/votianlt/service/PdfBoxService.java +++ b/src/main/java/de/assecutor/votianlt/service/PdfBoxService.java @@ -209,13 +209,22 @@ public class PdfBoxService { } private float drawItemsTable(PDPageContentStream contentStream, float yPosition, InvoiceData data) throws IOException { - float tableWidth = PDRectangle.A4.getWidth() - 2 * MARGIN; float[] columnWidths = {60, 300, 100, 100}; // Menge, Bezeichnung, Einzelpreis, Gesamt float rowHeight = 20; + // Calculate actual table width based on column widths + float actualTableWidth = 0; + for (float width : columnWidths) { + actualTableWidth += width; + } + + // Add 2cm margin to the right of the gray header + float rightMargin = 2 * 28.35f; // 2cm in points (1cm = 28.35pt) + float headerWidth = actualTableWidth + rightMargin; + // Table header contentStream.setNonStrokingColor(230/255f, 230/255f, 230/255f); // Light gray background - contentStream.addRect(MARGIN, yPosition - rowHeight, tableWidth, rowHeight); + contentStream.addRect(MARGIN, yPosition - rowHeight, headerWidth, rowHeight); contentStream.fill(); contentStream.setNonStrokingColor(0f, 0f, 0f); // Black @@ -340,37 +349,77 @@ public class PdfBoxService { private float drawPaymentTerms(PDPageContentStream contentStream, float yPosition, InvoiceData data) throws IOException { contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE); - // Payment terms - contentStream.beginText(); - contentStream.newLineAtOffset(MARGIN, yPosition); - contentStream.showText(data.getPaymentTerms() != null ? data.getPaymentTerms() : ""); - contentStream.endText(); + // Payment terms with text wrapping + String paymentTerms = data.getPaymentTerms(); + if (paymentTerms != null && !paymentTerms.trim().isEmpty()) { + float maxWidth = PDRectangle.A4.getWidth() - 2 * MARGIN; + yPosition = drawWrappedText(contentStream, paymentTerms, MARGIN, yPosition, maxWidth, FONT_SIZE); + } - return yPosition - 40; + return yPosition - 20; + } + + private float drawWrappedText(PDPageContentStream contentStream, String text, float x, float y, float maxWidth, float fontSize) throws IOException { + String[] words = text.split(" "); + StringBuilder currentLine = new StringBuilder(); + float currentY = y; + + contentStream.beginText(); + contentStream.newLineAtOffset(x, currentY); + + for (String word : words) { + String testLine = currentLine.length() > 0 ? currentLine + " " + word : word; + + // Estimate text width (rough calculation) + float textWidth = estimateTextWidth(testLine, fontSize); + + if (textWidth > maxWidth && currentLine.length() > 0) { + // Print current line and start new line + contentStream.showText(currentLine.toString()); + currentLine = new StringBuilder(word); + currentY -= LINE_HEIGHT; + contentStream.newLineAtOffset(0, -LINE_HEIGHT); + } else { + currentLine = new StringBuilder(testLine); + } + } + + // Print remaining text + if (currentLine.length() > 0) { + contentStream.showText(currentLine.toString()); + currentY -= LINE_HEIGHT; + } + + contentStream.endText(); + return currentY; + } + + private float estimateTextWidth(String text, float fontSize) { + // Rough estimation: average character width is about 0.6 * font size + return text.length() * fontSize * 0.6f; } private void drawFooter(PDPageContentStream contentStream, PDPage page, InvoiceData data) throws IOException { contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 8); - // Calculate footer position - start from bottom of page - float footerStartY = MARGIN + 60; // 60 points from bottom - - // Footer text (company details) - positioned at bottom + // Footer text (company details) - positioned at absolute bottom String footerText = data.getFooterText(); if (footerText != null) { String[] footerLines = footerText.split("
"); - float currentY = footerStartY; - // Calculate the total height needed for footer text - float totalFooterHeight = footerLines.length * 12; - currentY = footerStartY + totalFooterHeight - 12; // Start from top of footer area + // Calculate footer position from bottom of page + float lineSpacing = 10; // 10 points between lines + float footerBottomMargin = 30; // 30 points from absolute bottom + + // Start from the bottom and work upwards + float currentY = footerBottomMargin + (footerLines.length - 1) * lineSpacing; for (String line : footerLines) { contentStream.beginText(); contentStream.newLineAtOffset(MARGIN, currentY); contentStream.showText(line.trim()); contentStream.endText(); - currentY -= 12; + currentY -= lineSpacing; } } } diff --git a/src/main/resources/templates/invoice-template.html b/src/main/resources/templates/invoice-template.html index c6b7549..ed336bb 100644 --- a/src/main/resources/templates/invoice-template.html +++ b/src/main/resources/templates/invoice-template.html @@ -9,6 +9,11 @@ margin: 1.5cm 1cm 1.5cm 2cm; } + html, + body { + height: 100%; + } + body { font-family: Arial, sans-serif; font-size: 12pt; @@ -17,6 +22,8 @@ border: 2px solid #1b12b9; max-width: 21.001cm; background-color: transparent; + display: flex; + flex-direction: column; } .header { @@ -162,11 +169,16 @@ background-color: #e6e6e6; } + .page-content { + flex: 1 0 auto; + } + .footer-text { text-align: center; font-size: 8pt; font-family: Arial; - margin-top: 2cm; + margin-top: auto; + padding: 0.5cm 1cm; } .clear { @@ -175,85 +187,84 @@ -
-

- {{companyName}} +

+
+

+ {{companyName}} +

+
+

+ {{companySubtitle}} +

+
+ +
+

{{senderLine}}

+

{{recipientName}}

+

{{recipientDepartment}}

+

{{recipientStreet}}

+

{{recipientCity}}

+
+ +
+

{{companyStreet}}

+

{{companyCity}}

+

+

Tel.: {{companyPhone}}

+

{{companyWebsite}}

+
+ +
+ +

+ Fax + eMail +
+ {{companyFax}} + {{companyEmail}}

-
-

- {{companySubtitle}} + +

+ Datum
+ {{invoiceDate}}

+ +

Rechnung {{invoiceNumber}}

+ +

{{invoiceText}}

+ + + + + + + + + {{invoiceItems}} + + + + + + + + + + + + + + + + + + +
MengeBezeichnungEinzelpreisGesamt
Nettobetrag{{netAmount}}
+ {{vatRate}}% MwSt.{{vatAmount}}
Endbetrag{{totalAmount}}
+ +

{{paymentTerms}}

- -
-

{{senderLine}}

-

{{recipientName}}

-

{{recipientDepartment}}

-

{{recipientStreet}}

-

{{recipientCity}}

-
- -
-

{{companyStreet}}

-

{{companyCity}}

-

-

Tel.: {{companyPhone}}

-

{{companyWebsite}}

-
- -
- -

- Fax - eMail -
- {{companyFax}} - {{companyEmail}} -

- -

- Datum
- {{invoiceDate}} -

- -

Rechnung {{invoiceNumber}}

- -

{{invoiceText}}

- - - - - - - - - {{invoiceItems}} - - - - - - - - - - - - - - - - - - -
MengeBezeichnungEinzelpreisGesamt
Nettobetrag{{netAmount}}
+ {{vatRate}}% MwSt.{{vatAmount}}
Endbetrag{{totalAmount}}
- -
-

{{paymentTerms}}

-


- - \ No newline at end of file +