Erweiterungen
This commit is contained in:
@@ -0,0 +1,231 @@
|
|||||||
|
package de.assecutor.votianlt.controller;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.Message;
|
||||||
|
import de.assecutor.votianlt.model.MessageDirection;
|
||||||
|
import de.assecutor.votianlt.service.MessageService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST API controller for message operations.
|
||||||
|
* Provides endpoints for sending messages, retrieving messages, and marking messages as read.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/messages")
|
||||||
|
@Slf4j
|
||||||
|
public class MessageApiController {
|
||||||
|
|
||||||
|
private final MessageService messageService;
|
||||||
|
|
||||||
|
public MessageApiController(MessageService messageService) {
|
||||||
|
this.messageService = messageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a general message to a client
|
||||||
|
* POST /api/messages/send
|
||||||
|
* Body: { "content": "message text", "sender": "username", "receiver": "username" }
|
||||||
|
*/
|
||||||
|
@PostMapping("/send")
|
||||||
|
public ResponseEntity<Message> sendGeneralMessage(@RequestBody Map<String, String> request) {
|
||||||
|
try {
|
||||||
|
String content = request.get("content");
|
||||||
|
String sender = request.get("sender");
|
||||||
|
String receiver = request.get("receiver");
|
||||||
|
|
||||||
|
if (content == null || content.isBlank() ||
|
||||||
|
sender == null || sender.isBlank() ||
|
||||||
|
receiver == null || receiver.isBlank()) {
|
||||||
|
log.warn("Invalid message request: missing required fields");
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Message message = messageService.sendGeneralMessageToClient(content, sender, receiver);
|
||||||
|
log.info("General message sent from {} to {}", sender, receiver);
|
||||||
|
return ResponseEntity.ok(message);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error sending general message: {}", e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a job-related message to a client
|
||||||
|
* POST /api/messages/send-job-message
|
||||||
|
* Body: { "content": "message text", "sender": "username", "receiver": "username",
|
||||||
|
* "jobId": "job id", "jobNumber": "job number" }
|
||||||
|
*/
|
||||||
|
@PostMapping("/send-job-message")
|
||||||
|
public ResponseEntity<Message> sendJobMessage(@RequestBody Map<String, String> request) {
|
||||||
|
try {
|
||||||
|
String content = request.get("content");
|
||||||
|
String sender = request.get("sender");
|
||||||
|
String receiver = request.get("receiver");
|
||||||
|
String jobIdStr = request.get("jobId");
|
||||||
|
String jobNumber = request.get("jobNumber");
|
||||||
|
|
||||||
|
if (content == null || content.isBlank() ||
|
||||||
|
sender == null || sender.isBlank() ||
|
||||||
|
receiver == null || receiver.isBlank() ||
|
||||||
|
jobIdStr == null || jobIdStr.isBlank()) {
|
||||||
|
log.warn("Invalid job message request: missing required fields");
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectId jobId = new ObjectId(jobIdStr);
|
||||||
|
Message message = messageService.sendJobMessageToClient(content, sender, receiver, jobId, jobNumber);
|
||||||
|
log.info("Job-related message sent from {} to {} for job {}", sender, receiver, jobNumber);
|
||||||
|
return ResponseEntity.ok(message);
|
||||||
|
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.error("Invalid jobId format: {}", e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error sending job message: {}", e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all messages for a specific receiver
|
||||||
|
* GET /api/messages/receiver/{username}
|
||||||
|
*/
|
||||||
|
@GetMapping("/receiver/{username}")
|
||||||
|
public ResponseEntity<List<Message>> getMessagesForReceiver(@PathVariable String username) {
|
||||||
|
try {
|
||||||
|
List<Message> messages = messageService.getMessagesForReceiver(username);
|
||||||
|
return ResponseEntity.ok(messages);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error retrieving messages for receiver {}: {}", username, e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all unread messages for a specific receiver
|
||||||
|
* GET /api/messages/receiver/{username}/unread
|
||||||
|
*/
|
||||||
|
@GetMapping("/receiver/{username}/unread")
|
||||||
|
public ResponseEntity<List<Message>> getUnreadMessagesForReceiver(@PathVariable String username) {
|
||||||
|
try {
|
||||||
|
List<Message> messages = messageService.getUnreadMessagesForReceiver(username);
|
||||||
|
return ResponseEntity.ok(messages);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error retrieving unread messages for receiver {}: {}", username, e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get unread message count for a specific receiver
|
||||||
|
* GET /api/messages/receiver/{username}/unread-count
|
||||||
|
*/
|
||||||
|
@GetMapping("/receiver/{username}/unread-count")
|
||||||
|
public ResponseEntity<Map<String, Long>> getUnreadMessageCount(@PathVariable String username) {
|
||||||
|
try {
|
||||||
|
long count = messageService.getUnreadMessageCount(username);
|
||||||
|
return ResponseEntity.ok(Map.of("count", count));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error retrieving unread message count for receiver {}: {}", username, e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all messages related to a specific job
|
||||||
|
* GET /api/messages/job/{jobId}
|
||||||
|
*/
|
||||||
|
@GetMapping("/job/{jobId}")
|
||||||
|
public ResponseEntity<List<Message>> getMessagesForJob(@PathVariable String jobId) {
|
||||||
|
try {
|
||||||
|
ObjectId objectId = new ObjectId(jobId);
|
||||||
|
List<Message> messages = messageService.getMessagesForJob(objectId);
|
||||||
|
return ResponseEntity.ok(messages);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.error("Invalid jobId format: {}", e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error retrieving messages for job {}: {}", jobId, e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all messages (for admin/overview)
|
||||||
|
* GET /api/messages/all
|
||||||
|
*/
|
||||||
|
@GetMapping("/all")
|
||||||
|
public ResponseEntity<List<Message>> getAllMessages() {
|
||||||
|
try {
|
||||||
|
List<Message> messages = messageService.getAllMessages();
|
||||||
|
return ResponseEntity.ok(messages);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error retrieving all messages: {}", e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get messages by direction (incoming/outgoing)
|
||||||
|
* GET /api/messages/direction/{direction}
|
||||||
|
*/
|
||||||
|
@GetMapping("/direction/{direction}")
|
||||||
|
public ResponseEntity<List<Message>> getMessagesByDirection(@PathVariable String direction) {
|
||||||
|
try {
|
||||||
|
MessageDirection messageDirection = MessageDirection.valueOf(direction.toUpperCase());
|
||||||
|
List<Message> messages = messageService.getMessagesByDirection(messageDirection);
|
||||||
|
return ResponseEntity.ok(messages);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.error("Invalid direction: {}", direction);
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error retrieving messages by direction {}: {}", direction, e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a message as read
|
||||||
|
* PUT /api/messages/{messageId}/mark-read
|
||||||
|
*/
|
||||||
|
@PutMapping("/{messageId}/mark-read")
|
||||||
|
public ResponseEntity<Void> markMessageAsRead(@PathVariable String messageId) {
|
||||||
|
try {
|
||||||
|
ObjectId objectId = new ObjectId(messageId);
|
||||||
|
messageService.markAsRead(objectId);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.error("Invalid messageId format: {}", e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error marking message as read {}: {}", messageId, e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a message
|
||||||
|
* DELETE /api/messages/{messageId}
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{messageId}")
|
||||||
|
public ResponseEntity<Void> deleteMessage(@PathVariable String messageId) {
|
||||||
|
try {
|
||||||
|
ObjectId objectId = new ObjectId(messageId);
|
||||||
|
messageService.deleteMessage(objectId);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.error("Invalid messageId format: {}", e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error deleting message {}: {}", messageId, e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import de.assecutor.votianlt.model.Signature;
|
|||||||
import de.assecutor.votianlt.model.Comment;
|
import de.assecutor.votianlt.model.Comment;
|
||||||
import de.assecutor.votianlt.service.JobHistoryService;
|
import de.assecutor.votianlt.service.JobHistoryService;
|
||||||
import de.assecutor.votianlt.service.EmailService;
|
import de.assecutor.votianlt.service.EmailService;
|
||||||
|
import de.assecutor.votianlt.service.MessageService;
|
||||||
import de.assecutor.votianlt.model.JobStatus;
|
import de.assecutor.votianlt.model.JobStatus;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@@ -64,11 +65,13 @@ public class MessageController {
|
|||||||
private final CommentRepository commentRepository;
|
private final CommentRepository commentRepository;
|
||||||
private final JobHistoryService jobHistoryService;
|
private final JobHistoryService jobHistoryService;
|
||||||
private final EmailService emailService;
|
private final EmailService emailService;
|
||||||
|
private final MessageService messageService;
|
||||||
|
|
||||||
public MessageController(MqttPublisher mqttPublisher, AppUserRepository appUserRepository,
|
public MessageController(MqttPublisher mqttPublisher, AppUserRepository appUserRepository,
|
||||||
AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository,
|
AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository,
|
||||||
TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository,
|
TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository,
|
||||||
SignatureRepository signatureRepository, CommentRepository commentRepository, JobHistoryService jobHistoryService, EmailService emailService) {
|
SignatureRepository signatureRepository, CommentRepository commentRepository, JobHistoryService jobHistoryService,
|
||||||
|
EmailService emailService, MessageService messageService) {
|
||||||
this.mqttPublisher = mqttPublisher;
|
this.mqttPublisher = mqttPublisher;
|
||||||
this.appUserRepository = appUserRepository;
|
this.appUserRepository = appUserRepository;
|
||||||
this.appUserService = appUserService;
|
this.appUserService = appUserService;
|
||||||
@@ -81,6 +84,7 @@ public class MessageController {
|
|||||||
this.commentRepository = commentRepository;
|
this.commentRepository = commentRepository;
|
||||||
this.jobHistoryService = jobHistoryService;
|
this.jobHistoryService = jobHistoryService;
|
||||||
this.emailService = emailService;
|
this.emailService = emailService;
|
||||||
|
this.messageService = messageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -589,4 +593,61 @@ public class MessageController {
|
|||||||
private String getClientIdForUserId(String userId) {
|
private String getClientIdForUserId(String userId) {
|
||||||
return userClientIdMapping.get(userId);
|
return userClientIdMapping.get(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle incoming message from a client via MQTT.
|
||||||
|
* Client sends to /server/{clientId}/message with payload:
|
||||||
|
* {
|
||||||
|
* "sender": "appUserUsername",
|
||||||
|
* "receiver": "systemUserUsername",
|
||||||
|
* "content": "message text",
|
||||||
|
* "jobId": "optional job id",
|
||||||
|
* "jobNumber": "optional job number"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public void handleIncomingMessage(Map<String, Object> payload) {
|
||||||
|
log.info("MQTT Endpoint '/server/{clientId}/message' called with data: {}", payload);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract required fields
|
||||||
|
String sender = payload.get("sender") != null ? payload.get("sender").toString() : null;
|
||||||
|
String receiver = payload.get("receiver") != null ? payload.get("receiver").toString() : null;
|
||||||
|
String content = payload.get("content") != null ? payload.get("content").toString() : null;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (sender == null || sender.isBlank() ||
|
||||||
|
receiver == null || receiver.isBlank() ||
|
||||||
|
content == null || content.isBlank()) {
|
||||||
|
log.warn("Incoming message missing required fields: sender={}, receiver={}, content={}",
|
||||||
|
sender, receiver, content != null ? "present" : "null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract optional job-related fields
|
||||||
|
ObjectId jobId = null;
|
||||||
|
String jobNumber = null;
|
||||||
|
|
||||||
|
if (payload.get("jobId") != null) {
|
||||||
|
try {
|
||||||
|
String jobIdStr = payload.get("jobId").toString();
|
||||||
|
if (!jobIdStr.isBlank()) {
|
||||||
|
jobId = new ObjectId(jobIdStr);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.warn("Invalid jobId format in message: {}", payload.get("jobId"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.get("jobNumber") != null) {
|
||||||
|
jobNumber = payload.get("jobNumber").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the message using MessageService
|
||||||
|
messageService.receiveMessageFromClient(content, sender, receiver, jobId, jobNumber);
|
||||||
|
log.info("Successfully saved incoming message from {} to {}", sender, receiver);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error handling incoming message: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package de.assecutor.votianlt.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for summarizing message conversations by client
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ClientMessageSummary {
|
||||||
|
|
||||||
|
private String clientId;
|
||||||
|
private String clientName;
|
||||||
|
private String clientEmail;
|
||||||
|
private int totalMessages;
|
||||||
|
private int unreadCount;
|
||||||
|
private LocalDateTime lastMessageDate;
|
||||||
|
private String lastMessagePreview;
|
||||||
|
}
|
||||||
139
src/main/java/de/assecutor/votianlt/model/Message.java
Normal file
139
src/main/java/de/assecutor/votianlt/model/Message.java
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package de.assecutor.votianlt.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Field;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a message that can be sent between the server and clients.
|
||||||
|
* Messages can be either job-related or general messages.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Document(collection = "messages")
|
||||||
|
public class Message {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@JsonIgnore
|
||||||
|
private ObjectId id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content of the message
|
||||||
|
*/
|
||||||
|
@Field("content")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Username of the sender (app user or system user)
|
||||||
|
*/
|
||||||
|
@Field("sender")
|
||||||
|
private String sender;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Username of the receiver (app user or system user)
|
||||||
|
*/
|
||||||
|
@Field("receiver")
|
||||||
|
private String receiver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the message was created
|
||||||
|
*/
|
||||||
|
@Field("created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direction of the message: INCOMING (from client) or OUTGOING (to client)
|
||||||
|
*/
|
||||||
|
@Field("direction")
|
||||||
|
private MessageDirection direction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of message: JOB_RELATED or GENERAL
|
||||||
|
*/
|
||||||
|
@Field("message_type")
|
||||||
|
private MessageType messageType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional reference to a job (only for job-related messages)
|
||||||
|
*/
|
||||||
|
@Field("job_id")
|
||||||
|
private ObjectId jobId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional job number for easier reference (denormalized)
|
||||||
|
*/
|
||||||
|
@Field("job_number")
|
||||||
|
private String jobNumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the message has been read by the receiver
|
||||||
|
*/
|
||||||
|
@Field("is_read")
|
||||||
|
private boolean isRead;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the message was read
|
||||||
|
*/
|
||||||
|
@Field("read_at")
|
||||||
|
private LocalDateTime readAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for general messages
|
||||||
|
*/
|
||||||
|
public Message(String content, String sender, String receiver, MessageDirection direction) {
|
||||||
|
this.content = content;
|
||||||
|
this.sender = sender;
|
||||||
|
this.receiver = receiver;
|
||||||
|
this.direction = direction;
|
||||||
|
this.messageType = MessageType.GENERAL;
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
this.isRead = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for job-related messages
|
||||||
|
*/
|
||||||
|
public Message(String content, String sender, String receiver, MessageDirection direction,
|
||||||
|
ObjectId jobId, String jobNumber) {
|
||||||
|
this.content = content;
|
||||||
|
this.sender = sender;
|
||||||
|
this.receiver = receiver;
|
||||||
|
this.direction = direction;
|
||||||
|
this.messageType = MessageType.JOB_RELATED;
|
||||||
|
this.jobId = jobId;
|
||||||
|
this.jobNumber = jobNumber;
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
this.isRead = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the message as read
|
||||||
|
*/
|
||||||
|
public void markAsRead() {
|
||||||
|
this.isRead = true;
|
||||||
|
this.readAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ObjectId as string for JSON serialization
|
||||||
|
*/
|
||||||
|
@JsonGetter("id")
|
||||||
|
public String getIdAsString() {
|
||||||
|
return id != null ? id.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the job ObjectId as string for JSON serialization
|
||||||
|
*/
|
||||||
|
@JsonGetter("jobId")
|
||||||
|
public String getJobIdAsString() {
|
||||||
|
return jobId != null ? jobId.toString() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package de.assecutor.votianlt.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum representing the direction of a message
|
||||||
|
*/
|
||||||
|
public enum MessageDirection {
|
||||||
|
/**
|
||||||
|
* Message received from a client (app user)
|
||||||
|
*/
|
||||||
|
INCOMING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message sent to a client (app user)
|
||||||
|
*/
|
||||||
|
OUTGOING
|
||||||
|
}
|
||||||
16
src/main/java/de/assecutor/votianlt/model/MessageType.java
Normal file
16
src/main/java/de/assecutor/votianlt/model/MessageType.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package de.assecutor.votianlt.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum representing the type of message
|
||||||
|
*/
|
||||||
|
public enum MessageType {
|
||||||
|
/**
|
||||||
|
* General message not related to a specific job
|
||||||
|
*/
|
||||||
|
GENERAL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message related to a specific job
|
||||||
|
*/
|
||||||
|
JOB_RELATED
|
||||||
|
}
|
||||||
@@ -163,6 +163,13 @@ public class MqttV5ClientManager implements SmartLifecycle {
|
|||||||
de.assecutor.votianlt.dto.AppLoginRequest req = om.convertValue(payload,
|
de.assecutor.votianlt.dto.AppLoginRequest req = om.convertValue(payload,
|
||||||
de.assecutor.votianlt.dto.AppLoginRequest.class);
|
de.assecutor.votianlt.dto.AppLoginRequest.class);
|
||||||
messageController.handleAppLogin(req);
|
messageController.handleAppLogin(req);
|
||||||
|
} else if (topic.matches("/server/.+/message")) {
|
||||||
|
try {
|
||||||
|
// Handle incoming message from client
|
||||||
|
messageController.handleIncomingMessage(payload);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error handling incoming message on {}: {}", topic, e.getMessage(), e);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.debug("No route for topic {}", topic);
|
log.debug("No route for topic {}", topic);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,362 @@
|
|||||||
|
package de.assecutor.votianlt.pages.view;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.UI;
|
||||||
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.html.Div;
|
||||||
|
import com.vaadin.flow.component.html.H2;
|
||||||
|
import com.vaadin.flow.component.html.Main;
|
||||||
|
import com.vaadin.flow.component.html.Span;
|
||||||
|
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.Scroller;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
|
import com.vaadin.flow.component.textfield.TextArea;
|
||||||
|
import com.vaadin.flow.router.BeforeEnterEvent;
|
||||||
|
import com.vaadin.flow.router.BeforeEnterObserver;
|
||||||
|
import com.vaadin.flow.router.PageTitle;
|
||||||
|
import com.vaadin.flow.router.Route;
|
||||||
|
import com.vaadin.flow.router.RouteParameters;
|
||||||
|
import de.assecutor.votianlt.model.AppUser;
|
||||||
|
import de.assecutor.votianlt.pages.service.AppUserService;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
@Route(value = "message-details/:clientId/:conversationId", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||||
|
@PageTitle("Nachrichtenverlauf")
|
||||||
|
@RolesAllowed("USER")
|
||||||
|
@Slf4j
|
||||||
|
public class MessageDetailsView extends Main implements BeforeEnterObserver {
|
||||||
|
|
||||||
|
private final AppUserService appUserService;
|
||||||
|
|
||||||
|
private String clientId;
|
||||||
|
private String conversationId;
|
||||||
|
private VerticalLayout contentLayout;
|
||||||
|
private VerticalLayout messagesContainer;
|
||||||
|
private Scroller messagesScroller;
|
||||||
|
private Div scrollAnchor; // Marker element at the end of messages for scrolling
|
||||||
|
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
|
||||||
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
||||||
|
|
||||||
|
public MessageDetailsView(AppUserService appUserService) {
|
||||||
|
this.appUserService = appUserService;
|
||||||
|
|
||||||
|
// Set height to 100% to prevent page from growing beyond viewport
|
||||||
|
setHeightFull();
|
||||||
|
|
||||||
|
// Create main layout with fixed positioning
|
||||||
|
contentLayout = new VerticalLayout();
|
||||||
|
contentLayout.setPadding(true);
|
||||||
|
contentLayout.setSpacing(true);
|
||||||
|
contentLayout.setWidthFull();
|
||||||
|
contentLayout.setHeightFull();
|
||||||
|
contentLayout.getStyle().set("overflow", "hidden");
|
||||||
|
|
||||||
|
add(contentLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeEnter(BeforeEnterEvent event) {
|
||||||
|
// Extract route parameters from URL
|
||||||
|
RouteParameters parameters = event.getRouteParameters();
|
||||||
|
|
||||||
|
this.clientId = parameters.get("clientId").orElse(null);
|
||||||
|
this.conversationId = parameters.get("conversationId").orElse(null);
|
||||||
|
|
||||||
|
log.info("MessageDetailsView - clientId: {}, conversationId: {}", clientId, conversationId);
|
||||||
|
|
||||||
|
if (clientId == null || conversationId == null) {
|
||||||
|
log.warn("Missing required route parameters: clientId={}, conversationId={}", clientId, conversationId);
|
||||||
|
event.rerouteToError(IllegalArgumentException.class, "Missing required parameters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMessageDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadMessageDetails() {
|
||||||
|
contentLayout.removeAll();
|
||||||
|
|
||||||
|
// Get client info
|
||||||
|
AppUser client = null;
|
||||||
|
try {
|
||||||
|
ObjectId objectId = new ObjectId(clientId);
|
||||||
|
client = appUserService.findById(objectId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Could not find client with id: {}", clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
String clientName = client != null ?
|
||||||
|
client.getVorname() + " " + client.getNachname() : "Unbekannter Client";
|
||||||
|
|
||||||
|
// Determine conversation title
|
||||||
|
String conversationTitle = "Allgemeine Unterhaltung";
|
||||||
|
if (conversationId != null && conversationId.startsWith("job-")) {
|
||||||
|
conversationTitle = "Auftrag #" + conversationId.substring(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create header
|
||||||
|
HorizontalLayout headerLayout = createHeaderLayout(clientName, conversationTitle);
|
||||||
|
contentLayout.add(headerLayout);
|
||||||
|
|
||||||
|
// Create messages container (content for scrollable area)
|
||||||
|
messagesContainer = new VerticalLayout();
|
||||||
|
messagesContainer.setPadding(true);
|
||||||
|
messagesContainer.setSpacing(true);
|
||||||
|
messagesContainer.setWidthFull();
|
||||||
|
messagesContainer.getStyle().set("background-color", "#f0f0f0");
|
||||||
|
messagesContainer.getStyle().set("border-radius", "8px");
|
||||||
|
messagesContainer.getStyle().set("padding", "20px");
|
||||||
|
|
||||||
|
// Add test messages
|
||||||
|
loadTestMessages();
|
||||||
|
|
||||||
|
// Wrap messages container in Scroller for proper scrolling behavior
|
||||||
|
messagesScroller = new Scroller(messagesContainer);
|
||||||
|
messagesScroller.setWidthFull();
|
||||||
|
messagesScroller.setScrollDirection(Scroller.ScrollDirection.VERTICAL);
|
||||||
|
|
||||||
|
contentLayout.add(messagesScroller);
|
||||||
|
contentLayout.setFlexGrow(1, messagesScroller);
|
||||||
|
|
||||||
|
// Add message input area
|
||||||
|
HorizontalLayout inputLayout = createMessageInputArea();
|
||||||
|
contentLayout.add(inputLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HorizontalLayout createHeaderLayout(String clientName, String conversationTitle) {
|
||||||
|
Button backButton = new Button("Zurück", VaadinIcon.ARROW_LEFT.create());
|
||||||
|
backButton.addClickListener(e -> UI.getCurrent().navigate("user-messages/" + clientId));
|
||||||
|
|
||||||
|
VerticalLayout titleLayout = new VerticalLayout();
|
||||||
|
titleLayout.setPadding(false);
|
||||||
|
titleLayout.setSpacing(false);
|
||||||
|
|
||||||
|
H2 title = new H2(clientName);
|
||||||
|
title.getStyle().set("margin", "0");
|
||||||
|
|
||||||
|
Span subtitle = new Span(conversationTitle);
|
||||||
|
subtitle.getStyle().set("color", "#666666");
|
||||||
|
subtitle.getStyle().set("font-size", "14px");
|
||||||
|
|
||||||
|
titleLayout.add(title, subtitle);
|
||||||
|
|
||||||
|
HorizontalLayout layout = new HorizontalLayout(backButton, titleLayout);
|
||||||
|
layout.setWidthFull();
|
||||||
|
layout.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER);
|
||||||
|
layout.setSpacing(true);
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadTestMessages() {
|
||||||
|
// Test data - message thread
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
// Day 1 - older messages
|
||||||
|
messagesContainer.add(createDateSeparator(now.minusDays(2)));
|
||||||
|
messagesContainer.add(createIncomingMessage(
|
||||||
|
"Hallo, ich habe eine Frage zum Auftrag.",
|
||||||
|
now.minusDays(2).withHour(10).withMinute(30)
|
||||||
|
));
|
||||||
|
messagesContainer.add(createOutgoingMessage(
|
||||||
|
"Hallo! Gerne, wie kann ich Ihnen helfen?",
|
||||||
|
now.minusDays(2).withHour(10).withMinute(35)
|
||||||
|
));
|
||||||
|
messagesContainer.add(createIncomingMessage(
|
||||||
|
"Wann wird die Lieferung voraussichtlich ankommen?",
|
||||||
|
now.minusDays(2).withHour(10).withMinute(40)
|
||||||
|
));
|
||||||
|
messagesContainer.add(createOutgoingMessage(
|
||||||
|
"Die Lieferung ist für morgen zwischen 14:00 und 16:00 Uhr geplant.",
|
||||||
|
now.minusDays(2).withHour(10).withMinute(45)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Day 2 - yesterday
|
||||||
|
messagesContainer.add(createDateSeparator(now.minusDays(1)));
|
||||||
|
messagesContainer.add(createIncomingMessage(
|
||||||
|
"Vielen Dank für die Information!",
|
||||||
|
now.minusDays(1).withHour(9).withMinute(15)
|
||||||
|
));
|
||||||
|
messagesContainer.add(createOutgoingMessage(
|
||||||
|
"Gern geschehen! Melden Sie sich bei weiteren Fragen.",
|
||||||
|
now.minusDays(1).withHour(9).withMinute(20)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Today
|
||||||
|
messagesContainer.add(createDateSeparator(now));
|
||||||
|
messagesContainer.add(createIncomingMessage(
|
||||||
|
"Die Lieferung ist angekommen. Alles perfekt!",
|
||||||
|
now.minusHours(2).withMinute(0)
|
||||||
|
));
|
||||||
|
messagesContainer.add(createOutgoingMessage(
|
||||||
|
"Das freut mich zu hören! Vielen Dank für die Rückmeldung.",
|
||||||
|
now.minusHours(1).withMinute(30)
|
||||||
|
));
|
||||||
|
messagesContainer.add(createIncomingMessage(
|
||||||
|
"Können wir für nächste Woche einen neuen Auftrag vereinbaren?",
|
||||||
|
now.minusMinutes(30)
|
||||||
|
));
|
||||||
|
messagesContainer.add(createOutgoingMessage(
|
||||||
|
"Selbstverständlich! Ich erstelle Ihnen gleich ein Angebot.",
|
||||||
|
now.minusMinutes(15)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Add scroll anchor at the end - this invisible element will be our scroll target
|
||||||
|
scrollAnchor = new Div();
|
||||||
|
scrollAnchor.setId("scroll-anchor");
|
||||||
|
scrollAnchor.getStyle().set("height", "1px");
|
||||||
|
messagesContainer.add(scrollAnchor);
|
||||||
|
|
||||||
|
// Auto-scroll to bottom after messages are loaded
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Div createDateSeparator(LocalDateTime date) {
|
||||||
|
Div separator = new Div();
|
||||||
|
separator.setWidthFull();
|
||||||
|
separator.getStyle().set("text-align", "center");
|
||||||
|
separator.getStyle().set("margin", "20px 0");
|
||||||
|
|
||||||
|
Span dateSpan = new Span(date.format(DATE_FORMATTER));
|
||||||
|
dateSpan.getStyle().set("background-color", "#e0e0e0");
|
||||||
|
dateSpan.getStyle().set("padding", "5px 15px");
|
||||||
|
dateSpan.getStyle().set("border-radius", "15px");
|
||||||
|
dateSpan.getStyle().set("font-size", "12px");
|
||||||
|
dateSpan.getStyle().set("color", "#666666");
|
||||||
|
|
||||||
|
separator.add(dateSpan);
|
||||||
|
return separator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Div createIncomingMessage(String content, LocalDateTime timestamp) {
|
||||||
|
Div messageWrapper = new Div();
|
||||||
|
messageWrapper.setWidthFull();
|
||||||
|
messageWrapper.getStyle().set("display", "flex");
|
||||||
|
messageWrapper.getStyle().set("justify-content", "flex-start");
|
||||||
|
messageWrapper.getStyle().set("margin-bottom", "10px");
|
||||||
|
|
||||||
|
Div messageBubble = new Div();
|
||||||
|
messageBubble.getStyle().set("max-width", "60%");
|
||||||
|
messageBubble.getStyle().set("background-color", "#ffffff");
|
||||||
|
messageBubble.getStyle().set("padding", "10px 15px");
|
||||||
|
messageBubble.getStyle().set("border-radius", "18px");
|
||||||
|
messageBubble.getStyle().set("box-shadow", "0 1px 2px rgba(0,0,0,0.1)");
|
||||||
|
|
||||||
|
Span contentSpan = new Span(content);
|
||||||
|
contentSpan.getStyle().set("display", "block");
|
||||||
|
contentSpan.getStyle().set("word-wrap", "break-word");
|
||||||
|
|
||||||
|
Span timeSpan = new Span(timestamp.format(TIME_FORMATTER));
|
||||||
|
timeSpan.getStyle().set("font-size", "11px");
|
||||||
|
timeSpan.getStyle().set("color", "#999999");
|
||||||
|
timeSpan.getStyle().set("display", "block");
|
||||||
|
timeSpan.getStyle().set("text-align", "right");
|
||||||
|
timeSpan.getStyle().set("margin-top", "5px");
|
||||||
|
|
||||||
|
messageBubble.add(contentSpan, timeSpan);
|
||||||
|
messageWrapper.add(messageBubble);
|
||||||
|
|
||||||
|
return messageWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Div createOutgoingMessage(String content, LocalDateTime timestamp) {
|
||||||
|
Div messageWrapper = new Div();
|
||||||
|
messageWrapper.setWidthFull();
|
||||||
|
messageWrapper.getStyle().set("display", "flex");
|
||||||
|
messageWrapper.getStyle().set("justify-content", "flex-end");
|
||||||
|
messageWrapper.getStyle().set("margin-bottom", "10px");
|
||||||
|
|
||||||
|
Div messageBubble = new Div();
|
||||||
|
messageBubble.getStyle().set("max-width", "60%");
|
||||||
|
messageBubble.getStyle().set("background-color", "#dcf8c6");
|
||||||
|
messageBubble.getStyle().set("padding", "10px 15px");
|
||||||
|
messageBubble.getStyle().set("border-radius", "18px");
|
||||||
|
messageBubble.getStyle().set("box-shadow", "0 1px 2px rgba(0,0,0,0.1)");
|
||||||
|
|
||||||
|
Span contentSpan = new Span(content);
|
||||||
|
contentSpan.getStyle().set("display", "block");
|
||||||
|
contentSpan.getStyle().set("word-wrap", "break-word");
|
||||||
|
|
||||||
|
Span timeSpan = new Span(timestamp.format(TIME_FORMATTER));
|
||||||
|
timeSpan.getStyle().set("font-size", "11px");
|
||||||
|
timeSpan.getStyle().set("color", "#666666");
|
||||||
|
timeSpan.getStyle().set("display", "block");
|
||||||
|
timeSpan.getStyle().set("text-align", "right");
|
||||||
|
timeSpan.getStyle().set("margin-top", "5px");
|
||||||
|
|
||||||
|
messageBubble.add(contentSpan, timeSpan);
|
||||||
|
messageWrapper.add(messageBubble);
|
||||||
|
|
||||||
|
return messageWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HorizontalLayout createMessageInputArea() {
|
||||||
|
TextArea messageInput = new TextArea();
|
||||||
|
messageInput.setPlaceholder("Nachricht eingeben...");
|
||||||
|
messageInput.setWidthFull();
|
||||||
|
messageInput.getStyle().set("min-height", "60px");
|
||||||
|
messageInput.getStyle().set("max-height", "120px");
|
||||||
|
|
||||||
|
Button sendButton = new Button("Senden", VaadinIcon.PAPERPLANE.create());
|
||||||
|
sendButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_PRIMARY);
|
||||||
|
sendButton.getStyle().set("height", "60px");
|
||||||
|
|
||||||
|
sendButton.addClickListener(e -> {
|
||||||
|
String message = messageInput.getValue();
|
||||||
|
if (message != null && !message.trim().isEmpty()) {
|
||||||
|
// Add message to view (test data)
|
||||||
|
messagesContainer.add(createOutgoingMessage(message, LocalDateTime.now()));
|
||||||
|
messageInput.clear();
|
||||||
|
|
||||||
|
// Scroll to bottom
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
HorizontalLayout layout = new HorizontalLayout(messageInput, sendButton);
|
||||||
|
layout.setWidthFull();
|
||||||
|
layout.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.END);
|
||||||
|
layout.expand(messageInput);
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll the messages scroller to the bottom to show the latest message
|
||||||
|
* Uses scrollIntoView on the anchor element at the end of messages
|
||||||
|
*/
|
||||||
|
private void scrollToBottom() {
|
||||||
|
if (scrollAnchor != null) {
|
||||||
|
// Use beforeClientResponse to ensure all components are rendered and DOM is ready
|
||||||
|
UI.getCurrent().beforeClientResponse(scrollAnchor, context -> {
|
||||||
|
// Use scrollIntoView on the anchor element - this is more reliable than
|
||||||
|
// trying to manipulate scrollTop/scrollHeight
|
||||||
|
// Multiple delayed attempts ensure content is fully rendered and laid out
|
||||||
|
scrollAnchor.getElement().executeJs(
|
||||||
|
"const anchor = this;" +
|
||||||
|
"console.log('Scroll anchor found:', anchor);" +
|
||||||
|
// First attempt after 50ms - instant scroll
|
||||||
|
"setTimeout(() => {" +
|
||||||
|
" anchor.scrollIntoView({ behavior: 'instant', block: 'end' });" +
|
||||||
|
" console.log('Scroll attempt 1: scrollIntoView called (instant)');" +
|
||||||
|
"}, 50);" +
|
||||||
|
// Second attempt after 200ms - instant scroll
|
||||||
|
"setTimeout(() => {" +
|
||||||
|
" anchor.scrollIntoView({ behavior: 'instant', block: 'end' });" +
|
||||||
|
" console.log('Scroll attempt 2: scrollIntoView called (instant)');" +
|
||||||
|
"}, 200);" +
|
||||||
|
// Third attempt after 500ms - instant scroll
|
||||||
|
"setTimeout(() => {" +
|
||||||
|
" anchor.scrollIntoView({ behavior: 'instant', block: 'end' });" +
|
||||||
|
" console.log('Scroll attempt 3: scrollIntoView called (instant)');" +
|
||||||
|
"}, 500);"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
397
src/main/java/de/assecutor/votianlt/pages/view/MessagesView.java
Normal file
397
src/main/java/de/assecutor/votianlt/pages/view/MessagesView.java
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
package de.assecutor.votianlt.pages.view;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.UI;
|
||||||
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
|
import com.vaadin.flow.component.combobox.ComboBox;
|
||||||
|
import com.vaadin.flow.component.dialog.Dialog;
|
||||||
|
import com.vaadin.flow.component.grid.Grid;
|
||||||
|
import com.vaadin.flow.component.html.H2;
|
||||||
|
import com.vaadin.flow.component.html.Main;
|
||||||
|
import com.vaadin.flow.component.html.Span;
|
||||||
|
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||||
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
|
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
|
import com.vaadin.flow.component.textfield.TextArea;
|
||||||
|
import com.vaadin.flow.component.textfield.TextField;
|
||||||
|
import com.vaadin.flow.data.renderer.ComponentRenderer;
|
||||||
|
import com.vaadin.flow.router.Menu;
|
||||||
|
import com.vaadin.flow.router.PageTitle;
|
||||||
|
import com.vaadin.flow.router.Route;
|
||||||
|
import de.assecutor.votianlt.dto.ClientMessageSummary;
|
||||||
|
import de.assecutor.votianlt.model.AppUser;
|
||||||
|
import de.assecutor.votianlt.model.Job;
|
||||||
|
import de.assecutor.votianlt.model.Message;
|
||||||
|
import de.assecutor.votianlt.model.MessageDirection;
|
||||||
|
import de.assecutor.votianlt.model.User;
|
||||||
|
import de.assecutor.votianlt.pages.service.AppUserService;
|
||||||
|
import de.assecutor.votianlt.repository.JobRepository;
|
||||||
|
import de.assecutor.votianlt.security.SecurityService;
|
||||||
|
import de.assecutor.votianlt.service.MessageService;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Route(value = "messages", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||||
|
@PageTitle("Nachrichten")
|
||||||
|
@Menu(order = 1, icon = "vaadin:envelope", title = "Nachrichten")
|
||||||
|
@RolesAllowed("USER")
|
||||||
|
@Slf4j
|
||||||
|
public class MessagesView extends Main {
|
||||||
|
|
||||||
|
private final MessageService messageService;
|
||||||
|
private final SecurityService securityService;
|
||||||
|
private final AppUserService appUserService;
|
||||||
|
private final JobRepository jobRepository;
|
||||||
|
|
||||||
|
private Grid<ClientMessageSummary> clientGrid;
|
||||||
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
|
||||||
|
|
||||||
|
public MessagesView(MessageService messageService, SecurityService securityService,
|
||||||
|
AppUserService appUserService, JobRepository jobRepository) {
|
||||||
|
this.messageService = messageService;
|
||||||
|
this.securityService = securityService;
|
||||||
|
this.appUserService = appUserService;
|
||||||
|
this.jobRepository = jobRepository;
|
||||||
|
|
||||||
|
// Create main layout
|
||||||
|
VerticalLayout layout = new VerticalLayout();
|
||||||
|
layout.setPadding(true);
|
||||||
|
layout.setSpacing(true);
|
||||||
|
layout.setWidthFull();
|
||||||
|
|
||||||
|
// Add title and action buttons
|
||||||
|
HorizontalLayout headerLayout = createHeaderLayout();
|
||||||
|
|
||||||
|
// Create client grid
|
||||||
|
clientGrid = createClientGrid();
|
||||||
|
|
||||||
|
// Add components to layout
|
||||||
|
layout.add(headerLayout, clientGrid);
|
||||||
|
|
||||||
|
// Add layout to main view
|
||||||
|
add(layout);
|
||||||
|
|
||||||
|
// Load client summaries
|
||||||
|
loadClientSummaries();
|
||||||
|
}
|
||||||
|
|
||||||
|
private HorizontalLayout createHeaderLayout() {
|
||||||
|
H2 title = new H2("Nachrichten nach Client");
|
||||||
|
|
||||||
|
Button sendMessageButton = new Button("Nachricht senden", VaadinIcon.ENVELOPE_O.create());
|
||||||
|
sendMessageButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
sendMessageButton.addClickListener(e -> openSendMessageDialog());
|
||||||
|
|
||||||
|
Button refreshButton = new Button("Aktualisieren", VaadinIcon.REFRESH.create());
|
||||||
|
refreshButton.addClickListener(e -> loadClientSummaries());
|
||||||
|
|
||||||
|
HorizontalLayout layout = new HorizontalLayout(title, sendMessageButton, refreshButton);
|
||||||
|
layout.setWidthFull();
|
||||||
|
layout.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER);
|
||||||
|
layout.expand(title);
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Grid<ClientMessageSummary> createClientGrid() {
|
||||||
|
Grid<ClientMessageSummary> grid = new Grid<>(ClientMessageSummary.class, false);
|
||||||
|
grid.setWidthFull();
|
||||||
|
grid.setHeight("600px");
|
||||||
|
|
||||||
|
// Add columns
|
||||||
|
grid.addColumn(new ComponentRenderer<>(summary -> {
|
||||||
|
Span span = new Span(summary.getUnreadCount() > 0 ? "●" : "○");
|
||||||
|
span.getStyle().set("font-size", "20px");
|
||||||
|
if (summary.getUnreadCount() > 0) {
|
||||||
|
span.getStyle().set("color", "var(--lumo-primary-color)");
|
||||||
|
}
|
||||||
|
return span;
|
||||||
|
})).setHeader("Status").setWidth("80px").setFlexGrow(0);
|
||||||
|
|
||||||
|
grid.addColumn(ClientMessageSummary::getClientName).setHeader("Client").setAutoWidth(true);
|
||||||
|
grid.addColumn(ClientMessageSummary::getClientEmail).setHeader("E-Mail").setAutoWidth(true);
|
||||||
|
|
||||||
|
grid.addColumn(new ComponentRenderer<>(summary -> {
|
||||||
|
Span span = new Span(String.valueOf(summary.getTotalMessages()));
|
||||||
|
return span;
|
||||||
|
})).setHeader("Nachrichten").setWidth("120px").setFlexGrow(0);
|
||||||
|
|
||||||
|
grid.addColumn(new ComponentRenderer<>(summary -> {
|
||||||
|
if (summary.getUnreadCount() > 0) {
|
||||||
|
Span span = new Span(String.valueOf(summary.getUnreadCount()));
|
||||||
|
span.getStyle().set("color", "var(--lumo-primary-color)");
|
||||||
|
span.getStyle().set("font-weight", "bold");
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
return new Span("0");
|
||||||
|
})).setHeader("Ungelesen").setWidth("100px").setFlexGrow(0);
|
||||||
|
|
||||||
|
grid.addColumn(summary ->
|
||||||
|
summary.getLastMessageDate() != null ? summary.getLastMessageDate().format(DATE_FORMATTER) : "-"
|
||||||
|
).setHeader("Letzte Nachricht").setAutoWidth(true);
|
||||||
|
|
||||||
|
grid.addColumn(new ComponentRenderer<>(summary -> {
|
||||||
|
String preview = summary.getLastMessagePreview();
|
||||||
|
if (preview != null && preview.length() > 50) {
|
||||||
|
preview = preview.substring(0, 47) + "...";
|
||||||
|
}
|
||||||
|
return new Span(preview != null ? preview : "-");
|
||||||
|
})).setHeader("Vorschau").setAutoWidth(true);
|
||||||
|
|
||||||
|
// Add click listener to navigate to UserMessagesView
|
||||||
|
grid.addItemClickListener(event -> {
|
||||||
|
ClientMessageSummary summary = event.getItem();
|
||||||
|
UI.getCurrent().navigate("user-messages/" + summary.getClientId());
|
||||||
|
});
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadClientSummaries() {
|
||||||
|
try {
|
||||||
|
// Generate test data for display
|
||||||
|
List<ClientMessageSummary> summaries = generateTestData();
|
||||||
|
clientGrid.setItems(summaries);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error loading client summaries: {}", e.getMessage(), e);
|
||||||
|
Notification.show("Fehler beim Laden der Nachrichten", 3000, Notification.Position.MIDDLE)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ClientMessageSummary> generateTestData() {
|
||||||
|
List<ClientMessageSummary> summaries = new ArrayList<>();
|
||||||
|
|
||||||
|
// Test client 1 - Max Mustermann with unread messages
|
||||||
|
summaries.add(new ClientMessageSummary(
|
||||||
|
"client001",
|
||||||
|
"Max Mustermann",
|
||||||
|
"max.mustermann@example.com",
|
||||||
|
15,
|
||||||
|
3,
|
||||||
|
java.time.LocalDateTime.now().minusHours(2),
|
||||||
|
"Hallo, ich habe eine Frage zu meinem letzten Auftrag..."
|
||||||
|
));
|
||||||
|
|
||||||
|
// Test client 2 - Anna Schmidt with no unread messages
|
||||||
|
summaries.add(new ClientMessageSummary(
|
||||||
|
"client002",
|
||||||
|
"Anna Schmidt",
|
||||||
|
"anna.schmidt@example.com",
|
||||||
|
8,
|
||||||
|
0,
|
||||||
|
java.time.LocalDateTime.now().minusDays(1),
|
||||||
|
"Vielen Dank für die schnelle Bearbeitung!"
|
||||||
|
));
|
||||||
|
|
||||||
|
// Test client 3 - Peter Weber with many unread messages
|
||||||
|
summaries.add(new ClientMessageSummary(
|
||||||
|
"client003",
|
||||||
|
"Peter Weber",
|
||||||
|
"peter.weber@example.com",
|
||||||
|
25,
|
||||||
|
7,
|
||||||
|
java.time.LocalDateTime.now().minusMinutes(30),
|
||||||
|
"Können Sie mir bitte den aktuellen Status mitteilen?"
|
||||||
|
));
|
||||||
|
|
||||||
|
// Test client 4 - Lisa Müller with recent message
|
||||||
|
summaries.add(new ClientMessageSummary(
|
||||||
|
"client004",
|
||||||
|
"Lisa Müller",
|
||||||
|
"lisa.mueller@example.com",
|
||||||
|
12,
|
||||||
|
1,
|
||||||
|
java.time.LocalDateTime.now().minusMinutes(5),
|
||||||
|
"Wann wird die Lieferung ankommen?"
|
||||||
|
));
|
||||||
|
|
||||||
|
// Test client 5 - Thomas Becker with older messages
|
||||||
|
summaries.add(new ClientMessageSummary(
|
||||||
|
"client005",
|
||||||
|
"Thomas Becker",
|
||||||
|
"thomas.becker@example.com",
|
||||||
|
20,
|
||||||
|
0,
|
||||||
|
java.time.LocalDateTime.now().minusDays(5),
|
||||||
|
"Alles erledigt, danke für die Zusammenarbeit."
|
||||||
|
));
|
||||||
|
|
||||||
|
// Test client 6 - Sarah Wagner with unread messages
|
||||||
|
summaries.add(new ClientMessageSummary(
|
||||||
|
"client006",
|
||||||
|
"Sarah Wagner",
|
||||||
|
"sarah.wagner@example.com",
|
||||||
|
6,
|
||||||
|
2,
|
||||||
|
java.time.LocalDateTime.now().minusHours(8),
|
||||||
|
"Gibt es Updates zum Auftrag #12345?"
|
||||||
|
));
|
||||||
|
|
||||||
|
return summaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ClientMessageSummary> aggregateMessagesByClient(String currentUsername) {
|
||||||
|
// Get all messages for current user (received and sent)
|
||||||
|
List<Message> receivedMessages = messageService.getMessagesForReceiver(currentUsername);
|
||||||
|
List<Message> sentMessages = messageService.getMessagesByDirection(MessageDirection.OUTGOING)
|
||||||
|
.stream()
|
||||||
|
.filter(m -> currentUsername.equals(m.getSender()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<Message> allMessages = new ArrayList<>();
|
||||||
|
allMessages.addAll(receivedMessages);
|
||||||
|
allMessages.addAll(sentMessages);
|
||||||
|
|
||||||
|
// Group messages by client (sender or receiver that is not the current user)
|
||||||
|
Map<String, List<Message>> messagesByClient = allMessages.stream()
|
||||||
|
.collect(Collectors.groupingBy(message -> {
|
||||||
|
// Determine the "other party" (client)
|
||||||
|
if (currentUsername.equals(message.getSender())) {
|
||||||
|
return message.getReceiver();
|
||||||
|
} else {
|
||||||
|
return message.getSender();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create summaries for each client
|
||||||
|
List<ClientMessageSummary> summaries = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, List<Message>> entry : messagesByClient.entrySet()) {
|
||||||
|
String clientEmail = entry.getKey();
|
||||||
|
List<Message> clientMessages = entry.getValue();
|
||||||
|
|
||||||
|
// Find client info
|
||||||
|
AppUser appUser = appUserService.findAll().stream()
|
||||||
|
.filter(u -> clientEmail.equals(u.getEmail()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
String clientName = appUser != null ?
|
||||||
|
appUser.getVorname() + " " + appUser.getNachname() : clientEmail;
|
||||||
|
String clientId = appUser != null ? appUser.getIdAsString() : clientEmail;
|
||||||
|
|
||||||
|
// Calculate statistics
|
||||||
|
int totalMessages = clientMessages.size();
|
||||||
|
long unreadCount = clientMessages.stream()
|
||||||
|
.filter(m -> !m.isRead() && currentUsername.equals(m.getReceiver()))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
// Get last message
|
||||||
|
Message lastMessage = clientMessages.stream()
|
||||||
|
.max(Comparator.comparing(Message::getCreatedAt))
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
java.time.LocalDateTime lastMessageDate = lastMessage != null ? lastMessage.getCreatedAt() : null;
|
||||||
|
String lastMessagePreview = lastMessage != null ? lastMessage.getContent() : null;
|
||||||
|
|
||||||
|
summaries.add(new ClientMessageSummary(
|
||||||
|
clientId,
|
||||||
|
clientName,
|
||||||
|
clientEmail,
|
||||||
|
totalMessages,
|
||||||
|
(int) unreadCount,
|
||||||
|
lastMessageDate,
|
||||||
|
lastMessagePreview
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by last message date (most recent first)
|
||||||
|
summaries.sort((s1, s2) -> {
|
||||||
|
if (s1.getLastMessageDate() == null) return 1;
|
||||||
|
if (s2.getLastMessageDate() == null) return -1;
|
||||||
|
return s2.getLastMessageDate().compareTo(s1.getLastMessageDate());
|
||||||
|
});
|
||||||
|
|
||||||
|
return summaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openSendMessageDialog() {
|
||||||
|
Dialog dialog = new Dialog();
|
||||||
|
dialog.setHeaderTitle("Neue Nachricht senden");
|
||||||
|
dialog.setWidth("600px");
|
||||||
|
|
||||||
|
VerticalLayout layout = new VerticalLayout();
|
||||||
|
layout.setPadding(false);
|
||||||
|
layout.setSpacing(true);
|
||||||
|
|
||||||
|
// Receiver selection
|
||||||
|
ComboBox<AppUser> receiverCombo = new ComboBox<>("Empfänger (App-Benutzer)");
|
||||||
|
receiverCombo.setWidthFull();
|
||||||
|
receiverCombo.setItems(appUserService.findAll());
|
||||||
|
receiverCombo.setItemLabelGenerator(appUser ->
|
||||||
|
appUser.getVorname() + " " + appUser.getNachname() + " (" + appUser.getEmail() + ")"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Job selection (optional)
|
||||||
|
ComboBox<Job> jobCombo = new ComboBox<>("Auftrag (optional)");
|
||||||
|
jobCombo.setWidthFull();
|
||||||
|
jobCombo.setItems(jobRepository.findAll());
|
||||||
|
jobCombo.setItemLabelGenerator(job ->
|
||||||
|
job.getJobNumber() + " - " + (job.getPickupCity() != null ? job.getPickupCity() : "")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Message content
|
||||||
|
TextArea contentArea = new TextArea("Nachricht");
|
||||||
|
contentArea.setWidthFull();
|
||||||
|
contentArea.setHeight("200px");
|
||||||
|
|
||||||
|
layout.add(receiverCombo, jobCombo, contentArea);
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
Button sendButton = new Button("Senden", e -> {
|
||||||
|
if (receiverCombo.getValue() == null || contentArea.getValue().isBlank()) {
|
||||||
|
Notification.show("Bitte Empfänger und Nachricht eingeben", 3000, Notification.Position.MIDDLE)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(receiverCombo.getValue(), jobCombo.getValue(), contentArea.getValue());
|
||||||
|
dialog.close();
|
||||||
|
});
|
||||||
|
sendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
|
||||||
|
Button cancelButton = new Button("Abbrechen", e -> dialog.close());
|
||||||
|
|
||||||
|
dialog.getFooter().add(cancelButton, sendButton);
|
||||||
|
dialog.add(layout);
|
||||||
|
dialog.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendMessage(AppUser receiver, Job job, String content) {
|
||||||
|
try {
|
||||||
|
User currentUser = securityService.getCurrentDatabaseUser();
|
||||||
|
if (currentUser == null) {
|
||||||
|
Notification.show("Benutzer nicht gefunden", 3000, Notification.Position.MIDDLE)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String sender = currentUser.getEmail();
|
||||||
|
String receiverUsername = receiver.getEmail(); // Using email as username for app users
|
||||||
|
|
||||||
|
if (job != null) {
|
||||||
|
messageService.sendJobMessageToClient(content, sender, receiverUsername,
|
||||||
|
job.getId(), job.getJobNumber());
|
||||||
|
} else {
|
||||||
|
messageService.sendGeneralMessageToClient(content, sender, receiverUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification.show("Nachricht erfolgreich gesendet", 2000, Notification.Position.BOTTOM_START)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||||
|
loadClientSummaries();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error sending message: {}", e.getMessage(), e);
|
||||||
|
Notification.show("Fehler beim Senden der Nachricht", 3000, Notification.Position.MIDDLE)
|
||||||
|
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
package de.assecutor.votianlt.pages.view;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.UI;
|
||||||
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
|
import com.vaadin.flow.component.html.Div;
|
||||||
|
import com.vaadin.flow.component.html.H2;
|
||||||
|
import com.vaadin.flow.component.html.H3;
|
||||||
|
import com.vaadin.flow.component.html.Main;
|
||||||
|
import com.vaadin.flow.component.html.Span;
|
||||||
|
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
|
import com.vaadin.flow.router.BeforeEvent;
|
||||||
|
import com.vaadin.flow.router.HasUrlParameter;
|
||||||
|
import com.vaadin.flow.router.PageTitle;
|
||||||
|
import com.vaadin.flow.router.Route;
|
||||||
|
import de.assecutor.votianlt.model.AppUser;
|
||||||
|
import de.assecutor.votianlt.pages.service.AppUserService;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
@Route(value = "user-messages", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||||
|
@PageTitle("Nachrichten")
|
||||||
|
@RolesAllowed("USER")
|
||||||
|
@Slf4j
|
||||||
|
public class UserMessagesView extends Main implements HasUrlParameter<String> {
|
||||||
|
|
||||||
|
private final AppUserService appUserService;
|
||||||
|
|
||||||
|
private String clientId;
|
||||||
|
private VerticalLayout contentLayout;
|
||||||
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
|
||||||
|
|
||||||
|
public UserMessagesView(AppUserService appUserService) {
|
||||||
|
this.appUserService = appUserService;
|
||||||
|
|
||||||
|
// Create main layout
|
||||||
|
contentLayout = new VerticalLayout();
|
||||||
|
contentLayout.setPadding(true);
|
||||||
|
contentLayout.setSpacing(true);
|
||||||
|
contentLayout.setWidthFull();
|
||||||
|
|
||||||
|
add(contentLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setParameter(BeforeEvent event, String parameter) {
|
||||||
|
this.clientId = parameter;
|
||||||
|
loadClientMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadClientMessages() {
|
||||||
|
contentLayout.removeAll();
|
||||||
|
|
||||||
|
// Get client info
|
||||||
|
AppUser client = null;
|
||||||
|
try {
|
||||||
|
ObjectId objectId = new ObjectId(clientId);
|
||||||
|
client = appUserService.findById(objectId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Could not find client with id: {}", clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
String clientName = client != null ?
|
||||||
|
client.getVorname() + " " + client.getNachname() : "Unbekannter Client";
|
||||||
|
|
||||||
|
// Create header
|
||||||
|
HorizontalLayout headerLayout = createHeaderLayout(clientName);
|
||||||
|
contentLayout.add(headerLayout);
|
||||||
|
|
||||||
|
// Create section for general messages (only one chat for general conversation)
|
||||||
|
VerticalLayout generalSection = createGeneralMessagesSection();
|
||||||
|
|
||||||
|
// Create section for job-related messages
|
||||||
|
VerticalLayout jobSection = createJobMessagesSection();
|
||||||
|
|
||||||
|
contentLayout.add(generalSection, jobSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HorizontalLayout createHeaderLayout(String clientName) {
|
||||||
|
Button backButton = new Button("Zurück", VaadinIcon.ARROW_LEFT.create());
|
||||||
|
backButton.addClickListener(e -> UI.getCurrent().navigate("messages"));
|
||||||
|
|
||||||
|
H2 title = new H2("Nachrichten mit " + clientName);
|
||||||
|
|
||||||
|
HorizontalLayout layout = new HorizontalLayout(backButton, title);
|
||||||
|
layout.setWidthFull();
|
||||||
|
layout.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER);
|
||||||
|
layout.setSpacing(true);
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VerticalLayout createGeneralMessagesSection() {
|
||||||
|
VerticalLayout section = new VerticalLayout();
|
||||||
|
section.setPadding(true);
|
||||||
|
section.setSpacing(true);
|
||||||
|
section.getStyle().set("border", "1px solid #e0e0e0");
|
||||||
|
section.getStyle().set("border-radius", "8px");
|
||||||
|
section.setWidthFull();
|
||||||
|
section.getStyle().set("margin-right", "20px");
|
||||||
|
|
||||||
|
H3 title = new H3("Allgemeine Nachrichten");
|
||||||
|
section.add(title);
|
||||||
|
|
||||||
|
// Test data - only one general chat conversation
|
||||||
|
section.add(createMessageCard(
|
||||||
|
"Allgemeine Unterhaltung",
|
||||||
|
"Hallo, wie geht es Ihnen?",
|
||||||
|
LocalDateTime.now().minusHours(2),
|
||||||
|
5,
|
||||||
|
2,
|
||||||
|
"general"
|
||||||
|
));
|
||||||
|
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VerticalLayout createJobMessagesSection() {
|
||||||
|
VerticalLayout section = new VerticalLayout();
|
||||||
|
section.setPadding(true);
|
||||||
|
section.setSpacing(true);
|
||||||
|
section.getStyle().set("border", "1px solid #e0e0e0");
|
||||||
|
section.getStyle().set("border-radius", "8px");
|
||||||
|
section.setWidthFull();
|
||||||
|
section.getStyle().set("margin-right", "20px");
|
||||||
|
|
||||||
|
H3 title = new H3("Nachrichten zu Aufträgen");
|
||||||
|
section.add(title);
|
||||||
|
|
||||||
|
// Test data - job-related messages
|
||||||
|
section.add(createMessageCard(
|
||||||
|
"Auftrag #12345",
|
||||||
|
"Die Lieferung ist angekommen.",
|
||||||
|
LocalDateTime.now().minusHours(5),
|
||||||
|
8,
|
||||||
|
1,
|
||||||
|
"job-12345"
|
||||||
|
));
|
||||||
|
|
||||||
|
section.add(createMessageCard(
|
||||||
|
"Auftrag #12344",
|
||||||
|
"Bitte um Rückruf bezüglich der Abholung.",
|
||||||
|
LocalDateTime.now().minusDays(2),
|
||||||
|
12,
|
||||||
|
3,
|
||||||
|
"job-12344"
|
||||||
|
));
|
||||||
|
|
||||||
|
section.add(createMessageCard(
|
||||||
|
"Auftrag #12343",
|
||||||
|
"Auftrag wurde erfolgreich abgeschlossen.",
|
||||||
|
LocalDateTime.now().minusDays(3),
|
||||||
|
6,
|
||||||
|
0,
|
||||||
|
"job-12343"
|
||||||
|
));
|
||||||
|
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Div createMessageCard(String conversationTitle, String lastMessagePreview,
|
||||||
|
LocalDateTime lastMessageTime, int messageCount,
|
||||||
|
int unreadCount, String conversationId) {
|
||||||
|
Div card = new Div();
|
||||||
|
card.setWidthFull();
|
||||||
|
card.getStyle().set("padding", "15px");
|
||||||
|
card.getStyle().set("border", "1px solid #e0e0e0");
|
||||||
|
card.getStyle().set("border-radius", "8px");
|
||||||
|
card.getStyle().set("cursor", "pointer");
|
||||||
|
card.getStyle().set("background-color", "#ffffff");
|
||||||
|
card.getStyle().set("margin-bottom", "10px");
|
||||||
|
card.getStyle().set("max-width", "97.5%");
|
||||||
|
card.addClassName("message-card");
|
||||||
|
|
||||||
|
// Hover effect
|
||||||
|
card.getElement().addEventListener("mouseenter", e -> {
|
||||||
|
card.getStyle().set("background-color", "#f5f5f5");
|
||||||
|
});
|
||||||
|
card.getElement().addEventListener("mouseleave", e -> {
|
||||||
|
card.getStyle().set("background-color", "#ffffff");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Title row with unread indicator
|
||||||
|
HorizontalLayout titleRow = new HorizontalLayout();
|
||||||
|
titleRow.setWidthFull();
|
||||||
|
titleRow.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER);
|
||||||
|
|
||||||
|
Span titleSpan = new Span(conversationTitle);
|
||||||
|
titleSpan.getStyle().set("font-weight", "bold");
|
||||||
|
titleSpan.getStyle().set("font-size", "16px");
|
||||||
|
|
||||||
|
if (unreadCount > 0) {
|
||||||
|
Span unreadBadge = new Span(String.valueOf(unreadCount));
|
||||||
|
unreadBadge.getStyle().set("background-color", "var(--lumo-primary-color)");
|
||||||
|
unreadBadge.getStyle().set("color", "white");
|
||||||
|
unreadBadge.getStyle().set("border-radius", "50%");
|
||||||
|
unreadBadge.getStyle().set("padding", "2px 8px");
|
||||||
|
unreadBadge.getStyle().set("font-size", "12px");
|
||||||
|
unreadBadge.getStyle().set("font-weight", "bold");
|
||||||
|
titleRow.add(titleSpan, unreadBadge);
|
||||||
|
} else {
|
||||||
|
titleRow.add(titleSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
titleRow.expand(titleSpan);
|
||||||
|
|
||||||
|
// Preview text
|
||||||
|
Span preview = new Span(lastMessagePreview);
|
||||||
|
preview.getStyle().set("color", "#666666");
|
||||||
|
preview.getStyle().set("font-size", "14px");
|
||||||
|
|
||||||
|
// Metadata row
|
||||||
|
HorizontalLayout metaRow = new HorizontalLayout();
|
||||||
|
metaRow.setWidthFull();
|
||||||
|
|
||||||
|
Span timeSpan = new Span(lastMessageTime.format(DATE_FORMATTER));
|
||||||
|
timeSpan.getStyle().set("color", "#999999");
|
||||||
|
timeSpan.getStyle().set("font-size", "12px");
|
||||||
|
|
||||||
|
Span countSpan = new Span(messageCount + " Nachrichten");
|
||||||
|
countSpan.getStyle().set("color", "#999999");
|
||||||
|
countSpan.getStyle().set("font-size", "12px");
|
||||||
|
|
||||||
|
metaRow.add(timeSpan, countSpan);
|
||||||
|
metaRow.expand(timeSpan);
|
||||||
|
|
||||||
|
// Add all elements to card
|
||||||
|
VerticalLayout cardContent = new VerticalLayout(titleRow, preview, metaRow);
|
||||||
|
cardContent.setWidthFull();
|
||||||
|
cardContent.setPadding(false);
|
||||||
|
cardContent.setSpacing(false);
|
||||||
|
card.add(cardContent);
|
||||||
|
|
||||||
|
// Click listener to navigate to message details
|
||||||
|
card.addClickListener(e -> {
|
||||||
|
UI.getCurrent().navigate("message-details/" + clientId + "/" + conversationId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package de.assecutor.votianlt.repository;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.Message;
|
||||||
|
import de.assecutor.votianlt.model.MessageDirection;
|
||||||
|
import de.assecutor.votianlt.model.MessageType;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface MessageRepository extends MongoRepository<Message, ObjectId> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all messages for a specific receiver, ordered by creation time descending (newest first)
|
||||||
|
*/
|
||||||
|
List<Message> findByReceiverOrderByCreatedAtDesc(String receiver);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all messages sent by a specific sender, ordered by creation time descending
|
||||||
|
*/
|
||||||
|
List<Message> findBySenderOrderByCreatedAtDesc(String sender);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all unread messages for a specific receiver
|
||||||
|
*/
|
||||||
|
List<Message> findByReceiverAndIsReadFalseOrderByCreatedAtDesc(String receiver);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all messages related to a specific job
|
||||||
|
*/
|
||||||
|
List<Message> findByJobIdOrderByCreatedAtDesc(ObjectId jobId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all messages of a specific type for a receiver
|
||||||
|
*/
|
||||||
|
List<Message> findByReceiverAndMessageTypeOrderByCreatedAtDesc(String receiver, MessageType messageType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all messages by direction (incoming/outgoing)
|
||||||
|
*/
|
||||||
|
List<Message> findByDirectionOrderByCreatedAtDesc(MessageDirection direction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all messages between two users (in both directions)
|
||||||
|
*/
|
||||||
|
List<Message> findBySenderAndReceiverOrderByCreatedAtAsc(String sender, String receiver);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all unread messages count for a specific receiver
|
||||||
|
*/
|
||||||
|
long countByReceiverAndIsReadFalse(String receiver);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all messages for a specific receiver and job
|
||||||
|
*/
|
||||||
|
List<Message> findByReceiverAndJobIdOrderByCreatedAtDesc(String receiver, ObjectId jobId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all messages (for admin/overview), ordered by creation time descending
|
||||||
|
*/
|
||||||
|
List<Message> findAllByOrderByCreatedAtDesc();
|
||||||
|
}
|
||||||
193
src/main/java/de/assecutor/votianlt/service/MessageService.java
Normal file
193
src/main/java/de/assecutor/votianlt/service/MessageService.java
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package de.assecutor.votianlt.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import de.assecutor.votianlt.model.Message;
|
||||||
|
import de.assecutor.votianlt.model.MessageDirection;
|
||||||
|
import de.assecutor.votianlt.model.MessageType;
|
||||||
|
import de.assecutor.votianlt.mqtt.MqttPublisher;
|
||||||
|
import de.assecutor.votianlt.repository.MessageRepository;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class MessageService {
|
||||||
|
|
||||||
|
private final MessageRepository messageRepository;
|
||||||
|
private final MqttPublisher mqttPublisher;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public MessageService(MessageRepository messageRepository, MqttPublisher mqttPublisher) {
|
||||||
|
this.messageRepository = messageRepository;
|
||||||
|
this.mqttPublisher = mqttPublisher;
|
||||||
|
this.objectMapper = new ObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a message to the database
|
||||||
|
*/
|
||||||
|
public Message saveMessage(Message message) {
|
||||||
|
log.info("Saving message from {} to {}", message.getSender(), message.getReceiver());
|
||||||
|
return messageRepository.save(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a general message to a client via MQTT
|
||||||
|
*/
|
||||||
|
public Message sendGeneralMessageToClient(String content, String sender, String receiver) {
|
||||||
|
Message message = new Message(content, sender, receiver, MessageDirection.OUTGOING);
|
||||||
|
message = saveMessage(message);
|
||||||
|
publishMessageToMqtt(message, receiver);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a job-related message to a client via MQTT
|
||||||
|
*/
|
||||||
|
public Message sendJobMessageToClient(String content, String sender, String receiver,
|
||||||
|
ObjectId jobId, String jobNumber) {
|
||||||
|
Message message = new Message(content, sender, receiver, MessageDirection.OUTGOING, jobId, jobNumber);
|
||||||
|
message = saveMessage(message);
|
||||||
|
publishMessageToMqtt(message, receiver);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle incoming message from a client
|
||||||
|
*/
|
||||||
|
public Message receiveMessageFromClient(String content, String sender, String receiver,
|
||||||
|
ObjectId jobId, String jobNumber) {
|
||||||
|
Message message;
|
||||||
|
if (jobId != null) {
|
||||||
|
message = new Message(content, sender, receiver, MessageDirection.INCOMING, jobId, jobNumber);
|
||||||
|
} else {
|
||||||
|
message = new Message(content, sender, receiver, MessageDirection.INCOMING);
|
||||||
|
}
|
||||||
|
return saveMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish message to MQTT topic for the receiver
|
||||||
|
*/
|
||||||
|
private void publishMessageToMqtt(Message message, String receiver) {
|
||||||
|
try {
|
||||||
|
String topic = "/client/" + receiver + "/message";
|
||||||
|
Map<String, Object> payload = new HashMap<>();
|
||||||
|
payload.put("messageId", message.getIdAsString());
|
||||||
|
payload.put("content", message.getContent());
|
||||||
|
payload.put("sender", message.getSender());
|
||||||
|
payload.put("messageType", message.getMessageType().toString());
|
||||||
|
payload.put("createdAt", message.getCreatedAt().toString());
|
||||||
|
|
||||||
|
if (message.getJobId() != null) {
|
||||||
|
payload.put("jobId", message.getJobIdAsString());
|
||||||
|
payload.put("jobNumber", message.getJobNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
mqttPublisher.publishAsJson(topic, payload, false);
|
||||||
|
log.info("Published message to MQTT topic: {}", topic);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error publishing message to MQTT: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all messages for a specific receiver
|
||||||
|
*/
|
||||||
|
public List<Message> getMessagesForReceiver(String receiver) {
|
||||||
|
return messageRepository.findByReceiverOrderByCreatedAtDesc(receiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all unread messages for a specific receiver
|
||||||
|
*/
|
||||||
|
public List<Message> getUnreadMessagesForReceiver(String receiver) {
|
||||||
|
return messageRepository.findByReceiverAndIsReadFalseOrderByCreatedAtDesc(receiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all messages related to a specific job
|
||||||
|
*/
|
||||||
|
public List<Message> getMessagesForJob(ObjectId jobId) {
|
||||||
|
return messageRepository.findByJobIdOrderByCreatedAtDesc(jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all messages for a receiver and specific job
|
||||||
|
*/
|
||||||
|
public List<Message> getMessagesForReceiverAndJob(String receiver, ObjectId jobId) {
|
||||||
|
return messageRepository.findByReceiverAndJobIdOrderByCreatedAtDesc(receiver, jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all general messages for a receiver
|
||||||
|
*/
|
||||||
|
public List<Message> getGeneralMessagesForReceiver(String receiver) {
|
||||||
|
return messageRepository.findByReceiverAndMessageTypeOrderByCreatedAtDesc(receiver, MessageType.GENERAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all job-related messages for a receiver
|
||||||
|
*/
|
||||||
|
public List<Message> getJobRelatedMessagesForReceiver(String receiver) {
|
||||||
|
return messageRepository.findByReceiverAndMessageTypeOrderByCreatedAtDesc(receiver, MessageType.JOB_RELATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all messages (for admin/overview)
|
||||||
|
*/
|
||||||
|
public List<Message> getAllMessages() {
|
||||||
|
return messageRepository.findAllByOrderByCreatedAtDesc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get messages by direction
|
||||||
|
*/
|
||||||
|
public List<Message> getMessagesByDirection(MessageDirection direction) {
|
||||||
|
return messageRepository.findByDirectionOrderByCreatedAtDesc(direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a message as read
|
||||||
|
*/
|
||||||
|
public void markAsRead(ObjectId messageId) {
|
||||||
|
Optional<Message> messageOpt = messageRepository.findById(messageId);
|
||||||
|
if (messageOpt.isPresent()) {
|
||||||
|
Message message = messageOpt.get();
|
||||||
|
message.markAsRead();
|
||||||
|
messageRepository.save(message);
|
||||||
|
log.info("Marked message {} as read", messageId);
|
||||||
|
} else {
|
||||||
|
log.warn("Message {} not found", messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get unread message count for a receiver
|
||||||
|
*/
|
||||||
|
public long getUnreadMessageCount(String receiver) {
|
||||||
|
return messageRepository.countByReceiverAndIsReadFalse(receiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a message by ID
|
||||||
|
*/
|
||||||
|
public Optional<Message> getMessageById(ObjectId messageId) {
|
||||||
|
return messageRepository.findById(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a message
|
||||||
|
*/
|
||||||
|
public void deleteMessage(ObjectId messageId) {
|
||||||
|
messageRepository.deleteById(messageId);
|
||||||
|
log.info("Deleted message {}", messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user