diff --git a/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java b/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java index 2ffdeab4..cecedfbd 100644 --- a/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java @@ -2,6 +2,10 @@ package com.petshop.backend.dto.chat; public class MessageRequest { private String content; + private String attachmentUrl; + private String attachmentName; + private String attachmentMimeType; + private Long attachmentSizeBytes; public MessageRequest() { } @@ -13,4 +17,36 @@ public class MessageRequest { public void setContent(String content) { this.content = content; } + + public String getAttachmentUrl() { + return attachmentUrl; + } + + public void setAttachmentUrl(String attachmentUrl) { + this.attachmentUrl = attachmentUrl; + } + + public String getAttachmentName() { + return attachmentName; + } + + public void setAttachmentName(String attachmentName) { + this.attachmentName = attachmentName; + } + + public String getAttachmentMimeType() { + return attachmentMimeType; + } + + public void setAttachmentMimeType(String attachmentMimeType) { + this.attachmentMimeType = attachmentMimeType; + } + + public Long getAttachmentSizeBytes() { + return attachmentSizeBytes; + } + + public void setAttachmentSizeBytes(Long attachmentSizeBytes) { + this.attachmentSizeBytes = attachmentSizeBytes; + } } diff --git a/backend/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java b/backend/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java index 5bdaeb01..ce0d413b 100644 --- a/backend/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java @@ -12,6 +12,10 @@ public class MessageResponse { private String content; private LocalDateTime timestamp; private Boolean isRead; + private String attachmentUrl; + private String attachmentName; + private String attachmentMimeType; + private Long attachmentSizeBytes; public MessageResponse() { } @@ -34,6 +38,15 @@ public class MessageResponse { response.setContent(message.getContent()); response.setTimestamp(message.getTimestamp()); response.setIsRead(message.getIsRead()); + + + if (message.getAttachmentUrl() != null) { + response.setAttachmentUrl("/api/v1/chat/messages/" + message.getId() + "/attachment"); + } + + response.setAttachmentName(message.getAttachmentName()); + response.setAttachmentMimeType(message.getAttachmentMimeType()); + response.setAttachmentSizeBytes(message.getAttachmentSizeBytes()); return response; } @@ -92,4 +105,36 @@ public class MessageResponse { public void setIsRead(Boolean isRead) { this.isRead = isRead; } + + public String getAttachmentUrl() { + return attachmentUrl; + } + + public void setAttachmentUrl(String attachmentUrl) { + this.attachmentUrl = attachmentUrl; + } + + public String getAttachmentName() { + return attachmentName; + } + + public void setAttachmentName(String attachmentName) { + this.attachmentName = attachmentName; + } + + public String getAttachmentMimeType() { + return attachmentMimeType; + } + + public void setAttachmentMimeType(String attachmentMimeType) { + this.attachmentMimeType = attachmentMimeType; + } + + public Long getAttachmentSizeBytes() { + return attachmentSizeBytes; + } + + public void setAttachmentSizeBytes(Long attachmentSizeBytes) { + this.attachmentSizeBytes = attachmentSizeBytes; + } } diff --git a/backend/src/main/java/com/petshop/backend/entity/Message.java b/backend/src/main/java/com/petshop/backend/entity/Message.java index ca0f15d4..7c7bc498 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Message.java +++ b/backend/src/main/java/com/petshop/backend/entity/Message.java @@ -22,6 +22,17 @@ public class Message { @Column(columnDefinition = "TEXT") private String content; + @Column(length = 255) + private String attachmentUrl; + + @Column(length = 255) + private String attachmentName; + + @Column(length = 100) + private String attachmentMimeType; + + private Long attachmentSizeBytes; + @CreationTimestamp @Column(nullable = false, updatable = false) private LocalDateTime timestamp; @@ -88,4 +99,36 @@ public class Message { public void setIsRead(Boolean isRead) { this.isRead = isRead; } + + public String getAttachmentUrl() { + return attachmentUrl; + } + + public void setAttachmentUrl(String attachmentUrl) { + this.attachmentUrl = attachmentUrl; + } + + public String getAttachmentName() { + return attachmentName; + } + + public void setAttachmentName(String attachmentName) { + this.attachmentName = attachmentName; + } + + public String getAttachmentMimeType() { + return attachmentMimeType; + } + + public void setAttachmentMimeType(String attachmentMimeType) { + this.attachmentMimeType = attachmentMimeType; + } + + public Long getAttachmentSizeBytes() { + return attachmentSizeBytes; + } + + public void setAttachmentSizeBytes(Long attachmentSizeBytes) { + this.attachmentSizeBytes = attachmentSizeBytes; + } } diff --git a/backend/src/main/java/com/petshop/backend/service/ChatService.java b/backend/src/main/java/com/petshop/backend/service/ChatService.java index 723765e4..41d2dfd6 100644 --- a/backend/src/main/java/com/petshop/backend/service/ChatService.java +++ b/backend/src/main/java/com/petshop/backend/service/ChatService.java @@ -15,7 +15,9 @@ import com.petshop.backend.repository.UserRepository; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -27,15 +29,18 @@ public class ChatService { private final MessageRepository messageRepository; private final UserRepository userRepository; private final AvatarStorageService avatarStorageService; + private final ChatAttachmentStorageService attachmentStorageService; public ChatService(ConversationRepository conversationRepository, MessageRepository messageRepository, UserRepository userRepository, - AvatarStorageService avatarStorageService) { + AvatarStorageService avatarStorageService, + ChatAttachmentStorageService attachmentStorageService) { this.conversationRepository = conversationRepository; this.messageRepository = messageRepository; this.userRepository = userRepository; this.avatarStorageService = avatarStorageService; + this.attachmentStorageService = attachmentStorageService; } @Transactional @@ -104,8 +109,8 @@ public class ChatService { List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1); String lastMessage = last != null && last.getContent() != null ? last.getContent() : ""; - Long lastSenderId = last != null ? last.getSenderId() : null; + Long lastSenderId = last != null ? last.getSenderId() : null; return ConversationResponse.fromEntity(conversation, lastMessage, lastSenderId); } @@ -131,6 +136,10 @@ public class ChatService { message.setConversationId(conversationId); message.setSenderId(userId); message.setContent(request.getContent()); + message.setAttachmentUrl(request.getAttachmentUrl()); + message.setAttachmentName(request.getAttachmentName()); + message.setAttachmentMimeType(request.getAttachmentMimeType()); + message.setAttachmentSizeBytes(request.getAttachmentSizeBytes()); message.setIsRead(false); message = messageRepository.save(message); @@ -146,6 +155,52 @@ public class ChatService { return toMessageResponse(message); } + @Transactional + public MessageResponse sendMessageWithAttachment(Long conversationId, Long userId, User.Role role, MultipartFile file, String content) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + + if (conversation.getStatus() == Conversation.ConversationStatus.CLOSED) { + throw new AccessDeniedException("Conversation is closed"); + } + + if (!hasConversationAccess(conversation, userId, role)) { + if (role == User.Role.CUSTOMER) { + throw new AccessDeniedException("You can only send messages to your own conversations"); + } + if (role == User.Role.STAFF) { + throw new AccessDeniedException("You can only reply to conversations assigned to you or unassigned conversations"); + } + } + + try { + String attachmentUrl = attachmentStorageService.storeAttachment(file); + Message message = new Message(); + message.setConversationId(conversationId); + message.setSenderId(userId); + message.setContent(content); + message.setAttachmentUrl(attachmentUrl); + message.setAttachmentName(file.getOriginalFilename()); + message.setAttachmentMimeType(file.getContentType()); + message.setAttachmentSizeBytes(file.getSize()); + message.setIsRead(false); + message = messageRepository.save(message); + + if (role == User.Role.STAFF && conversation.getStaffId() == null) { + conversation.setStaffId(userId); + } + + if (role == User.Role.STAFF) { + conversation.setMode(Conversation.ConversationMode.HUMAN); + conversationRepository.save(conversation); + } + + return toMessageResponse(message); + } catch (IOException e) { + throw new RuntimeException("Failed to store attachment", e); + } + } + @Transactional public ConversationResponse requestHumanTakeover(Long conversationId, Long userId, User.Role role) { Conversation conversation = conversationRepository.findById(conversationId)