Refactor user management

This commit is contained in:
2026-04-09 12:28:33 -06:00
parent 70c883b01b
commit 675bf36908
8 changed files with 179 additions and 121 deletions

View File

@@ -0,0 +1,48 @@
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.user.UserRequest;
import org.example.petshopdesktop.api.dto.user.UserResponse;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class CustomerApi {
private static final CustomerApi INSTANCE = new CustomerApi();
private final ApiClient apiClient;
private CustomerApi() {
this.apiClient = ApiClient.getInstance();
}
public static CustomerApi getInstance() {
return INSTANCE;
}
public List<UserResponse> listCustomers(String query) throws Exception {
String path = "/api/v1/customers?page=0&size=1000";
if (query != null && !query.isEmpty()) {
path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8);
}
String response = apiClient.getRawResponse(path);
PageResponse<UserResponse> pageResponse = apiClient.getObjectMapper().readValue(
response,
new TypeReference<PageResponse<UserResponse>>() {}
);
if (pageResponse == null) {
throw new IllegalStateException("Null response from customers endpoint");
}
return pageResponse.getContent();
}
public UserResponse updateCustomer(Long id, UserRequest request) throws Exception {
return apiClient.put("/api/v1/customers/" + id, request, UserResponse.class);
}
public UserResponse createCustomer(UserRequest request) throws Exception {
return apiClient.post("/api/v1/customers", request, UserResponse.class);
}
}

View File

@@ -41,4 +41,8 @@ public class UserApi {
public UserResponse createUser(UserRequest request) throws Exception {
return apiClient.post("/api/v1/users", request, UserResponse.class);
}
public UserResponse updateUser(Long id, UserRequest request) throws Exception {
return apiClient.put("/api/v1/users/" + id, request, UserResponse.class);
}
}

View File

@@ -370,8 +370,8 @@ public class MainLayoutController {
btnPurchaseOrders.setManaged(isAdmin);
if (btnStaffAccounts != null) {
btnStaffAccounts.setVisible(isAdmin);
btnStaffAccounts.setManaged(isAdmin);
btnStaffAccounts.setVisible(true);
btnStaffAccounts.setManaged(true);
}
if (lblAdminSection != null) {

View File

@@ -16,14 +16,12 @@ 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.employee.EmployeeResponse;
import org.example.petshopdesktop.api.endpoints.EmployeeApi;
import org.example.petshopdesktop.api.dto.user.UserResponse;
import org.example.petshopdesktop.api.endpoints.UserApi;
import org.example.petshopdesktop.api.endpoints.CustomerApi;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.models.StaffAccount;
import org.example.petshopdesktop.util.ActivityLogger;
import java.sql.Timestamp;
import java.time.ZoneId;
import java.util.List;
import java.util.Comparator;
import java.util.stream.Collectors;
@@ -31,25 +29,28 @@ import java.util.stream.Collectors;
public class StaffAccountsController {
@FXML
private TableView<StaffAccount> tvStaff;
private TableView<UserResponse> tvStaff;
@FXML
private TableColumn<StaffAccount, String> colUsername;
private TableColumn<UserResponse, String> colUsername;
@FXML
private TableColumn<StaffAccount, String> colName;
private TableColumn<UserResponse, String> colName;
@FXML
private TableColumn<StaffAccount, String> colEmail;
private TableColumn<UserResponse, String> colEmail;
@FXML
private TableColumn<StaffAccount, String> colPhone;
private TableColumn<UserResponse, String> colPhone;
@FXML
private TableColumn<StaffAccount, String> colStatus;
private TableColumn<UserResponse, String> colRole;
@FXML
private TableColumn<StaffAccount, java.sql.Timestamp> colCreated;
private TableColumn<UserResponse, String> colStatus;
@FXML
private TableColumn<UserResponse, Object> colCreated;
@FXML
private TextField txtSearch;
@@ -63,8 +64,8 @@ public class StaffAccountsController {
@FXML
private Button btnEditAccount;
private final ObservableList<StaffAccount> staffAccounts = FXCollections.observableArrayList();
private FilteredList<StaffAccount> filtered;
private final ObservableList<UserResponse> staffAccounts = FXCollections.observableArrayList();
private FilteredList<UserResponse> filtered;
@FXML
public void initialize() {
@@ -72,7 +73,20 @@ public class StaffAccountsController {
colName.setCellValueFactory(new PropertyValueFactory<>("fullName"));
colEmail.setCellValueFactory(new PropertyValueFactory<>("email"));
colPhone.setCellValueFactory(new PropertyValueFactory<>("phone"));
colStatus.setCellValueFactory(new PropertyValueFactory<>("status"));
colRole.setCellValueFactory(new PropertyValueFactory<>("role"));
colStatus.setCellValueFactory(new PropertyValueFactory<>("active"));
colStatus.setCellFactory(column -> new javafx.scene.control.TableCell<UserResponse, String>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
Boolean active = getTableRow().getItem().getActive();
setText(active != null && active ? "Active" : "Inactive");
}
}
});
colCreated.setCellValueFactory(new PropertyValueFactory<>("createdAt"));
filtered = new FilteredList<>(staffAccounts, a -> true);
@@ -86,16 +100,6 @@ public class StaffAccountsController {
}
});
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);
}
@@ -103,6 +107,9 @@ public class StaffAccountsController {
refresh();
}
refresh();
}
@FXML
void btnRefreshClicked(ActionEvent event) {
refresh();
@@ -130,26 +137,36 @@ public class StaffAccountsController {
@FXML
void btnEditAccountClicked(ActionEvent event) {
lblError.setText("");
StaffAccount selected = tvStaff.getSelectionModel().getSelectedItem();
UserResponse selected = tvStaff.getSelectionModel().getSelectedItem();
if (selected == null) {
lblError.setText("Select a staff account to edit.");
lblError.setText("Select a user account to edit.");
return;
}
UserSession session = UserSession.getInstance();
boolean isAdmin = session.isAdmin();
boolean targetIsAdmin = "ADMIN".equalsIgnoreCase(selected.getRole());
if (isAdmin && targetIsAdmin && !selected.getId().equals(session.getUserId())) {
lblError.setText("Admins cannot edit other admin accounts.");
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.setTitle("Edit User Account");
dialog.setScene(new Scene(loader.load()));
dialog.setResizable(false);
var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController();
controller.setStaffAccount(selected);
controller.setUser(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.");
ActivityLogger.getInstance().logException("StaffAccountsController.btnEditAccountClicked", e, "Opening user edit dialog");
lblError.setText("Could not open user account editor.");
}
}
@@ -159,62 +176,35 @@ public class StaffAccountsController {
new Thread(() -> {
try {
List<EmployeeResponse> employees = EmployeeApi.getInstance().listEmployees(null);
List<StaffAccount> accounts = employees.stream()
.map(this::mapToStaffAccount)
.sorted(Comparator.comparing(StaffAccount::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder())))
UserSession session = UserSession.getInstance();
List<UserResponse> users;
if (session.isAdmin()) {
users = UserApi.getInstance().listUsers(null);
} else {
users = CustomerApi.getInstance().listCustomers(null);
}
List<UserResponse> sortedUsers = users.stream()
.sorted(Comparator.comparing(UserResponse::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder())))
.collect(Collectors.toList());
Platform.runLater(() -> {
staffAccounts.setAll(accounts);
staffAccounts.setAll(sortedUsers);
tvStaff.setDisable(false);
});
} catch (Exception e) {
ActivityLogger.getInstance().logException("StaffAccountsController.refresh", e, "Loading staff accounts");
ActivityLogger.getInstance().logException("StaffAccountsController.refresh", e, "Loading user accounts");
Platform.runLater(() -> {
String message = e.getMessage();
lblError.setText(message == null || message.isBlank()
? "Could not load staff accounts."
: "Could not load staff accounts: " + message);
? "Could not load user accounts."
: "Could not load user accounts: " + message);
tvStaff.setDisable(false);
});
}
}).start();
}
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 firstName = employee.getFirstName() != null ? employee.getFirstName() : "";
String lastName = employee.getLastName() != null ? employee.getLastName() : "";
if (firstName.isBlank() && lastName.isBlank()) {
String fullName = employee.getFullName() != null ? employee.getFullName() : "";
String[] names = splitFullName(fullName);
firstName = names[0];
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, role, active, createdAt);
}
private String[] splitFullName(String fullName) {
if (fullName == null || fullName.trim().isEmpty()) {
return new String[]{"", ""};
}
String[] parts = fullName.trim().split("\\s+", 2);
String firstName = parts.length > 0 ? parts[0] : "";
String lastName = parts.length > 1 ? parts[1] : "";
return new String[]{firstName, lastName};
}
private void applyFilter(String text) {
String q = text == null ? "" : text.trim().toLowerCase();
if (q.isEmpty()) {
@@ -227,7 +217,7 @@ public class StaffAccountsController {
|| safe(a.getFullName()).contains(q)
|| safe(a.getEmail()).contains(q)
|| safe(a.getPhone()).contains(q)
|| safe(a.getStatus()).contains(q)
|| safe(a.getRole()).contains(q)
);
}

View File

@@ -9,10 +9,11 @@ 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.api.dto.user.UserRequest;
import org.example.petshopdesktop.api.dto.user.UserResponse;
import org.example.petshopdesktop.api.endpoints.UserApi;
import org.example.petshopdesktop.api.endpoints.CustomerApi;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.models.StaffAccount;
import org.example.petshopdesktop.util.ActivityLogger;
public class StaffEditDialogController {
@@ -44,22 +45,34 @@ public class StaffEditDialogController {
@FXML
private Button btnSave;
private StaffAccount staffAccount;
private UserResponse user;
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());
public void setUser(UserResponse user) {
this.user = user;
String fullName = user.getFullName() == null ? "" : user.getFullName();
String[] names = splitFullName(fullName);
txtFirstName.setText(names[0]);
txtLastName.setText(names[1]);
txtEmail.setText(user.getEmail());
txtPhone.setText(user.getPhone());
txtUsername.setText(user.getUsername());
}
private String[] splitFullName(String fullName) {
if (fullName == null || fullName.trim().isEmpty()) {
return new String[]{"", ""};
}
String[] parts = fullName.trim().split("\\s+", 2);
String firstName = parts.length > 0 ? parts[0] : "";
String lastName = parts.length > 1 ? parts[1] : "";
return new String[]{firstName, lastName};
}
@FXML
void btnSaveClicked(ActionEvent event) {
lblError.setText("");
if (staffAccount == null) {
lblError.setText("No staff account selected.");
if (user == null) {
lblError.setText("No user selected.");
return;
}
@@ -105,26 +118,26 @@ public class StaffEditDialogController {
new Thread(() -> {
try {
Long storeId = UserSession.getInstance().getStoreId();
EmployeeRequest request = new EmployeeRequest();
UserRequest request = new UserRequest();
request.setUsername(username);
request.setPassword(password.isEmpty() ? null : password);
request.setFirstName(firstName);
request.setLastName(lastName);
request.setFullName(firstName + " " + lastName);
request.setEmail(email);
request.setPhone(phone);
request.setRole(staffAccount.getRole());
request.setStaffRole("Staff");
request.setPrimaryStoreId(storeId);
request.setActive(staffAccount.isActive());
request.setRole(user.getRole());
request.setActive(user.getActive());
EmployeeApi.getInstance().updateEmployee(staffAccount.getEmployeeId(), request);
UserSession session = UserSession.getInstance();
if (session.isAdmin()) {
UserApi.getInstance().updateUser(user.getId(), request);
} else {
CustomerApi.getInstance().updateCustomer(user.getId(), 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();
ActivityLogger.getInstance().logException("StaffEditDialogController.btnSaveClicked", e, "Updating user");
String msg = e.getMessage() == null ? "Could not update user." : e.getMessage();
Platform.runLater(() -> {
lblError.setText(msg);
btnSave.setDisable(false);

View File

@@ -9,8 +9,9 @@ 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.employee.EmployeeRequest;
import org.example.petshopdesktop.api.endpoints.EmployeeApi;
import org.example.petshopdesktop.api.dto.user.UserRequest;
import org.example.petshopdesktop.api.endpoints.UserApi;
import org.example.petshopdesktop.api.endpoints.CustomerApi;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.Validator;
import org.example.petshopdesktop.util.ActivityLogger;
@@ -90,33 +91,34 @@ public class StaffRegisterDialogController {
new Thread(() -> {
try {
Long storeId = UserSession.getInstance().getStoreId();
EmployeeRequest request = new EmployeeRequest();
UserSession session = UserSession.getInstance();
UserRequest request = new UserRequest();
request.setUsername(username);
request.setPassword(password);
request.setFirstName(firstName);
request.setLastName(lastName);
request.setFullName(firstName + " " + lastName);
request.setEmail(email);
request.setPhone(phone);
request.setRole("STAFF");
request.setStaffRole("Staff");
request.setPrimaryStoreId(storeId);
request.setActive(true);
EmployeeApi.getInstance().createEmployee(request);
if (session.isAdmin()) {
request.setRole("STAFF");
UserApi.getInstance().createUser(request);
} else {
request.setRole("CUSTOMER");
CustomerApi.getInstance().createCustomer(request);
}
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Staff Account");
alert.setTitle("Account Created");
alert.setHeaderText(null);
alert.setContentText("Staff account created. You can log in now.");
alert.setContentText("Account created successfully.");
alert.showAndWait();
close();
});
} catch (Exception e) {
ActivityLogger.getInstance().logException("StaffRegisterDialogController.btnCreateClicked", e, "Creating staff account");
String msg = e.getMessage() == null ? "Could not create staff account." : e.getMessage();
ActivityLogger.getInstance().logException("StaffRegisterDialogController.btnCreateClicked", e, "Creating account");
String msg = e.getMessage() == null ? "Could not create account." : e.getMessage();
Platform.runLater(() -> {
if (msg.toLowerCase().contains("duplicate") || msg.toLowerCase().contains("unique")) {
lblError.setText("Username already exists.");

View File

@@ -211,7 +211,7 @@
<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="Staff Accounts" textFill="#cbd5e1">
<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>

View File

@@ -19,7 +19,7 @@
<children>
<HBox alignment="CENTER_LEFT" spacing="20.0">
<children>
<Label text="Staff Accounts" textFill="#2c3e50">
<Label text="User Accounts" textFill="#2c3e50">
<font>
<Font name="System Bold" size="30.0" />
</font>
@@ -57,7 +57,7 @@
<Insets bottom="10.0" left="15.0" right="15.0" top="10.0" />
</padding>
<children>
<TextField fx:id="txtSearch" promptText="Search staff..." style="-fx-border-width: 0; -fx-background-color: transparent;" HBox.hgrow="ALWAYS">
<TextField fx:id="txtSearch" promptText="Search users..." style="-fx-border-width: 0; -fx-background-color: transparent;" HBox.hgrow="ALWAYS">
<font>
<Font size="15.0" />
</font>
@@ -67,12 +67,13 @@
<TableView fx:id="tvStaff" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
<columns>
<TableColumn fx:id="colUsername" prefWidth="160.0" text="Username" />
<TableColumn fx:id="colName" prefWidth="190.0" text="Name" />
<TableColumn fx:id="colEmail" prefWidth="230.0" text="Email" />
<TableColumn fx:id="colPhone" prefWidth="140.0" text="Phone" />
<TableColumn fx:id="colUsername" prefWidth="140.0" text="Username" />
<TableColumn fx:id="colName" prefWidth="170.0" text="Name" />
<TableColumn fx:id="colEmail" prefWidth="210.0" text="Email" />
<TableColumn fx:id="colPhone" prefWidth="130.0" text="Phone" />
<TableColumn fx:id="colRole" prefWidth="100.0" text="Role" />
<TableColumn fx:id="colStatus" prefWidth="90.0" text="Status" />
<TableColumn fx:id="colCreated" prefWidth="160.0" text="Created" />
<TableColumn fx:id="colCreated" prefWidth="150.0" text="Created" />
</columns>
</TableView>