Add log viewer

This commit is contained in:
2026-04-11 22:54:27 -06:00
parent c69820241f
commit 50c344091f
7 changed files with 394 additions and 9 deletions

View File

@@ -34,6 +34,7 @@ module org.example.petshopdesktop {
opens org.example.petshopdesktop.api.dto.employee to com.fasterxml.jackson.databind;
opens org.example.petshopdesktop.api.dto.analytics to com.fasterxml.jackson.databind;
opens org.example.petshopdesktop.api.dto.purchaseorder to com.fasterxml.jackson.databind;
opens org.example.petshopdesktop.api.dto.activity to com.fasterxml.jackson.databind;
exports org.example.petshopdesktop;
exports org.example.petshopdesktop.controllers;

View File

@@ -0,0 +1,126 @@
package org.example.petshopdesktop.api.dto.activity;
import java.time.LocalDateTime;
public class ActivityLogResponse {
private Long logId;
private Long userId;
private String username;
private String fullName;
private String role;
private Long storeId;
private String storeName;
private String usernameSnapshot;
private String fullNameSnapshot;
private String roleSnapshot;
private String storeNameSnapshot;
private String activity;
private LocalDateTime logTimestamp;
public ActivityLogResponse() {
}
public Long getLogId() {
return logId;
}
public void setLogId(Long logId) {
this.logId = logId;
}
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 getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
public String getStoreName() {
return storeName;
}
public void setStoreName(String storeName) {
this.storeName = storeName;
}
public String getUsernameSnapshot() {
return usernameSnapshot;
}
public void setUsernameSnapshot(String usernameSnapshot) {
this.usernameSnapshot = usernameSnapshot;
}
public String getFullNameSnapshot() {
return fullNameSnapshot;
}
public void setFullNameSnapshot(String fullNameSnapshot) {
this.fullNameSnapshot = fullNameSnapshot;
}
public String getRoleSnapshot() {
return roleSnapshot;
}
public void setRoleSnapshot(String roleSnapshot) {
this.roleSnapshot = roleSnapshot;
}
public String getStoreNameSnapshot() {
return storeNameSnapshot;
}
public void setStoreNameSnapshot(String storeNameSnapshot) {
this.storeNameSnapshot = storeNameSnapshot;
}
public String getActivity() {
return activity;
}
public void setActivity(String activity) {
this.activity = activity;
}
public LocalDateTime getLogTimestamp() {
return logTimestamp;
}
public void setLogTimestamp(LocalDateTime logTimestamp) {
this.logTimestamp = logTimestamp;
}
}

View File

@@ -0,0 +1,29 @@
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.activity.ActivityLogResponse;
import java.util.List;
public class ActivityLogApi {
private static final ActivityLogApi INSTANCE = new ActivityLogApi();
private final ApiClient apiClient;
private ActivityLogApi() {
this.apiClient = ApiClient.getInstance();
}
public static ActivityLogApi getInstance() {
return INSTANCE;
}
public List<ActivityLogResponse> getActivityLogs(int limit) throws Exception {
String path = "/api/v1/activity-logs?limit=" + limit;
String response = apiClient.getRawResponse(path);
return apiClient.getObjectMapper().readValue(
response,
new TypeReference<List<ActivityLogResponse>>() {}
);
}
}

View File

@@ -0,0 +1,154 @@
package org.example.petshopdesktop.controllers;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import org.example.petshopdesktop.api.dto.activity.ActivityLogResponse;
import org.example.petshopdesktop.api.endpoints.ActivityLogApi;
import org.example.petshopdesktop.util.ActivityLogger;
import org.example.petshopdesktop.util.TableViewSupport;
import java.time.format.DateTimeFormatter;
import java.util.List;
public class ActivityLogController {
@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 Label lblStatus;
@FXML
private Label lblError;
private final ObservableList<ActivityLogResponse> activityLogs = FXCollections.observableArrayList();
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<>() {
@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));
} 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())));
colStore.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(displayStore(data.getValue())));
colActivity.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(nullToBlank(data.getValue().getActivity())));
TableViewSupport.bindSortedItems(tvActivityLogs, new javafx.collections.transformation.FilteredList<>(activityLogs, a -> true));
loadLogs();
}
@FXML
void btnRefreshClicked(ActionEvent event) {
loadLogs();
}
private void loadLogs() {
lblError.setText("");
tvActivityLogs.setDisable(true);
btnRefresh.setDisable(true);
new Thread(() -> {
try {
List<ActivityLogResponse> content = ActivityLogApi.getInstance().getActivityLogs(DEFAULT_LIMIT);
List<ActivityLogResponse> safeContent = content != null ? content : List.of();
Platform.runLater(() -> {
activityLogs.setAll(safeContent);
tvActivityLogs.setDisable(false);
btnRefresh.setDisable(false);
TableViewSupport.flashStatus(lblStatus, "Refreshed");
});
} catch (Exception e) {
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());
tvActivityLogs.setDisable(false);
btnRefresh.setDisable(false);
});
}
}).start();
}
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();
}
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();
}
return nullToBlank(log.getRole());
}
private String displayStore(ActivityLogResponse log) {
if (log == null) {
return "";
}
if (log.getStoreNameSnapshot() != null && !log.getStoreNameSnapshot().isBlank()) {
return log.getStoreNameSnapshot();
}
return nullToBlank(log.getStoreName());
}
private String nullToBlank(String value) {
return value == null ? "" : value;
}
}

View File

@@ -70,6 +70,9 @@ public class MainLayoutController {
@FXML
private Button btnProducts;
@FXML
private Button btnActivityLogs;
@FXML
private Button btnSalesHistory;
@@ -178,6 +181,12 @@ public class MainLayoutController {
updateButtons(btnAnalytics);
}
@FXML
void btnActivityLogsClicked(ActionEvent event) {
loadView("activity-log-view.fxml");
updateButtons(btnActivityLogs);
}
@FXML
void btnServicesClicked(ActionEvent event) {
loadView("service-view.fxml");
@@ -401,6 +410,11 @@ public class MainLayoutController {
btnAnalytics.setManaged(canViewAnalytics);
}
if (btnActivityLogs != null) {
btnActivityLogs.setVisible(isAdmin);
btnActivityLogs.setManaged(isAdmin);
}
btnSalesHistory.setText(isAdmin ? "Sales History" : "Sales");
// Initial chat state and subscription
@@ -451,6 +465,7 @@ public class MainLayoutController {
btnPurchaseOrders,
btnStaffAccounts,
btnAnalytics,
btnActivityLogs,
btnChat
};