Add log viewer
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>>() {}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -220,16 +220,24 @@
|
||||
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
|
||||
</padding>
|
||||
</Button>
|
||||
<Button fx:id="btnStaffAccounts" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnStaffAccountsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="User Accounts" textFill="#cbd5e1">
|
||||
<font>
|
||||
<Font name="System" size="12.0" />
|
||||
</font>
|
||||
<padding>
|
||||
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
|
||||
</padding>
|
||||
</Button>
|
||||
<Button fx:id="btnStaffAccounts" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnStaffAccountsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="User Accounts" textFill="#cbd5e1">
|
||||
<font>
|
||||
<Font name="System" size="12.0" />
|
||||
</font>
|
||||
<padding>
|
||||
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
|
||||
</padding>
|
||||
</Button>
|
||||
<Button fx:id="btnActivityLogs" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnActivityLogsClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Activity Logs" textFill="#cbd5e1">
|
||||
<font>
|
||||
<Font name="System" size="12.0" />
|
||||
</font>
|
||||
<padding>
|
||||
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
|
||||
</padding>
|
||||
</Button>
|
||||
|
||||
<Button fx:id="btnLogout" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnLogoutClicked" style="-fx-background-color: rgba(255,255,255,0.08); -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Logout" textFill="#e2e8f0">
|
||||
<Button fx:id="btnLogout" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnLogoutClicked" style="-fx-background-color: rgba(255,255,255,0.08); -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Logout" textFill="#e2e8f0">
|
||||
<font>
|
||||
<Font name="System" size="12.0" />
|
||||
</font>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TableColumn?>
|
||||
<?import javafx.scene.control.TableView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<VBox spacing="18.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.ActivityLogController">
|
||||
<padding>
|
||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
||||
</padding>
|
||||
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" spacing="20.0">
|
||||
<children>
|
||||
<Label text="Activity Logs" textFill="#2c3e50">
|
||||
<font>
|
||||
<Font name="System Bold" size="30.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<Button fx:id="btnRefresh" mnemonicParsing="false" onAction="#btnRefreshClicked" prefHeight="44.0" prefWidth="118.0" style="-fx-background-color: #4ECDC4; -fx-cursor: hand; -fx-background-radius: 8;" text="Refresh" textFill="WHITE">
|
||||
<font>
|
||||
<Font name="System Bold" size="14.0" />
|
||||
</font>
|
||||
<padding>
|
||||
<Insets bottom="12.0" left="24.0" right="24.0" top="12.0" />
|
||||
</padding>
|
||||
</Button>
|
||||
</children>
|
||||
</HBox>
|
||||
|
||||
<Label text="Showing most recent 2000 activity records" textFill="#64748b" />
|
||||
|
||||
<TableView fx:id="tvActivityLogs" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TableColumn fx:id="colTimestamp" prefWidth="170.0" text="Timestamp" />
|
||||
<TableColumn fx:id="colUser" prefWidth="170.0" text="User" />
|
||||
<TableColumn fx:id="colRole" prefWidth="90.0" text="Role" />
|
||||
<TableColumn fx:id="colStore" prefWidth="160.0" text="Store" />
|
||||
<TableColumn fx:id="colActivity" prefWidth="520.0" text="Activity" />
|
||||
</columns>
|
||||
</TableView>
|
||||
|
||||
<Label fx:id="lblStatus" text="" textFill="#64748b" visible="false" managed="true" />
|
||||
|
||||
<Label fx:id="lblError" text="" textFill="#FF6B6B" wrapText="true" />
|
||||
</children>
|
||||
</VBox>
|
||||
Reference in New Issue
Block a user