Erweiterungen

This commit is contained in:
2025-09-15 17:49:22 +02:00
parent 3fa56da257
commit ba9b47be89
9 changed files with 534 additions and 11 deletions

Binary file not shown.

View File

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

View File

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

View File

@@ -17,7 +17,10 @@ public interface AppUserRepository extends MongoRepository<AppUser, ObjectId> {
AppUser findByEmail(String email);
AppUser findByPasswordCode(String passwordCode);
// Find AppUser by appCode for task completion notifications
java.util.Optional<AppUser> findByAppCode(String appCode);
// Custom query methods can be added here if needed
// List<AppUser> findByBezeichnung(String bezeichnung);
}

View File

@@ -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<User, String> {
public interface UserRepository extends MongoRepository<User, ObjectId> {
Optional<User> findByEmail(String email);

View File

@@ -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<Job> 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<AppUser> 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<User> 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<Job> 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<AppUser> 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<User> 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<Job> 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<Job> 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<User> 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);
}
}

View File

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