diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..b1e98a9
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,83 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Development Commands
+
+- **Start development server**: `./mvnw` (runs Spring Boot with Vaadin dev mode)
+- **Build for production**: `./mvnw -Pproduction package`
+- **Clean build**: `./mvnw clean compile`
+
+## Architecture Overview
+
+This is a **Vaadin Spring Boot** application for job/task management with real-time mobile app communication via MQTT. The system manages logistics jobs with tasks that mobile app users complete.
+
+### Core Architecture Layers
+
+**Frontend**: Vaadin Flow views (server-side rendered)
+- `src/main/java/de/assecutor/votianlt/pages/view/` - Main UI views
+- `src/main/java/de/assecutor/votianlt/pages/base/ui/` - Shared UI components
+
+**Backend Services**:
+- `src/main/java/de/assecutor/votianlt/service/` - Business logic
+- `src/main/java/de/assecutor/votianlt/controller/` - MQTT message handling
+- `src/main/java/de/assecutor/votianlt/repository/` - MongoDB data access
+
+**Models**:
+- `src/main/java/de/assecutor/votianlt/model/` - Domain entities
+- Task hierarchy: `BaseTask` with subtypes (`PhotoTask`, `BarcodeTask`, `SignatureTask`, etc.)
+
+### Key Architectural Patterns
+
+**Job-Task Relationship**: Jobs contain multiple ordered tasks. Tasks have completion states and can store completion data (photos, barcodes, signatures).
+
+**User Hierarchy**:
+- `User` - Web interface users (job managers)
+- `AppUser` - Mobile app users (task executors)
+- `AppUser.owner` field links to `User` for notifications
+
+**MQTT Communication**:
+- `MqttV5ClientManager` handles bidirectional communication with mobile apps
+- `MessageController` routes inbound MQTT messages and processes task completions
+- Topics: `/server/{clientId}/task_completed`, `/server/login`, etc.
+
+**History Tracking**: `JobHistoryService` logs all job/task changes with detailed audit trail displayed in `JobHistoryView`.
+
+**Email Notifications**: `EmailService` sends notifications for job creation, task completion, and job completion using Spring Mail with SMTP.
+
+## Data Storage
+
+**MongoDB Collections**:
+- `jobs` - Main job entities with status tracking
+- `tasks` - Polymorphic task storage (discriminated by `taskType`)
+- `job_history` - Audit trail for all job changes
+- `photos`, `barcodes`, `signatures` - Task completion data
+- `users` - Web interface users
+- `app_user` - Mobile app users
+- `cargo_item` - Job cargo/delivery items
+
+## Configuration
+
+**Database**: MongoDB at `192.168.180.25:27017/votianlt`
+**MQTT**: HiveMQ client connects to `mqtt-2.assecutor.de` with credentials `app`/`apppwd`
+**Email**: SMTP via `mailhub.assecutor.org:587` using Spring Boot mail auto-configuration
+
+## Development Environment
+
+**Java 21** with **Spring Boot 3.4.3** and **Vaadin 24.7.0**
+**Security**: Spring Security with role-based access (`USER` role required)
+**Profiles**: `production` profile available for optimized builds
+
+## Key Integration Points
+
+When adding new task types:
+1. Extend `BaseTask` and add to `@JsonSubTypes`
+2. Add completion logic in `MessageController.handleTaskCompleted()`
+3. Update `JobHistoryView` for task-specific previews if needed
+
+When modifying job status flow:
+1. Update `JobStatus` enum
+2. Modify `EmailService.updateJobStatusToCompleted()` logic
+3. Consider email notification templates
+
+MQTT message routing follows pattern: extract `taskType` from payload, route to appropriate processor method in `MessageController`.
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 61256ac..b2c9c1a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -103,10 +103,10 @@
openpdf
1.3.30
+
- com.sun.mail
- jakarta.mail
- 2.0.1
+ org.springframework.boot
+ spring-boot-starter-mail
diff --git a/src/main/bundles/prod.bundle b/src/main/bundles/prod.bundle
index 01d0be8..26f8738 100644
Binary files a/src/main/bundles/prod.bundle and b/src/main/bundles/prod.bundle differ
diff --git a/src/main/java/de/assecutor/votianlt/controller/MessageController.java b/src/main/java/de/assecutor/votianlt/controller/MessageController.java
index d766b10..eb4aec2 100644
--- a/src/main/java/de/assecutor/votianlt/controller/MessageController.java
+++ b/src/main/java/de/assecutor/votianlt/controller/MessageController.java
@@ -19,6 +19,7 @@ import de.assecutor.votianlt.model.Photo;
import de.assecutor.votianlt.model.Barcode;
import de.assecutor.votianlt.model.Signature;
import de.assecutor.votianlt.service.JobHistoryService;
+import de.assecutor.votianlt.service.EmailService;
import lombok.extern.slf4j.Slf4j;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
@@ -57,8 +58,9 @@ public class MessageController {
private final BarcodeRepository barcodeRepository;
private final SignatureRepository signatureRepository;
private final JobHistoryService jobHistoryService;
+ private final EmailService emailService;
- public MessageController(MqttPublisher mqttPublisher, AppUserRepository appUserRepository, AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository, TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository, SignatureRepository signatureRepository, JobHistoryService jobHistoryService) {
+ public MessageController(MqttPublisher mqttPublisher, AppUserRepository appUserRepository, AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository, TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository, SignatureRepository signatureRepository, JobHistoryService jobHistoryService, EmailService emailService) {
this.mqttPublisher = mqttPublisher;
this.appUserRepository = appUserRepository;
this.appUserService = appUserService;
@@ -69,6 +71,7 @@ public class MessageController {
this.barcodeRepository = barcodeRepository;
this.signatureRepository = signatureRepository;
this.jobHistoryService = jobHistoryService;
+ this.emailService = emailService;
}
/**
@@ -427,6 +430,20 @@ public class MessageController {
log.warn("Failed to log task completion history for task {}: {}", taskIdStr, e.getMessage());
}
+ // Send email notification for task completion
+ try {
+ ObjectId jobId = new ObjectId(task.getJobIdAsString());
+ String taskType = task.getTaskType() != null ? task.getTaskType().toString() : "Unknown";
+ String completedBy = task.getCompletedBy();
+
+ emailService.sendTaskCompletionNotification(jobId, taskType, taskIdStr, completedBy);
+
+ // Check if this was the last task and send job completion notification
+ emailService.checkAndSendJobCompletionNotification(jobId, completedBy);
+ } catch (Exception e) {
+ log.warn("Failed to send task completion email notification for task {}: {}", taskIdStr, e.getMessage());
+ }
+
log.info("Task marked completed. taskId={}, completedBy={}, extraData={}",
taskIdStr, task.getCompletedBy(), extraDataSummary);
} catch (IllegalArgumentException ex) {
diff --git a/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java b/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java
index c81f9a0..73d60f7 100644
--- a/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java
+++ b/src/main/java/de/assecutor/votianlt/pages/service/AddJobService.java
@@ -9,6 +9,7 @@ import de.assecutor.votianlt.repository.TaskRepository;
import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.repository.CargoItemRepository;
import de.assecutor.votianlt.service.JobHistoryService;
+import de.assecutor.votianlt.service.EmailService;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -30,6 +31,7 @@ public class AddJobService {
private final TaskRepository taskRepository;
private final SecurityService securityService;
private final JobHistoryService jobHistoryService;
+ private final EmailService emailService;
/**
* Speichert einen neuen Auftrag samt CargoItems und Tasks
* @param job der Auftrag
@@ -107,6 +109,13 @@ public class AddJobService {
log.warn("Failed to log job creation history for job {}: {}", savedJob.getIdAsString(), e.getMessage());
}
+ // E-Mail-Benachrichtigung für Job-Erstellung senden
+ try {
+ emailService.sendJobCreationNotification(savedJob.getId(), savedJob.getCreatedBy());
+ } catch (Exception e) {
+ log.warn("Failed to send job creation email notification for job {}: {}", savedJob.getIdAsString(), e.getMessage());
+ }
+
log.info("Auftrag erfolgreich gespeichert: {}", savedJob.getJobNumber());
return savedJob;
diff --git a/src/main/java/de/assecutor/votianlt/repository/AppUserRepository.java b/src/main/java/de/assecutor/votianlt/repository/AppUserRepository.java
index b884d76..99ea269 100644
--- a/src/main/java/de/assecutor/votianlt/repository/AppUserRepository.java
+++ b/src/main/java/de/assecutor/votianlt/repository/AppUserRepository.java
@@ -17,7 +17,10 @@ public interface AppUserRepository extends MongoRepository {
AppUser findByEmail(String email);
AppUser findByPasswordCode(String passwordCode);
-
+
+ // Find AppUser by appCode for task completion notifications
+ java.util.Optional findByAppCode(String appCode);
+
// Custom query methods can be added here if needed
// List findByBezeichnung(String bezeichnung);
}
diff --git a/src/main/java/de/assecutor/votianlt/repository/UserRepository.java b/src/main/java/de/assecutor/votianlt/repository/UserRepository.java
index 47488e4..b8a0324 100644
--- a/src/main/java/de/assecutor/votianlt/repository/UserRepository.java
+++ b/src/main/java/de/assecutor/votianlt/repository/UserRepository.java
@@ -1,13 +1,14 @@
package de.assecutor.votianlt.repository;
import de.assecutor.votianlt.model.User;
+import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
-public interface UserRepository extends MongoRepository {
+public interface UserRepository extends MongoRepository {
Optional findByEmail(String email);
diff --git a/src/main/java/de/assecutor/votianlt/service/EmailService.java b/src/main/java/de/assecutor/votianlt/service/EmailService.java
new file mode 100644
index 0000000..309c97c
--- /dev/null
+++ b/src/main/java/de/assecutor/votianlt/service/EmailService.java
@@ -0,0 +1,408 @@
+package de.assecutor.votianlt.service;
+
+import de.assecutor.votianlt.model.AppUser;
+import de.assecutor.votianlt.model.Job;
+import de.assecutor.votianlt.model.JobStatus;
+import de.assecutor.votianlt.model.User;
+import de.assecutor.votianlt.repository.AppUserRepository;
+import de.assecutor.votianlt.repository.JobRepository;
+import de.assecutor.votianlt.repository.TaskRepository;
+import de.assecutor.votianlt.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.bson.types.ObjectId;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class EmailService {
+
+ private final AppUserRepository appUserRepository;
+ private final UserRepository userRepository;
+ private final JobRepository jobRepository;
+ private final TaskRepository taskRepository;
+ private final JavaMailSender mailSender;
+
+ @Value("${spring.mail.username}")
+ private String smtpUsername;
+
+ public void sendTaskCompletionNotification(ObjectId jobId, String taskType, String taskId, String completedBy) {
+ try {
+ // Load job to get context information
+ Optional jobOpt = jobRepository.findById(jobId);
+ if (jobOpt.isEmpty()) {
+ log.warn("Job not found for task completion notification: {}", jobId);
+ return;
+ }
+ Job job = jobOpt.get();
+
+ // Find the app user who completed the task
+ Optional appUserOpt = appUserRepository.findByAppCode(completedBy);
+ if (appUserOpt.isEmpty()) {
+ log.warn("AppUser not found for task completion notification: {}", completedBy);
+ return;
+ }
+ AppUser appUser = appUserOpt.get();
+
+ // Find the owner (User) of the AppUser
+ if (appUser.getOwner() == null) {
+ log.warn("AppUser has no owner for task completion notification: {}", completedBy);
+ return;
+ }
+
+ Optional userOpt = userRepository.findById(appUser.getOwner());
+ if (userOpt.isEmpty()) {
+ log.warn("Owner user not found for task completion notification: {}", appUser.getOwner());
+ return;
+ }
+ User user = userOpt.get();
+
+ // Check if user has email
+ if (user.getEmail() == null || user.getEmail().isBlank()) {
+ log.warn("User has no email address for task completion notification: {}", user.getId());
+ return;
+ }
+
+ // Send email
+ sendEmail(user, job, taskType, taskId, appUser);
+ log.info("Task completion notification sent to {} for job {} task {}", user.getEmail(), job.getJobNumber(), taskId);
+
+ } catch (Exception e) {
+ log.error("Failed to send task completion notification for job {} task {}: {}", jobId, taskId, e.getMessage(), e);
+ }
+ }
+
+ private void sendEmail(User user, Job job, String taskType, String taskId, AppUser appUser) {
+ SimpleMailMessage message = new SimpleMailMessage();
+ message.setFrom(smtpUsername);
+ message.setTo(user.getEmail());
+ message.setSubject("Aufgabe abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
+
+ String fullName = buildFullName(user);
+ String appUserName = buildAppUserName(appUser);
+ String taskTypeName = getTaskTypeDisplayName(taskType);
+
+ StringBuilder body = new StringBuilder();
+ body.append("Hallo ").append(fullName).append(",\n\n");
+ body.append("eine Aufgabe wurde von ").append(appUserName).append(" abgeschlossen:\n\n");
+ body.append("Job: ").append(job.getJobNumber() != null ? job.getJobNumber() : "Unbekannt").append("\n");
+ if (job.getDeliveryCompany() != null) {
+ body.append("Kunde: ").append(job.getDeliveryCompany()).append("\n");
+ }
+ body.append("Aufgabe: ").append(taskTypeName).append("\n");
+ body.append("Abgeschlossen von: ").append(appUserName).append("\n\n");
+
+ if (job.getPickupCity() != null || job.getDeliveryCity() != null) {
+ body.append("Route: ");
+ if (job.getPickupCity() != null) {
+ body.append(job.getPickupCity());
+ }
+ if (job.getPickupCity() != null && job.getDeliveryCity() != null) {
+ body.append(" → ");
+ }
+ if (job.getDeliveryCity() != null) {
+ body.append(job.getDeliveryCity());
+ }
+ body.append("\n\n");
+ }
+
+ body.append("Mit freundlichen Grüßen,\n");
+ body.append("Ihr Votianlt-System");
+
+ message.setText(body.toString());
+ mailSender.send(message);
+ }
+
+ private String buildFullName(User user) {
+ StringBuilder name = new StringBuilder();
+ if (user.getTitle() != null && !user.getTitle().isBlank()) {
+ name.append(user.getTitle()).append(" ");
+ }
+ if (user.getFirstname() != null && !user.getFirstname().isBlank()) {
+ name.append(user.getFirstname()).append(" ");
+ }
+ if (user.getName() != null && !user.getName().isBlank()) {
+ name.append(user.getName());
+ }
+ String fullName = name.toString().trim();
+ return fullName.isEmpty() ? "Benutzer" : fullName;
+ }
+
+ private String buildAppUserName(AppUser appUser) {
+ StringBuilder name = new StringBuilder();
+ if (appUser.getVorname() != null && !appUser.getVorname().isBlank()) {
+ name.append(appUser.getVorname()).append(" ");
+ }
+ if (appUser.getNachname() != null && !appUser.getNachname().isBlank()) {
+ name.append(appUser.getNachname());
+ }
+ String fullName = name.toString().trim();
+ if (fullName.isEmpty() && appUser.getBezeichnung() != null && !appUser.getBezeichnung().isBlank()) {
+ return appUser.getBezeichnung();
+ }
+ return fullName.isEmpty() ? "App-Benutzer" : fullName;
+ }
+
+ private String getTaskTypeDisplayName(String taskType) {
+ if (taskType == null) return "Unbekannte Aufgabe";
+
+ return switch (taskType.toUpperCase()) {
+ case "PHOTO" -> "Foto-Aufgabe";
+ case "SIGNATURE" -> "Unterschrift";
+ case "BARCODE" -> "Barcode scannen";
+ case "CONFIRMATION" -> "Bestätigung";
+ case "TODO_LIST" -> "Checkliste";
+ default -> taskType;
+ };
+ }
+
+ public void checkAndSendJobCompletionNotification(ObjectId jobId, String completedBy) {
+ try {
+ // Check if all tasks for this job are completed
+ var allTasks = taskRepository.findByJobIdOrderByTaskOrderAsc(jobId);
+ if (allTasks.isEmpty()) {
+ log.debug("No tasks found for job {}", jobId);
+ return;
+ }
+
+ boolean allCompleted = allTasks.stream().allMatch(task -> task.isCompleted());
+
+ if (allCompleted) {
+ log.info("All tasks completed for job {}, updating job status and sending completion notification", jobId);
+
+ // Update job status to COMPLETED
+ updateJobStatusToCompleted(jobId);
+
+ // Send completion notification
+ sendJobCompletionNotification(jobId, completedBy);
+ } else {
+ long completedCount = allTasks.stream().mapToLong(task -> task.isCompleted() ? 1L : 0L).sum();
+ log.debug("Job {} not yet complete: {}/{} tasks completed", jobId, completedCount, allTasks.size());
+ }
+ } catch (Exception e) {
+ log.error("Failed to check job completion for job {}: {}", jobId, e.getMessage(), e);
+ }
+ }
+
+ private void sendJobCompletionNotification(ObjectId jobId, String completedBy) {
+ try {
+ // Load job
+ Optional jobOpt = jobRepository.findById(jobId);
+ if (jobOpt.isEmpty()) {
+ log.warn("Job not found for completion notification: {}", jobId);
+ return;
+ }
+ Job job = jobOpt.get();
+
+ // Find the app user who completed the last task
+ Optional appUserOpt = appUserRepository.findByAppCode(completedBy);
+ if (appUserOpt.isEmpty()) {
+ log.warn("AppUser not found for job completion notification: {}", completedBy);
+ return;
+ }
+ AppUser appUser = appUserOpt.get();
+
+ // Find the owner (User) of the AppUser
+ if (appUser.getOwner() == null) {
+ log.warn("AppUser has no owner for job completion notification: {}", completedBy);
+ return;
+ }
+
+ Optional userOpt = userRepository.findById(appUser.getOwner());
+ if (userOpt.isEmpty()) {
+ log.warn("Owner user not found for job completion notification: {}", appUser.getOwner());
+ return;
+ }
+ User user = userOpt.get();
+
+ // Check if user has email
+ if (user.getEmail() == null || user.getEmail().isBlank()) {
+ log.warn("User has no email address for job completion notification: {}", user.getId());
+ return;
+ }
+
+ // Send job completion email
+ sendJobCompletionEmail(user, job, appUser);
+ log.info("Job completion notification sent to {} for job {}", user.getEmail(), job.getJobNumber());
+
+ } catch (Exception e) {
+ log.error("Failed to send job completion notification for job {}: {}", jobId, e.getMessage(), e);
+ }
+ }
+
+ private void sendJobCompletionEmail(User user, Job job, AppUser appUser) {
+ SimpleMailMessage message = new SimpleMailMessage();
+ message.setFrom(smtpUsername);
+ message.setTo(user.getEmail());
+ message.setSubject("Job abgeschlossen - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
+
+ String fullName = buildFullName(user);
+ String appUserName = buildAppUserName(appUser);
+
+ // Count completed tasks
+ var allTasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId());
+ int taskCount = allTasks.size();
+
+ StringBuilder body = new StringBuilder();
+ body.append("Hallo ").append(fullName).append(",\n\n");
+ body.append("alle Aufgaben für den folgenden Job wurden erfolgreich abgeschlossen:\n\n");
+ body.append("Job: ").append(job.getJobNumber() != null ? job.getJobNumber() : "Unbekannt").append("\n");
+ if (job.getDeliveryCompany() != null) {
+ body.append("Kunde: ").append(job.getDeliveryCompany()).append("\n");
+ }
+
+ if (job.getPickupCity() != null || job.getDeliveryCity() != null) {
+ body.append("Route: ");
+ if (job.getPickupCity() != null) {
+ body.append(job.getPickupCity());
+ }
+ if (job.getPickupCity() != null && job.getDeliveryCity() != null) {
+ body.append(" → ");
+ }
+ if (job.getDeliveryCity() != null) {
+ body.append(job.getDeliveryCity());
+ }
+ body.append("\n");
+ }
+
+ body.append("Anzahl erledigter Aufgaben: ").append(taskCount).append("\n");
+ body.append("Abgeschlossen von: ").append(appUserName).append("\n\n");
+
+ body.append("Der Job ist nun vollständig erledigt und kann weiterverarbeitet werden.\n\n");
+ body.append("Mit freundlichen Grüßen,\n");
+ body.append("Ihr Votianlt-System");
+
+ message.setText(body.toString());
+ mailSender.send(message);
+ }
+
+ private void updateJobStatusToCompleted(ObjectId jobId) {
+ try {
+ Optional jobOpt = jobRepository.findById(jobId);
+ if (jobOpt.isEmpty()) {
+ log.warn("Job not found for status update: {}", jobId);
+ return;
+ }
+
+ Job job = jobOpt.get();
+ JobStatus oldStatus = job.getStatus();
+
+ // Only update if not already completed
+ if (job.getStatus() != JobStatus.COMPLETED) {
+ job.setStatus(JobStatus.COMPLETED);
+ job.setUpdatedAt(java.time.LocalDateTime.now());
+ jobRepository.save(job);
+
+ log.info("Job status updated from {} to COMPLETED for job {}",
+ oldStatus != null ? oldStatus : "null", job.getJobNumber());
+ } else {
+ log.debug("Job {} already has COMPLETED status", job.getJobNumber());
+ }
+ } catch (Exception e) {
+ log.error("Failed to update job status to COMPLETED for job {}: {}", jobId, e.getMessage(), e);
+ }
+ }
+
+ public void sendJobCreationNotification(ObjectId jobId, String createdBy) {
+ try {
+ // Load job
+ Optional jobOpt = jobRepository.findById(jobId);
+ if (jobOpt.isEmpty()) {
+ log.warn("Job not found for creation notification: {}", jobId);
+ return;
+ }
+ Job job = jobOpt.get();
+
+ // Find the user who created the job (using createdBy as User ID string)
+ ObjectId createdByUserId;
+ try {
+ createdByUserId = new ObjectId(createdBy);
+ } catch (IllegalArgumentException e) {
+ log.warn("Invalid createdBy format for job creation notification: {}", createdBy);
+ return;
+ }
+
+ Optional userOpt = userRepository.findById(createdByUserId);
+ if (userOpt.isEmpty()) {
+ log.warn("User not found for job creation notification: {}", createdBy);
+ return;
+ }
+ User user = userOpt.get();
+
+ // Check if user has email
+ if (user.getEmail() == null || user.getEmail().isBlank()) {
+ log.warn("User has no email address for job creation notification: {}", user.getId());
+ return;
+ }
+
+ // Send job creation email
+ sendJobCreationEmail(user, job);
+ log.info("Job creation notification sent to {} for job {}", user.getEmail(), job.getJobNumber());
+
+ } catch (Exception e) {
+ log.error("Failed to send job creation notification for job {}: {}", jobId, e.getMessage(), e);
+ }
+ }
+
+ private void sendJobCreationEmail(User user, Job job) {
+ SimpleMailMessage message = new SimpleMailMessage();
+ message.setFrom(smtpUsername);
+ message.setTo(user.getEmail());
+ message.setSubject("Neuer Job erstellt - " + (job.getJobNumber() != null ? job.getJobNumber() : "Job " + job.getId()));
+
+ String fullName = buildFullName(user);
+
+ // Count tasks for this job
+ var allTasks = taskRepository.findByJobIdOrderByTaskOrderAsc(job.getId());
+ int taskCount = allTasks.size();
+
+ StringBuilder body = new StringBuilder();
+ body.append("Hallo ").append(fullName).append(",\n\n");
+ body.append("ein neuer Job wurde erfolgreich erstellt:\n\n");
+ body.append("Job: ").append(job.getJobNumber() != null ? job.getJobNumber() : "Unbekannt").append("\n");
+
+ if (job.getDeliveryCompany() != null) {
+ body.append("Kunde: ").append(job.getDeliveryCompany()).append("\n");
+ }
+
+ if (job.getPickupCity() != null || job.getDeliveryCity() != null) {
+ body.append("Route: ");
+ if (job.getPickupCity() != null) {
+ body.append(job.getPickupCity());
+ }
+ if (job.getPickupCity() != null && job.getDeliveryCity() != null) {
+ body.append(" → ");
+ }
+ if (job.getDeliveryCity() != null) {
+ body.append(job.getDeliveryCity());
+ }
+ body.append("\n");
+ }
+
+ if (taskCount > 0) {
+ body.append("Anzahl Aufgaben: ").append(taskCount).append("\n");
+ }
+
+ body.append("Status: ").append(job.getStatus() != null ? job.getStatus().getDisplayName() : "Unbekannt").append("\n");
+
+ if (job.getRemark() != null && !job.getRemark().isBlank()) {
+ body.append("Bemerkung: ").append(job.getRemark()).append("\n");
+ }
+
+ body.append("Erstellt am: ").append(job.getCreatedAt() != null ?
+ job.getCreatedAt().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")) : "Unbekannt").append("\n\n");
+
+ body.append("Der Job ist nun im System verfügbar und kann bearbeitet werden.\n\n");
+ body.append("Mit freundlichen Grüßen,\n");
+ body.append("Ihr Votianlt-System");
+
+ message.setText(body.toString());
+ mailSender.send(message);
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index f9858f0..b022eb6 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -22,11 +22,13 @@ spring.data.mongodb.socket-timeout=30000
spring.data.mongodb.connect-timeout=10000
spring.data.mongodb.server-selection-timeout=5000
-# Mail Configuration
-mail.smtp.username=no-reply@appcreation.de
-mail.smtp.password=SV1705CA!noreply
-mail.smtp.host=smtp.ionos.de
-mail.smtp.port=587
+# Mail Configuration (Spring Boot Standard)
+spring.mail.host=mailhub.assecutor.org
+spring.mail.port=587
+spring.mail.username=noreply@assecutor.org
+spring.mail.password=OStRIL,_,31
+spring.mail.properties.mail.smtp.auth=true
+spring.mail.properties.mail.smtp.starttls.enable=true
# HTTP request size limits for large payloads
server.max-http-request-header-size=8MB