From f1671aef2c13bba9e4de884ef37a01519dea185d Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 15:42:55 -0600 Subject: [PATCH 1/7] 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 f3d2431dfb71d2ebec62e1f7a991662e8ac0ce62 Mon Sep 17 00:00:00 2001 From: Harkamal Date: Wed, 15 Apr 2026 15:44:45 -0600 Subject: [PATCH 2/7] 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 f95e1e310df92ca3cf879eae25c008d8844a598d Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 15:48:17 -0600 Subject: [PATCH 3/7] 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 4/7] 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 5/7] rebuild stripe key -- 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 6/7] 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 @@