Erweiterungen
This commit is contained in:
19
AGENTS.md
Normal file
19
AGENTS.md
Normal file
@@ -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.
|
||||||
@@ -209,13 +209,22 @@ public class PdfBoxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private float drawItemsTable(PDPageContentStream contentStream, float yPosition, InvoiceData data) throws IOException {
|
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[] columnWidths = {60, 300, 100, 100}; // Menge, Bezeichnung, Einzelpreis, Gesamt
|
||||||
float rowHeight = 20;
|
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
|
// Table header
|
||||||
contentStream.setNonStrokingColor(230/255f, 230/255f, 230/255f); // Light gray background
|
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.fill();
|
||||||
|
|
||||||
contentStream.setNonStrokingColor(0f, 0f, 0f); // Black
|
contentStream.setNonStrokingColor(0f, 0f, 0f); // Black
|
||||||
@@ -340,37 +349,77 @@ public class PdfBoxService {
|
|||||||
private float drawPaymentTerms(PDPageContentStream contentStream, float yPosition, InvoiceData data) throws IOException {
|
private float drawPaymentTerms(PDPageContentStream contentStream, float yPosition, InvoiceData data) throws IOException {
|
||||||
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE);
|
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE);
|
||||||
|
|
||||||
// Payment terms
|
// Payment terms with text wrapping
|
||||||
contentStream.beginText();
|
String paymentTerms = data.getPaymentTerms();
|
||||||
contentStream.newLineAtOffset(MARGIN, yPosition);
|
if (paymentTerms != null && !paymentTerms.trim().isEmpty()) {
|
||||||
contentStream.showText(data.getPaymentTerms() != null ? data.getPaymentTerms() : "");
|
float maxWidth = PDRectangle.A4.getWidth() - 2 * MARGIN;
|
||||||
contentStream.endText();
|
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 {
|
private void drawFooter(PDPageContentStream contentStream, PDPage page, InvoiceData data) throws IOException {
|
||||||
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 8);
|
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 8);
|
||||||
|
|
||||||
// Calculate footer position - start from bottom of page
|
// Footer text (company details) - positioned at absolute bottom
|
||||||
float footerStartY = MARGIN + 60; // 60 points from bottom
|
|
||||||
|
|
||||||
// Footer text (company details) - positioned at bottom
|
|
||||||
String footerText = data.getFooterText();
|
String footerText = data.getFooterText();
|
||||||
if (footerText != null) {
|
if (footerText != null) {
|
||||||
String[] footerLines = footerText.split("<br>");
|
String[] footerLines = footerText.split("<br>");
|
||||||
float currentY = footerStartY;
|
|
||||||
|
|
||||||
// Calculate the total height needed for footer text
|
// Calculate footer position from bottom of page
|
||||||
float totalFooterHeight = footerLines.length * 12;
|
float lineSpacing = 10; // 10 points between lines
|
||||||
currentY = footerStartY + totalFooterHeight - 12; // Start from top of footer area
|
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) {
|
for (String line : footerLines) {
|
||||||
contentStream.beginText();
|
contentStream.beginText();
|
||||||
contentStream.newLineAtOffset(MARGIN, currentY);
|
contentStream.newLineAtOffset(MARGIN, currentY);
|
||||||
contentStream.showText(line.trim());
|
contentStream.showText(line.trim());
|
||||||
contentStream.endText();
|
contentStream.endText();
|
||||||
currentY -= 12;
|
currentY -= lineSpacing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
margin: 1.5cm 1cm 1.5cm 2cm;
|
margin: 1.5cm 1cm 1.5cm 2cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
font-size: 12pt;
|
font-size: 12pt;
|
||||||
@@ -17,6 +22,8 @@
|
|||||||
border: 2px solid #1b12b9;
|
border: 2px solid #1b12b9;
|
||||||
max-width: 21.001cm;
|
max-width: 21.001cm;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@@ -162,11 +169,16 @@
|
|||||||
background-color: #e6e6e6;
|
background-color: #e6e6e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.footer-text {
|
.footer-text {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 8pt;
|
font-size: 8pt;
|
||||||
font-family: Arial;
|
font-family: Arial;
|
||||||
margin-top: 2cm;
|
margin-top: auto;
|
||||||
|
padding: 0.5cm 1cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear {
|
.clear {
|
||||||
@@ -175,85 +187,84 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="header">
|
<div class="page-content">
|
||||||
<p class="company-name">
|
<div class="header">
|
||||||
<b>{{companyName}}</b>
|
<p class="company-name">
|
||||||
|
<b>{{companyName}}</b>
|
||||||
|
</p>
|
||||||
|
<hr>
|
||||||
|
<p class="company-subtitle">
|
||||||
|
<b>{{companySubtitle}}</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="recipient-box">
|
||||||
|
<p class="sender-line">{{senderLine}}</p>
|
||||||
|
<p class="recipient-content">{{recipientName}}</p>
|
||||||
|
<p class="recipient-content">{{recipientDepartment}}</p>
|
||||||
|
<p class="recipient-content">{{recipientStreet}}</p>
|
||||||
|
<p class="recipient-content">{{recipientCity}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="company-address">
|
||||||
|
<p>{{companyStreet}}</p>
|
||||||
|
<p>{{companyCity}}</p>
|
||||||
|
<p> </p>
|
||||||
|
<p>Tel.: {{companyPhone}}</p>
|
||||||
|
<p>{{companyWebsite}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clear"></div>
|
||||||
|
|
||||||
|
<p class="contact-info">
|
||||||
|
Fax
|
||||||
|
<span style="margin-left: 60px">eMail</span>
|
||||||
|
<br/>
|
||||||
|
{{companyFax}}
|
||||||
|
<span>{{companyEmail}}</span>
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
|
||||||
<p class="company-subtitle">
|
<p class="invoice-date">
|
||||||
<b>{{companySubtitle}}</b>
|
<span>Datum<br></span>
|
||||||
|
<span>{{invoiceDate}}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p class="subject">Rechnung {{invoiceNumber}}</p>
|
||||||
|
|
||||||
|
<p class="text-body">{{invoiceText}}</p>
|
||||||
|
|
||||||
|
<table class="invoice-table">
|
||||||
|
<tr>
|
||||||
|
<td class="table-header" style="width:2cm;">Menge</td>
|
||||||
|
<td class="table-header" style="width:9.999cm;">Bezeichnung</td>
|
||||||
|
<td class="table-header" style="width:3cm;">Einzelpreis</td>
|
||||||
|
<td class="table-header" style="width:3cm;">Gesamt</td>
|
||||||
|
</tr>
|
||||||
|
{{invoiceItems}}
|
||||||
|
<tr>
|
||||||
|
<td class="table-cell" style="width:2cm;"> </td>
|
||||||
|
<td class="table-cell" style="width:9.999cm;"> </td>
|
||||||
|
<td class="table-header" style="width:3cm;">Nettobetrag</td>
|
||||||
|
<td class="table-cell summary-row table-cell-right" style="width:3cm;">{{netAmount}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="table-cell" style="width:2cm;"> </td>
|
||||||
|
<td class="table-cell" style="width:9.999cm;"> </td>
|
||||||
|
<td class="table-cell table-cell-right" style="width:3cm;">+ {{vatRate}}% MwSt.</td>
|
||||||
|
<td class="table-cell table-cell-right" style="width:3cm;">{{vatAmount}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="table-cell" style="width:2cm;"> </td>
|
||||||
|
<td class="table-cell" style="width:9.999cm;"> </td>
|
||||||
|
<td class="table-header" style="width:3cm;">Endbetrag</td>
|
||||||
|
<td class="table-cell summary-row table-cell-right" style="width:3cm;">{{totalAmount}}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p class="text-body">{{paymentTerms}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="recipient-box">
|
|
||||||
<p class="sender-line">{{senderLine}}</p>
|
|
||||||
<p class="recipient-content">{{recipientName}}</p>
|
|
||||||
<p class="recipient-content">{{recipientDepartment}}</p>
|
|
||||||
<p class="recipient-content">{{recipientStreet}}</p>
|
|
||||||
<p class="recipient-content">{{recipientCity}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="company-address">
|
|
||||||
<p>{{companyStreet}}</p>
|
|
||||||
<p>{{companyCity}}</p>
|
|
||||||
<p> </p>
|
|
||||||
<p>Tel.: {{companyPhone}}</p>
|
|
||||||
<p>{{companyWebsite}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="clear"></div>
|
|
||||||
|
|
||||||
<p class="contact-info">
|
|
||||||
Fax
|
|
||||||
<span style="margin-left: 60px">eMail</span>
|
|
||||||
<br/>
|
|
||||||
{{companyFax}}
|
|
||||||
<span>{{companyEmail}}</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="invoice-date">
|
|
||||||
<span>Datum<br></span>
|
|
||||||
<span>{{invoiceDate}}</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="subject">Rechnung {{invoiceNumber}}</p>
|
|
||||||
|
|
||||||
<p class="text-body">{{invoiceText}}</p>
|
|
||||||
|
|
||||||
<table class="invoice-table">
|
|
||||||
<tr>
|
|
||||||
<td class="table-header" style="width:2cm;">Menge</td>
|
|
||||||
<td class="table-header" style="width:9.999cm;">Bezeichnung</td>
|
|
||||||
<td class="table-header" style="width:3cm;">Einzelpreis</td>
|
|
||||||
<td class="table-header" style="width:3cm;">Gesamt</td>
|
|
||||||
</tr>
|
|
||||||
{{invoiceItems}}
|
|
||||||
<tr>
|
|
||||||
<td class="table-cell" style="width:2cm;"> </td>
|
|
||||||
<td class="table-cell" style="width:9.999cm;"> </td>
|
|
||||||
<td class="table-header" style="width:3cm;">Nettobetrag</td>
|
|
||||||
<td class="table-cell summary-row table-cell-right" style="width:3cm;">{{netAmount}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="table-cell" style="width:2cm;"> </td>
|
|
||||||
<td class="table-cell" style="width:9.999cm;"> </td>
|
|
||||||
<td class="table-cell table-cell-right" style="width:3cm;">+ {{vatRate}}% MwSt.</td>
|
|
||||||
<td class="table-cell table-cell-right" style="width:3cm;">{{vatAmount}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="table-cell" style="width:2cm;"> </td>
|
|
||||||
<td class="table-cell" style="width:9.999cm;"> </td>
|
|
||||||
<td class="table-header" style="width:3cm;">Endbetrag</td>
|
|
||||||
<td class="table-cell summary-row table-cell-right" style="width:3cm;">{{totalAmount}}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<p class="text-body">{{paymentTerms}}</p>
|
|
||||||
<br><br><br>
|
|
||||||
|
|
||||||
<p class="footer-text">
|
<p class="footer-text">
|
||||||
<span>{{footerText}}</span>
|
<span>{{footerText}}</span>
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user