From 2cd04d63e13241ac60c381cc36612d0c217284f3 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 15:55:49 -0600 Subject: [PATCH] user avatar in edit dialogs --- .../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 @@