feat: Rechnungsvorlage und Statusmails für Version 0.9.25
- HHA-Systemrechnungsvorlage mit durchgehendem Rahmen und PDF-Vorschau ergänzen - Zeichenreihenfolge im Editor und Schriftformatierung beim PDF-Export korrigieren - Stationsbetreff vereinheitlichen und Tätigkeitsbeschreibung in Aufgabenmails aufnehmen - Regressionstests, aktualisierte Frontend-Bundles und Backend-Version 0.9.25 aufnehmen - Docker-Release-Build mit sauberem Maven-Build und Stacktrace ausführen
This commit is contained in:
1 parent
b642f42954
commit
8044d82f04
13 files changed
+551
-34
No files matched your search
Binary file not shown.
Binary file not shown.
@@ -233,28 +233,9 @@ window.initProfileInvoiceGenerator = function() {
|
||||
var img = new Image();
|
||||
img.onload = function() {
|
||||
imageCache[el.imageData] = img;
|
||||
// Calculate dimensions to maintain aspect ratio
|
||||
var imgAspect = img.width / img.height;
|
||||
var boxAspect = w / h;
|
||||
var drawW, drawH, drawX, drawY;
|
||||
|
||||
if (imgAspect > boxAspect) {
|
||||
drawW = w;
|
||||
drawH = w / imgAspect;
|
||||
drawX = x;
|
||||
drawY = y + (h - drawH) / 2;
|
||||
} else {
|
||||
drawW = h * imgAspect;
|
||||
drawH = h;
|
||||
drawX = x + (w - drawW) / 2;
|
||||
drawY = y;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, drawX, drawY, drawW, drawH);
|
||||
// Redraw selection if this element is selected
|
||||
if (selectedElement && selectedElement.id === el.id) {
|
||||
drawSelection(el);
|
||||
}
|
||||
// Redraw in element order: a letterhead loaded asynchronously must
|
||||
// stay behind the invoice text, including after resize or removal.
|
||||
draw();
|
||||
};
|
||||
img.src = el.imageData;
|
||||
}
|
||||
|
||||
@@ -405,7 +405,8 @@ public class MessageController {
|
||||
try {
|
||||
String taskType = task.getTaskType() != null ? task.getTaskType().toString() : "Unknown";
|
||||
String completedBy = resolveTaskActor(task, appUserId);
|
||||
emailService.sendTaskCompletionNotification(jobId, taskType, taskIdStr, completedBy);
|
||||
emailService.sendTaskCompletionNotification(jobId, taskType, taskIdStr, completedBy,
|
||||
task.getDescription());
|
||||
// Job completion is no longer auto-triggered by task completion.
|
||||
// It is now driven by explicit station_completed messages from the app
|
||||
// (see handleStationCompleted).
|
||||
|
||||
@@ -324,7 +324,7 @@ public class CustomerInvoiceService {
|
||||
htmlBuilder.append("<style>");
|
||||
htmlBuilder.append("@page { size: A4; margin: 0; }");
|
||||
htmlBuilder.append(
|
||||
"body { margin: 0; padding: 0; width: 210mm; height: 297mm; position: relative; font-family: Arial, sans-serif; }");
|
||||
"body { margin: 0; padding: 0; width: 210mm; height: 297mm; position: relative; font-family: Helvetica, Arial, sans-serif; }");
|
||||
htmlBuilder.append(".element { position: absolute; box-sizing: border-box; overflow: hidden; }");
|
||||
htmlBuilder.append(".text { white-space: nowrap; overflow: visible; }");
|
||||
htmlBuilder.append(".line { border-top: 1px solid #333; }");
|
||||
@@ -403,6 +403,9 @@ public class CustomerInvoiceService {
|
||||
htmlBuilder.append("height:").append(String.format(java.util.Locale.US, "%.2f", mmHeight))
|
||||
.append("mm;");
|
||||
htmlBuilder.append("font-size:").append(fontSize).append("pt;");
|
||||
String fontStyle = element.path("fontStyle").asText("");
|
||||
htmlBuilder.append("font-weight:").append(fontStyle.contains("bold") ? "bold;" : "normal;");
|
||||
htmlBuilder.append("font-style:").append(fontStyle.contains("italic") ? "italic;" : "normal;");
|
||||
htmlBuilder.append("line-height:")
|
||||
.append(String.format(java.util.Locale.US, "%.2f", fontSize * 1.2)).append("pt;");
|
||||
htmlBuilder.append("color:").append(color).append(";");
|
||||
@@ -626,7 +629,7 @@ public class CustomerInvoiceService {
|
||||
htmlBuilder.append("<style>");
|
||||
htmlBuilder.append("@page { size: A4; margin: 0; }");
|
||||
htmlBuilder.append(
|
||||
"body { margin: 0; padding: 0; width: 210mm; height: 297mm; position: relative; font-family: Arial, sans-serif; }");
|
||||
"body { margin: 0; padding: 0; width: 210mm; height: 297mm; position: relative; font-family: Helvetica, Arial, sans-serif; }");
|
||||
htmlBuilder.append(".element { position: absolute; box-sizing: border-box; overflow: hidden; }");
|
||||
htmlBuilder.append(".text { white-space: nowrap; overflow: visible; }");
|
||||
htmlBuilder.append(".line { border-top: 1px solid #333; }");
|
||||
@@ -718,6 +721,9 @@ public class CustomerInvoiceService {
|
||||
htmlBuilder.append("height:").append(String.format(java.util.Locale.US, "%.2f", mmHeight))
|
||||
.append("mm;");
|
||||
htmlBuilder.append("font-size:").append(fontSize).append("pt;");
|
||||
String fontStyle = element.path("fontStyle").asText("");
|
||||
htmlBuilder.append("font-weight:").append(fontStyle.contains("bold") ? "bold;" : "normal;");
|
||||
htmlBuilder.append("font-style:").append(fontStyle.contains("italic") ? "italic;" : "normal;");
|
||||
htmlBuilder.append("line-height:")
|
||||
.append(String.format(java.util.Locale.US, "%.2f", fontSize * 1.2)).append("pt;");
|
||||
htmlBuilder.append("color:").append(color).append(";");
|
||||
|
||||
@@ -38,7 +38,8 @@ public class EmailService {
|
||||
private String smtpUsername;
|
||||
|
||||
@Async
|
||||
public void sendTaskCompletionNotification(ObjectId jobId, String taskType, String taskId, String completedBy) {
|
||||
public void sendTaskCompletionNotification(ObjectId jobId, String taskType, String taskId, String completedBy,
|
||||
String taskDescription) {
|
||||
try {
|
||||
// Load job to get context information
|
||||
Optional<Job> jobOpt = jobRepository.findById(jobId);
|
||||
@@ -65,7 +66,7 @@ public class EmailService {
|
||||
String completedByName = resolveCompletedByName(job, completedBy);
|
||||
|
||||
// Send email
|
||||
sendEmail(user, job, taskType, completedByName);
|
||||
sendEmail(user, job, taskType, completedByName, taskDescription);
|
||||
log.info("Task completion notification sent to {} for job {} task {}", user.getEmail(), job.getJobNumber(),
|
||||
taskId);
|
||||
|
||||
@@ -75,7 +76,7 @@ public class EmailService {
|
||||
}
|
||||
}
|
||||
|
||||
private void sendEmail(User user, Job job, String taskType, String completedByName) {
|
||||
private void sendEmail(User user, Job job, String taskType, String completedByName, String taskDescription) {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setFrom(smtpUsername);
|
||||
message.setTo(user.getEmail());
|
||||
@@ -93,6 +94,9 @@ public class EmailService {
|
||||
body.append("Kunde: ").append(job.getDeliveryCompany()).append("\n");
|
||||
}
|
||||
body.append("Aufgabe: ").append(taskTypeName).append("\n");
|
||||
if (taskDescription != null && !taskDescription.isBlank()) {
|
||||
body.append("Beschreibung: ").append(taskDescription.strip()).append("\n");
|
||||
}
|
||||
body.append("Abgeschlossen von: ").append(completedByName).append("\n\n");
|
||||
|
||||
String deliveryCities = job.getDeliveryCitiesDisplay();
|
||||
@@ -227,7 +231,7 @@ public class EmailService {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setFrom(smtpUsername);
|
||||
message.setTo(user.getEmail());
|
||||
message.setSubject("Abgeschlossene Station - " + jobNumber + " - Station " + (stationOrder + 1));
|
||||
message.setSubject("Station abgeschlossen - " + jobNumber + " - Station " + (stationOrder + 1));
|
||||
StringBuilder body = new StringBuilder();
|
||||
body.append("Hallo ").append(buildFullName(user)).append(",\n\n");
|
||||
body.append("die folgende Station wurde abgeschlossen:\n\n");
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"id": "hha-letterhead",
|
||||
"type": "image",
|
||||
"text": "",
|
||||
"xPercent": "0.00000",
|
||||
"yPercent": "0.00000",
|
||||
"widthPercent": "100.00000",
|
||||
"heightPercent": "100.00000",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null,
|
||||
"imageData": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iNTk1IiBoZWlnaHQ9Ijg0MiIgdmlld0JveD0iMCAwIDU5NSA4NDIiPgogIDxkZWZzPgogICAgPGltYWdlIGlkPSJvcmlnaW5hbC1sb2dvIiB4PSIwIiB5PSIwIiB3aWR0aD0iNTk1IiBoZWlnaHQ9IjcwLjg4IiB4bGluazpocmVmPSJkYXRhOmltYWdlL2pwZWc7YmFzZTY0LC85ai80QUFRU2taSlJnQUJBUUFBQVFBQkFBRC8yd0JEQUFNQ0FnTUNBZ01EQXdNRUF3TUVCUWdGQlFRRUJRb0hCd1lJREFvTURBc0tDd3NORGhJUURRNFJEZ3NMRUJZUUVSTVVGUlVWREE4WEdCWVVHQklVRlJULzJ3QkRBUU1FQkFVRUJRa0ZCUWtVRFFzTkZCUVVGQlFVRkJRVUZCUVVGQlFVRkJRVUZCUVVGQlFVRkJRVUZCUVVGQlFVRkJRVUZCUVVGQlFVRkJRVUZCUVVGQlQvd2dBUkNBSlBFMkVEQVNJQUFoRUJBeEVCLzhRQUhRQUJBUUFEQVFFQkFRRUFBQUFBQUFBQUFBZ0dCd2tGQkFNQkF2L0VBQnNCQVFBQkJRRUFBQUFBQUFBQUFBQUFBQUFEQVFJRUJRWUgvOW9BREFNQkFBSVFBeEFBQUFHcVFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQLine truncated
|
||||
},
|
||||
{
|
||||
"id": "hha-issuer-street",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "64.36975",
|
||||
"yPercent": "13.42043",
|
||||
"widthPercent": "29.07563",
|
||||
"heightPercent": "1.56770",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#1a11b9",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.street"
|
||||
},
|
||||
{
|
||||
"id": "hha-issuer-city",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "64.36975",
|
||||
"yPercent": "14.98812",
|
||||
"widthPercent": "29.07563",
|
||||
"heightPercent": "1.56770",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#1a11b9",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.city"
|
||||
},
|
||||
{
|
||||
"id": "hha-phone-label",
|
||||
"type": "text",
|
||||
"text": "Tel.:",
|
||||
"xPercent": "64.36975",
|
||||
"yPercent": "17.93349",
|
||||
"widthPercent": "4.36975",
|
||||
"heightPercent": "1.56770",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#1a11b9",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "hha-issuer-phone",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "68.73950",
|
||||
"yPercent": "17.93349",
|
||||
"widthPercent": "24.03361",
|
||||
"heightPercent": "1.56770",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#1a11b9",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.phone"
|
||||
},
|
||||
{
|
||||
"id": "hha-issuer-website",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "64.36975",
|
||||
"yPercent": "19.50119",
|
||||
"widthPercent": "28.57143",
|
||||
"heightPercent": "1.56770",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#1a11b9",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.website"
|
||||
},
|
||||
{
|
||||
"id": "hha-sender-line",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "9.57983",
|
||||
"yPercent": "18.64608",
|
||||
"widthPercent": "52.94118",
|
||||
"heightPercent": "0.99762",
|
||||
"fontSize": 7,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.sender_line"
|
||||
},
|
||||
{
|
||||
"id": "hha-recipient-company_name",
|
||||
"type": "text",
|
||||
"text": "Musterfirma GmbH",
|
||||
"xPercent": "9.57983",
|
||||
"yPercent": "20.19002",
|
||||
"widthPercent": "51.26050",
|
||||
"heightPercent": "1.71021",
|
||||
"fontSize": 12,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": true,
|
||||
"isCustomer": true,
|
||||
"variable": "customer.company_name"
|
||||
},
|
||||
{
|
||||
"id": "hha-recipient-contact_name",
|
||||
"type": "text",
|
||||
"text": "Max Mustermann",
|
||||
"xPercent": "9.57983",
|
||||
"yPercent": "21.90024",
|
||||
"widthPercent": "51.26050",
|
||||
"heightPercent": "1.71021",
|
||||
"fontSize": 12,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": true,
|
||||
"isCustomer": true,
|
||||
"variable": "customer.contact_name"
|
||||
},
|
||||
{
|
||||
"id": "hha-recipient-street",
|
||||
"type": "text",
|
||||
"text": "Musterstraße 1",
|
||||
"xPercent": "9.57983",
|
||||
"yPercent": "23.61045",
|
||||
"widthPercent": "51.26050",
|
||||
"heightPercent": "1.71021",
|
||||
"fontSize": 12,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": true,
|
||||
"isCustomer": true,
|
||||
"variable": "customer.street"
|
||||
},
|
||||
{
|
||||
"id": "hha-recipient-city",
|
||||
"type": "text",
|
||||
"text": "12345 Musterstadt",
|
||||
"xPercent": "9.57983",
|
||||
"yPercent": "25.32067",
|
||||
"widthPercent": "51.26050",
|
||||
"heightPercent": "1.71021",
|
||||
"fontSize": 12,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": true,
|
||||
"isCustomer": true,
|
||||
"variable": "customer.city"
|
||||
},
|
||||
{
|
||||
"id": "hha-fax-label",
|
||||
"type": "text",
|
||||
"text": "Fax",
|
||||
"xPercent": "64.36975",
|
||||
"yPercent": "27.07838",
|
||||
"widthPercent": "8.40336",
|
||||
"heightPercent": "1.14014",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "hha-fax-number",
|
||||
"type": "text",
|
||||
"text": "040-181237719",
|
||||
"xPercent": "64.36975",
|
||||
"yPercent": "28.50356",
|
||||
"widthPercent": "9.74790",
|
||||
"heightPercent": "1.14014",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "hha-email-label",
|
||||
"type": "text",
|
||||
"text": "E-Mail",
|
||||
"xPercent": "74.45378",
|
||||
"yPercent": "27.07838",
|
||||
"widthPercent": "17.81513",
|
||||
"heightPercent": "1.14014",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "hha-issuer-email",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "74.45378",
|
||||
"yPercent": "28.50356",
|
||||
"widthPercent": "18.65546",
|
||||
"heightPercent": "1.14014",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.email"
|
||||
},
|
||||
{
|
||||
"id": "hha-your-reference",
|
||||
"type": "text",
|
||||
"text": "Ihr Zeichen, Ihre Nachricht vom",
|
||||
"xPercent": "11.42857",
|
||||
"yPercent": "35.51069",
|
||||
"widthPercent": "23.19328",
|
||||
"heightPercent": "1.14014",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "hha-our-reference",
|
||||
"type": "text",
|
||||
"text": "Unser Zeichen, unsere Nachricht vom",
|
||||
"xPercent": "35.63025",
|
||||
"yPercent": "35.51069",
|
||||
"widthPercent": "24.36975",
|
||||
"heightPercent": "1.14014",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "hha-contact-label",
|
||||
"type": "text",
|
||||
"text": "Telefon, Name",
|
||||
"xPercent": "59.83193",
|
||||
"yPercent": "35.51069",
|
||||
"widthPercent": "16.80672",
|
||||
"heightPercent": "1.14014",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "hha-date-label",
|
||||
"type": "text",
|
||||
"text": "Datum",
|
||||
"xPercent": "84.03361",
|
||||
"yPercent": "35.51069",
|
||||
"widthPercent": "8.73950",
|
||||
"heightPercent": "1.14014",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "hha-invoice-date",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "84.03361",
|
||||
"yPercent": "36.93587",
|
||||
"widthPercent": "8.90756",
|
||||
"heightPercent": "1.14014",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.invoice_date"
|
||||
},
|
||||
{
|
||||
"id": "hha-invoice-title",
|
||||
"type": "header",
|
||||
"text": "Rechnung",
|
||||
"xPercent": "9.57983",
|
||||
"yPercent": "40.38005",
|
||||
"widthPercent": "10.75630",
|
||||
"heightPercent": "1.71021",
|
||||
"fontSize": 12,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "hha-invoice-number",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "20.33613",
|
||||
"yPercent": "40.38005",
|
||||
"widthPercent": "50.42017",
|
||||
"heightPercent": "1.71021",
|
||||
"fontSize": 12,
|
||||
"fontStyle": "bold",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.invoice_number"
|
||||
},
|
||||
{
|
||||
"id": "hha-intro",
|
||||
"type": "text",
|
||||
"text": "Gemäß unserem Nutzungsvertrag berechnen wir Ihnen\nfolgende Leistungen:",
|
||||
"xPercent": "9.57983",
|
||||
"yPercent": "44.53682",
|
||||
"widthPercent": "83.36134",
|
||||
"heightPercent": "3.42043",
|
||||
"fontSize": 12,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "hha-positions",
|
||||
"type": "text",
|
||||
"text": "Rechnungspositionen",
|
||||
"xPercent": "9.57983",
|
||||
"yPercent": "49.04988",
|
||||
"widthPercent": "83.36134",
|
||||
"heightPercent": "26.72209",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "services.list"
|
||||
},
|
||||
{
|
||||
"id": "hha-payment-label",
|
||||
"type": "text",
|
||||
"text": "Zahlungsbedingungen:",
|
||||
"xPercent": "9.57983",
|
||||
"yPercent": "85.03563",
|
||||
"widthPercent": "22.68908",
|
||||
"heightPercent": "1.71021",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "hha-payment-text",
|
||||
"type": "text",
|
||||
"text": "Gesamtbetrag bis spätestens zum 10. Werktag nach\nRechnungserhalt auf unser u. g. Konto.",
|
||||
"xPercent": "33.27731",
|
||||
"yPercent": "85.03563",
|
||||
"widthPercent": "59.66387",
|
||||
"heightPercent": "3.42043",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "left",
|
||||
"color": "#000000",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
},
|
||||
{
|
||||
"id": "hha-footer",
|
||||
"type": "text",
|
||||
"text": "",
|
||||
"xPercent": "9.24370",
|
||||
"yPercent": "92.75534",
|
||||
"widthPercent": "84.03361",
|
||||
"heightPercent": "3.42043",
|
||||
"fontSize": 8,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "center",
|
||||
"color": "#000000",
|
||||
"isStatic": true,
|
||||
"isCustomer": false,
|
||||
"variable": "masterdata.footer"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const generator = readFileSync(new URL('../../main/frontend/invoice-generator/profile-invoice-generator.js', import.meta.url), 'utf8');
|
||||
|
||||
function createEditor() {
|
||||
const painted = [];
|
||||
const pendingImages = [];
|
||||
const context = new Proxy({
|
||||
measureText: text => ({ width: text.length * 6 }),
|
||||
drawImage: image => painted.push(['image', image.src]),
|
||||
fillText: text => painted.push(['text', text])
|
||||
}, { get: (target, key) => target[key] ?? (() => {}) });
|
||||
const canvas = { style: {}, getContext: () => context, addEventListener() {}, setAttribute() {} };
|
||||
const container = {
|
||||
style: {},
|
||||
getBoundingClientRect: () => ({ width: 615, height: 862 }),
|
||||
appendChild() {},
|
||||
addEventListener() {}
|
||||
};
|
||||
const window = { addEventListener() {} };
|
||||
class Image {
|
||||
width = 595;
|
||||
height = 842;
|
||||
constructor() { pendingImages.push(this); }
|
||||
}
|
||||
vm.runInNewContext(generator, {
|
||||
window,
|
||||
document: { getElementById: () => container, createElement: () => canvas },
|
||||
Image,
|
||||
console: { log() {}, error(error) { throw new Error(String(error)); } }
|
||||
});
|
||||
window.initProfileInvoiceGenerator();
|
||||
window.loadProfileTemplate({ elements: [
|
||||
{ id: 'letterhead', type: 'image', imageData: 'data:image/svg+xml;base64,test', x: 0, y: 0, width: 595, height: 842 },
|
||||
{ id: 'invoice-text', type: 'text', text: 'Rechnung', x: 50, y: 250, width: 100, height: 30 }
|
||||
] });
|
||||
return { window, painted, pendingImages };
|
||||
}
|
||||
|
||||
test('invoice text stays above a letterhead that finishes loading later', () => {
|
||||
const { painted, pendingImages } = createEditor();
|
||||
assert.deepEqual(painted, [['text', 'Rechnung']]);
|
||||
painted.length = 0;
|
||||
pendingImages[0].onload();
|
||||
assert.deepEqual(painted, [
|
||||
['image', 'data:image/svg+xml;base64,test'],
|
||||
['text', 'Rechnung']
|
||||
]);
|
||||
});
|
||||
|
||||
test('a removed letterhead is not painted when its pending image finishes loading', () => {
|
||||
const { window, painted, pendingImages } = createEditor();
|
||||
window.deleteProfileElement('letterhead');
|
||||
painted.length = 0;
|
||||
pendingImages[0].onload();
|
||||
assert.deepEqual(painted, [['text', 'Rechnung']]);
|
||||
});
|
||||
@@ -87,6 +87,8 @@ class TaskCompletionHistoryTest {
|
||||
verify(taskRepository).save(task);
|
||||
verify(jobHistoryService).logTaskCompletion(eq(job.getId()), eq(task.getTaskType()), eq(task.getIdAsString()),
|
||||
eq(actor), eq(task.getDisplayName()), anyString(), eq(task.getDescription()));
|
||||
verify(emailService).sendTaskCompletionNotification(job.getId(), task.getTaskType(), task.getIdAsString(),
|
||||
actor, "Am Seiteneingang abgeben.");
|
||||
if (task instanceof PhotoTask) {
|
||||
verify(photoRepository).save(argThat(photo -> actor.equals(photo.getCompletedBy())));
|
||||
} else if (task instanceof SignatureTask) {
|
||||
|
||||
@@ -20,6 +20,9 @@ import org.bson.types.ObjectId;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
@@ -57,7 +60,7 @@ class EmailServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendTaskCompletionNotificationUsesAssignedAppUserName() {
|
||||
void sendTaskCompletionNotificationIncludesDescriptionAndAssignedAppUserName() {
|
||||
User webUser = createWebUser();
|
||||
AppUser assignedAppUser = createAssignedAppUser();
|
||||
Job job = createJob(webUser, assignedAppUser);
|
||||
@@ -66,7 +69,8 @@ class EmailServiceTest {
|
||||
when(userRepository.findById(webUser.getId())).thenReturn(Optional.of(webUser));
|
||||
when(appUserRepository.findById(assignedAppUser.getId())).thenReturn(Optional.of(assignedAppUser));
|
||||
|
||||
emailService.sendTaskCompletionNotification(job.getId(), "PHOTO", "task-1", "ignored");
|
||||
emailService.sendTaskCompletionNotification(job.getId(), "PHOTO", "task-1", "ignored",
|
||||
" Ware am Seiteneingang fotografieren.\nAuch die Verpackung dokumentieren. ");
|
||||
|
||||
verify(mailSender).send(mailCaptor.capture());
|
||||
String body = mailCaptor.getValue().getText();
|
||||
@@ -74,6 +78,27 @@ class EmailServiceTest {
|
||||
assertThat(body).contains("Abgeschlossen von: Max Mustermann");
|
||||
assertThat(body).contains("Hallo Dr. Anna Unternehmer,");
|
||||
assertThat(body).contains("Auftrag: ").doesNotContain("Job:");
|
||||
assertThat(body).contains("Aufgabe: Foto-Aufgabe\nBeschreibung: Ware am Seiteneingang fotografieren.\n"
|
||||
+ "Auch die Verpackung dokumentieren.\nAbgeschlossen von: Max Mustermann");
|
||||
assertThat(mailCaptor.getValue().getSubject()).isEqualTo("Aufgabe abgeschlossen - JOB-2026-001");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@NullAndEmptySource
|
||||
@ValueSource(strings = { " \n\t " })
|
||||
void taskCompletionEmailWithoutDescriptionStillSends(String description) {
|
||||
User webUser = createWebUser();
|
||||
AppUser assignedAppUser = createAssignedAppUser();
|
||||
Job job = createJob(webUser, assignedAppUser);
|
||||
when(jobRepository.findById(job.getId())).thenReturn(Optional.of(job));
|
||||
when(userRepository.findById(webUser.getId())).thenReturn(Optional.of(webUser));
|
||||
when(appUserRepository.findById(assignedAppUser.getId())).thenReturn(Optional.of(assignedAppUser));
|
||||
|
||||
emailService.sendTaskCompletionNotification(job.getId(), "CONFIRMATION", "task-1", "ignored", description);
|
||||
|
||||
verify(mailSender).send(mailCaptor.capture());
|
||||
assertThat(mailCaptor.getValue().getText()).contains("Aufgabe: Bestätigung\nAbgeschlossen von: Max Mustermann")
|
||||
.doesNotContain("Beschreibung:", "null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -134,7 +159,7 @@ class EmailServiceTest {
|
||||
SimpleMailMessage mail = mailCaptor.getValue();
|
||||
assertThat(mail.getTo()).containsExactly("anna@example.com");
|
||||
assertThat(mail.getFrom()).isEqualTo("noreply@example.com");
|
||||
assertThat(mail.getSubject()).isEqualTo("Abgeschlossene Station - AUFTRAG20260929001 - Station 2");
|
||||
assertThat(mail.getSubject()).isEqualTo("Station abgeschlossen - AUFTRAG20260929001 - Station 2");
|
||||
assertThat(mail.getText()).contains("Hallo Dr. Anna Unternehmer,", "Auftrag: AUFTRAG20260929001",
|
||||
"Station: 2", "Empfänger GmbH", "Petra Beispiel", "Hafenstraße 12", "Tor 3", "20457 Hamburg",
|
||||
"Abgeschlossen von: Erika Musterfrau").doesNotContain("null", "Max Mustermann", "Job:");
|
||||
|
||||
Reference in new issue
Block a user