diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceListController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceListController.java index e187096..29fec53 100644 --- a/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceListController.java +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceListController.java @@ -47,6 +47,7 @@ public class ResourceListController extends Controller { @FXML private Button prevButton; @FXML private Button nextButton; @FXML private Button logoutButton; + @FXML private Button usersButton; private final ObservableList rows = FXCollections.observableArrayList(); private long offset = 0; @@ -67,7 +68,6 @@ private void initialize() { sortCombo.setOnAction(e -> reloadFromStart()); resourceTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); - // Initial load on open. Guarded for FXML tooling previews where no services are wired. if (services != null && services.getResourceService() != null) { reloadFromStart(); } @@ -126,6 +126,11 @@ private void setupRbac() { deleteButton.setVisible(canModify); deleteButton.setManaged(canModify); updateDeleteButtonState(); + + // Кнопка «Пользователи» доступна только администраторам. + boolean isAdmin = authService != null && authService.isAdmin(); + usersButton.setVisible(isAdmin); + usersButton.setManaged(isAdmin); } private void updateDeleteButtonState() { @@ -134,6 +139,11 @@ private void updateDeleteButtonState() { deleteButton.setDisable(!(canModify && hasSelection)); } + @FXML + private void onOpenUsers(ActionEvent e) { + navigator.goTo("/fxml/UserList.fxml"); + } + // ===== Data loading ===== private void reloadFromStart() { diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/UserCreateFormController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/UserCreateFormController.java new file mode 100644 index 0000000..e5a15f2 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/UserCreateFormController.java @@ -0,0 +1,97 @@ +package com.DesktopApplicationClientJava.controllers; + +import com.DesktopApplicationClientJava.controllers.utils.Controller; +import com.DesktopApplicationClientJava.entities.Role; +import com.DesktopApplicationClientJava.entities.User; +import com.DesktopApplicationClientJava.services.ServiceException; +import com.DesktopApplicationClientJava.services.data.RegisterData; +import com.DesktopApplicationClientJava.validation.EmailValidator; +import javafx.collections.FXCollections; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.*; + +public class UserCreateFormController extends Controller { + + @FXML private TextField emailField; + @FXML private PasswordField passwordField; + @FXML private Label errorLabel; + @FXML private Button submitButton; + @FXML private ChoiceBox rolesChoiceBox; + @FXML private TextField firstNameField; + @FXML private TextField lastNameField; + + @FXML + public void initialize() { + // Способ 1: через setItems + rolesChoiceBox.setItems( + FXCollections.observableArrayList( + Role.USER.toString(), Role.MODER.toString(), Role.ADMIN.toString())); + // Установить значение по умолчанию (опционально) + rolesChoiceBox.setValue(Role.USER.toString()); + + System.out.println(rolesChoiceBox.getValue()); + } + + @FXML + private void onSubmit(ActionEvent event) { + String email = emailField.getText() == null ? "" : emailField.getText().trim(); + String password = passwordField.getText() == null ? "" : passwordField.getText(); + String roleStirng = rolesChoiceBox.getValue() == null ? "" : rolesChoiceBox.getValue(); + String firstName = firstNameField.getText() == null ? "" : firstNameField.getText().trim(); + String lastName = lastNameField.getText() == null ? "" : lastNameField.getText().trim(); + + if (email.isBlank() + || password.isBlank() + || roleStirng.isBlank() + || firstName.isBlank() + || lastName.isBlank()) { + showError("Заполните все поля"); + return; + } + + if (!(roleStirng.equals(Role.USER.toString()) + || roleStirng.equals(Role.MODER.toString()) + || roleStirng.equals(Role.ADMIN.toString()))) { + showError("Роль не определена"); + return; + } + + Role role = Role.valueOf(roleStirng); + + String emailError = EmailValidator.validate(email); + if (emailError != null) { + showError(emailError); + return; + } + + try { + RegisterData registerData = new RegisterData(email, password, role, firstName, lastName); + User user = services.getUsersService().create(registerData); + if (user == null) { + return; + } + hideError(); + navigator.goTo("/fxml/UserList.fxml"); + } catch (ServiceException e) { + showError(e.getMessage()); + } + } + + @FXML + void onCancel(ActionEvent event) { + navigator.goTo("/fxml/UserList.fxml"); + } + + private void showError(String message) { + errorLabel.setText(message); + errorLabel.setVisible(true); + errorLabel.setManaged(true); + } + + private void hideError() { + errorLabel.setText(""); + errorLabel.setVisible(false); + errorLabel.setManaged(false); + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/UserDetailsController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/UserDetailsController.java new file mode 100644 index 0000000..35af61c --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/UserDetailsController.java @@ -0,0 +1,244 @@ +package com.DesktopApplicationClientJava.controllers; + +import com.DesktopApplicationClientJava.controllers.utils.Controller; +import com.DesktopApplicationClientJava.entities.User; +import com.DesktopApplicationClientJava.services.ServiceException; +import java.util.UUID; +import javafx.concurrent.Task; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Label; + +public class UserDetailsController extends Controller { + + @FXML private Label fullNameLabel; + @FXML private Label emailLabel; + @FXML private Label roleLabel; + @FXML private Label enabledLabel; + @FXML private Label uuidLabel; + @FXML private Label errorLabel; + @FXML private Label statusLabel; + @FXML private Button backButton; + @FXML private Button editButton; + @FXML private Button deleteButton; + + private UUID userId; + private User current; + + // ===== Lifecycle ===== + + @FXML + private void initialize() { + hideError(); + + // Кнопки редактирования и удаления доступны только администраторам. + var authService = services == null ? null : services.getAuthService(); + boolean isAdmin = authService != null && authService.isAdmin(); + + editButton.setVisible(isAdmin); + editButton.setManaged(isAdmin); + + deleteButton.setVisible(isAdmin); + deleteButton.setManaged(isAdmin); + } + + /** Navigator sets the id after FXML load, so the setter triggers loading. */ + public void setUserId(UUID userId) { + this.userId = userId; + if (fullNameLabel != null && userId != null && services != null) { + load(); + } + } + + // ===== Loading ===== + + private void load() { + hideError(); + statusLabel.setText("Загрузка…"); + setContentVisible(false); + + var task = + new Task() { + @Override + protected User call() { + return services.getUsersService().getById(userId); + } + }; + + task.setOnSucceeded( + e -> { + current = task.getValue(); + statusLabel.setText(""); + if (current == null) { + showError("Пользователь не найден"); + return; + } + showUser(current); + setContentVisible(true); + }); + + task.setOnFailed( + e -> { + statusLabel.setText(""); + var ex = task.getException(); + String msg = + ex instanceof ServiceException se + ? se.getMessage() + : "Не удалось загрузить пользователя"; + showError(msg); + }); + + startDaemon(task, "user-details-load"); + } + + private void showUser(User user) { + String first = user.getFirstName() == null ? "" : user.getFirstName(); + String last = user.getLastName() == null ? "" : user.getLastName(); + String full = (first + " " + last).trim(); + fullNameLabel.setText(full.isEmpty() ? "Без имени" : full); + + emailLabel.setText(user.getEmail() == null ? "" : user.getEmail()); + roleLabel.setText(user.getRole() == null ? "" : user.getRole().name()); + enabledLabel.setText(Boolean.TRUE.equals(user.getEnabled()) ? "Да" : "Нет"); + uuidLabel.setText(user.getUuid() == null ? "" : user.getUuid().toString()); + + updateDeleteButtonState(user); + } + + /** + * Админ не может удалить собственную учётную запись. Кнопка видна только администраторам (см. + * initialize), но дополнительно гасится, если просматривается именно текущий пользователь. + */ + private void updateDeleteButtonState(User user) { + var authService = services == null ? null : services.getAuthService(); + var me = authService == null ? null : authService.getCurrentUser(); + boolean isSelf = + me != null && user != null && user.getUuid() != null && user.getUuid().equals(me.getUuid()); + deleteButton.setDisable(isSelf); + } + + // ===== Event handlers ===== + + @FXML + private void onBack(ActionEvent e) { + navigator.goTo("/fxml/UserList.fxml"); + } + + @FXML + private void onEdit(ActionEvent e) { + if (current == null || current.getUuid() == null) { + return; + } + navigator.goTo( + "/fxml/UserUpdateForm.fxml", + c -> ((UserUpdateFormController) c).setUserId(current.getUuid())); + } + + @FXML + private void onDelete(ActionEvent e) { + if (current == null || current.getUuid() == null) { + return; + } + + // Двойная защита: даже если кнопку как-то активировали, себя удалить нельзя. + var authService = services == null ? null : services.getAuthService(); + var me = authService == null ? null : authService.getCurrentUser(); + if (me != null && current.getUuid().equals(me.getUuid())) { + showError("Нельзя удалить собственную учётную запись"); + return; + } + + if (!confirmDelete(current)) { + return; + } + + hideError(); + setBusy(true); + + var uuid = current.getUuid(); + var task = + new Task() { + @Override + protected Void call() { + services.getUsersService().delete(uuid); + return null; + } + }; + + task.setOnSucceeded( + ev -> { + setBusy(false); + navigator.goTo("/fxml/UserList.fxml"); + }); + + task.setOnFailed( + ev -> { + setBusy(false); + var ex = task.getException(); + String msg = + ex instanceof ServiceException se + ? se.getMessage() + : "Не удалось удалить пользователя"; + showError(msg); + }); + + startDaemon(task, "user-delete"); + } + + // ===== Confirmation dialog ===== + + private boolean confirmDelete(User user) { + var alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Удаление пользователя"); + alert.setHeaderText(null); + alert.setContentText( + "Удалить пользователя \"" + + (user.getEmail() == null ? user.getUuid().toString() : user.getEmail()) + + "\"? Действие необратимо."); + + var ok = new ButtonType("Удалить", ButtonBar.ButtonData.OK_DONE); + var cancel = new ButtonType("Отмена", ButtonBar.ButtonData.CANCEL_CLOSE); + alert.getButtonTypes().setAll(ok, cancel); + + return alert.showAndWait().filter(ok::equals).isPresent(); + } + + // ===== UI helpers ===== + + private void setContentVisible(boolean visible) { + fullNameLabel.setVisible(visible); + emailLabel.setVisible(visible); + roleLabel.setVisible(visible); + enabledLabel.setVisible(visible); + uuidLabel.setVisible(visible); + } + + private void setBusy(boolean busy) { + deleteButton.setDisable(busy); + editButton.setDisable(busy); + backButton.setDisable(busy); + statusLabel.setText(busy ? "Удаление…" : ""); + } + + private void showError(String message) { + errorLabel.setText(message); + errorLabel.setVisible(true); + errorLabel.setManaged(true); + } + + private void hideError() { + errorLabel.setText(""); + errorLabel.setVisible(false); + errorLabel.setManaged(false); + } + + private static void startDaemon(Task task, String name) { + var thread = new Thread(task, name); + thread.setDaemon(true); + thread.start(); + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/UserUpdateFormController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/UserUpdateFormController.java new file mode 100644 index 0000000..41a1672 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/UserUpdateFormController.java @@ -0,0 +1,219 @@ +package com.DesktopApplicationClientJava.controllers; + +import com.DesktopApplicationClientJava.controllers.utils.Controller; +import com.DesktopApplicationClientJava.entities.Role; +import com.DesktopApplicationClientJava.entities.User; +import com.DesktopApplicationClientJava.services.ServiceException; +import com.DesktopApplicationClientJava.services.data.UpdateUserData; +import java.util.UUID; +import javafx.collections.FXCollections; +import javafx.concurrent.Task; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; + +public class UserUpdateFormController extends Controller { + + @FXML private Label subtitleLabel; + @FXML private TextField emailField; + @FXML private ChoiceBox roleChoiceBox; + @FXML private TextField firstNameField; + @FXML private TextField lastNameField; + @FXML private CheckBox enabledCheckBox; + @FXML private Label errorLabel; + @FXML private Button saveButton; + @FXML private Button cancelButton; + + private UUID userId; + private User current; + + // ===== Lifecycle ===== + + @FXML + private void initialize() { + hideError(); + roleChoiceBox.setItems(FXCollections.observableArrayList(Role.values())); + } + + /** Вызывается навигатором при переходе на форму. */ + public void setUserId(UUID userId) { + this.userId = userId; + loadUser(); + } + + // ===== Loading ===== + + private void loadUser() { + if (userId == null || services == null || services.getUsersService() == null) { + return; + } + hideError(); + setFormDisabled(true); + + var task = + new Task() { + @Override + protected User call() { + return services.getUsersService().getById(userId); + } + }; + + task.setOnSucceeded( + e -> { + current = task.getValue(); + if (current == null) { + showError("Пользователь не найден"); + setFormDisabled(true); + return; + } + populateForm(current); + setFormDisabled(false); + }); + + task.setOnFailed( + e -> { + var ex = task.getException(); + String msg = + ex instanceof ServiceException se + ? se.getMessage() + : "Не удалось загрузить пользователя"; + showError(msg); + setFormDisabled(true); + }); + + startDaemon(task, "user-update-load"); + } + + private void populateForm(User user) { + emailField.setText(user.getEmail() == null ? "" : user.getEmail()); + firstNameField.setText(user.getFirstName() == null ? "" : user.getFirstName()); + lastNameField.setText(user.getLastName() == null ? "" : user.getLastName()); + roleChoiceBox.getSelectionModel().select(user.getRole()); + enabledCheckBox.setSelected(Boolean.TRUE.equals(user.getEnabled())); + subtitleLabel.setText(user.getEmail() == null ? "" : user.getEmail()); + } + + // ===== Event handlers ===== + + @FXML + private void onSave(ActionEvent e) { + if (current == null) { + return; + } + var data = collectAndValidate(); + if (data == null) { + return; + } + + hideError(); + setFormDisabled(true); + + var task = + new Task() { + @Override + protected User call() { + return services.getUsersService().update(current.getUuid(), data); + } + }; + + task.setOnSucceeded( + ev -> { + setFormDisabled(false); + navigator.goTo("/fxml/UserList.fxml"); + }); + + task.setOnFailed( + ev -> { + setFormDisabled(false); + var ex = task.getException(); + String msg = + ex instanceof ServiceException se + ? se.getMessage() + : "Не удалось сохранить изменения"; + showError(msg); + }); + + startDaemon(task, "user-update-save"); + } + + @FXML + private void onCancel(ActionEvent e) { + navigator.goTo("/fxml/UserList.fxml"); + } + + // ===== Validation / data ===== + + /** + * Собирает данные формы. Если валидация не прошла — показывает ошибку и возвращает {@code null}. + */ + private UpdateUserData collectAndValidate() { + String email = safeTrim(emailField.getText()); + String firstName = safeTrim(firstNameField.getText()); + String lastName = safeTrim(lastNameField.getText()); + Role role = roleChoiceBox.getValue(); + boolean enabled = enabledCheckBox.isSelected(); + + if (email == null || email.isBlank()) { + showError("Укажите почту"); + return null; + } + if (!email.contains("@")) { + showError("Почта указана некорректно"); + return null; + } + if (firstName == null || firstName.isBlank()) { + showError("Укажите имя"); + return null; + } + if (lastName == null || lastName.isBlank()) { + showError("Укажите фамилию"); + return null; + } + if (role == null) { + showError("Выберите роль"); + return null; + } + + // NB: UpdateUserData не содержит пароль — смена пароля выполняется отдельно. + // Поле passwordField оставлено в форме для будущей поддержки, но сейчас + // в UpdateUserData не передаётся. + return new UpdateUserData(email, role, firstName, lastName, enabled); + } + + // ===== UI helpers ===== + + private void setFormDisabled(boolean disabled) { + emailField.setDisable(disabled); + roleChoiceBox.setDisable(disabled); + firstNameField.setDisable(disabled); + lastNameField.setDisable(disabled); + enabledCheckBox.setDisable(disabled); + saveButton.setDisable(disabled); + } + + private void showError(String message) { + errorLabel.setText(message); + errorLabel.setVisible(true); + errorLabel.setManaged(true); + } + + private void hideError() { + errorLabel.setText(""); + errorLabel.setVisible(false); + errorLabel.setManaged(false); + } + + private static String safeTrim(String value) { + return value == null ? null : value.trim(); + } + + private static void startDaemon(Task task, String name) { + var thread = new Thread(task, name); + thread.setDaemon(true); + thread.start(); + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/UsersListController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/UsersListController.java new file mode 100644 index 0000000..5caa1e6 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/UsersListController.java @@ -0,0 +1,339 @@ +package com.DesktopApplicationClientJava.controllers; + +import com.DesktopApplicationClientJava.controllers.utils.Controller; +import com.DesktopApplicationClientJava.entities.User; +import com.DesktopApplicationClientJava.services.ServiceException; +import com.DesktopApplicationClientJava.services.filter.UserFilter; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.concurrent.Task; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableRow; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.cell.PropertyValueFactory; + +public class UsersListController extends Controller { + + private static final int PAGE_SIZE = 20; + + /** Насколько широко тянем список при активном поиске (клиентская фильтрация). */ + private static final int SEARCH_LIMIT = 500; + + @FXML private Label userInfoLabel; + @FXML private Label errorLabel; + @FXML private Label pageInfoLabel; + @FXML private Label statusLabel; + @FXML private TextField searchField; + @FXML private ComboBox sortCombo; + @FXML private TableView usersTable; + @FXML private TableColumn emailColumn; + @FXML private TableColumn firstNameColumn; + @FXML private TableColumn lastNameColumn; + @FXML private TableColumn roleColumn; + @FXML private TableColumn enabledColumn; + @FXML private Button createButton; + @FXML private Button prevButton; + @FXML private Button nextButton; + @FXML private Button logoutButton; + @FXML private Button resourcesButton; + + private final ObservableList rows = FXCollections.observableArrayList(); + private long offset = 0; + + // ===== Lifecycle ===== + + @FXML + private void initialize() { + hideError(); + setupUser(); + setupSort(); + setupTable(); + setupRbac(); + + searchField.setOnAction(e -> reloadFromStart()); + sortCombo.setOnAction(e -> reloadFromStart()); + usersTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); + + if (services != null && services.getUsersService() != null) { + reloadFromStart(); + } + } + + // ===== UI setup ===== + + private void setupUser() { + var authService = services == null ? null : services.getAuthService(); + var me = authService == null ? null : authService.getCurrentUser(); + userInfoLabel.setText( + me == null ? "" : me.getFirstName() + " " + me.getLastName() + " · " + me.getRole()); + } + + private void setupSort() { + sortCombo + .getItems() + .setAll("По email (А-Я)", "По email (Я-А)", "По имени (А-Я)", "По фамилии (А-Я)"); + sortCombo.getSelectionModel().selectFirst(); + } + + @FXML + private void onOpenResources(ActionEvent e) { + navigator.goTo("/fxml/ResourceList.fxml"); + } + + private void setupTable() { + emailColumn.setCellValueFactory(new PropertyValueFactory<>("email")); + firstNameColumn.setCellValueFactory(new PropertyValueFactory<>("firstName")); + lastNameColumn.setCellValueFactory(new PropertyValueFactory<>("lastName")); + roleColumn.setCellValueFactory( + c -> + new SimpleStringProperty( + c.getValue().getRole() == null ? "" : c.getValue().getRole().name())); + enabledColumn.setCellValueFactory( + c -> + new SimpleStringProperty( + Boolean.TRUE.equals(c.getValue().getEnabled()) ? "Да" : "Нет")); + + usersTable.setItems(rows); + usersTable.setPlaceholder(new Label("Пользователей пока нет")); + + usersTable.setRowFactory( + tv -> { + var row = new TableRow(); + row.setOnMouseClicked( + e -> { + if (e.getClickCount() == 2 && !row.isEmpty()) { + openDetails(row.getItem()); + } + }); + return row; + }); + } + + private void setupRbac() { + var authService = services == null ? null : services.getAuthService(); + boolean canCreate = authService != null && authService.isAdmin(); + createButton.setVisible(canCreate); + createButton.setManaged(canCreate); + } + + // ===== Data loading ===== + + private void reloadFromStart() { + offset = 0; + loadAsync(); + } + + private void loadAsync() { + hideError(); + statusLabel.setText("Загрузка…"); + + final String query = currentQuery(); + final boolean searching = query != null; + final UserFilter filter = buildFilter(query); + final SortSpec sort = resolveSort(); + final long currentOffset = offset; + // При активном поиске тянем широкую выборку и фильтруем локально, + // чтобы не потерять совпадения по имени/фамилии. + final long fetchCount = searching ? SEARCH_LIMIT : PAGE_SIZE; + final long fetchOffset = searching ? 0 : currentOffset; + + var task = + new Task>() { + @Override + protected List call() { + return services.getUsersService().search(filter, fetchOffset, fetchCount); + } + }; + + task.setOnSucceeded( + e -> { + List page = task.getValue(); + if (page == null) { + page = List.of(); + } + var list = new ArrayList<>(page); + + // Клиентская фильтрация по подстроке (email + имя + фамилия). + if (searching) { + list.removeIf(u -> !matchesQuery(u, query)); + } + + sort.sort(list); + rows.setAll(list); + + updatePaginationUi(searching ? list.size() : page.size(), searching); + statusLabel.setText(searching ? "Найдено: " + list.size() : ""); + }); + + task.setOnFailed( + e -> { + var ex = task.getException(); + String msg = + ex instanceof ServiceException se ? se.getMessage() : "Не удалось загрузить список"; + showError(msg); + rows.clear(); + updatePaginationUi(0, searching); + statusLabel.setText(""); + }); + + var thread = new Thread(task, "users-list-load"); + thread.setDaemon(true); + thread.start(); + } + + private String currentQuery() { + String raw = searchField.getText(); + if (raw == null) { + return null; + } + String trimmed = raw.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + /** + * Различные бэкенды по-разному комбинируют поля фильтра. Чтобы поиск работал предсказуемо, на + * сервер отправляем только email-фильтр (когда запрос похож на email), а имя/фамилию фильтруем + * локально. + */ + private UserFilter buildFilter(String query) { + if (query == null) { + return UserFilter.empty(); + } + boolean looksLikeEmail = query.contains("@"); + return looksLikeEmail ? new UserFilter(query, null, null, null, null) : UserFilter.empty(); + } + + private static boolean matchesQuery(User u, String query) { + if (u == null) { + return false; + } + String needle = query.toLowerCase(Locale.ROOT); + return containsIgnoreCase(u.getEmail(), needle) + || containsIgnoreCase(u.getFirstName(), needle) + || containsIgnoreCase(u.getLastName(), needle); + } + + private static boolean containsIgnoreCase(String value, String lowerNeedle) { + return value != null && value.toLowerCase(Locale.ROOT).contains(lowerNeedle); + } + + private record SortSpec(java.util.Comparator comparator) { + void sort(List list) { + if (comparator != null) { + list.sort(comparator); + } + } + } + + private SortSpec resolveSort() { + String choice = sortCombo.getValue(); + if (choice == null) { + return new SortSpec(null); + } + return switch (choice) { + case "По email (А-Я)" -> + new SortSpec( + java.util.Comparator.comparing( + u -> u.getEmail() == null ? "" : u.getEmail(), String.CASE_INSENSITIVE_ORDER)); + case "По email (Я-А)" -> + new SortSpec( + java.util.Comparator.comparing( + (User u) -> u.getEmail() == null ? "" : u.getEmail(), + String.CASE_INSENSITIVE_ORDER) + .reversed()); + case "По имени (А-Я)" -> + new SortSpec( + java.util.Comparator.comparing( + u -> u.getFirstName() == null ? "" : u.getFirstName(), + String.CASE_INSENSITIVE_ORDER)); + case "По фамилии (А-Я)" -> + new SortSpec( + java.util.Comparator.comparing( + u -> u.getLastName() == null ? "" : u.getLastName(), + String.CASE_INSENSITIVE_ORDER)); + default -> new SortSpec(null); + }; + } + + private void updatePaginationUi(int pageSize, boolean searching) { + if (searching) { + // При активном поиске мы показываем «плоский» отфильтрованный список + // без серверной пагинации — кнопки не имеют смысла. + pageInfoLabel.setText("Показано " + rows.size()); + prevButton.setDisable(true); + nextButton.setDisable(true); + return; + } + long from = rows.isEmpty() ? 0 : offset + 1; + long to = offset + pageSize; + pageInfoLabel.setText("Показано " + from + "–" + to); + prevButton.setDisable(offset == 0); + nextButton.setDisable(pageSize < PAGE_SIZE); + } + + // ===== Event handlers ===== + + @FXML + private void onCreate(ActionEvent e) { + navigator.goTo("/fxml/UserCreateForm.fxml"); + } + + @FXML + private void onLogout(ActionEvent e) { + services.getAuthService().logout(); + navigator.goTo("/fxml/LoginForm.fxml"); + } + + @FXML + private void onSearch(ActionEvent e) { + offset = 0; + loadAsync(); + } + + @FXML + private void onSort(ActionEvent e) { + offset = 0; + loadAsync(); + } + + @FXML + private void onNext(ActionEvent e) { + offset += PAGE_SIZE; + loadAsync(); + } + + @FXML + private void onPrev(ActionEvent e) { + offset = Math.max(0, offset - PAGE_SIZE); + loadAsync(); + } + + private void openDetails(User user) { + navigator.goTo( + "/fxml/UserDetails.fxml", c -> ((UserDetailsController) c).setUserId(user.getUuid())); + } + + // ===== helpers ===== + + private void showError(String message) { + errorLabel.setText(message); + errorLabel.setVisible(true); + errorLabel.setManaged(true); + } + + private void hideError() { + errorLabel.setText(""); + errorLabel.setVisible(false); + errorLabel.setManaged(false); + } +} diff --git a/src/main/resources/fxml/ResourceList.fxml b/src/main/resources/fxml/ResourceList.fxml index f693b35..797ba3d 100644 --- a/src/main/resources/fxml/ResourceList.fxml +++ b/src/main/resources/fxml/ResourceList.fxml @@ -25,6 +25,10 @@