From dcf41675e43241800280eda46a3614c557f02ee7 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 14 Apr 2026 19:59:39 -0600 Subject: [PATCH 1/2] fix backend issues --- .../com/petshop/backend/controller/RefundController.java | 4 ++-- .../com/petshop/backend/controller/SaleController.java | 2 +- .../backend/exception/GlobalExceptionHandler.java | 3 ++- .../petshop/backend/repository/ProductRepository.java | 4 ++++ .../java/com/petshop/backend/service/ProductService.java | 9 +++++++++ backend/src/main/resources/application.yml | 2 ++ 6 files changed, 20 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/RefundController.java b/backend/src/main/java/com/petshop/backend/controller/RefundController.java index 0001df1e..92fd22b3 100644 --- a/backend/src/main/java/com/petshop/backend/controller/RefundController.java +++ b/backend/src/main/java/com/petshop/backend/controller/RefundController.java @@ -30,7 +30,7 @@ public class RefundController { } @PostMapping - @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF')") public ResponseEntity createRefund(@Valid @RequestBody RefundRequest request) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String role = authentication.getAuthorities().stream() @@ -85,7 +85,7 @@ public class RefundController { } @PutMapping("/{id}") - @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") + @PreAuthorize("hasRole('STAFF')") public ResponseEntity updateRefund(@PathVariable Long id, @Valid @RequestBody RefundUpdateRequest request) { return ResponseEntity.ok(refundService.updateRefundStatus(id, request.getStatus())); } diff --git a/backend/src/main/java/com/petshop/backend/controller/SaleController.java b/backend/src/main/java/com/petshop/backend/controller/SaleController.java index dffa63e4..cdb64c78 100644 --- a/backend/src/main/java/com/petshop/backend/controller/SaleController.java +++ b/backend/src/main/java/com/petshop/backend/controller/SaleController.java @@ -40,7 +40,7 @@ public class SaleController { } @PostMapping - @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") + @PreAuthorize("hasRole('STAFF')") public ResponseEntity createSale(@Valid @RequestBody SaleRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(saleService.createSale(request)); } diff --git a/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java index 1f9434a5..7c2b475f 100644 --- a/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java @@ -42,9 +42,10 @@ public class GlobalExceptionHandler { errors.put(fieldName, errorMessage); }); + String firstMessage = errors.values().stream().findFirst().orElse("Validation failed"); Map response = new HashMap<>(); response.put("status", HttpStatus.BAD_REQUEST.value()); - response.put("message", "Validation failed"); + response.put("message", firstMessage); response.put("errors", errors); response.put("details", buildDetails(ex)); response.put("path", request.getRequestURI()); diff --git a/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java b/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java index 7122e6ea..140031d0 100644 --- a/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java @@ -11,6 +11,10 @@ import org.springframework.stereotype.Repository; @Repository public interface ProductRepository extends JpaRepository { + boolean existsByProdNameIgnoreCase(String prodName); + + boolean existsByProdNameIgnoreCaseAndProdIdNot(String prodName, Long prodId); + @Query("SELECT p FROM Product p WHERE " + "(:q IS NULL OR LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.prodDesc, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + "(:categoryId IS NULL OR p.category.categoryId = :categoryId)") diff --git a/backend/src/main/java/com/petshop/backend/service/ProductService.java b/backend/src/main/java/com/petshop/backend/service/ProductService.java index d18890d5..38f30e84 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductService.java @@ -5,6 +5,7 @@ import com.petshop.backend.dto.product.ProductRequest; import com.petshop.backend.dto.product.ProductResponse; import com.petshop.backend.entity.Category; import com.petshop.backend.entity.Product; +import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.CategoryRepository; import com.petshop.backend.repository.ProductRepository; @@ -45,6 +46,10 @@ public class ProductService { @Transactional public ProductResponse createProduct(ProductRequest request) { + if (productRepository.existsByProdNameIgnoreCase(request.getProdName())) { + throw new BusinessException("A product with this name already exists"); + } + Category category = categoryRepository.findById(request.getCategoryId()) .orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + request.getCategoryId())); @@ -63,6 +68,10 @@ public class ProductService { Product product = productRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + id)); + if (productRepository.existsByProdNameIgnoreCaseAndProdIdNot(request.getProdName(), id)) { + throw new BusinessException("A product with this name already exists"); + } + Category category = categoryRepository.findById(request.getCategoryId()) .orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + request.getCategoryId())); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 4f424b93..f65764c0 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -68,6 +68,8 @@ openrouter: model: ${OPENROUTER_MODEL:openrouter/free} logging: + file: + name: ${LOG_FILE:log.txt} level: com.petshop: ${LOG_LEVEL:INFO} org.springframework.security: ${LOG_LEVEL_SECURITY:WARN} From 52a3b2cd3b08ab15e5d88d91211c93c723d3cce0 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 14 Apr 2026 19:59:42 -0600 Subject: [PATCH 2/2] add desktop chat notifications --- .../petshopdesktop/PetShopApplication.java | 5 ++ .../api/ChatRealtimeClient.java | 19 +++++- .../controllers/ChatController.java | 3 + .../util/DesktopNotificationService.java | 63 +++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 desktop/src/main/java/org/example/petshopdesktop/util/DesktopNotificationService.java diff --git a/desktop/src/main/java/org/example/petshopdesktop/PetShopApplication.java b/desktop/src/main/java/org/example/petshopdesktop/PetShopApplication.java index 21d7d9ef..18296405 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/PetShopApplication.java +++ b/desktop/src/main/java/org/example/petshopdesktop/PetShopApplication.java @@ -5,6 +5,7 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.stage.Stage; +import org.example.petshopdesktop.util.DesktopNotificationService; import java.io.IOException; import java.util.Objects; @@ -26,5 +27,9 @@ public class PetShopApplication extends Application { stage.setScene(scene); stage.show(); + + DesktopNotificationService.getInstance().init( + stage.getIcons().isEmpty() ? null : stage.getIcons().get(0) + ); } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java b/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java index 0462fe6b..d29d3aab 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java @@ -4,6 +4,7 @@ import org.example.petshopdesktop.api.dto.chat.ConversationResponse; import org.example.petshopdesktop.api.dto.chat.MessageRequest; import org.example.petshopdesktop.api.dto.chat.MessageResponse; import org.example.petshopdesktop.auth.UserSession; +import org.example.petshopdesktop.util.DesktopNotificationService; import java.net.URI; import java.net.http.HttpClient; @@ -72,6 +73,16 @@ public class ChatRealtimeClient implements WebSocket.Listener { updateNotificationState(); } + public void markConversationReplied(Long conversationId, Long senderId) { + synchronized (lock) { + ConversationResponse conv = globalConversations.get(conversationId); + if (conv != null) { + conv.setLastSenderId(senderId); + } + } + updateNotificationState(); + } + public boolean hasActionableChats() { synchronized (lock) { UserSession session = UserSession.getInstance(); @@ -333,7 +344,13 @@ public class ChatRealtimeClient implements WebSocket.Listener { if (messageListener != null) { messageListener.accept(message); } - + + Long currentUserId = UserSession.getInstance().getUserId(); + if (message.getSenderId() != null && !message.getSenderId().equals(currentUserId)) { + DesktopNotificationService.getInstance() + .notifyNewMessage(message.getSenderDisplayName(), message.getContent()); + } + // Also update globalConversation last sender if this is the active conversation synchronized (lock) { ConversationResponse conv = globalConversations.get(message.getConversationId()); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java index cd9c9c29..6d1082fd 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java @@ -196,6 +196,7 @@ public class ChatController { MessageResponse response = ChatApi.getInstance().sendMessage(convId, request); Platform.runLater(() -> { btnSend.setDisable(false); + realtimeClient.markConversationReplied(convId, UserSession.getInstance().getUserId()); appendMessageIfSelected(response); if (selectedAttachmentFile != null) { clearLocalAttachment(); @@ -380,6 +381,7 @@ public class ChatController { List response = ChatApi.getInstance().listConversations(); response.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder()))); Platform.runLater(() -> { + realtimeClient.initializeState(response); conversations.setAll(response); refreshSections(); restoreSelection(); @@ -471,6 +473,7 @@ public class ChatController { .findFirst() .ifPresent(conversation -> { conversation.setLastMessage(message.getContent()); + conversation.setLastSenderId(message.getSenderId()); conversation.setUpdatedAt(message.getTimestamp()); conversations.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder()))); refreshSections(); diff --git a/desktop/src/main/java/org/example/petshopdesktop/util/DesktopNotificationService.java b/desktop/src/main/java/org/example/petshopdesktop/util/DesktopNotificationService.java new file mode 100644 index 00000000..4f9da43f --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/util/DesktopNotificationService.java @@ -0,0 +1,63 @@ +package org.example.petshopdesktop.util; + +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.image.Image; + +import java.awt.SystemTray; +import java.awt.Toolkit; +import java.awt.TrayIcon; +import java.awt.image.BufferedImage; + +public class DesktopNotificationService { + private static final DesktopNotificationService INSTANCE = new DesktopNotificationService(); + + private TrayIcon trayIcon; + private boolean trayInitialized = false; + + private DesktopNotificationService() {} + + public static DesktopNotificationService getInstance() { + return INSTANCE; + } + + public void init(Image appIcon) { + if (!SystemTray.isSupported()) return; + try { + BufferedImage image; + if (appIcon != null) { + image = SwingFXUtils.fromFXImage(appIcon, null); + } else { + image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB); + } + trayIcon = new TrayIcon(image, "Leon's Pet Store"); + trayIcon.setImageAutoSize(true); + SystemTray.getSystemTray().add(trayIcon); + trayInitialized = true; + } catch (Exception ignored) {} + } + + public void notifyNewMessage(String senderName, String content) { + Thread t = new Thread(() -> { + Toolkit.getDefaultToolkit().beep(); + showNotification(senderName, content); + }); + t.setDaemon(true); + t.start(); + } + + private void showNotification(String senderName, String content) { + String title = senderName != null ? senderName : "New Message"; + String body = content != null + ? (content.length() > 100 ? content.substring(0, 100) + "..." : content) + : "Attachment"; + + if (trayInitialized && trayIcon != null) { + trayIcon.displayMessage(title, body, TrayIcon.MessageType.INFO); + return; + } + + try { + new ProcessBuilder("notify-send", title, body).start(); + } catch (Exception ignored) {} + } +}