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 @@
-