added filtering to activity logs for desktop

This commit is contained in:
Alex
2026-04-15 02:10:20 -06:00
parent a23171359f
commit a2c8df16b7
4 changed files with 202 additions and 110 deletions

View File

@@ -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<ActivityLogResponse> 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<ActivityLogResponse, Object> colTimestamp;
@FXML
private TableColumn<ActivityLogResponse, String> colUser;
@FXML
private TableColumn<ActivityLogResponse, String> colRole;
@FXML
private TableColumn<ActivityLogResponse, String> colStore;
@FXML
private TableColumn<ActivityLogResponse, String> 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<ActivityLogResponse> tvActivityLogs;
@FXML private TableColumn<ActivityLogResponse, Object> colTimestamp;
@FXML private TableColumn<ActivityLogResponse, String> colUser;
@FXML private TableColumn<ActivityLogResponse, String> colRole;
@FXML private TableColumn<ActivityLogResponse, String> colStore;
@FXML private TableColumn<ActivityLogResponse, String> colActivity;
@FXML private Button btnRefresh;
@FXML private DatePicker dpStart;
@FXML private DatePicker dpEnd;
@FXML private CheckBox chkHideViewOnly;
@FXML private TextField tfSearch;
@FXML private ComboBox<String> cbRoleFilter;
@FXML private ComboBox<String> cbStoreFilter;
@FXML private Label lblStatus;
@FXML private Label lblError;
private final ObservableList<ActivityLogResponse> activityLogs = FXCollections.observableArrayList();
private FilteredList<ActivityLogResponse> 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<ActivityLogResponse> logs) {
TreeSet<String> stores = new TreeSet<>();
for (ActivityLogResponse log : logs) {
String s = displayStore(log);
if (!s.isBlank()) stores.add(s);
}
String current = cbStoreFilter.getValue();
ObservableList<String> 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());
}