From 84f9e2aa8d809737c54f881461cb42ad792258e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BE=D0=BB=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=93=D0=B5?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D0=BD?= Date: Tue, 1 Sep 2026 13:27:21 +0400 Subject: [PATCH] feat(search): add collection search menu and filter settings - add search menu with filter configuration and result actions - add support for AND and OR filter relations - add range filters for group number, average grade, and record book number - update current collection menu to delegate search operations --- .../ui/collection/CurrentCollectionMenu.java | 15 +- .../ui/collection/search/FilterRelation.java | 6 + .../search/SearchCollectionMenu.java | 191 ++++++++++ .../ui/collection/search/SearchSettings.java | 116 ++++++ .../search/SearchSettingsWindow.java | 355 ++++++++++++++++++ 5 files changed, 673 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/ru/project/ui/collection/search/FilterRelation.java create mode 100644 app/src/main/java/ru/project/ui/collection/search/SearchCollectionMenu.java create mode 100644 app/src/main/java/ru/project/ui/collection/search/SearchSettings.java create mode 100644 app/src/main/java/ru/project/ui/collection/search/SearchSettingsWindow.java diff --git a/app/src/main/java/ru/project/ui/collection/CurrentCollectionMenu.java b/app/src/main/java/ru/project/ui/collection/CurrentCollectionMenu.java index 6f72f17..516c020 100644 --- a/app/src/main/java/ru/project/ui/collection/CurrentCollectionMenu.java +++ b/app/src/main/java/ru/project/ui/collection/CurrentCollectionMenu.java @@ -4,6 +4,7 @@ import com.googlecode.lanterna.gui2.ActionListBox; import com.googlecode.lanterna.gui2.WindowBasedTextGUI; import ru.project.model.AppState; import ru.project.ui.base.BaseModalWindow; +import ru.project.ui.collection.search.SearchCollectionMenu; import ru.project.ui.collection.sorting.SortCollectionMenu; import ru.project.ui.common.CollectionDisplayWindow; import ru.project.ui.common.CollectionFileSaveWindow; @@ -13,8 +14,8 @@ public class CurrentCollectionMenu extends BaseModalWindow { private final AppState state; private final SortCollectionMenu sortCollectionMenu; + private final SearchCollectionMenu searchCollectionMenu; - // private final SearchCollectionMenu searchCollectionMenu; // private final CountOccurrencesMenu countOccurrencesMenu; public CurrentCollectionMenu(WindowBasedTextGUI gui, AppState state) { @@ -25,18 +26,18 @@ public class CurrentCollectionMenu extends BaseModalWindow { this.sortCollectionMenu = new SortCollectionMenu(gui, state); - // this.searchCollectionMenu = new SearchCollectionMenu(gui, state); + this.searchCollectionMenu = new SearchCollectionMenu(gui, state); + // this.countCollectionMenu = new CountOccurrencesMenu(gui, state); ActionListBox menu = new ActionListBox(); - menu.addItem("Показать коллекцию...", this::showCurrentCollection); menu.addItem("Сохранить коллекцию в файл...", this::saveCurrentCollection); menu.addItem("Меню сортировки коллекции", sortCollectionMenu::showModal); - menu.addItem("Меню поиска элемента в коллекции", this::searchCollection); + menu.addItem("Меню поиска элемента в коллекции", searchCollectionMenu::showModal); menu.addItem("Меню подсчёта количества вхождений элемента", this::countOccurrences); @@ -57,12 +58,6 @@ public class CurrentCollectionMenu extends BaseModalWindow { window.showModal(); } - private void searchCollection() { - // SearchCollectionMenu searchCollectionMenu = new SearchCollectionMenu(gui, state); - // - // searchCollectionMenu.showModal(); - } - private void countOccurrences() { // CountOccurrencesMenu countOccurrencesMenu = new CountOccurrencesMenu(gui, state); // diff --git a/app/src/main/java/ru/project/ui/collection/search/FilterRelation.java b/app/src/main/java/ru/project/ui/collection/search/FilterRelation.java new file mode 100644 index 0000000..71c6cd2 --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/search/FilterRelation.java @@ -0,0 +1,6 @@ +package ru.project.ui.collection.search; + +public enum FilterRelation { + AND, + OR +} diff --git a/app/src/main/java/ru/project/ui/collection/search/SearchCollectionMenu.java b/app/src/main/java/ru/project/ui/collection/search/SearchCollectionMenu.java new file mode 100644 index 0000000..e820849 --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/search/SearchCollectionMenu.java @@ -0,0 +1,191 @@ +package ru.project.ui.collection.search; + +import com.googlecode.lanterna.gui2.ActionListBox; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import ru.project.collection.MyLinkedList; +import ru.project.collection.MyList; +import ru.project.model.AppState; +import ru.project.search.AverageGradeRangeCriteria; +import ru.project.search.CompositeStudentSearchCriteria; +import ru.project.search.GroupNumberRangeCriteria; +import ru.project.search.RecordBookNumberRangeCriteria; +import ru.project.search.SearchOperation; +import ru.project.search.StudentSearchCriteria; +import ru.project.search.StudentSearchService; +import ru.project.student.Student; +import ru.project.ui.base.BaseModalWindow; +import ru.project.ui.common.CollectionDisplayWindow; +import ru.project.ui.common.CollectionFileSaveWindow; +import ru.project.ui.common.MessageWindow; + +public class SearchCollectionMenu extends BaseModalWindow { + + private final AppState state; + + private final SearchSettingsWindow searchSettingsWindow; + + private SearchSettings searchSettings; + private MyList searchResultCollection; + + public SearchCollectionMenu(WindowBasedTextGUI gui, AppState state) { + + super("Меню поиска элемента в коллекции", gui); + + this.state = state; + this.searchSettings = new SearchSettings(); + this.searchResultCollection = new MyLinkedList<>(); + + this.searchSettingsWindow = + new SearchSettingsWindow(gui, settings -> this.searchSettings = settings); + + ActionListBox menu = new ActionListBox(); + + menu.addItem("Выполнить поиск", this::updateSearchResult); + + menu.addItem("Показать результат поиска", this::showSearchResult); + + menu.addItem("Сохранить результат поиска в файл", this::saveSearchResult); + + menu.addItem("Настроить процесс поиска...", searchSettingsWindow::showModal); + + menu.addItem("Выйти", this::close); + + setComponent(menu); + } + + private void updateSearchResult() { + MyList collection = state.getMainCollection(); + + if (collection == null || collection.isEmpty()) { + MessageWindow.showModal(gui, "Коллекция для поиска пуста."); + return; + } + + if (!hasEnabledFilters()) { + MessageWindow.showModal(gui, "Не выбран ни один фильтр."); + return; + } + + if (!hasValidFilterRanges()) { + return; + } + + StudentSearchCriteria criteria = createSearchCriteria(); + + StudentSearchService searchService = new StudentSearchService(criteria); + + searchResultCollection = searchService.find(collection); + + MessageWindow.showModal(gui, "Результат поиска обновлён."); + } + + private StudentSearchCriteria createSearchCriteria() { + SearchOperation operation = + searchSettings.getRelation() == FilterRelation.AND + ? SearchOperation.INTERSECTION + : SearchOperation.UNION; + + CompositeStudentSearchCriteria compositeCriteria = + new CompositeStudentSearchCriteria(operation); + + if (searchSettings.isGroupNumberFilterEnabled()) { + compositeCriteria.add( + new GroupNumberRangeCriteria( + searchSettings.getMinGroupNumber(), searchSettings.getMaxGroupNumber())); + } + + if (searchSettings.isAverageGradeFilterEnabled()) { + compositeCriteria.add( + new AverageGradeRangeCriteria( + searchSettings.getMinAverageGrade(), searchSettings.getMaxAverageGrade())); + } + + if (searchSettings.isRecordBookNumberFilterEnabled()) { + compositeCriteria.add( + new RecordBookNumberRangeCriteria( + searchSettings.getMinRecordBookNumber(), searchSettings.getMaxRecordBookNumber())); + } + + return compositeCriteria; + } + + private boolean hasEnabledFilters() { + return searchSettings.isGroupNumberFilterEnabled() + || searchSettings.isAverageGradeFilterEnabled() + || searchSettings.isRecordBookNumberFilterEnabled(); + } + + private boolean hasValidFilterRanges() { + StringBuilder errors = new StringBuilder(); + + if (!hasValidGroupNumberRange()) { + errors.append("Для фильтра по номеру группы задайте минимальное и максимальное значения.\n"); + } + + if (!hasValidAverageGradeRange()) { + errors.append("Для фильтра по среднему баллу задайте минимальное и максимальное значения.\n"); + } + + if (!hasValidRecordBookNumberRange()) { + errors.append( + "Для фильтра по номеру зачётной книжки задайте минимальное и максимальное значения.\n"); + } + + if (errors.length() > 0) { + MessageWindow.showModal(gui, errors.toString()); + return false; + } + + return true; + } + + private boolean hasValidGroupNumberRange() { + if (!searchSettings.isGroupNumberFilterEnabled()) { + return true; + } + + return searchSettings.getMinGroupNumber() != null && searchSettings.getMaxGroupNumber() != null; + } + + private boolean hasValidAverageGradeRange() { + if (!searchSettings.isAverageGradeFilterEnabled()) { + return true; + } + + return searchSettings.getMinAverageGrade() != null + && searchSettings.getMaxAverageGrade() != null; + } + + private boolean hasValidRecordBookNumberRange() { + if (!searchSettings.isRecordBookNumberFilterEnabled()) { + return true; + } + + return searchSettings.getMinRecordBookNumber() != null + && searchSettings.getMaxRecordBookNumber() != null; + } + + private void showSearchResult() { + if (searchResultCollection == null || searchResultCollection.isEmpty()) { + + MessageWindow.showModal(gui, "Результат поиска пуст."); + return; + } + + CollectionDisplayWindow window = new CollectionDisplayWindow(gui, searchResultCollection); + + window.showModal(); + } + + private void saveSearchResult() { + if (searchResultCollection == null || searchResultCollection.isEmpty()) { + + MessageWindow.showModal(gui, "Результат поиска пуст."); + return; + } + + CollectionFileSaveWindow window = new CollectionFileSaveWindow(gui, searchResultCollection); + + window.showModal(); + } +} diff --git a/app/src/main/java/ru/project/ui/collection/search/SearchSettings.java b/app/src/main/java/ru/project/ui/collection/search/SearchSettings.java new file mode 100644 index 0000000..058a59a --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/search/SearchSettings.java @@ -0,0 +1,116 @@ +package ru.project.ui.collection.search; + +public class SearchSettings { + + private FilterRelation relation; + + private boolean groupNumberFilterEnabled; + private boolean averageGradeFilterEnabled; + private boolean recordBookNumberFilterEnabled; + + private String minGroupNumber; + private String maxGroupNumber; + + private Double minAverageGrade; + private Double maxAverageGrade; + + private Integer minRecordBookNumber; + private Integer maxRecordBookNumber; + + public SearchSettings() { + this.relation = FilterRelation.AND; + + this.groupNumberFilterEnabled = false; + this.averageGradeFilterEnabled = false; + this.recordBookNumberFilterEnabled = false; + + this.minGroupNumber = null; + this.maxGroupNumber = null; + + this.minAverageGrade = null; + this.maxAverageGrade = null; + + this.minRecordBookNumber = null; + this.maxRecordBookNumber = null; + } + + public FilterRelation getRelation() { + return relation; + } + + public void setRelation(FilterRelation relation) { + this.relation = relation; + } + + public boolean isGroupNumberFilterEnabled() { + return groupNumberFilterEnabled; + } + + public void setGroupNumberFilterEnabled(boolean enabled) { + this.groupNumberFilterEnabled = enabled; + } + + public boolean isAverageGradeFilterEnabled() { + return averageGradeFilterEnabled; + } + + public void setAverageGradeFilterEnabled(boolean enabled) { + this.averageGradeFilterEnabled = enabled; + } + + public boolean isRecordBookNumberFilterEnabled() { + return recordBookNumberFilterEnabled; + } + + public void setRecordBookNumberFilterEnabled(boolean enabled) { + this.recordBookNumberFilterEnabled = enabled; + } + + public String getMinGroupNumber() { + return minGroupNumber; + } + + public void setMinGroupNumber(String minGroupNumber) { + this.minGroupNumber = minGroupNumber; + } + + public String getMaxGroupNumber() { + return maxGroupNumber; + } + + public void setMaxGroupNumber(String maxGroupNumber) { + this.maxGroupNumber = maxGroupNumber; + } + + public Double getMinAverageGrade() { + return minAverageGrade; + } + + public void setMinAverageGrade(Double minAverageGrade) { + this.minAverageGrade = minAverageGrade; + } + + public Double getMaxAverageGrade() { + return maxAverageGrade; + } + + public void setMaxAverageGrade(Double maxAverageGrade) { + this.maxAverageGrade = maxAverageGrade; + } + + public Integer getMinRecordBookNumber() { + return minRecordBookNumber; + } + + public void setMinRecordBookNumber(Integer minRecordBookNumber) { + this.minRecordBookNumber = minRecordBookNumber; + } + + public Integer getMaxRecordBookNumber() { + return maxRecordBookNumber; + } + + public void setMaxRecordBookNumber(Integer maxRecordBookNumber) { + this.maxRecordBookNumber = maxRecordBookNumber; + } +} diff --git a/app/src/main/java/ru/project/ui/collection/search/SearchSettingsWindow.java b/app/src/main/java/ru/project/ui/collection/search/SearchSettingsWindow.java new file mode 100644 index 0000000..059352c --- /dev/null +++ b/app/src/main/java/ru/project/ui/collection/search/SearchSettingsWindow.java @@ -0,0 +1,355 @@ +package ru.project.ui.collection.search; + +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.CheckBox; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.RadioBoxList; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import java.util.function.Consumer; +import java.util.function.Predicate; +import ru.project.student.Student; +import ru.project.ui.base.BaseModalWindow; +import ru.project.ui.common.InputValueWindow; +import ru.project.ui.common.MessageWindow; + +public class SearchSettingsWindow extends BaseModalWindow { + + private final SearchSettings settings; + private final Consumer onApply; + + private final CheckBox groupNumberFilterCheckBox; + private final CheckBox averageGradeFilterCheckBox; + private final CheckBox recordBookNumberFilterCheckBox; + + private final Label minGroupNumberLabel; + private final Label maxGroupNumberLabel; + + private final Label minAverageGradeLabel; + private final Label maxAverageGradeLabel; + + private final Label minRecordBookNumberLabel; + private final Label maxRecordBookNumberLabel; + + private final RadioBoxList relationList; + + public SearchSettingsWindow(WindowBasedTextGUI gui, Consumer onApply) { + + super("Настройка процесса поиска", gui); + + this.settings = new SearchSettings(); + this.onApply = onApply; + + Panel panel = new Panel(); + panel.setLayoutManager(new LinearLayout()); + + panel.addComponent(new Label("Отношение фильтров:")); + + relationList = new RadioBoxList<>(); + relationList.addItem("И"); + relationList.addItem("ИЛИ"); + relationList.setCheckedItemIndex(0); + + panel.addComponent(relationList); + + groupNumberFilterCheckBox = new CheckBox("Фильтр по номеру группы"); + + averageGradeFilterCheckBox = new CheckBox("Фильтр по среднему баллу"); + + recordBookNumberFilterCheckBox = new CheckBox("Фильтр по номеру зачётной книжки"); + + panel.addComponent(groupNumberFilterCheckBox); + + minGroupNumberLabel = + new Label("Минимум номера группы: " + getValueOrDefault(settings.getMinGroupNumber())); + + maxGroupNumberLabel = + new Label("Максимум номера группы: " + getValueOrDefault(settings.getMaxGroupNumber())); + + panel.addComponent(minGroupNumberLabel); + panel.addComponent(maxGroupNumberLabel); + + panel.addComponent(new Button("Задать минимум номера группы...", this::setMinGroupNumber)); + + panel.addComponent(new Button("Задать максимум номера группы...", this::setMaxGroupNumber)); + + panel.addComponent(averageGradeFilterCheckBox); + + minAverageGradeLabel = + new Label("Минимум среднего балла: " + getValueOrDefault(settings.getMinAverageGrade())); + + maxAverageGradeLabel = + new Label("Максимум среднего балла: " + getValueOrDefault(settings.getMaxAverageGrade())); + + panel.addComponent(minAverageGradeLabel); + panel.addComponent(maxAverageGradeLabel); + + panel.addComponent(new Button("Задать минимум среднего балла...", this::setMinAverageGrade)); + + panel.addComponent(new Button("Задать максимум среднего балла...", this::setMaxAverageGrade)); + + panel.addComponent(recordBookNumberFilterCheckBox); + + minRecordBookNumberLabel = + new Label( + "Минимум номера зачётной книжки: " + + getValueOrDefault(settings.getMinRecordBookNumber())); + + maxRecordBookNumberLabel = + new Label( + "Максимум номера зачётной книжки: " + + getValueOrDefault(settings.getMaxRecordBookNumber())); + + panel.addComponent(minRecordBookNumberLabel); + panel.addComponent(maxRecordBookNumberLabel); + + panel.addComponent( + new Button("Задать минимум номера зачётной книжки...", this::setMinRecordBookNumber)); + + panel.addComponent( + new Button("Задать максимум номера зачётной книжки...", this::setMaxRecordBookNumber)); + + panel.addComponent(new Button("Применить", this::apply)); + + panel.addComponent(new Button("Выйти", this::close)); + + setComponent(panel); + } + + private void setMinGroupNumber() { + showGroupNumberInput( + "Минимальный номер группы", + settings.getMinGroupNumber(), + value -> { + if (!value.matches(Student.getGroupNumberPattern())) { + + MessageWindow.showModal(gui, "Номер группы должен соответствовать формату A12."); + + return false; + } + + if (settings.getMaxGroupNumber() != null + && value.compareTo(settings.getMaxGroupNumber()) > 0) { + + MessageWindow.showModal( + gui, "Минимальный номер группы не может быть больше максимального."); + + return false; + } + + settings.setMinGroupNumber(value); + + minGroupNumberLabel.setText("Минимум номера группы: " + value); + + return true; + }); + } + + private void setMaxGroupNumber() { + showGroupNumberInput( + "Максимальный номер группы", + settings.getMaxGroupNumber(), + value -> { + if (!value.matches(Student.getGroupNumberPattern())) { + + MessageWindow.showModal(gui, "Номер группы должен соответствовать формату A12."); + + return false; + } + + if (settings.getMinGroupNumber() != null + && value.compareTo(settings.getMinGroupNumber()) < 0) { + + MessageWindow.showModal( + gui, "Максимальный номер группы не может быть меньше минимального."); + + return false; + } + + settings.setMaxGroupNumber(value); + + maxGroupNumberLabel.setText("Максимум номера группы: " + value); + + return true; + }); + } + + private void showGroupNumberInput(String title, String initialValue, Predicate onSave) { + + InputValueWindow window = + new InputValueWindow( + gui, title, "Введите номер группы (например, A12):", initialValue, onSave); + + window.showModal(); + } + + private void setMinAverageGrade() { + showAverageGradeInput( + "Минимальный средний балл", + settings.getMinAverageGrade(), + value -> { + if (settings.getMaxAverageGrade() != null && value > settings.getMaxAverageGrade()) { + + MessageWindow.showModal( + gui, "Минимальный средний балл не может быть больше максимального."); + + return false; + } + + settings.setMinAverageGrade(value); + + minAverageGradeLabel.setText("Минимум среднего балла: " + value); + + return true; + }); + } + + private void setMaxAverageGrade() { + showAverageGradeInput( + "Максимальный средний балл", + settings.getMaxAverageGrade(), + value -> { + if (settings.getMinAverageGrade() != null && value < settings.getMinAverageGrade()) { + + MessageWindow.showModal( + gui, "Максимальный средний балл не может быть меньше минимального."); + + return false; + } + + settings.setMaxAverageGrade(value); + + maxAverageGradeLabel.setText("Максимум среднего балла: " + value); + + return true; + }); + } + + private void showAverageGradeInput(String title, Double initialValue, Predicate onSave) { + + InputValueWindow window = + new InputValueWindow( + gui, + title, + "Введите значение от 0.0 до 5.0:", + initialValue == null ? null : String.valueOf(initialValue), + value -> { + try { + double parsedValue = Double.parseDouble(value); + + if (parsedValue < Student.getMinAverageGrade() + || parsedValue > Student.getMaxAverageGrade()) { + + MessageWindow.showModal(gui, "Средний балл должен быть от 0.0 до 5.0."); + + return false; + } + + return onSave.test(parsedValue); + + } catch (NumberFormatException e) { + MessageWindow.showModal(gui, "Введите корректное число."); + + return false; + } + }); + + window.showModal(); + } + + private void setMinRecordBookNumber() { + showRecordBookNumberInput( + "Минимальный номер зачётной книжки", + settings.getMinRecordBookNumber(), + value -> { + if (settings.getMaxRecordBookNumber() != null + && value > settings.getMaxRecordBookNumber()) { + + MessageWindow.showModal( + gui, "Минимальный номер зачётной книжки не может быть больше максимального."); + + return false; + } + + settings.setMinRecordBookNumber(value); + + minRecordBookNumberLabel.setText("Минимум номера зачётной книжки: " + value); + + return true; + }); + } + + private void setMaxRecordBookNumber() { + showRecordBookNumberInput( + "Максимальный номер зачётной книжки", + settings.getMaxRecordBookNumber(), + value -> { + if (settings.getMinRecordBookNumber() != null + && value < settings.getMinRecordBookNumber()) { + + MessageWindow.showModal( + gui, "Максимальный номер зачётной книжки не может быть меньше минимального."); + + return false; + } + + settings.setMaxRecordBookNumber(value); + + maxRecordBookNumberLabel.setText("Максимум номера зачётной книжки: " + value); + + return true; + }); + } + + private void showRecordBookNumberInput( + String title, Integer initialValue, Predicate onSave) { + + InputValueWindow window = + new InputValueWindow( + gui, + title, + "Введите номер зачётной книжки:", + initialValue == null ? null : String.valueOf(initialValue), + value -> { + try { + int parsedValue = Integer.parseInt(value); + + if (parsedValue < Student.getMinRecordBookNumber()) { + + MessageWindow.showModal(gui, "Номер зачётной книжки должен быть положительным."); + + return false; + } + + return onSave.test(parsedValue); + + } catch (NumberFormatException e) { + MessageWindow.showModal(gui, "Введите целое число."); + + return false; + } + }); + + window.showModal(); + } + + private void apply() { + settings.setGroupNumberFilterEnabled(groupNumberFilterCheckBox.isChecked()); + + settings.setAverageGradeFilterEnabled(averageGradeFilterCheckBox.isChecked()); + + settings.setRecordBookNumberFilterEnabled(recordBookNumberFilterCheckBox.isChecked()); + + settings.setRelation( + relationList.getCheckedItemIndex() == 0 ? FilterRelation.AND : FilterRelation.OR); + + onApply.accept(settings); + + MessageWindow.showModal(gui, "Настройки процесса поиска успешно применены."); + } + + private String getValueOrDefault(Object value) { + return value == null ? "не задан" : String.valueOf(value); + } +}