diff --git a/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java b/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java index 7433b38..68bc9a3 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java @@ -29,6 +29,7 @@ import com.vaadin.flow.router.Route; import com.vaadin.flow.router.RouteParameters; import com.vaadin.flow.shared.Registration; import de.assecutor.votianlt.model.AppUser; +import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.model.Message; import de.assecutor.votianlt.model.MessageContentType; import de.assecutor.votianlt.model.MessageOrigin; @@ -154,6 +155,8 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver { this.jobNumberContext = filteredMessages.stream().map(Message::getJobNumber) .filter(value -> value != null && !value.isBlank()).findFirst().orElse(null); + ensureJobContextForConversation(filteredMessages); + String conversationTitle = resolveConversationTitle(filteredMessages, conversationId); HorizontalLayout headerLayout = createHeaderLayout(clientName, conversationTitle); @@ -763,6 +766,8 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver { String sender = Optional.ofNullable(securityService.getCurrentUsername()).filter(name -> !name.isBlank()) .orElse("System"); + ensureJobContextForConversation(currentMessages); + try { Message saved; if (jobConversation) { @@ -843,6 +848,60 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver { return value.replaceAll("[^a-zA-Z0-9_-]", "_").toLowerCase(); } + private void ensureJobContextForConversation(List messages) { + if (!jobConversation) { + jobIdContext = null; + jobNumberContext = null; + return; + } + + String token = extractJobConversationToken(); + if (token == null || token.isBlank()) { + return; + } + + if (jobIdContext == null && ObjectId.isValid(token)) { + try { + jobIdContext = new ObjectId(token); + } catch (IllegalArgumentException ex) { + log.debug("Conversation token {} could not be converted to ObjectId", token, ex); + } + } + + if (messages != null && (jobNumberContext == null || jobNumberContext.isBlank())) { + jobNumberContext = messages.stream() + .map(Message::getJobNumber) + .filter(value -> value != null && !value.isBlank()) + .findFirst() + .orElse(jobNumberContext); + } + + if (jobIdContext == null || jobNumberContext == null || jobNumberContext.isBlank()) { + Optional jobOptional = messageService.findJobByIdentifier(token); + if (jobOptional.isEmpty() && jobNumberContext != null && !jobNumberContext.isBlank()) { + jobOptional = messageService.findJobByIdentifier(jobNumberContext); + } + if (jobOptional.isPresent()) { + Job job = jobOptional.get(); + jobIdContext = Optional.ofNullable(job.getId()).orElse(jobIdContext); + jobNumberContext = Optional.ofNullable(job.getJobNumber()) + .filter(value -> !value.isBlank()) + .orElse(jobNumberContext); + } + } + + if (jobNumberContext == null || jobNumberContext.isBlank()) { + jobNumberContext = token; + } + } + + private String extractJobConversationToken() { + if (!jobConversation || conversationId == null || !conversationId.startsWith("job-")) { + return null; + } + return conversationId.length() > 4 ? conversationId.substring(4) : ""; + } + private String resolveConversationTitle(List messages, String conversationId) { if (conversationId == null) { return "Konversation"; @@ -946,7 +1005,9 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver { if (currentMessages != null) { // Add new message to the list currentMessages.add(message); - + + ensureJobContextForConversation(currentMessages); + // Re-render all messages with the new message included renderMessages(); diff --git a/src/main/java/de/assecutor/votianlt/service/MessageService.java b/src/main/java/de/assecutor/votianlt/service/MessageService.java index 9706ba6..7847db8 100644 --- a/src/main/java/de/assecutor/votianlt/service/MessageService.java +++ b/src/main/java/de/assecutor/votianlt/service/MessageService.java @@ -1,5 +1,6 @@ package de.assecutor.votianlt.service; +import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.model.Message; import de.assecutor.votianlt.model.MessageContentType; import de.assecutor.votianlt.model.MessageOrigin; @@ -8,6 +9,7 @@ import de.assecutor.votianlt.dto.ChatMessageInboundPayload; import de.assecutor.votianlt.dto.ChatMessageOutboundPayload; import de.assecutor.votianlt.event.MessageReceivedEvent; import de.assecutor.votianlt.mqtt.MqttPublisher; +import de.assecutor.votianlt.repository.JobRepository; import de.assecutor.votianlt.repository.MessageRepository; import lombok.extern.slf4j.Slf4j; import org.bson.types.ObjectId; @@ -23,12 +25,14 @@ import java.util.Optional; public class MessageService { private final MessageRepository messageRepository; + private final JobRepository jobRepository; private final MqttPublisher mqttPublisher; private final ApplicationEventPublisher eventPublisher; - public MessageService(MessageRepository messageRepository, MqttPublisher mqttPublisher, - ApplicationEventPublisher eventPublisher) { + public MessageService(MessageRepository messageRepository, JobRepository jobRepository, + MqttPublisher mqttPublisher, ApplicationEventPublisher eventPublisher) { this.messageRepository = messageRepository; + this.jobRepository = jobRepository; this.mqttPublisher = mqttPublisher; this.eventPublisher = eventPublisher; } @@ -66,7 +70,9 @@ public class MessageService { public Message sendJobMessageToClient(String content, String sender, String receiver, MessageContentType contentType, ObjectId jobId, String jobNumber) { - Message message = new Message(content, sender, receiver, MessageOrigin.SERVER, contentType, jobId, jobNumber); + JobContext context = resolveJobContext(jobId, jobNumber); + Message message = new Message(content, sender, receiver, MessageOrigin.SERVER, contentType, + context.jobId(), context.jobNumber()); message = saveMessage(message); publishMessageToMqtt(message, receiver); return message; @@ -79,8 +85,9 @@ public class MessageService { Message message; MessageContentType contentType = payload.contentType(); if (payload.hasJobContext()) { + JobContext context = resolveJobContext(payload.jobId(), payload.jobNumber()); message = new Message(payload.content(), payload.sender(), payload.receiver(), - MessageOrigin.CLIENT, contentType, payload.jobId(), payload.jobNumber()); + MessageOrigin.CLIENT, contentType, context.jobId(), context.jobNumber()); } else { message = new Message(payload.content(), payload.sender(), payload.receiver(), MessageOrigin.CLIENT, contentType); @@ -222,4 +229,102 @@ public class MessageService { messageRepository.deleteById(messageId); log.info("Deleted message {}", messageId); } + + public Optional findJobByIdentifier(String identifier) { + if (identifier == null || identifier.isBlank()) { + return Optional.empty(); + } + + Optional jobOptional = Optional.empty(); + if (ObjectId.isValid(identifier)) { + try { + jobOptional = jobRepository.findById(new ObjectId(identifier)); + } catch (IllegalArgumentException ex) { + log.debug("Identifier {} could not be parsed as ObjectId: {}", identifier, ex.getMessage()); + } + } + + if (jobOptional.isPresent()) { + return jobOptional; + } + + return lookupJobByNumber(identifier); + } + + private JobContext resolveJobContext(ObjectId jobId, String jobNumber) { + ObjectId resolvedJobId = jobId; + String resolvedJobNumber = jobNumber; + + Optional jobOptional = lookupJob(jobId, jobNumber); + if (jobOptional.isPresent()) { + Job job = jobOptional.get(); + resolvedJobId = job.getId(); + resolvedJobNumber = Optional.ofNullable(job.getJobNumber()).orElse(resolvedJobNumber); + } + + return new JobContext(resolvedJobId, resolvedJobNumber); + } + + private Optional lookupJob(ObjectId jobId, String jobNumber) { + Optional jobOptional = Optional.empty(); + + if (jobId != null) { + jobOptional = jobRepository.findById(jobId); + if (jobOptional.isPresent()) { + return jobOptional; + } + } + + if (jobNumber == null || jobNumber.isBlank()) { + return jobOptional; + } + + jobOptional = jobRepository.findByJobNumber(jobNumber); + if (jobOptional.isPresent()) { + return jobOptional; + } + + return lookupJobByNumber(jobNumber); + } + + private Optional lookupJobByNumber(String candidate) { + if (candidate == null || candidate.isBlank()) { + return Optional.empty(); + } + + String normalizedCandidate = normalizeJobToken(candidate); + + Optional jobOptional = jobRepository.findByJobNumber(candidate); + if (jobOptional.isPresent()) { + return jobOptional; + } + + String relaxedCandidate = candidate.replace('_', ' '); + if (!relaxedCandidate.equals(candidate)) { + jobOptional = jobRepository.findByJobNumber(relaxedCandidate); + if (jobOptional.isPresent()) { + return jobOptional; + } + } + + List fuzzyMatches = jobRepository.findByJobNumberContainingIgnoreCase(relaxedCandidate); + if (fuzzyMatches.isEmpty() && !relaxedCandidate.equals(candidate)) { + fuzzyMatches = jobRepository.findByJobNumberContainingIgnoreCase(candidate); + } + if (fuzzyMatches.isEmpty()) { + fuzzyMatches = jobRepository.findAll(); + } + + return fuzzyMatches.stream() + .filter(job -> normalizeJobToken(job.getJobNumber()).equalsIgnoreCase(normalizedCandidate)) + .findFirst(); + } + + private String normalizeJobToken(String value) { + return value == null ? "" : value.replaceAll("[^a-zA-Z0-9_-]", "_"); + } + + private record JobContext(ObjectId jobId, String jobNumber) { + } + }