diff --git a/backend/src/main/java/com/petshop/backend/config/ActivityLoggingFilter.java b/backend/src/main/java/com/petshop/backend/config/ActivityLoggingFilter.java index 660ddeda..0c30cece 100644 --- a/backend/src/main/java/com/petshop/backend/config/ActivityLoggingFilter.java +++ b/backend/src/main/java/com/petshop/backend/config/ActivityLoggingFilter.java @@ -124,11 +124,15 @@ public class ActivityLoggingFilter extends OncePerRequestFilter { } } case "chat" -> { - if ("conversations".equals(seg3) && isPost) return "Started a new chat conversation"; - if (seg3IsId && "messages".equals(sub) && isPost) return "Sent a chat message"; - if (seg3IsId && "attachments".equals(sub) && isPost) return "Sent a file in chat"; - if (seg3IsId && "request-human".equals(sub) && isPost) return "Requested human support in chat"; - if (seg3IsId && isPut) return "Updated chat conversation #" + id; + if ("conversations".equals(seg3) && isPost && seg4 == null) return "Started a new chat conversation"; + if ("conversations".equals(seg3) && seg4IsId) { + String convId = seg4; + String chatSub = parts.length > 5 ? parts[5] : null; + if ("messages".equals(seg5) && isPost) return "Sent a chat message"; + if ("attachments".equals(seg5) && isPost) return "Sent a file in chat"; + if ("request-human".equals(seg5) && isPost) return "Requested human support in chat"; + if (chatSub == null && isPut) return "Updated chat conversation #" + convId; + } } case "ai-chat" -> { if ("message".equals(seg3) && isPost) return "Sent a message to the AI assistant"; diff --git a/backend/src/main/java/com/petshop/backend/service/EmailService.java b/backend/src/main/java/com/petshop/backend/service/EmailService.java index 667aad08..f92d36ad 100644 --- a/backend/src/main/java/com/petshop/backend/service/EmailService.java +++ b/backend/src/main/java/com/petshop/backend/service/EmailService.java @@ -145,10 +145,10 @@ public class EmailService { .html(html) .build(); resend.emails().send(options); - activityLogService.record(recipientUserId, "Email sent: " + subject + " → " + to); + activityLogService.record(recipientUserId, "Sent an email | Email sent: " + subject + " → " + to); } catch (Exception ex) { log.error("Failed to send email '{}' to {}: {}", subject, to, ex.getMessage()); - activityLogService.record(recipientUserId, "Email failed: " + subject + " → " + to); + activityLogService.record(recipientUserId, "Failed to send email | Email failed: " + subject + " → " + to); } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ActivityLogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ActivityLogController.java index 02bf0ce3..06f94874 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ActivityLogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ActivityLogController.java @@ -8,10 +8,14 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; import javafx.scene.control.DatePicker; import javafx.scene.control.Label; +import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; import org.example.petshopdesktop.api.dto.activity.ActivityLogResponse; import org.example.petshopdesktop.api.endpoints.ActivityLogApi; import org.example.petshopdesktop.util.ActivityLogger; @@ -20,73 +24,105 @@ import org.example.petshopdesktop.util.TableViewSupport; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.TreeSet; public class ActivityLogController { - @FXML - private TableView tvActivityLogs; + private static final String SEPARATOR_NEW = " | "; + private static final String SEPARATOR_OLD = " \u00b7 "; + private static final DateTimeFormatter DISPLAY_FORMATTER = DateTimeFormatter.ofPattern("MMM d, HH:mm"); + private static final int DEFAULT_LIMIT = 2000; - @FXML - private TableColumn colTimestamp; - - @FXML - private TableColumn colUser; - - @FXML - private TableColumn colRole; - - @FXML - private TableColumn colStore; - - @FXML - private TableColumn colActivity; - - @FXML - private Button btnRefresh; - - @FXML - private DatePicker dpStart; - - @FXML - private DatePicker dpEnd; - - @FXML - private CheckBox chkHideViewOnly; - - @FXML - private Label lblStatus; - - @FXML - private Label lblError; + @FXML private TableView tvActivityLogs; + @FXML private TableColumn colTimestamp; + @FXML private TableColumn colUser; + @FXML private TableColumn colRole; + @FXML private TableColumn colStore; + @FXML private TableColumn colActivity; + @FXML private Button btnRefresh; + @FXML private DatePicker dpStart; + @FXML private DatePicker dpEnd; + @FXML private CheckBox chkHideViewOnly; + @FXML private TextField tfSearch; + @FXML private ComboBox cbRoleFilter; + @FXML private ComboBox cbStoreFilter; + @FXML private Label lblStatus; + @FXML private Label lblError; private final ObservableList activityLogs = FXCollections.observableArrayList(); private FilteredList filteredLogs; - private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - private static final int DEFAULT_LIMIT = 2000; @FXML public void initialize() { colTimestamp.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getLogTimestamp())); - colTimestamp.setCellFactory(column -> new javafx.scene.control.TableCell<>() { + colTimestamp.setCellFactory(col -> new TableCell<>() { @Override protected void updateItem(Object item, boolean empty) { super.updateItem(item, empty); if (empty || item == null) { setText(null); } else if (item instanceof java.time.LocalDateTime time) { - setText(time.format(formatter)); + setText(time.format(DISPLAY_FORMATTER)); } else { setText(item.toString()); } } }); + colUser.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(displayUser(data.getValue()))); - colRole.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(displayRole(data.getValue()))); + colRole.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(formatRole(displayRole(data.getValue())))); colStore.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(displayStore(data.getValue()))); + colActivity.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(nullToBlank(data.getValue().getActivity()))); + colActivity.setCellFactory(col -> new TableCell<>() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null || item.isBlank()) { + setGraphic(null); + setText(null); + return; + } + int sep = item.indexOf(SEPARATOR_NEW); + int sepLen = SEPARATOR_NEW.length(); + if (sep < 0) { + sep = item.indexOf(SEPARATOR_OLD); + sepLen = SEPARATOR_OLD.length(); + } + if (sep >= 0) { + String description = item.substring(0, sep).trim(); + String technical = item.substring(sep + sepLen).trim(); + Label lblDesc = new Label(description); + lblDesc.setStyle("-fx-font-weight: bold; -fx-text-fill: #1f2937;"); + lblDesc.setWrapText(true); + Label lblTech = new Label(technical); + lblTech.setStyle("-fx-font-size: 11px; -fx-text-fill: #94a3b8; -fx-font-family: monospace;"); + lblTech.setWrapText(true); + VBox box = new VBox(2, lblDesc, lblTech); + setGraphic(box); + setText(null); + } else { + setGraphic(null); + setText(item); + } + } + }); + + tvActivityLogs.setFixedCellSize(52); + + cbRoleFilter.setItems(FXCollections.observableArrayList("All Roles", "ADMIN", "STAFF", "CUSTOMER")); + cbRoleFilter.getSelectionModel().selectFirst(); + cbRoleFilter.setOnAction(e -> applyFilters()); + + cbStoreFilter.setItems(FXCollections.observableArrayList("All Stores")); + cbStoreFilter.getSelectionModel().selectFirst(); + cbStoreFilter.setOnAction(e -> applyFilters()); + + tfSearch.textProperty().addListener((obs, oldVal, newVal) -> applyFilters()); filteredLogs = new FilteredList<>(activityLogs, a -> true); TableViewSupport.bindSortedItems(tvActivityLogs, filteredLogs); + dpStart.setValue(LocalDate.now().minusDays(30)); dpEnd.setValue(LocalDate.now()); loadLogs(); @@ -103,22 +139,59 @@ public class ActivityLogController { } @FXML - void onHideViewOnlyChanged(ActionEvent event) { - applyViewOnlyFilter(); + void onFilterChanged(ActionEvent event) { + applyFilters(); } - private void applyViewOnlyFilter() { - boolean hide = chkHideViewOnly != null && chkHideViewOnly.isSelected(); - if (filteredLogs != null) { - filteredLogs.setPredicate(hide - ? a -> { - String act = a.getActivity(); - if (act == null) return true; - String trimmed = act.stripLeading(); - return !trimmed.startsWith("View"); + private void applyFilters() { + String search = tfSearch != null && tfSearch.getText() != null ? tfSearch.getText().trim().toLowerCase() : ""; + String role = cbRoleFilter != null ? cbRoleFilter.getValue() : null; + String store = cbStoreFilter != null ? cbStoreFilter.getValue() : null; + boolean hideViewOnly = chkHideViewOnly != null && chkHideViewOnly.isSelected(); + + if (filteredLogs == null) return; + + filteredLogs.setPredicate(log -> { + if (hideViewOnly) { + String act = log.getActivity(); + if (act != null && act.stripLeading().startsWith("View")) return false; + } + + if (role != null && !role.equals("All Roles")) { + String logRole = displayRole(log); + if (!role.equalsIgnoreCase(logRole)) return false; + } + + if (store != null && !store.equals("All Stores")) { + if (!store.equals(displayStore(log))) return false; + } + + if (!search.isEmpty()) { + String activity = nullToBlank(log.getActivity()).toLowerCase(); + String user = displayUser(log).toLowerCase(); + String username = nullToBlank(log.getUsername()).toLowerCase(); + String usernameSnap = nullToBlank(log.getUsernameSnapshot()).toLowerCase(); + if (!activity.contains(search) && !user.contains(search) + && !username.contains(search) && !usernameSnap.contains(search)) { + return false; } - : a -> true); + } + + return true; + }); + } + + private void populateStoreFilter(List logs) { + TreeSet stores = new TreeSet<>(); + for (ActivityLogResponse log : logs) { + String s = displayStore(log); + if (!s.isBlank()) stores.add(s); } + String current = cbStoreFilter.getValue(); + ObservableList options = FXCollections.observableArrayList("All Stores"); + options.addAll(stores); + cbStoreFilter.setItems(options); + cbStoreFilter.setValue(options.contains(current) ? current : "All Stores"); } private void loadLogs() { @@ -126,8 +199,8 @@ public class ActivityLogController { tvActivityLogs.setDisable(true); btnRefresh.setDisable(true); - java.time.LocalDate startDate = dpStart != null ? dpStart.getValue() : null; - java.time.LocalDate endDate = dpEnd != null ? dpEnd.getValue() : null; + LocalDate startDate = dpStart != null ? dpStart.getValue() : null; + LocalDate endDate = dpEnd != null ? dpEnd.getValue() : null; new Thread(() -> { try { @@ -136,7 +209,8 @@ public class ActivityLogController { Platform.runLater(() -> { activityLogs.setAll(safeContent); - applyViewOnlyFilter(); + populateStoreFilter(safeContent); + applyFilters(); tvActivityLogs.setDisable(false); btnRefresh.setDisable(false); TableViewSupport.flashStatus(lblStatus, "Refreshed"); @@ -145,8 +219,8 @@ public class ActivityLogController { ActivityLogger.getInstance().logException("ActivityLogController.loadLogs", e, "Loading activity logs"); Platform.runLater(() -> { lblError.setText(e.getMessage() == null || e.getMessage().isBlank() - ? "Could not load activity logs." - : "Could not load activity logs: " + e.getMessage()); + ? "Could not load activity logs." + : "Could not load activity logs: " + e.getMessage()); tvActivityLogs.setDisable(false); btnRefresh.setDisable(false); }); @@ -155,41 +229,33 @@ public class ActivityLogController { } private String displayUser(ActivityLogResponse log) { - if (log == null) { - return ""; - } - if (log.getFullNameSnapshot() != null && !log.getFullNameSnapshot().isBlank()) { - return log.getFullNameSnapshot(); - } - if (log.getFullName() != null && !log.getFullName().isBlank()) { - return log.getFullName(); - } - if (log.getUsernameSnapshot() != null && !log.getUsernameSnapshot().isBlank()) { - return log.getUsernameSnapshot(); - } - if (log.getUsername() != null && !log.getUsername().isBlank()) { - return log.getUsername(); - } + if (log == null) return ""; + if (log.getFullNameSnapshot() != null && !log.getFullNameSnapshot().isBlank()) return log.getFullNameSnapshot(); + if (log.getFullName() != null && !log.getFullName().isBlank()) return log.getFullName(); + if (log.getUsernameSnapshot() != null && !log.getUsernameSnapshot().isBlank()) return log.getUsernameSnapshot(); + if (log.getUsername() != null && !log.getUsername().isBlank()) return log.getUsername(); return log.getUserId() != null ? String.valueOf(log.getUserId()) : ""; } private String displayRole(ActivityLogResponse log) { - if (log == null) { - return ""; - } - if (log.getRoleSnapshot() != null && !log.getRoleSnapshot().isBlank()) { - return log.getRoleSnapshot(); - } + if (log == null) return ""; + if (log.getRoleSnapshot() != null && !log.getRoleSnapshot().isBlank()) return log.getRoleSnapshot(); return nullToBlank(log.getRole()); } + private String formatRole(String role) { + if (role == null) return ""; + return switch (role.toUpperCase()) { + case "ADMIN" -> "Admin"; + case "STAFF" -> "Staff"; + case "CUSTOMER" -> "Customer"; + default -> role; + }; + } + private String displayStore(ActivityLogResponse log) { - if (log == null) { - return ""; - } - if (log.getStoreNameSnapshot() != null && !log.getStoreNameSnapshot().isBlank()) { - return log.getStoreNameSnapshot(); - } + if (log == null) return ""; + if (log.getStoreNameSnapshot() != null && !log.getStoreNameSnapshot().isBlank()) return log.getStoreNameSnapshot(); return nullToBlank(log.getStoreName()); } diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/activity-log-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/activity-log-view.fxml index c474f8c9..afc632ac 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/activity-log-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/activity-log-view.fxml @@ -3,57 +3,79 @@ + + + + - + - + + - - -