From 4f564acd2b5298eca1518444b922c7cb0c1d4e80 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 15:38:28 -0600 Subject: [PATCH 01/13] rebuild stripe key -- 2.49.1 From f1671aef2c13bba9e4de884ef37a01519dea185d Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 15:42:55 -0600 Subject: [PATCH 02/13] restore cart across devices --- web/context/CartContext.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/context/CartContext.js b/web/context/CartContext.js index f1c76220..f0511aaf 100644 --- a/web/context/CartContext.js +++ b/web/context/CartContext.js @@ -45,6 +45,12 @@ export function CartProvider({ children }) { } }, []); + useEffect(() => { + if (user?.storeId && !localStorage.getItem(STORE_KEY)) { + setStoreId(user.storeId); + } + }, [user, setStoreId]); + const refreshCart = useCallback(async () => { if (!token || !selectedStoreId) { setCart(null); -- 2.49.1 From 5a6a63eef6e4c7d6b4a200621c4f3722e8f594e0 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 15:42:59 -0600 Subject: [PATCH 03/13] rebuild stripe key -- 2.49.1 From f3d2431dfb71d2ebec62e1f7a991662e8ac0ce62 Mon Sep 17 00:00:00 2001 From: Harkamal Date: Wed, 15 Apr 2026 15:44:45 -0600 Subject: [PATCH 04/13] center navbar links (#311) --- web/app/globals.css | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/web/app/globals.css b/web/app/globals.css index ec01a40b..1b27a6bb 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -28,9 +28,9 @@ body { box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); z-index: 1000; padding: 0.5rem 2rem; - display: flex; + display: grid; + grid-template-columns: 1fr auto 1fr; align-items: center; - justify-content: space-between; min-height: 70px; /* border-radius: 0px 0px 10px 10px; */ } @@ -57,7 +57,6 @@ body { display: flex; align-items: center; gap: 1.25rem; - flex: 1; justify-content: center; } @@ -1022,15 +1021,11 @@ body { /* Auth/nav */ -.navbar { - justify-content: space-between; -} - .nav-auth { display: flex; align-items: center; gap: 0.5rem; - margin-left: auto; + justify-self: end; padding-left: 1.5rem; flex-shrink: 0; } -- 2.49.1 From be07381bc0c251ba5740f348efc1b56dec66a06a Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 15:46:46 -0600 Subject: [PATCH 05/13] fix sale and adoption bugs --- .../java/com/petshop/backend/controller/SaleController.java | 2 +- .../java/com/petshop/backend/dto/adoption/AdoptionRequest.java | 1 + .../main/java/com/petshop/backend/dto/sale/SaleItemRequest.java | 1 + .../src/main/java/com/petshop/backend/service/SaleService.java | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) 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 cdb64c78..dffa63e4 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("hasRole('STAFF')") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") 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/dto/adoption/AdoptionRequest.java b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java index 7fb9ceac..0a45996f 100644 --- a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java @@ -20,6 +20,7 @@ public class AdoptionRequest { private Long employeeId; + @NotNull(message = "Source store ID is required") private Long sourceStoreId; private String paymentMethod; diff --git a/backend/src/main/java/com/petshop/backend/dto/sale/SaleItemRequest.java b/backend/src/main/java/com/petshop/backend/dto/sale/SaleItemRequest.java index 94e094c3..dad7011a 100644 --- a/backend/src/main/java/com/petshop/backend/dto/sale/SaleItemRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/sale/SaleItemRequest.java @@ -9,6 +9,7 @@ public class SaleItemRequest { private Long prodId; @NotNull(message = "Quantity is required") + @Positive(message = "Quantity must be positive") private Integer quantity; public Long getProdId() { diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index b53a4c4c..bb86d34c 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -172,7 +172,7 @@ public class SaleService { BigDecimal refundTotal; if (originalSubtotal != null && originalSubtotal.compareTo(BigDecimal.ZERO) > 0) { - BigDecimal ratio = subtotalAmount.divide(originalSubtotal, 10, RoundingMode.HALF_UP); + BigDecimal ratio = subtotalAmount.abs().divide(originalSubtotal, 10, RoundingMode.HALF_UP); refundTotal = originalSale.getTotalAmount().abs().multiply(ratio).negate().setScale(2, RoundingMode.HALF_UP); if (originalSale.getLoyaltyDiscountAmount() != null) { loyaltyDiscountRefunded = originalSale.getLoyaltyDiscountAmount().multiply(ratio).setScale(2, RoundingMode.HALF_UP); -- 2.49.1 From f95e1e310df92ca3cf879eae25c008d8844a598d Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 15:48:17 -0600 Subject: [PATCH 06/13] lock refunds against duplicates --- .../com/petshop/backend/repository/RefundRepository.java | 9 +++++++++ .../com/petshop/backend/repository/SaleRepository.java | 6 ++++++ .../java/com/petshop/backend/service/RefundService.java | 2 +- .../java/com/petshop/backend/service/SaleService.java | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/repository/RefundRepository.java b/backend/src/main/java/com/petshop/backend/repository/RefundRepository.java index 92ba34a8..fe507c3f 100644 --- a/backend/src/main/java/com/petshop/backend/repository/RefundRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/RefundRepository.java @@ -1,14 +1,23 @@ package com.petshop.backend.repository; import com.petshop.backend.entity.Refund; +import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface RefundRepository extends JpaRepository { List findByCustomerIdOrderByCreatedAtDesc(Long customerId); List findAllByOrderByCreatedAtDesc(); List findBySaleId(Long saleId); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT r FROM Refund r WHERE r.id = :id") + Optional findByIdForUpdate(@Param("id") Long id); } diff --git a/backend/src/main/java/com/petshop/backend/repository/SaleRepository.java b/backend/src/main/java/com/petshop/backend/repository/SaleRepository.java index 4313ba51..19a93ee9 100644 --- a/backend/src/main/java/com/petshop/backend/repository/SaleRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/SaleRepository.java @@ -1,10 +1,12 @@ package com.petshop.backend.repository; import com.petshop.backend.entity.Sale; +import jakarta.persistence.LockModeType; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.time.LocalDateTime; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -42,5 +44,9 @@ public interface SaleRepository extends JpaRepository { List findByOriginalSaleSaleId(Long originalSaleId); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT s FROM Sale s WHERE s.saleId = :id") + Optional findByIdForUpdate(@Param("id") Long id); + Optional findByCartCartId(Long cartId); } diff --git a/backend/src/main/java/com/petshop/backend/service/RefundService.java b/backend/src/main/java/com/petshop/backend/service/RefundService.java index 961efb24..6cfff23d 100644 --- a/backend/src/main/java/com/petshop/backend/service/RefundService.java +++ b/backend/src/main/java/com/petshop/backend/service/RefundService.java @@ -112,7 +112,7 @@ public class RefundService { @Transactional public RefundResponse updateRefundStatus(Long id, String status) { - Refund refund = refundRepository.findById(id) + Refund refund = refundRepository.findByIdForUpdate(id) .orElseThrow(() -> new ResourceNotFoundException("Refund not found")); Refund.RefundStatus newStatus; diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index b53a4c4c..37d4e338 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -107,7 +107,7 @@ public class SaleService { } if (sale.getIsRefund() && request.getOriginalSaleId() != null) { - Sale originalSale = saleRepository.findById(request.getOriginalSaleId()) + Sale originalSale = saleRepository.findByIdForUpdate(request.getOriginalSaleId()) .orElseThrow(() -> new ResourceNotFoundException("Original sale not found with id: " + request.getOriginalSaleId())); sale.setOriginalSale(originalSale); } -- 2.49.1 From 8b473c19f84a071b6b5811bee95837f6cae9c299 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 15:49:47 -0600 Subject: [PATCH 07/13] fix loyalty points display --- .../detailfragments/SaleDetailFragment.java | 11 ++++------- .../com/petshop/backend/dto/sale/SaleResponse.java | 9 +++++++++ .../main/java/com/petshop/backend/entity/Sale.java | 11 +++++++++++ .../java/com/petshop/backend/service/SaleService.java | 3 +++ .../petshopdesktop/controllers/SaleController.java | 4 +++- .../dialogcontrollers/SaleDetailDialogController.java | 6 ++++++ .../org/example/petshopdesktop/models/SaleDetail.java | 8 +++++++- .../dialogviews/sale-detail-dialog-view.fxml | 2 +- 8 files changed, 44 insertions(+), 10 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java index 4963b272..e592fb7d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java @@ -206,15 +206,12 @@ public class SaleDetailFragment extends Fragment { binding.llEmployeeDiscount.setVisibility(View.GONE); } - if (sale.getPointsDiscountAmount() != null && sale.getPointsDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { - binding.llLoyaltyDiscount.setVisibility(View.VISIBLE); - binding.tvSaleLoyaltyDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getPointsDiscountAmount())); - if (sale.getPointsUsed() != null) { - binding.tvLoyaltyDiscountLabel.setText("Loyalty Discount (" + sale.getPointsUsed() + " pts):"); - } - } else if (sale.getLoyaltyDiscountAmount() != null && sale.getLoyaltyDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { + if (sale.getLoyaltyDiscountAmount() != null && sale.getLoyaltyDiscountAmount().compareTo(BigDecimal.ZERO) > 0) { binding.llLoyaltyDiscount.setVisibility(View.VISIBLE); binding.tvSaleLoyaltyDiscount.setText("-$" + String.format(Locale.getDefault(), "%.2f", sale.getLoyaltyDiscountAmount())); + if (sale.getPointsUsed() != null && sale.getPointsUsed() > 0) { + binding.tvLoyaltyDiscountLabel.setText("Loyalty Discount (" + sale.getPointsUsed() + " pts):"); + } } else { binding.llLoyaltyDiscount.setVisibility(View.GONE); } diff --git a/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java b/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java index 8f2b4fe5..af116212 100644 --- a/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java @@ -20,6 +20,7 @@ public class SaleResponse { private BigDecimal employeeDiscountAmount; private BigDecimal loyaltyDiscountAmount; private Integer pointsEarned; + private Integer pointsUsed; private String channel; private Long couponId; private Long cartId; @@ -145,6 +146,14 @@ public class SaleResponse { this.pointsEarned = pointsEarned; } + public Integer getPointsUsed() { + return pointsUsed; + } + + public void setPointsUsed(Integer pointsUsed) { + this.pointsUsed = pointsUsed; + } + public String getChannel() { return channel; } diff --git a/backend/src/main/java/com/petshop/backend/entity/Sale.java b/backend/src/main/java/com/petshop/backend/entity/Sale.java index e61204fd..b052e17d 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Sale.java +++ b/backend/src/main/java/com/petshop/backend/entity/Sale.java @@ -73,6 +73,9 @@ public class Sale { @Column(nullable = false) private Integer pointsEarned = 0; + @Column(nullable = false) + private Integer pointsUsed = 0; + @OneToMany(mappedBy = "sale", cascade = CascadeType.ALL) private List items = new ArrayList<>(); @@ -224,6 +227,14 @@ public class Sale { this.pointsEarned = pointsEarned; } + public Integer getPointsUsed() { + return pointsUsed; + } + + public void setPointsUsed(Integer pointsUsed) { + this.pointsUsed = pointsUsed; + } + public List getItems() { return items; } diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index b53a4c4c..bff502a9 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -200,6 +200,7 @@ public class SaleService { sale.setEmployeeDiscountAmount(BigDecimal.ZERO); sale.setLoyaltyDiscountAmount(loyaltyDiscountRefunded); sale.setPointsEarned(0); + sale.setPointsUsed(0); } else { if (request.getItems() == null || request.getItems().isEmpty()) { throw new BusinessException("At least one item is required"); @@ -254,6 +255,7 @@ public class SaleService { pointsDeducted = toPointsUsed(loyaltyDiscount); } sale.setLoyaltyDiscountAmount(loyaltyDiscount); + sale.setPointsUsed(pointsDeducted); BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount).subtract(loyaltyDiscount); sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO)); @@ -376,6 +378,7 @@ public class SaleService { response.setEmployeeDiscountAmount(sale.getEmployeeDiscountAmount()); response.setLoyaltyDiscountAmount(sale.getLoyaltyDiscountAmount()); response.setPointsEarned(sale.getPointsEarned()); + response.setPointsUsed(sale.getPointsUsed()); response.setChannel(sale.getChannel()); if (sale.getCoupon() != null) { response.setCouponId(sale.getCoupon().getCouponId()); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java index 8eaaf7af..7e71dc32 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -841,6 +841,7 @@ public class SaleController { double subtotal = sale.getSubtotalAmount() != null ? sale.getSubtotalAmount().doubleValue() : 0.0; double couponDiscount = sale.getCouponDiscountAmount() != null ? sale.getCouponDiscountAmount().doubleValue() : 0.0; double loyaltyDiscount = sale.getLoyaltyDiscountAmount() != null ? sale.getLoyaltyDiscountAmount().doubleValue() : 0.0; + int pointsUsed = sale.getPointsUsed() != null ? sale.getPointsUsed() : 0; return new SaleDetail( sale.getSaleId().intValue(), sale.getSaleDate(), @@ -852,7 +853,8 @@ public class SaleController { sale.getCustomerName(), subtotal, couponDiscount, - loyaltyDiscount + loyaltyDiscount, + pointsUsed ); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java index 332be5e8..4774c14d 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java @@ -33,6 +33,7 @@ public class SaleDetailDialogController { @FXML private javafx.scene.layout.HBox hbDetailCouponDiscount; @FXML private Label lblDetailCouponDiscount; @FXML private javafx.scene.layout.HBox hbDetailLoyaltyDiscount; + @FXML private Label lblDetailLoyaltyDiscountTitle; @FXML private Label lblDetailLoyaltyDiscount; @FXML private Label lblTotal; @FXML private Button btnRefund; @@ -86,6 +87,11 @@ public class SaleDetailDialogController { if (sale.getLoyaltyDiscountAmount() > 0.001) { lblDetailLoyaltyDiscount.setText("-" + currency.format(sale.getLoyaltyDiscountAmount())); + if (sale.getPointsUsed() > 0) { + lblDetailLoyaltyDiscountTitle.setText("Loyalty Discount (" + sale.getPointsUsed() + " pts):"); + } else { + lblDetailLoyaltyDiscountTitle.setText("Loyalty Discount:"); + } hbDetailLoyaltyDiscount.setVisible(true); hbDetailLoyaltyDiscount.setManaged(true); } else { diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java b/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java index 13f500c4..63d3cf47 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java +++ b/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java @@ -15,8 +15,9 @@ public class SaleDetail { private final double subtotalAmount; private final double couponDiscountAmount; private final double loyaltyDiscountAmount; + private final int pointsUsed; - public SaleDetail(int saleId, LocalDateTime saleDate, double totalAmount, String paymentMethod, String employeeName, boolean refund, ObservableList items, String customerName, double subtotalAmount, double couponDiscountAmount, double loyaltyDiscountAmount) { + public SaleDetail(int saleId, LocalDateTime saleDate, double totalAmount, String paymentMethod, String employeeName, boolean refund, ObservableList items, String customerName, double subtotalAmount, double couponDiscountAmount, double loyaltyDiscountAmount, int pointsUsed) { this.saleId = saleId; this.saleDate = saleDate; this.totalAmount = totalAmount; @@ -28,6 +29,7 @@ public class SaleDetail { this.subtotalAmount = subtotalAmount; this.couponDiscountAmount = couponDiscountAmount; this.loyaltyDiscountAmount = loyaltyDiscountAmount; + this.pointsUsed = pointsUsed; } public int getSaleId() { @@ -74,6 +76,10 @@ public class SaleDetail { return loyaltyDiscountAmount; } + public int getPointsUsed() { + return pointsUsed; + } + public static class SaleDetailItem { private final int prodId; private final String productName; diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/sale-detail-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/sale-detail-dialog-view.fxml index 4365bef7..45a3f440 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/sale-detail-dialog-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/sale-detail-dialog-view.fxml @@ -69,7 +69,7 @@ - + -- 2.49.1 From 65bfa1d06f8946d53247b1565451523ffa42e684 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 15:51:01 -0600 Subject: [PATCH 08/13] rebuild stripe key -- 2.49.1 From 8b9c4b899fcd19cbad102b233079fd141781766c Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 15:54:46 -0600 Subject: [PATCH 09/13] fix auth and logic bugs --- .../com/petshop/backend/controller/ContactController.java | 3 ++- .../java/com/petshop/backend/controller/StoreController.java | 4 ++++ .../src/main/java/com/petshop/backend/dto/pet/PetRequest.java | 2 +- .../java/com/petshop/backend/service/AppointmentService.java | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/ContactController.java b/backend/src/main/java/com/petshop/backend/controller/ContactController.java index c6b4cf1d..cfd4326f 100644 --- a/backend/src/main/java/com/petshop/backend/controller/ContactController.java +++ b/backend/src/main/java/com/petshop/backend/controller/ContactController.java @@ -1,6 +1,7 @@ package com.petshop.backend.controller; import com.petshop.backend.entity.User; +import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.EmailService; import com.petshop.backend.util.AuthenticationHelper; @@ -33,7 +34,7 @@ public class ContactController { @PostMapping public ResponseEntity sendContactEmail(@Valid @RequestBody ContactRequest req) { Long userId = AuthenticationHelper.getAuthenticatedUserId(); - User user = userRepository.findById(userId).orElseThrow(); + User user = userRepository.findById(userId).orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); emailService.sendContactMessage(user, req.subject(), req.body()); return ResponseEntity.ok().build(); } diff --git a/backend/src/main/java/com/petshop/backend/controller/StoreController.java b/backend/src/main/java/com/petshop/backend/controller/StoreController.java index 7dfb2b01..dc7bf8f5 100644 --- a/backend/src/main/java/com/petshop/backend/controller/StoreController.java +++ b/backend/src/main/java/com/petshop/backend/controller/StoreController.java @@ -35,11 +35,13 @@ public class StoreController { } @PostMapping + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity createStore(@Valid @RequestBody StoreRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(storeService.createStore(request)); } @PutMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity updateStore( @PathVariable Long id, @Valid @RequestBody StoreRequest request) { @@ -47,12 +49,14 @@ public class StoreController { } @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity deleteStore(@PathVariable Long id) { storeService.deleteStore(id); return ResponseEntity.noContent().build(); } @DeleteMapping + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity bulkDeleteStores(@Valid @RequestBody BulkDeleteRequest request) { storeService.bulkDeleteStores(request); return ResponseEntity.noContent().build(); diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java b/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java index 9a92581a..ff4f9ad2 100644 --- a/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java @@ -100,7 +100,7 @@ public class PetRequest { Objects.equals(petSpecies, that.petSpecies) && Objects.equals(petBreed, that.petBreed) && Objects.equals(petAge, that.petAge) && - petStatus == that.petStatus && + Objects.equals(petStatus, that.petStatus) && Objects.equals(petPrice, that.petPrice); } diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index bea0c682..ed34bb91 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -264,7 +264,7 @@ public class AppointmentService { List pastBookedAppointments = appointmentRepository.findPastBookedAppointments(currentDate, currentTime); for (Appointment appointment : pastBookedAppointments) { - appointment.setAppointmentStatus("COMPLETED"); + appointment.setAppointmentStatus("Completed"); appointmentRepository.save(appointment); } -- 2.49.1 From 2031ecc99c8fc5b340f743857603d0899c171c9e Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 15:58:34 -0600 Subject: [PATCH 10/13] fix validation and 500 bugs --- .../java/com/petshop/backend/controller/ChatController.java | 3 ++- .../petshop/backend/dto/appointment/AppointmentRequest.java | 3 ++- .../java/com/petshop/backend/dto/service/ServiceRequest.java | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/ChatController.java b/backend/src/main/java/com/petshop/backend/controller/ChatController.java index e9d8459a..94fb229a 100644 --- a/backend/src/main/java/com/petshop/backend/controller/ChatController.java +++ b/backend/src/main/java/com/petshop/backend/controller/ChatController.java @@ -7,6 +7,7 @@ import com.petshop.backend.dto.chat.MessageResponse; import com.petshop.backend.dto.chat.UpdateConversationRequest; import com.petshop.backend.entity.Message; import com.petshop.backend.entity.User; +import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.MessageRepository; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.ChatAttachmentStorageService; @@ -115,7 +116,7 @@ public class ChatController { public ResponseEntity getMessageAttachment(@PathVariable Long messageId) { User user = getCurrentUser(); Message message = messageRepository.findById(messageId) - .orElseThrow(() -> new RuntimeException("Message not found")); + .orElseThrow(() -> new ResourceNotFoundException("Message not found with id: " + messageId)); if (!chatService.hasConversationAccess(message.getConversationId(), user.getId(), user.getRole())) { throw new AccessDeniedException("Access denied to this message attachment"); diff --git a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java index 9ddb9ad2..d1c3847a 100644 --- a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java @@ -1,5 +1,6 @@ package com.petshop.backend.dto.appointment; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.time.LocalTime; @@ -21,7 +22,7 @@ public class AppointmentRequest { @NotNull(message = "Appointment time is required") private LocalTime appointmentTime; - @NotNull(message = "Appointment status is required") + @NotBlank(message = "Appointment status is required") private String appointmentStatus; private Long petId; diff --git a/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java b/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java index c84ac5f7..977b72cc 100644 --- a/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java @@ -18,6 +18,7 @@ public class ServiceRequest { @Positive(message = "Price must be positive") private BigDecimal servicePrice; + @NotNull(message = "Service duration is required") @Positive(message = "Duration must be positive") private Integer serviceDuration; -- 2.49.1 From b65868b4d552fa61dc292fbd8a26b86c552ceade Mon Sep 17 00:00:00 2001 From: Harkamal Date: Wed, 15 Apr 2026 15:58:46 -0600 Subject: [PATCH 11/13] user avatar in edit dialogs (#312) --- .../controller/UserAvatarController.java | 75 ++++++++++++++++++ .../backend/dto/user/UserResponse.java | 9 +++ .../petshop/backend/service/UserService.java | 1 + .../api/dto/employee/EmployeeResponse.java | 3 + .../api/dto/user/UserResponse.java | 9 +++ .../petshopdesktop/api/endpoints/UserApi.java | 9 +++ .../CustomerEditDialogController.java | 77 +++++++++++++++++++ .../StaffEditDialogController.java | 76 ++++++++++++++++++ .../customer-edit-dialog-view.fxml | 18 +++++ .../dialogviews/staff-edit-dialog-view.fxml | 18 +++++ 10 files changed, 295 insertions(+) diff --git a/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java b/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java index b236cf8b..68ef41ef 100644 --- a/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java +++ b/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java @@ -4,12 +4,21 @@ import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.AvatarStorageService; import org.springframework.core.io.Resource; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; @RestController @RequestMapping("/api/v1/users") @@ -40,4 +49,70 @@ public class UserAvatarController { return ResponseEntity.notFound().build(); } } + + @PostMapping("/{userId}/avatar") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity uploadUserAvatar(@PathVariable Long userId, @RequestParam("avatar") MultipartFile file) { + User user = userRepository.findById(userId).orElse(null); + if (user == null) { + return ResponseEntity.notFound().build(); + } + + if (file.isEmpty()) { + Map error = new HashMap<>(); + error.put("message", "Please select a file to upload"); + return ResponseEntity.badRequest().body(error); + } + + if (file.getSize() > 5 * 1024 * 1024) { + Map error = new HashMap<>(); + error.put("message", "File size must not exceed 5MB"); + return ResponseEntity.badRequest().body(error); + } + + String contentType = file.getContentType(); + if (contentType == null || (!contentType.equals("image/jpeg") && !contentType.equals("image/png") && !contentType.equals("image/gif"))) { + Map error = new HashMap<>(); + error.put("message", "Only JPG, PNG, and GIF images are allowed"); + return ResponseEntity.badRequest().body(error); + } + + try { + avatarStorageService.deleteAvatar(user); + String avatarPath = avatarStorageService.storeAvatar(file); + user.setAvatarUrl(avatarPath); + userRepository.save(user); + + Map result = new HashMap<>(); + result.put("avatarUrl", avatarStorageService.toOwnerAvatarUrl(user)); + result.put("message", "Avatar uploaded successfully"); + return ResponseEntity.ok(result); + } catch (IOException e) { + Map error = new HashMap<>(); + error.put("message", "Failed to upload avatar: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } + + @DeleteMapping("/{userId}/avatar") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity deleteUserAvatar(@PathVariable Long userId) { + User user = userRepository.findById(userId).orElse(null); + if (user == null) { + return ResponseEntity.notFound().build(); + } + + try { + avatarStorageService.deleteAvatar(user); + user.setAvatarUrl(null); + userRepository.save(user); + Map result = new HashMap<>(); + result.put("message", "Avatar removed successfully"); + return ResponseEntity.ok(result); + } catch (IOException e) { + Map error = new HashMap<>(); + error.put("message", "Failed to remove avatar: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } } diff --git a/backend/src/main/java/com/petshop/backend/dto/user/UserResponse.java b/backend/src/main/java/com/petshop/backend/dto/user/UserResponse.java index 6c4d15b5..6c8ffd68 100644 --- a/backend/src/main/java/com/petshop/backend/dto/user/UserResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/user/UserResponse.java @@ -16,6 +16,7 @@ public class UserResponse { private Long primaryStoreId; private Integer loyaltyPoints; private Boolean active; + private String avatarUrl; private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -118,6 +119,14 @@ public class UserResponse { this.active = active; } + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/backend/src/main/java/com/petshop/backend/service/UserService.java b/backend/src/main/java/com/petshop/backend/service/UserService.java index 4cf83d35..6eb8c2fa 100644 --- a/backend/src/main/java/com/petshop/backend/service/UserService.java +++ b/backend/src/main/java/com/petshop/backend/service/UserService.java @@ -205,6 +205,7 @@ public class UserService { response.setPrimaryStoreId(user.getPrimaryStore() != null ? user.getPrimaryStore().getStoreId() : null); response.setLoyaltyPoints(user.getLoyaltyPoints()); response.setActive(user.getActive()); + response.setAvatarUrl(user.getAvatarUrl() != null ? "/api/v1/users/" + user.getId() + "/avatar/file" : null); response.setCreatedAt(user.getCreatedAt()); response.setUpdatedAt(user.getUpdatedAt()); return response; diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java index f9ce7f96..c6a32d23 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java @@ -16,6 +16,7 @@ public class EmployeeResponse { private String staffRole; private Long primaryStoreId; private Boolean active; + private String avatarUrl; private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -45,6 +46,8 @@ public class EmployeeResponse { public void setPrimaryStoreId(Long primaryStoreId) { this.primaryStoreId = primaryStoreId; } public Boolean getActive() { return active; } public void setActive(Boolean active) { this.active = active; } + public String getAvatarUrl() { return avatarUrl; } + public void setAvatarUrl(String avatarUrl) { this.avatarUrl = avatarUrl; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public LocalDateTime getUpdatedAt() { return updatedAt; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java index f9fc4eac..2e1daa0e 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java @@ -11,6 +11,7 @@ public class UserResponse { private String role; private Boolean active; private Integer loyaltyPoints; + private String avatarUrl; private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -81,6 +82,14 @@ public class UserResponse { this.loyaltyPoints = loyaltyPoints; } + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/UserApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/UserApi.java index e927f03c..27439ee9 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/UserApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/UserApi.java @@ -8,6 +8,7 @@ import org.example.petshopdesktop.api.dto.user.UserResponse; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.List; public class UserApi { @@ -45,4 +46,12 @@ public class UserApi { public UserResponse updateUser(Long id, UserRequest request) throws Exception { return apiClient.put("/api/v1/users/" + id, request, UserResponse.class); } + + public void uploadUserAvatar(Long userId, Path filePath) throws Exception { + apiClient.postMultipart("/api/v1/users/" + userId + "/avatar", "avatar", filePath, Object.class); + } + + public void deleteUserAvatar(Long userId) throws Exception { + apiClient.delete("/api/v1/users/" + userId + "/avatar"); + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/CustomerEditDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/CustomerEditDialogController.java index 292c21be..92c800e3 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/CustomerEditDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/CustomerEditDialogController.java @@ -9,15 +9,21 @@ import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; +import javafx.scene.image.ImageView; import javafx.stage.Stage; import org.example.petshopdesktop.Validator; import org.example.petshopdesktop.api.dto.user.UserRequest; import org.example.petshopdesktop.api.dto.user.UserResponse; import org.example.petshopdesktop.api.endpoints.CustomerApi; +import org.example.petshopdesktop.api.endpoints.UserApi; import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.DesktopImageSupport; +import org.example.petshopdesktop.util.FilePickerSupport; import org.example.petshopdesktop.util.TextFieldFormatSupport; +import java.io.File; + public class CustomerEditDialogController { @FXML private TextField txtFirstName; @@ -32,7 +38,15 @@ public class CustomerEditDialogController { @FXML private Label lblError; @FXML private Button btnSave; + @FXML private ImageView imgAvatarPreview; + @FXML private Label lblAvatarStatus; + @FXML private Button btnChangeAvatar; + @FXML private Button btnRemoveAvatar; + private UserResponse customer; + private File selectedAvatarFile; + private String currentAvatarUrl; + private boolean removeAvatarRequested; @FXML void initialize() { @@ -41,6 +55,9 @@ public class CustomerEditDialogController { boolean isAdmin = UserSession.getInstance().isAdmin(); txtLoyaltyPoints.setDisable(!isAdmin); + + btnChangeAvatar.setOnMouseClicked(e -> handleChangeAvatar()); + btnRemoveAvatar.setOnMouseClicked(e -> handleRemoveAvatar()); } public void setCustomer(UserResponse user) { @@ -55,6 +72,65 @@ public class CustomerEditDialogController { cbActive.setValue(Boolean.TRUE.equals(user.getActive()) ? "Active" : "Inactive"); int pts = user.getLoyaltyPoints() != null ? user.getLoyaltyPoints() : 0; txtLoyaltyPoints.setText(String.valueOf(pts)); + + currentAvatarUrl = user.getAvatarUrl(); + refreshAvatarPreview(); + } + + private void handleChangeAvatar() { + File file = FilePickerSupport.pickImageFile(btnSave.getScene().getWindow()); + if (file == null) return; + selectedAvatarFile = file; + removeAvatarRequested = false; + lblAvatarStatus.setText("Selected: " + file.getName()); + DesktopImageSupport.loadImageInto(imgAvatarPreview, file.toURI().toString(), 90, 90); + btnRemoveAvatar.setDisable(false); + } + + private void handleRemoveAvatar() { + selectedAvatarFile = null; + removeAvatarRequested = true; + currentAvatarUrl = null; + refreshAvatarPreview(); + } + + private void applyAvatarChanges(Long userId) throws Exception { + String previousAvatarUrl = currentAvatarUrl; + if (removeAvatarRequested) { + try { + UserApi.getInstance().deleteUserAvatar(userId); + } catch (Exception ignored) { + } + } + if (selectedAvatarFile != null) { + UserApi.getInstance().uploadUserAvatar(userId, selectedAvatarFile.toPath()); + currentAvatarUrl = "/api/v1/users/" + userId + "/avatar/file"; + } else if (removeAvatarRequested) { + currentAvatarUrl = null; + } + DesktopImageSupport.evict(previousAvatarUrl); + DesktopImageSupport.evict(currentAvatarUrl); + selectedAvatarFile = null; + removeAvatarRequested = false; + } + + private void refreshAvatarPreview() { + if (imgAvatarPreview == null || lblAvatarStatus == null || btnRemoveAvatar == null) return; + imgAvatarPreview.setImage(null); + if (selectedAvatarFile != null) { + lblAvatarStatus.setText("Selected: " + selectedAvatarFile.getName()); + DesktopImageSupport.loadImageInto(imgAvatarPreview, selectedAvatarFile.toURI().toString(), 90, 90); + btnRemoveAvatar.setDisable(false); + return; + } + if (currentAvatarUrl != null && !currentAvatarUrl.isBlank()) { + lblAvatarStatus.setText("Current avatar loaded"); + DesktopImageSupport.loadImageInto(imgAvatarPreview, currentAvatarUrl, 90, 90); + btnRemoveAvatar.setDisable(false); + return; + } + lblAvatarStatus.setText("No avatar"); + btnRemoveAvatar.setDisable(true); } private String[] splitFullName(String fullName) { @@ -148,6 +224,7 @@ public class CustomerEditDialogController { if (finalLoyaltyPoints != null) request.setLoyaltyPoints(finalLoyaltyPoints); CustomerApi.getInstance().updateCustomer(customer.getId(), request); + applyAvatarChanges(customer.getId()); Platform.runLater(this::close); } catch (Exception e) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java index 23a6a00a..86a8603a 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java @@ -10,6 +10,7 @@ import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; +import javafx.scene.image.ImageView; import javafx.stage.Stage; import org.example.petshopdesktop.Validator; import org.example.petshopdesktop.api.dto.common.DropdownOption; @@ -17,9 +18,13 @@ import org.example.petshopdesktop.api.dto.employee.EmployeeRequest; import org.example.petshopdesktop.api.dto.employee.EmployeeResponse; import org.example.petshopdesktop.api.endpoints.DropdownApi; import org.example.petshopdesktop.api.endpoints.EmployeeApi; +import org.example.petshopdesktop.api.endpoints.UserApi; import org.example.petshopdesktop.util.ActivityLogger; +import org.example.petshopdesktop.util.DesktopImageSupport; +import org.example.petshopdesktop.util.FilePickerSupport; import org.example.petshopdesktop.util.TextFieldFormatSupport; +import java.io.File; import java.util.List; public class StaffEditDialogController { @@ -37,8 +42,16 @@ public class StaffEditDialogController { @FXML private Label lblError; @FXML private Button btnSave; + @FXML private ImageView imgAvatarPreview; + @FXML private Label lblAvatarStatus; + @FXML private Button btnChangeAvatar; + @FXML private Button btnRemoveAvatar; + private EmployeeResponse employee; private Long pendingStoreId = null; + private File selectedAvatarFile; + private String currentAvatarUrl; + private boolean removeAvatarRequested; @FXML void initialize() { @@ -60,6 +73,9 @@ public class StaffEditDialogController { } }); + btnChangeAvatar.setOnMouseClicked(e -> handleChangeAvatar()); + btnRemoveAvatar.setOnMouseClicked(e -> handleRemoveAvatar()); + loadStores(); } @@ -110,6 +126,65 @@ public class StaffEditDialogController { pendingStoreId = emp.getPrimaryStoreId(); applyPendingStore(); + + currentAvatarUrl = emp.getAvatarUrl(); + refreshAvatarPreview(); + } + + private void handleChangeAvatar() { + File file = FilePickerSupport.pickImageFile(btnSave.getScene().getWindow()); + if (file == null) return; + selectedAvatarFile = file; + removeAvatarRequested = false; + lblAvatarStatus.setText("Selected: " + file.getName()); + DesktopImageSupport.loadImageInto(imgAvatarPreview, file.toURI().toString(), 90, 90); + btnRemoveAvatar.setDisable(false); + } + + private void handleRemoveAvatar() { + selectedAvatarFile = null; + removeAvatarRequested = true; + currentAvatarUrl = null; + refreshAvatarPreview(); + } + + private void applyAvatarChanges(Long userId) throws Exception { + String previousAvatarUrl = currentAvatarUrl; + if (removeAvatarRequested) { + try { + UserApi.getInstance().deleteUserAvatar(userId); + } catch (Exception ignored) { + } + } + if (selectedAvatarFile != null) { + UserApi.getInstance().uploadUserAvatar(userId, selectedAvatarFile.toPath()); + currentAvatarUrl = "/api/v1/users/" + userId + "/avatar/file"; + } else if (removeAvatarRequested) { + currentAvatarUrl = null; + } + DesktopImageSupport.evict(previousAvatarUrl); + DesktopImageSupport.evict(currentAvatarUrl); + selectedAvatarFile = null; + removeAvatarRequested = false; + } + + private void refreshAvatarPreview() { + if (imgAvatarPreview == null || lblAvatarStatus == null || btnRemoveAvatar == null) return; + imgAvatarPreview.setImage(null); + if (selectedAvatarFile != null) { + lblAvatarStatus.setText("Selected: " + selectedAvatarFile.getName()); + DesktopImageSupport.loadImageInto(imgAvatarPreview, selectedAvatarFile.toURI().toString(), 90, 90); + btnRemoveAvatar.setDisable(false); + return; + } + if (currentAvatarUrl != null && !currentAvatarUrl.isBlank()) { + lblAvatarStatus.setText("Current avatar loaded"); + DesktopImageSupport.loadImageInto(imgAvatarPreview, currentAvatarUrl, 90, 90); + btnRemoveAvatar.setDisable(false); + return; + } + lblAvatarStatus.setText("No avatar"); + btnRemoveAvatar.setDisable(true); } private String[] splitFullName(String fullName) { @@ -193,6 +268,7 @@ public class StaffEditDialogController { request.setPrimaryStoreId(storeId); EmployeeApi.getInstance().updateEmployee(employee.getId(), request); + applyAvatarChanges(employee.getUserId()); Platform.runLater(this::close); } catch (Exception e) { diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/customer-edit-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/customer-edit-dialog-view.fxml index fe0b34d7..f089cb06 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/customer-edit-dialog-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/customer-edit-dialog-view.fxml @@ -6,6 +6,7 @@ + @@ -24,6 +25,23 @@