From 99fe718d2c097d6c33b6e93586b17d38300ede6e Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 11 Mar 2026 14:06:22 -0600 Subject: [PATCH 1/6] Polish chat ui --- .../api/ChatRealtimeClient.java | 5 + .../controllers/ChatController.java | 2 +- .../petshopdesktop/main-layout-view.fxml | 4 +- .../modelviews/analytics-view.fxml | 4 +- .../petshopdesktop/modelviews/chat-view.fxml | 6 +- .../petshopdesktop/styles/desktop-ui.css | 119 ++++++++++++++++++ 6 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/org/example/petshopdesktop/styles/desktop-ui.css diff --git a/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java b/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java index da6d94c3..fcb93c0c 100644 --- a/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java +++ b/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java @@ -37,6 +37,7 @@ public class ChatRealtimeClient implements WebSocket.Listener { private Consumer conversationListener; private Consumer messageListener; private Consumer statusListener; + private volatile String currentStatus = "Chat disconnected"; private ChatRealtimeClient() { this.httpClient = HttpClient.newBuilder() @@ -58,6 +59,9 @@ public class ChatRealtimeClient implements WebSocket.Listener { public void setStatusListener(Consumer statusListener) { this.statusListener = statusListener; + if (statusListener != null) { + statusListener.accept(currentStatus); + } } public void connect() { @@ -286,6 +290,7 @@ public class ChatRealtimeClient implements WebSocket.Listener { } private void publishStatus(String status) { + currentStatus = status; if (statusListener != null) { statusListener.accept(status); } diff --git a/src/main/java/org/example/petshopdesktop/controllers/ChatController.java b/src/main/java/org/example/petshopdesktop/controllers/ChatController.java index 6911960d..bc343639 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/ChatController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/ChatController.java @@ -107,7 +107,7 @@ public class ChatController { realtimeClient.setConversationListener(conversation -> Platform.runLater(() -> upsertConversation(conversation))); realtimeClient.setMessageListener(message -> Platform.runLater(() -> appendMessageIfSelected(message))); realtimeClient.setStatusListener(status -> Platform.runLater(() -> lblChatStatus.setText(status))); - realtimeClient.connect(); + realtimeClient.subscribeToConversations(); loadCustomers(); loadConversations(); diff --git a/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml b/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml index e301357f..9daa91e1 100644 --- a/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml +++ b/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml @@ -12,7 +12,7 @@ - + @@ -47,7 +47,7 @@ - + diff --git a/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml b/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml index cdf1f90b..dec7a883 100644 --- a/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml +++ b/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml @@ -14,7 +14,7 @@ - + @@ -41,7 +41,7 @@ - + diff --git a/src/main/resources/org/example/petshopdesktop/modelviews/chat-view.fxml b/src/main/resources/org/example/petshopdesktop/modelviews/chat-view.fxml index f701a9b5..e425ec67 100644 --- a/src/main/resources/org/example/petshopdesktop/modelviews/chat-view.fxml +++ b/src/main/resources/org/example/petshopdesktop/modelviews/chat-view.fxml @@ -14,7 +14,7 @@ - + @@ -38,7 +38,7 @@ @@ -53,7 +53,7 @@ - + diff --git a/src/main/resources/org/example/petshopdesktop/styles/desktop-ui.css b/src/main/resources/org/example/petshopdesktop/styles/desktop-ui.css new file mode 100644 index 00000000..9b029a2a --- /dev/null +++ b/src/main/resources/org/example/petshopdesktop/styles/desktop-ui.css @@ -0,0 +1,119 @@ +.sidebar-scroll-pane { + -fx-background-color: transparent; + -fx-background-insets: 0; + -fx-padding: 0 4 0 0; +} + +.sidebar-scroll-pane > .viewport { + -fx-background-color: #2C3E50; +} + +.sidebar-scroll-pane .scroll-bar:vertical, +.chat-conversation-list .scroll-bar:vertical, +.chat-messages-scroll-pane .scroll-bar:vertical { + -fx-background-color: transparent; + -fx-pref-width: 10; + -fx-padding: 0; +} + +.sidebar-scroll-pane .scroll-bar:vertical .track, +.chat-conversation-list .scroll-bar:vertical .track, +.chat-messages-scroll-pane .scroll-bar:vertical .track { + -fx-background-color: rgba(148, 163, 184, 0.18); + -fx-background-radius: 999; +} + +.sidebar-scroll-pane .scroll-bar:vertical .thumb, +.chat-conversation-list .scroll-bar:vertical .thumb, +.chat-messages-scroll-pane .scroll-bar:vertical .thumb { + -fx-background-color: rgba(203, 213, 225, 0.52); + -fx-background-radius: 999; +} + +.sidebar-scroll-pane .scroll-bar:vertical .increment-button, +.sidebar-scroll-pane .scroll-bar:vertical .decrement-button, +.chat-conversation-list .scroll-bar:vertical .increment-button, +.chat-conversation-list .scroll-bar:vertical .decrement-button, +.chat-messages-scroll-pane .scroll-bar:vertical .increment-button, +.chat-messages-scroll-pane .scroll-bar:vertical .decrement-button { + -fx-padding: 0; + -fx-background-color: transparent; +} + +.sidebar-scroll-pane .scroll-bar:vertical .increment-arrow, +.sidebar-scroll-pane .scroll-bar:vertical .decrement-arrow, +.chat-conversation-list .scroll-bar:vertical .increment-arrow, +.chat-conversation-list .scroll-bar:vertical .decrement-arrow, +.chat-messages-scroll-pane .scroll-bar:vertical .increment-arrow, +.chat-messages-scroll-pane .scroll-bar:vertical .decrement-arrow { + -fx-shape: ''; + -fx-padding: 0; +} + +.analytics-tabs { + -fx-tab-min-height: 34; + -fx-tab-max-height: 34; +} + +.analytics-tabs > .tab-header-area { + -fx-padding: 0 0 8 0; +} + +.analytics-tabs > .tab-header-area > .headers-region > .tab { + -fx-background-color: transparent; + -fx-background-radius: 12 12 0 0; + -fx-border-color: transparent transparent rgba(148, 163, 184, 0.35) transparent; + -fx-border-width: 0 0 2 0; + -fx-padding: 8 16 8 16; +} + +.analytics-tabs > .tab-header-area > .headers-region > .tab:selected { + -fx-background-color: white; + -fx-border-color: transparent transparent #ff6b6b transparent; + -fx-effect: dropshadow(gaussian, rgba(15, 23, 42, 0.08), 10, 0.2, 0, 2); +} + +.analytics-tabs > .tab-header-area > .headers-region > .tab .tab-label { + -fx-text-fill: #64748b; + -fx-font-weight: 700; +} + +.analytics-tabs > .tab-header-area > .headers-region > .tab:selected .tab-label { + -fx-text-fill: #1f2937; +} + +.analytics-tabs > .tab-header-area > .tab-header-background { + -fx-background-color: transparent; +} + +.analytics-tabs > .tab-content-area { + -fx-background-color: transparent; + -fx-padding: 0; +} + +.chat-conversation-list { + -fx-background-insets: 0; + -fx-background-color: transparent; +} + +.chat-conversation-list .list-cell { + -fx-background-color: transparent; + -fx-background-radius: 14; + -fx-padding: 10 10 10 10; +} + +.chat-conversation-list .list-cell:filled:selected, +.chat-conversation-list .list-cell:filled:hover { + -fx-background-color: #eef2ff; +} + +.chat-conversation-list .list-cell:filled:selected { + -fx-border-color: #c7d2fe; + -fx-border-radius: 14; + -fx-background-insets: 0; +} + +.chat-messages-scroll-pane, +.chat-messages-scroll-pane > .viewport { + -fx-background-color: transparent; +} From 819bd924922271683e7356d0ffc3ee968b31f4cb Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 11 Mar 2026 14:21:40 -0600 Subject: [PATCH 2/6] Fix refund flow --- .../controllers/SaleController.java | 5 + .../RefundDialogController.java | 99 +++++++++++++++++-- 2 files changed, 97 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/example/petshopdesktop/controllers/SaleController.java b/src/main/java/org/example/petshopdesktop/controllers/SaleController.java index d85bffab..c3866b1e 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -372,6 +372,11 @@ public class SaleController { private void openRefundDialog() { try { SaleLineItem selectedSale = tvSales.getSelectionModel().getSelectedItem(); + if (selectedSale != null && selectedSale.isRefund()) { + showError("Refund", "Select an original sale, not an existing refund."); + return; + } + FXMLLoader loader = new FXMLLoader(getClass().getResource( "/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml")); Stage dialog = new Stage(); diff --git a/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java b/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java index 581b579d..860a9921 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java @@ -86,6 +86,8 @@ public class RefundDialogController { private Button btnCancel; private SaleResponse currentSale; + private final List baseOriginalItems = new ArrayList<>(); + private final ObservableList originalItems = FXCollections.observableArrayList(); private final ObservableList refundItems = FXCollections.observableArrayList(); private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); @@ -102,6 +104,7 @@ public class RefundDialogController { colOriginalQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); colOriginalUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); colOriginalTotal.setCellValueFactory(new PropertyValueFactory<>("lineTotal")); + tvOriginalItems.setItems(originalItems); tvOriginalItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); colRefundProduct.setCellValueFactory(new PropertyValueFactory<>("productName")); @@ -143,6 +146,11 @@ public class RefundDialogController { try { List allSales = SaleApi.getInstance().listSales(0, 1000, null); currentSale = SaleApi.getInstance().getSale(saleId); + if (Boolean.TRUE.equals(currentSale.getIsRefund())) { + clearLoadedSale(); + showError("Load Sale", "Select an original sale, not a refund record."); + return; + } List previousRefunds = allSales.stream() .filter(s -> Boolean.TRUE.equals(s.getIsRefund()) && saleId.equals(s.getOriginalSaleId())) .collect(Collectors.toList()); @@ -161,10 +169,13 @@ public class RefundDialogController { return; } - tvOriginalItems.setItems(FXCollections.observableArrayList(refundableItems)); + baseOriginalItems.clear(); + baseOriginalItems.addAll(copySaleItems(refundableItems)); + originalItems.setAll(copySaleItems(refundableItems)); cbPaymentMethod.getSelectionModel().select(currentSale.getPaymentMethod()); refundItems.clear(); + updateOriginalItemAvailability(); updateRefundTotal(); } catch (Exception e) { @@ -215,12 +226,8 @@ public class RefundDialogController { return; } - refundItems.add(new RefundItem( - selected.getProdId().intValue(), - selected.getProductName(), - quantity, - selected.getUnitPrice().doubleValue() - )); + addOrMergeRefundItem(selected, quantity); + updateOriginalItemAvailability(); updateRefundTotal(); } catch (NumberFormatException e) { @@ -234,6 +241,7 @@ public class RefundDialogController { RefundItem selected = tvRefundItems.getSelectionModel().getSelectedItem(); if (selected != null) { refundItems.remove(selected); + updateOriginalItemAvailability(); updateRefundTotal(); } } @@ -309,6 +317,83 @@ public class RefundDialogController { closeDialog(); } + private void clearLoadedSale() { + currentSale = null; + lblSaleInfo.setText(""); + baseOriginalItems.clear(); + originalItems.clear(); + refundItems.clear(); + updateRefundTotal(); + } + + private void addOrMergeRefundItem(SaleItemResponse selected, int quantity) { + for (int i = 0; i < refundItems.size(); i++) { + RefundItem existing = refundItems.get(i); + if (existing.getProdId() == selected.getProdId().intValue()) { + refundItems.set(i, new RefundItem( + existing.getProdId(), + existing.getProductName(), + existing.getQuantity() + quantity, + existing.getUnitPrice() + )); + return; + } + } + + refundItems.add(new RefundItem( + selected.getProdId().intValue(), + selected.getProductName(), + quantity, + selected.getUnitPrice().doubleValue() + )); + } + + private void updateOriginalItemAvailability() { + if (currentSale == null) { + baseOriginalItems.clear(); + originalItems.clear(); + return; + } + + Map pendingRefunds = new HashMap<>(); + for (RefundItem refundItem : refundItems) { + pendingRefunds.merge((long) refundItem.getProdId(), refundItem.getQuantity(), Integer::sum); + } + + List refreshedItems = new ArrayList<>(); + for (SaleItemResponse originalItem : baseOriginalItems) { + SaleItemResponse refreshedItem = copySaleItem(originalItem); + int pending = pendingRefunds.getOrDefault(refreshedItem.getProdId(), 0); + refreshedItem.setQuantity(Math.max(0, refreshedItem.getQuantity() - pending)); + if (refreshedItem.getQuantity() > 0) { + refreshedItems.add(refreshedItem); + } + } + + originalItems.setAll(refreshedItems); + tvOriginalItems.getSelectionModel().clearSelection(); + tvOriginalItems.refresh(); + tvRefundItems.refresh(); + } + + private List copySaleItems(List items) { + List copies = new ArrayList<>(); + for (SaleItemResponse item : items) { + copies.add(copySaleItem(item)); + } + return copies; + } + + private SaleItemResponse copySaleItem(SaleItemResponse source) { + SaleItemResponse copy = new SaleItemResponse(); + copy.setSaleItemId(source.getSaleItemId()); + copy.setProdId(source.getProdId()); + copy.setProductName(source.getProductName()); + copy.setQuantity(source.getQuantity()); + copy.setUnitPrice(source.getUnitPrice()); + return copy; + } + private void updateRefundTotal() { double total = refundItems.stream().mapToDouble(RefundItem::getTotal).sum(); lblRefundTotal.setText(currency.format(total)); From 82d35feb9ad905bc4a8532f87faaa6147c9e2405 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 11 Mar 2026 14:21:57 -0600 Subject: [PATCH 3/6] Add avatar controls --- .gitignore | 1 + .../example/petshopdesktop/api/ApiClient.java | 34 +++++ .../api/dto/auth/AvatarUploadResponse.java | 25 ++++ .../petshopdesktop/api/endpoints/AuthApi.java | 32 +++++ .../petshopdesktop/auth/UserSession.java | 14 ++ .../controllers/LoginController.java | 2 + .../controllers/MainLayoutController.java | 129 ++++++++++++++++++ .../petshopdesktop/main-layout-view.fxml | 28 ++++ 8 files changed, 265 insertions(+) create mode 100644 src/main/java/org/example/petshopdesktop/api/dto/auth/AvatarUploadResponse.java create mode 100644 src/main/java/org/example/petshopdesktop/api/endpoints/AuthApi.java diff --git a/.gitignore b/.gitignore index d0c951ec..c5df67b2 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ log.txt *.class *.out *.err +tmp/ diff --git a/src/main/java/org/example/petshopdesktop/api/ApiClient.java b/src/main/java/org/example/petshopdesktop/api/ApiClient.java index a2e0e279..c0fbd874 100644 --- a/src/main/java/org/example/petshopdesktop/api/ApiClient.java +++ b/src/main/java/org/example/petshopdesktop/api/ApiClient.java @@ -8,7 +8,11 @@ import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; +import java.util.UUID; public class ApiClient { private static final ApiClient INSTANCE = new ApiClient(); @@ -83,6 +87,36 @@ public class ApiClient { return handleResponse(response, responseClass); } + public T postMultipart(String path, String partName, Path filePath, Class responseClass) throws Exception { + String boundary = "----PetShopDesktop" + UUID.randomUUID(); + String mimeType = Files.probeContentType(filePath); + if (mimeType == null || mimeType.isBlank()) { + mimeType = "application/octet-stream"; + } + + byte[] fileBytes = Files.readAllBytes(filePath); + String fileName = filePath.getFileName().toString(); + byte[] prefix = ("--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\"" + partName + "\"; filename=\"" + fileName + "\"\r\n" + + "Content-Type: " + mimeType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8); + byte[] suffix = ("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8); + byte[] body = new byte[prefix.length + fileBytes.length + suffix.length]; + System.arraycopy(prefix, 0, body, 0, prefix.length); + System.arraycopy(fileBytes, 0, body, prefix.length, fileBytes.length); + System.arraycopy(suffix, 0, body, prefix.length + fileBytes.length, suffix.length); + + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .header("Content-Type", "multipart/form-data; boundary=" + boundary) + .POST(HttpRequest.BodyPublishers.ofByteArray(body)) + .timeout(Duration.ofSeconds(30)); + + addAuthHeader(builder); + + HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + return handleResponse(response, responseClass); + } + public T put(String path, Object requestBody, Class responseClass) throws Exception { String jsonBody = objectMapper.writeValueAsString(requestBody); diff --git a/src/main/java/org/example/petshopdesktop/api/dto/auth/AvatarUploadResponse.java b/src/main/java/org/example/petshopdesktop/api/dto/auth/AvatarUploadResponse.java new file mode 100644 index 00000000..24dadcca --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/api/dto/auth/AvatarUploadResponse.java @@ -0,0 +1,25 @@ +package org.example.petshopdesktop.api.dto.auth; + +public class AvatarUploadResponse { + private String avatarUrl; + private String message; + + public AvatarUploadResponse() { + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/org/example/petshopdesktop/api/endpoints/AuthApi.java b/src/main/java/org/example/petshopdesktop/api/endpoints/AuthApi.java new file mode 100644 index 00000000..a273a738 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/api/endpoints/AuthApi.java @@ -0,0 +1,32 @@ +package org.example.petshopdesktop.api.endpoints; + +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.auth.AvatarUploadResponse; +import org.example.petshopdesktop.api.dto.auth.UserInfoResponse; + +import java.nio.file.Path; + +public class AuthApi { + private static final AuthApi INSTANCE = new AuthApi(); + private final ApiClient apiClient; + + private AuthApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static AuthApi getInstance() { + return INSTANCE; + } + + public UserInfoResponse getCurrentUser() throws Exception { + return apiClient.get("/api/v1/auth/me", UserInfoResponse.class); + } + + public AvatarUploadResponse uploadAvatar(Path filePath) throws Exception { + return apiClient.postMultipart("/api/v1/auth/me/avatar", "avatar", filePath, AvatarUploadResponse.class); + } + + public void deleteAvatar() throws Exception { + apiClient.delete("/api/v1/auth/me/avatar"); + } +} diff --git a/src/main/java/org/example/petshopdesktop/auth/UserSession.java b/src/main/java/org/example/petshopdesktop/auth/UserSession.java index 162ad845..a578d0e4 100644 --- a/src/main/java/org/example/petshopdesktop/auth/UserSession.java +++ b/src/main/java/org/example/petshopdesktop/auth/UserSession.java @@ -10,6 +10,7 @@ public class UserSession { private Role role; private String jwtToken; private Long storeId; + private String avatarUrl; private UserSession() {} @@ -37,6 +38,7 @@ public class UserSession { this.role = null; this.jwtToken = null; this.storeId = null; + this.avatarUrl = null; } public Long getUserId() { @@ -71,6 +73,18 @@ public class UserSession { this.storeId = storeId; } + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + public boolean isLoggedIn() { return username != null && role != null; } diff --git a/src/main/java/org/example/petshopdesktop/controllers/LoginController.java b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java index c353f0cf..70c70e12 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/LoginController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java @@ -81,6 +81,8 @@ public class LoginController { } UserSession.getInstance().login(userInfo.getId(), username, role, token); UserSession.getInstance().setStoreId(userInfo.getStoreId()); + UserSession.getInstance().setEmployeeName(userInfo.getFullName() == null || userInfo.getFullName().isBlank() ? username : userInfo.getFullName()); + UserSession.getInstance().setAvatarUrl(userInfo.getAvatarUrl()); openMainLayout(); diff --git a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java index a4b24d8a..b262be0b 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java @@ -1,5 +1,6 @@ package org.example.petshopdesktop.controllers; +import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; @@ -9,10 +10,19 @@ import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.Separator; +import javafx.scene.image.Image; import javafx.scene.input.MouseEvent; import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.paint.ImagePattern; +import javafx.scene.shape.Circle; +import javafx.stage.FileChooser; import javafx.stage.Stage; +import org.example.petshopdesktop.api.ApiConfig; import org.example.petshopdesktop.api.ChatRealtimeClient; +import org.example.petshopdesktop.api.dto.auth.AvatarUploadResponse; +import org.example.petshopdesktop.api.dto.auth.UserInfoResponse; +import org.example.petshopdesktop.api.endpoints.AuthApi; import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.ui.SvgWebViewFactory; import org.example.petshopdesktop.util.ActivityLogger; @@ -79,6 +89,15 @@ public class MainLayoutController { @FXML private StackPane logoContainer; + @FXML + private StackPane avatarPreview; + + @FXML + private Button btnChangeAvatar; + + @FXML + private Button btnRemoveAvatar; + @FXML private Label lblUsername; @@ -184,6 +203,42 @@ public class MainLayoutController { } } + @FXML + void btnChangeAvatarClicked(ActionEvent event) { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Choose Profile Picture"); + chooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg", "*.jpeg", "*.gif") + ); + java.io.File file = chooser.showOpenDialog(btnChangeAvatar.getScene().getWindow()); + if (file == null) { + return; + } + + try { + AvatarUploadResponse response = AuthApi.getInstance().uploadAvatar(file.toPath()); + UserSession.getInstance().setAvatarUrl(response.getAvatarUrl()); + renderAvatar(UserSession.getInstance().getEmployeeName(), response.getAvatarUrl()); + btnRemoveAvatar.setDisable(response.getAvatarUrl() == null || response.getAvatarUrl().isBlank()); + } catch (Exception e) { + ActivityLogger.getInstance().logException("MainLayoutController.btnChangeAvatarClicked", e, "Uploading avatar"); + showAvatarError(e.getMessage() != null ? e.getMessage() : "Could not upload profile picture."); + } + } + + @FXML + void btnRemoveAvatarClicked(ActionEvent event) { + try { + AuthApi.getInstance().deleteAvatar(); + UserSession.getInstance().setAvatarUrl(null); + renderAvatar(UserSession.getInstance().getEmployeeName(), null); + btnRemoveAvatar.setDisable(true); + } catch (Exception e) { + ActivityLogger.getInstance().logException("MainLayoutController.btnRemoveAvatarClicked", e, "Deleting avatar"); + showAvatarError(e.getMessage() != null ? e.getMessage() : "Could not remove profile picture."); + } + } + @FXML void btnLogoutClicked(ActionEvent event) { ChatRealtimeClient.getInstance().disconnect(); @@ -208,6 +263,9 @@ public class MainLayoutController { @FXML public void initialize() { logoContainer.getChildren().setAll(SvgWebViewFactory.build("/org/example/petshopdesktop/images/leons-pet-store-badge-light.svg", 94)); + renderAvatar(UserSession.getInstance().getEmployeeName(), UserSession.getInstance().getAvatarUrl()); + btnRemoveAvatar.setDisable(UserSession.getInstance().getAvatarUrl() == null || UserSession.getInstance().getAvatarUrl().isBlank()); + refreshProfileHeader(); applyRBAC(); UserSession session = UserSession.getInstance(); @@ -220,6 +278,77 @@ public class MainLayoutController { } } + private void refreshProfileHeader() { + new Thread(() -> { + try { + UserInfoResponse userInfo = AuthApi.getInstance().getCurrentUser(); + String displayName = userInfo.getFullName() == null || userInfo.getFullName().isBlank() + ? UserSession.getInstance().getUsername() + : userInfo.getFullName(); + Platform.runLater(() -> { + UserSession.getInstance().setEmployeeName(displayName); + UserSession.getInstance().setAvatarUrl(userInfo.getAvatarUrl()); + lblUsername.setText(displayName); + renderAvatar(displayName, userInfo.getAvatarUrl()); + btnRemoveAvatar.setDisable(userInfo.getAvatarUrl() == null || userInfo.getAvatarUrl().isBlank()); + }); + } catch (Exception e) { + Platform.runLater(() -> renderAvatar(UserSession.getInstance().getEmployeeName(), UserSession.getInstance().getAvatarUrl())); + } + }).start(); + } + + private void renderAvatar(String displayName, String avatarUrl) { + Circle border = new Circle(29); + border.setFill(Color.web("#dbe4ee")); + + Circle circle = new Circle(26); + Label initials = new Label(initials(displayName)); + initials.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 16px;"); + + if (avatarUrl != null && !avatarUrl.isBlank()) { + try { + String resolvedUrl = avatarUrl.startsWith("http") ? avatarUrl : ApiConfig.getInstance().getBaseUrl() + avatarUrl; + Image image = new Image(resolvedUrl, 52, 52, true, true, true); + if (!image.isError()) { + circle.setFill(new ImagePattern(image)); + initials.setVisible(false); + } else { + circle.setFill(Color.web("#4ECDC4")); + initials.setVisible(true); + } + } catch (Exception e) { + circle.setFill(Color.web("#4ECDC4")); + initials.setVisible(true); + } + } else { + circle.setFill(Color.web("#4ECDC4")); + initials.setVisible(true); + } + + avatarPreview.getChildren().setAll(border, circle, initials); + } + + private String initials(String displayName) { + if (displayName == null || displayName.isBlank()) { + return "?"; + } + + String[] parts = displayName.trim().split("\\s+"); + if (parts.length == 1) { + return parts[0].substring(0, 1).toUpperCase(); + } + return (parts[0].substring(0, 1) + parts[parts.length - 1].substring(0, 1)).toUpperCase(); + } + + private void showAvatarError(String message) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Profile Picture"); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + private void applyRBAC() { UserSession session = UserSession.getInstance(); diff --git a/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml b/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml index 9daa91e1..4e1cee6f 100644 --- a/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml +++ b/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml @@ -44,6 +44,34 @@ + + + + + + + + + + + + + + From 42f8f568ae3fd561b4fba4cdc3f22fe7ba941777 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sat, 14 Mar 2026 20:12:48 -0600 Subject: [PATCH 4/6] use user phone --- .../api/dto/auth/UserInfoResponse.java | 9 +++++++++ .../petshopdesktop/api/dto/user/UserRequest.java | 9 +++++++++ .../petshopdesktop/api/dto/user/UserResponse.java | 9 +++++++++ .../controllers/StaffAccountsController.java | 2 +- .../StaffRegisterDialogController.java | 12 ++++++++++++ 5 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/petshopdesktop/api/dto/auth/UserInfoResponse.java b/src/main/java/org/example/petshopdesktop/api/dto/auth/UserInfoResponse.java index 4fc49442..fe83893c 100644 --- a/src/main/java/org/example/petshopdesktop/api/dto/auth/UserInfoResponse.java +++ b/src/main/java/org/example/petshopdesktop/api/dto/auth/UserInfoResponse.java @@ -5,6 +5,7 @@ public class UserInfoResponse { private String username; private String email; private String fullName; + private String phone; private String avatarUrl; private String role; private Long storeId; @@ -45,6 +46,14 @@ public class UserInfoResponse { this.fullName = fullName; } + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + public String getAvatarUrl() { return avatarUrl; } diff --git a/src/main/java/org/example/petshopdesktop/api/dto/user/UserRequest.java b/src/main/java/org/example/petshopdesktop/api/dto/user/UserRequest.java index 6a1884a8..a6c9f669 100644 --- a/src/main/java/org/example/petshopdesktop/api/dto/user/UserRequest.java +++ b/src/main/java/org/example/petshopdesktop/api/dto/user/UserRequest.java @@ -5,6 +5,7 @@ public class UserRequest { private String password; private String fullName; private String email; + private String phone; private String role; private Boolean active; @@ -43,6 +44,14 @@ public class UserRequest { this.email = email; } + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + public String getRole() { return role; } diff --git a/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java b/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java index 32d997a2..3a42f128 100644 --- a/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java +++ b/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java @@ -7,6 +7,7 @@ public class UserResponse { private String username; private String fullName; private String email; + private String phone; private String role; private Boolean active; private LocalDateTime createdAt; @@ -47,6 +48,14 @@ public class UserResponse { this.email = email; } + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + public String getRole() { return role; } diff --git a/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java b/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java index 59cf5dfb..3788b645 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java @@ -143,7 +143,7 @@ public class StaffAccountsController { String firstName = names[0]; String lastName = names[1]; String email = user.getEmail() != null ? user.getEmail() : ""; - String phone = ""; + String phone = user.getPhone() != null ? user.getPhone() : ""; boolean active = user.getActive() != null ? user.getActive() : false; Timestamp createdAt = user.getCreatedAt() != null ? Timestamp.from(user.getCreatedAt().atZone(ZoneId.systemDefault()).toInstant()) diff --git a/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java b/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java index ae0d6600..9b480b96 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java @@ -11,6 +11,7 @@ import javafx.scene.control.TextField; import javafx.stage.Stage; import org.example.petshopdesktop.api.dto.user.UserRequest; import org.example.petshopdesktop.api.endpoints.UserApi; +import org.example.petshopdesktop.Validator; import org.example.petshopdesktop.util.ActivityLogger; public class StaffRegisterDialogController { @@ -49,6 +50,7 @@ public class StaffRegisterDialogController { String firstName = value(txtFirstName); String lastName = value(txtLastName); String email = value(txtEmail); + String phone = value(txtPhone); String username = value(txtUsername); String password = txtPassword.getText() == null ? "" : txtPassword.getText(); String confirm = txtPasswordConfirm.getText() == null ? "" : txtPasswordConfirm.getText(); @@ -61,6 +63,15 @@ public class StaffRegisterDialogController { lblError.setText("Email is required."); return; } + if (phone.isBlank()) { + lblError.setText("Phone is required."); + return; + } + String phoneError = Validator.isValidPhoneNumber(phone, "Phone"); + if (!phoneError.isEmpty()) { + lblError.setText(phoneError.trim()); + return; + } if (username.isBlank()) { lblError.setText("Username is required."); return; @@ -83,6 +94,7 @@ public class StaffRegisterDialogController { request.setPassword(password); request.setFullName(firstName + " " + lastName); request.setEmail(email); + request.setPhone(phone); request.setRole("STAFF"); request.setActive(true); From 4b024984cdb3bedf12ab5484b11417dc2bb702d3 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sat, 14 Mar 2026 20:22:14 -0600 Subject: [PATCH 5/6] use employee api --- .../api/dto/employee/EmployeeRequest.java | 29 ++++++++++++ .../api/dto/employee/EmployeeResponse.java | 43 ++++++++++++++++++ .../api/endpoints/EmployeeApi.java | 44 +++++++++++++++++++ .../controllers/StaffAccountsController.java | 29 ++++++------ .../StaffRegisterDialogController.java | 11 ++--- 5 files changed, 137 insertions(+), 19 deletions(-) create mode 100644 src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeRequest.java create mode 100644 src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java create mode 100644 src/main/java/org/example/petshopdesktop/api/endpoints/EmployeeApi.java diff --git a/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeRequest.java b/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeRequest.java new file mode 100644 index 00000000..f047f641 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeRequest.java @@ -0,0 +1,29 @@ +package org.example.petshopdesktop.api.dto.employee; + +public class EmployeeRequest { + private String username; + private String password; + private String firstName; + private String lastName; + private String email; + private String phone; + private String role; + private Boolean active; + + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } + public Boolean getActive() { return active; } + public void setActive(Boolean active) { this.active = active; } +} diff --git a/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java b/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java new file mode 100644 index 00000000..030488c1 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java @@ -0,0 +1,43 @@ +package org.example.petshopdesktop.api.dto.employee; + +import java.time.LocalDateTime; + +public class EmployeeResponse { + private Long employeeId; + private Long userId; + private String username; + private String firstName; + private String lastName; + private String fullName; + private String email; + private String phone; + private String role; + private Boolean active; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public Long getEmployeeId() { return employeeId; } + public void setEmployeeId(Long employeeId) { this.employeeId = employeeId; } + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + public String getFullName() { return fullName; } + public void setFullName(String fullName) { this.fullName = fullName; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } + public Boolean getActive() { return active; } + public void setActive(Boolean active) { this.active = active; } + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + public LocalDateTime getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } +} diff --git a/src/main/java/org/example/petshopdesktop/api/endpoints/EmployeeApi.java b/src/main/java/org/example/petshopdesktop/api/endpoints/EmployeeApi.java new file mode 100644 index 00000000..6cc81e29 --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/api/endpoints/EmployeeApi.java @@ -0,0 +1,44 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.common.PageResponse; +import org.example.petshopdesktop.api.dto.employee.EmployeeRequest; +import org.example.petshopdesktop.api.dto.employee.EmployeeResponse; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class EmployeeApi { + private static final EmployeeApi INSTANCE = new EmployeeApi(); + private final ApiClient apiClient; + + private EmployeeApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static EmployeeApi getInstance() { + return INSTANCE; + } + + public List listEmployees(String query) throws Exception { + String path = "/api/v1/employees?page=0&size=1000"; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from employees endpoint"); + } + return pageResponse.getContent(); + } + + public EmployeeResponse createEmployee(EmployeeRequest request) throws Exception { + return apiClient.post("/api/v1/employees", request, EmployeeResponse.class); + } +} diff --git a/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java b/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java index 3788b645..746b3361 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java @@ -16,8 +16,8 @@ import javafx.scene.control.TextField; import javafx.scene.control.cell.PropertyValueFactory; import javafx.stage.Modality; import javafx.stage.Stage; -import org.example.petshopdesktop.api.dto.user.UserResponse; -import org.example.petshopdesktop.api.endpoints.UserApi; +import org.example.petshopdesktop.api.dto.employee.EmployeeResponse; +import org.example.petshopdesktop.api.endpoints.EmployeeApi; import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.models.StaffAccount; import org.example.petshopdesktop.util.ActivityLogger; @@ -116,8 +116,8 @@ public class StaffAccountsController { new Thread(() -> { try { - List users = UserApi.getInstance().listUsers(null); - List accounts = users.stream() + List employees = EmployeeApi.getInstance().listEmployees(null); + List accounts = employees.stream() .map(this::mapToStaffAccount) .collect(Collectors.toList()); @@ -135,21 +135,22 @@ public class StaffAccountsController { }).start(); } - private StaffAccount mapToStaffAccount(UserResponse user) { - long id = user.getId() != null ? user.getId() : 0L; - String username = user.getUsername(); - String fullName = user.getFullName() != null ? user.getFullName() : ""; + private StaffAccount mapToStaffAccount(EmployeeResponse employee) { + long userId = employee.getUserId() != null ? employee.getUserId() : 0L; + long employeeId = employee.getEmployeeId() != null ? employee.getEmployeeId() : 0L; + String username = employee.getUsername(); + String fullName = employee.getFullName() != null ? employee.getFullName() : ""; String[] names = splitFullName(fullName); String firstName = names[0]; String lastName = names[1]; - String email = user.getEmail() != null ? user.getEmail() : ""; - String phone = user.getPhone() != null ? user.getPhone() : ""; - boolean active = user.getActive() != null ? user.getActive() : false; - Timestamp createdAt = user.getCreatedAt() != null - ? Timestamp.from(user.getCreatedAt().atZone(ZoneId.systemDefault()).toInstant()) + String email = employee.getEmail() != null ? employee.getEmail() : ""; + String phone = employee.getPhone() != null ? employee.getPhone() : ""; + boolean active = employee.getActive() != null ? employee.getActive() : false; + Timestamp createdAt = employee.getCreatedAt() != null + ? Timestamp.from(employee.getCreatedAt().atZone(ZoneId.systemDefault()).toInstant()) : null; - return new StaffAccount(id, id, username, firstName, lastName, email, phone, active, createdAt); + return new StaffAccount(userId, employeeId, username, firstName, lastName, email, phone, active, createdAt); } private String[] splitFullName(String fullName) { diff --git a/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java b/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java index 9b480b96..8d121dde 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java @@ -9,8 +9,8 @@ import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; import javafx.stage.Stage; -import org.example.petshopdesktop.api.dto.user.UserRequest; -import org.example.petshopdesktop.api.endpoints.UserApi; +import org.example.petshopdesktop.api.dto.employee.EmployeeRequest; +import org.example.petshopdesktop.api.endpoints.EmployeeApi; import org.example.petshopdesktop.Validator; import org.example.petshopdesktop.util.ActivityLogger; @@ -89,16 +89,17 @@ public class StaffRegisterDialogController { new Thread(() -> { try { - UserRequest request = new UserRequest(); + EmployeeRequest request = new EmployeeRequest(); request.setUsername(username); request.setPassword(password); - request.setFullName(firstName + " " + lastName); + request.setFirstName(firstName); + request.setLastName(lastName); request.setEmail(email); request.setPhone(phone); request.setRole("STAFF"); request.setActive(true); - UserApi.getInstance().createUser(request); + EmployeeApi.getInstance().createEmployee(request); Platform.runLater(() -> { Alert alert = new Alert(Alert.AlertType.INFORMATION); From 730d39c63efeede47611b1823209ebbfe71c8b43 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sat, 14 Mar 2026 21:35:38 -0600 Subject: [PATCH 6/6] edit staff accounts --- .../api/endpoints/EmployeeApi.java | 4 + .../controllers/StaffAccountsController.java | 45 +++++- .../StaffEditDialogController.java | 144 ++++++++++++++++++ .../petshopdesktop/models/StaffAccount.java | 8 +- .../dialogviews/staff-edit-dialog-view.fxml | 96 ++++++++++++ .../modelviews/staff-accounts-view.fxml | 8 + 6 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java create mode 100644 src/main/resources/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml diff --git a/src/main/java/org/example/petshopdesktop/api/endpoints/EmployeeApi.java b/src/main/java/org/example/petshopdesktop/api/endpoints/EmployeeApi.java index 6cc81e29..e3a8ad52 100644 --- a/src/main/java/org/example/petshopdesktop/api/endpoints/EmployeeApi.java +++ b/src/main/java/org/example/petshopdesktop/api/endpoints/EmployeeApi.java @@ -41,4 +41,8 @@ public class EmployeeApi { public EmployeeResponse createEmployee(EmployeeRequest request) throws Exception { return apiClient.post("/api/v1/employees", request, EmployeeResponse.class); } + + public EmployeeResponse updateEmployee(Long id, EmployeeRequest request) throws Exception { + return apiClient.put("/api/v1/employees/" + id, request, EmployeeResponse.class); + } } diff --git a/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java b/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java index 746b3361..ffb67aa9 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java @@ -59,6 +59,9 @@ public class StaffAccountsController { @FXML private Button btnCreateAccount; + @FXML + private Button btnEditAccount; + private final ObservableList staffAccounts = FXCollections.observableArrayList(); private FilteredList filtered; @@ -76,13 +79,26 @@ public class StaffAccountsController { txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n)); + tvStaff.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> { + if (btnEditAccount != null) { + btnEditAccount.setDisable(newValue == null); + } + }); + if (!UserSession.getInstance().isAdmin()) { lblError.setText("Access restricted."); tvStaff.setDisable(true); btnCreateAccount.setDisable(true); + if (btnEditAccount != null) { + btnEditAccount.setDisable(true); + } return; } + if (btnEditAccount != null) { + btnEditAccount.setDisable(true); + } + refresh(); } @@ -110,6 +126,32 @@ public class StaffAccountsController { } } + @FXML + void btnEditAccountClicked(ActionEvent event) { + lblError.setText(""); + StaffAccount selected = tvStaff.getSelectionModel().getSelectedItem(); + if (selected == null) { + lblError.setText("Select a staff account to edit."); + return; + } + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml")); + Stage dialog = new Stage(); + dialog.initOwner(tvStaff.getScene().getWindow()); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setTitle("Edit Staff Account"); + dialog.setScene(new Scene(loader.load())); + dialog.setResizable(false); + var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController(); + controller.setStaffAccount(selected); + dialog.showAndWait(); + refresh(); + } catch (Exception e) { + ActivityLogger.getInstance().logException("StaffAccountsController.btnEditAccountClicked", e, "Opening staff edit dialog"); + lblError.setText("Could not open staff account editor."); + } + } + private void refresh() { lblError.setText(""); tvStaff.setDisable(true); @@ -145,12 +187,13 @@ public class StaffAccountsController { String lastName = names[1]; String email = employee.getEmail() != null ? employee.getEmail() : ""; String phone = employee.getPhone() != null ? employee.getPhone() : ""; + String role = employee.getRole() != null ? employee.getRole() : "STAFF"; boolean active = employee.getActive() != null ? employee.getActive() : false; Timestamp createdAt = employee.getCreatedAt() != null ? Timestamp.from(employee.getCreatedAt().atZone(ZoneId.systemDefault()).toInstant()) : null; - return new StaffAccount(userId, employeeId, username, firstName, lastName, email, phone, active, createdAt); + return new StaffAccount(userId, employeeId, username, firstName, lastName, email, phone, role, active, createdAt); } private String[] splitFullName(String fullName) { diff --git a/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java b/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java new file mode 100644 index 00000000..5ed6316e --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java @@ -0,0 +1,144 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.stage.Stage; +import org.example.petshopdesktop.Validator; +import org.example.petshopdesktop.api.dto.employee.EmployeeRequest; +import org.example.petshopdesktop.api.endpoints.EmployeeApi; +import org.example.petshopdesktop.models.StaffAccount; +import org.example.petshopdesktop.util.ActivityLogger; + +public class StaffEditDialogController { + + @FXML + private TextField txtFirstName; + + @FXML + private TextField txtLastName; + + @FXML + private TextField txtEmail; + + @FXML + private TextField txtPhone; + + @FXML + private TextField txtUsername; + + @FXML + private PasswordField txtPassword; + + @FXML + private PasswordField txtPasswordConfirm; + + @FXML + private Label lblError; + + @FXML + private Button btnSave; + + private StaffAccount staffAccount; + + public void setStaffAccount(StaffAccount staffAccount) { + this.staffAccount = staffAccount; + txtFirstName.setText(staffAccount.getFirstName()); + txtLastName.setText(staffAccount.getLastName()); + txtEmail.setText(staffAccount.getEmail()); + txtPhone.setText(staffAccount.getPhone()); + txtUsername.setText(staffAccount.getUsername()); + } + + @FXML + void btnSaveClicked(ActionEvent event) { + lblError.setText(""); + if (staffAccount == null) { + lblError.setText("No staff account selected."); + return; + } + + String firstName = value(txtFirstName); + String lastName = value(txtLastName); + String email = value(txtEmail); + String phone = value(txtPhone); + String username = value(txtUsername); + String password = txtPassword.getText() == null ? "" : txtPassword.getText().trim(); + String confirm = txtPasswordConfirm.getText() == null ? "" : txtPasswordConfirm.getText().trim(); + + if (firstName.isBlank() || lastName.isBlank()) { + lblError.setText("First name and last name are required."); + return; + } + if (email.isBlank()) { + lblError.setText("Email is required."); + return; + } + if (phone.isBlank()) { + lblError.setText("Phone is required."); + return; + } + String phoneError = Validator.isValidPhoneNumber(phone, "Phone"); + if (!phoneError.isEmpty()) { + lblError.setText(phoneError.trim()); + return; + } + if (username.isBlank()) { + lblError.setText("Username is required."); + return; + } + if (!password.isEmpty() && password.length() < 6) { + lblError.setText("Password must be at least 6 characters."); + return; + } + if (!password.equals(confirm)) { + lblError.setText("Passwords do not match."); + return; + } + + btnSave.setDisable(true); + + new Thread(() -> { + try { + EmployeeRequest request = new EmployeeRequest(); + request.setUsername(username); + request.setPassword(password.isEmpty() ? null : password); + request.setFirstName(firstName); + request.setLastName(lastName); + request.setEmail(email); + request.setPhone(phone); + request.setRole(staffAccount.getRole()); + request.setActive(staffAccount.isActive()); + + EmployeeApi.getInstance().updateEmployee(staffAccount.getEmployeeId(), request); + + Platform.runLater(this::close); + } catch (Exception e) { + ActivityLogger.getInstance().logException("StaffEditDialogController.btnSaveClicked", e, "Updating staff account"); + String msg = e.getMessage() == null ? "Could not update staff account." : e.getMessage(); + Platform.runLater(() -> { + lblError.setText(msg); + btnSave.setDisable(false); + }); + } + }).start(); + } + + @FXML + void btnCancelClicked(ActionEvent event) { + close(); + } + + private void close() { + Stage stage = (Stage) btnSave.getScene().getWindow(); + stage.close(); + } + + private static String value(TextField tf) { + return tf.getText() == null ? "" : tf.getText().trim(); + } +} diff --git a/src/main/java/org/example/petshopdesktop/models/StaffAccount.java b/src/main/java/org/example/petshopdesktop/models/StaffAccount.java index e1b0ab62..59923338 100644 --- a/src/main/java/org/example/petshopdesktop/models/StaffAccount.java +++ b/src/main/java/org/example/petshopdesktop/models/StaffAccount.java @@ -10,10 +10,11 @@ public class StaffAccount { private final String lastName; private final String email; private final String phone; + private final String role; private final boolean active; private final Timestamp createdAt; - public StaffAccount(long userId, long employeeId, String username, String firstName, String lastName, String email, String phone, boolean active, Timestamp createdAt) { + public StaffAccount(long userId, long employeeId, String username, String firstName, String lastName, String email, String phone, String role, boolean active, Timestamp createdAt) { this.userId = userId; this.employeeId = employeeId; this.username = username; @@ -21,6 +22,7 @@ public class StaffAccount { this.lastName = lastName; this.email = email; this.phone = phone; + this.role = role; this.active = active; this.createdAt = createdAt; } @@ -59,6 +61,10 @@ public class StaffAccount { return phone; } + public String getRole() { + return role; + } + public boolean isActive() { return active; } diff --git a/src/main/resources/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml b/src/main/resources/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml new file mode 100644 index 00000000..354ea20d --- /dev/null +++ b/src/main/resources/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + +