Add biggest update for UI. Create Ordering module
This commit is contained in:
24
pom.xml
24
pom.xml
@@ -12,6 +12,7 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<junit.version>5.12.1</junit.version>
|
<junit.version>5.12.1</junit.version>
|
||||||
|
<poi.version>5.2.5</poi.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -61,17 +62,19 @@
|
|||||||
<artifactId>bootstrapfx-core</artifactId>
|
<artifactId>bootstrapfx-core</artifactId>
|
||||||
<version>0.4.0</version>
|
<version>0.4.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Apache POI for Excel (XLSX) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>eu.hansolo</groupId>
|
<groupId>org.apache.poi</groupId>
|
||||||
<artifactId>tilesfx</artifactId>
|
<artifactId>poi</artifactId>
|
||||||
<version>21.0.9</version>
|
<version>${poi.version}</version>
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.openjfx</groupId>
|
|
||||||
<artifactId>*</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.poi</groupId>
|
||||||
|
<artifactId>poi-ooxml</artifactId>
|
||||||
|
<version>${poi.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit-jupiter-api</artifactId>
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
@@ -103,10 +106,9 @@
|
|||||||
<version>0.0.8</version>
|
<version>0.0.8</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<!-- Default configuration for running with: mvn clean javafx:run -->
|
|
||||||
<id>default-cli</id>
|
<id>default-cli</id>
|
||||||
<configuration>
|
<configuration>
|
||||||
<mainClass>com.dsol.pki_management/com.dsol.pki_management.HelloApplication</mainClass>
|
<mainClass>com.dsol.pki_management.app/com.dsol.pki_management.app.PKIApplication</mainClass>
|
||||||
<launcher>app</launcher>
|
<launcher>app</launcher>
|
||||||
<jlinkZipName>app</jlinkZipName>
|
<jlinkZipName>app</jlinkZipName>
|
||||||
<jlinkImageName>app</jlinkImageName>
|
<jlinkImageName>app</jlinkImageName>
|
||||||
|
|||||||
@@ -0,0 +1,393 @@
|
|||||||
|
package com.dsol.pki_management.components;
|
||||||
|
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.input.Clipboard;
|
||||||
|
import javafx.scene.input.ClipboardContent;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyCodeCombination;
|
||||||
|
import javafx.scene.input.KeyCombination;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Утилитный класс для настройки TableView с поддержкой выделения ячеек и копирования (как в Excel)
|
||||||
|
*/
|
||||||
|
public class TableViewEnhancer {
|
||||||
|
|
||||||
|
// Хранилище для отслеживания выделенных ячеек
|
||||||
|
private static final java.util.Map<TableView<?>, ObservableList<TablePosition<?, ?>>> selectedCellsMap = new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Хранилище для отслеживания начальной позиции при drag selection
|
||||||
|
private static final java.util.Map<TableView<?>, TablePosition<?, ?>> dragStartMap = new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Настраивает таблицу для выделения ячеек и копирования
|
||||||
|
* @param tableView Таблица для настройки
|
||||||
|
* @param <T> Тип данных строки таблицы
|
||||||
|
*/
|
||||||
|
public static <T> void enhance(TableView<T> tableView) {
|
||||||
|
if (tableView == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем список для отслеживания выделенных ячеек
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ObservableList<TablePosition<?, ?>> selectedCells = FXCollections.observableArrayList();
|
||||||
|
selectedCellsMap.put(tableView, selectedCells);
|
||||||
|
|
||||||
|
setupCellSelection(tableView);
|
||||||
|
setupCopyHandler(tableView);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Настраивает режим выделения ячеек вместо строк
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> void setupCellSelection(TableView<T> tableView) {
|
||||||
|
TableSelectionModel<T> selectionModel = tableView.getSelectionModel();
|
||||||
|
|
||||||
|
// Включаем выделение ячеек
|
||||||
|
selectionModel.setCellSelectionEnabled(true);
|
||||||
|
|
||||||
|
// Разрешаем множественное выделение
|
||||||
|
selectionModel.setSelectionMode(SelectionMode.MULTIPLE);
|
||||||
|
|
||||||
|
// Отслеживаем изменения выделения
|
||||||
|
ObservableList<TablePosition<?, ?>> trackedCells = selectedCellsMap.get(tableView);
|
||||||
|
|
||||||
|
// Обработчик начала выделения (mouse pressed)
|
||||||
|
tableView.setOnMousePressed(event -> {
|
||||||
|
if (event.getTarget() instanceof TableCell) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
TableCell<T, ?> clickedCell = (TableCell<T, ?>) event.getTarget();
|
||||||
|
TableRow<T> row = clickedCell.getTableRow();
|
||||||
|
|
||||||
|
if (row != null) {
|
||||||
|
int clickedRow = row.getIndex();
|
||||||
|
TableColumn<T, ?> clickedColumn = clickedCell.getTableColumn();
|
||||||
|
|
||||||
|
if (clickedRow >= 0 && clickedColumn != null) {
|
||||||
|
// Создаем начальную позицию для drag selection
|
||||||
|
TablePosition<T, ?> startPos = new TablePosition<>(tableView, clickedRow, clickedColumn);
|
||||||
|
dragStartMap.put(tableView, startPos);
|
||||||
|
|
||||||
|
// Если не Shift+Click, очищаем выделение и выделяем одну ячейку
|
||||||
|
if (!event.isShiftDown() && !event.isControlDown()) {
|
||||||
|
selectionModel.clearSelection();
|
||||||
|
selectionModel.select(clickedRow, clickedColumn);
|
||||||
|
trackedCells.clear();
|
||||||
|
trackedCells.add(startPos);
|
||||||
|
} else if (event.isShiftDown()) {
|
||||||
|
// Shift+Click: выделяем диапазон от последней выделенной ячейки
|
||||||
|
TablePosition<T, ?> currentSelection = trackedCells != null && !trackedCells.isEmpty()
|
||||||
|
? (TablePosition<T, ?>) trackedCells.get(trackedCells.size() - 1)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (currentSelection != null) {
|
||||||
|
int startRow = Math.min(currentSelection.getRow(), clickedRow);
|
||||||
|
int endRow = Math.max(currentSelection.getRow(), clickedRow);
|
||||||
|
int startCol = Math.min(currentSelection.getColumn(), tableView.getColumns().indexOf(clickedColumn));
|
||||||
|
int endCol = Math.max(currentSelection.getColumn(), tableView.getColumns().indexOf(clickedColumn));
|
||||||
|
|
||||||
|
selectionModel.clearSelection();
|
||||||
|
trackedCells.clear();
|
||||||
|
for (int r = startRow; r <= endRow; r++) {
|
||||||
|
for (int c = startCol; c <= endCol; c++) {
|
||||||
|
if (r >= 0 && r < tableView.getItems().size()
|
||||||
|
&& c >= 0 && c < tableView.getColumns().size()) {
|
||||||
|
TableColumn<T, ?> col = tableView.getColumns().get(c);
|
||||||
|
selectionModel.select(r, col);
|
||||||
|
TablePosition<T, ?> pos = new TablePosition<>(tableView, r, col);
|
||||||
|
trackedCells.add(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.consume();
|
||||||
|
}
|
||||||
|
} else if (event.isControlDown()) {
|
||||||
|
// Ctrl+Click: добавляем/удаляем ячейку из выделения
|
||||||
|
TablePosition<T, ?> pos = new TablePosition<>(tableView, clickedRow, clickedColumn);
|
||||||
|
if (trackedCells.contains(pos)) {
|
||||||
|
selectionModel.clearSelection(clickedRow, clickedColumn);
|
||||||
|
trackedCells.remove(pos);
|
||||||
|
} else {
|
||||||
|
selectionModel.select(clickedRow, clickedColumn);
|
||||||
|
trackedCells.add(pos);
|
||||||
|
}
|
||||||
|
event.consume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик перетаскивания мыши для выделения диапазона (drag selection)
|
||||||
|
tableView.setOnMouseDragged(event -> {
|
||||||
|
TablePosition<?, ?> startPos = dragStartMap.get(tableView);
|
||||||
|
if (startPos == null) return;
|
||||||
|
|
||||||
|
// Находим ячейку под курсором мыши
|
||||||
|
javafx.geometry.Point2D localPoint = tableView.screenToLocal(event.getScreenX(), event.getScreenY());
|
||||||
|
int draggedRow = -1;
|
||||||
|
int draggedCol = -1;
|
||||||
|
|
||||||
|
// Вычисляем строку на основе Y координаты
|
||||||
|
// Учитываем заголовок таблицы (если есть)
|
||||||
|
double headerHeight = tableView.lookup(".column-header-background") != null
|
||||||
|
? tableView.lookup(".column-header-background").getBoundsInLocal().getHeight()
|
||||||
|
: 0;
|
||||||
|
double y = localPoint.getY() - headerHeight;
|
||||||
|
|
||||||
|
// Вычисляем индекс строки
|
||||||
|
double rowHeight = tableView.getFixedCellSize() > 0
|
||||||
|
? tableView.getFixedCellSize()
|
||||||
|
: 25.0; // примерная высота строки по умолчанию
|
||||||
|
|
||||||
|
if (y >= 0) {
|
||||||
|
draggedRow = (int) (y / rowHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вычисляем колонку на основе X координаты
|
||||||
|
double x = localPoint.getX();
|
||||||
|
double currentX = 0;
|
||||||
|
for (int i = 0; i < tableView.getColumns().size(); i++) {
|
||||||
|
TableColumn<T, ?> col = tableView.getColumns().get(i);
|
||||||
|
double colWidth = col.getWidth();
|
||||||
|
if (x >= currentX && x < currentX + colWidth) {
|
||||||
|
draggedCol = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentX += colWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ограничиваем индексы валидными значениями
|
||||||
|
if (draggedRow < 0) draggedRow = 0;
|
||||||
|
if (draggedRow >= tableView.getItems().size()) draggedRow = tableView.getItems().size() - 1;
|
||||||
|
if (draggedCol < 0) draggedCol = 0;
|
||||||
|
if (draggedCol >= tableView.getColumns().size()) draggedCol = tableView.getColumns().size() - 1;
|
||||||
|
|
||||||
|
// Выделяем диапазон
|
||||||
|
if (draggedRow >= 0 && draggedRow < tableView.getItems().size()
|
||||||
|
&& draggedCol >= 0 && draggedCol < tableView.getColumns().size()) {
|
||||||
|
int startRow = Math.min(startPos.getRow(), draggedRow);
|
||||||
|
int endRow = Math.max(startPos.getRow(), draggedRow);
|
||||||
|
int startCol = Math.min(startPos.getColumn(), draggedCol);
|
||||||
|
int endCol = Math.max(startPos.getColumn(), draggedCol);
|
||||||
|
|
||||||
|
selectionModel.clearSelection();
|
||||||
|
trackedCells.clear();
|
||||||
|
|
||||||
|
for (int r = startRow; r <= endRow; r++) {
|
||||||
|
for (int c = startCol; c <= endCol; c++) {
|
||||||
|
if (r >= 0 && r < tableView.getItems().size()
|
||||||
|
&& c >= 0 && c < tableView.getColumns().size()) {
|
||||||
|
TableColumn<T, ?> col = tableView.getColumns().get(c);
|
||||||
|
selectionModel.select(r, col);
|
||||||
|
TablePosition<T, ?> pos = new TablePosition<>(tableView, r, col);
|
||||||
|
trackedCells.add(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.consume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик окончания перетаскивания
|
||||||
|
tableView.setOnMouseReleased(event -> {
|
||||||
|
dragStartMap.remove(tableView);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Сброс выделения при клике вне таблицы
|
||||||
|
// Устанавливаем обработчик на сцену, если она доступна
|
||||||
|
tableView.sceneProperty().addListener((obs, oldScene, newScene) -> {
|
||||||
|
if (newScene != null && oldScene == null) {
|
||||||
|
// Добавляем обработчик только один раз при первой установке сцены
|
||||||
|
newScene.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_CLICKED, event -> {
|
||||||
|
// Проверяем, был ли клик вне таблицы
|
||||||
|
javafx.scene.Node target = (javafx.scene.Node) event.getTarget();
|
||||||
|
if (!isNodeInTableView(tableView, target)) {
|
||||||
|
selectionModel.clearSelection();
|
||||||
|
if (trackedCells != null) {
|
||||||
|
trackedCells.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, находится ли узел внутри TableView
|
||||||
|
*/
|
||||||
|
private static <T> boolean isNodeInTableView(TableView<T> tableView, javafx.scene.Node node) {
|
||||||
|
javafx.scene.Node current = node;
|
||||||
|
while (current != null) {
|
||||||
|
if (current == tableView) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет отслеживаемый список выделенных ячеек на основе текущего выделения модели
|
||||||
|
* Используется как fallback, если trackedCells не синхронизирован
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> void updateTrackedCells(TableView<T> tableView, ObservableList<TablePosition<?, ?>> trackedCells) {
|
||||||
|
if (trackedCells == null) return;
|
||||||
|
|
||||||
|
// Обновляем на основе текущего выделения в модели
|
||||||
|
// В режиме cell selection нужно проверять каждую ячейку отдельно
|
||||||
|
TableSelectionModel<T> selectionModel = tableView.getSelectionModel();
|
||||||
|
ObservableList<Integer> selectedIndices = selectionModel.getSelectedIndices();
|
||||||
|
|
||||||
|
trackedCells.clear();
|
||||||
|
|
||||||
|
// Проходим по всем выделенным строкам и проверяем каждую колонку
|
||||||
|
for (Integer rowIndex : selectedIndices) {
|
||||||
|
if (rowIndex >= 0 && rowIndex < tableView.getItems().size()) {
|
||||||
|
for (TableColumn<T, ?> column : tableView.getColumns()) {
|
||||||
|
// Проверяем, выделена ли конкретная ячейка
|
||||||
|
// В JavaFX с cell selection это можно проверить через попытку выделения
|
||||||
|
// или через отслеживание состояния
|
||||||
|
try {
|
||||||
|
// Пытаемся использовать рефлексию для проверки выделения ячейки
|
||||||
|
java.lang.reflect.Method isSelectedMethod = selectionModel.getClass()
|
||||||
|
.getMethod("isSelected", int.class, TableColumn.class);
|
||||||
|
Boolean isSelected = (Boolean) isSelectedMethod.invoke(selectionModel, rowIndex, column);
|
||||||
|
if (Boolean.TRUE.equals(isSelected)) {
|
||||||
|
TablePosition<T, ?> pos = new TablePosition<>(tableView, rowIndex, column);
|
||||||
|
if (!trackedCells.contains(pos)) {
|
||||||
|
trackedCells.add(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Если метод недоступен, добавляем все ячейки выделенных строк
|
||||||
|
// Это не идеально, но лучше чем ничего
|
||||||
|
TablePosition<T, ?> pos = new TablePosition<>(tableView, rowIndex, column);
|
||||||
|
if (!trackedCells.contains(pos)) {
|
||||||
|
trackedCells.add(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Настраивает обработчик копирования (Ctrl+C)
|
||||||
|
*/
|
||||||
|
private static <T> void setupCopyHandler(TableView<T> tableView) {
|
||||||
|
KeyCodeCombination copyKey = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_DOWN);
|
||||||
|
|
||||||
|
tableView.setOnKeyPressed(event -> {
|
||||||
|
if (copyKey.match(event)) {
|
||||||
|
copySelectedCells(tableView);
|
||||||
|
event.consume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Копирует выделенные ячейки в буфер обмена
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> void copySelectedCells(TableView<T> tableView) {
|
||||||
|
TableSelectionModel<T> selectionModel = tableView.getSelectionModel();
|
||||||
|
|
||||||
|
if (!selectionModel.isCellSelectionEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем отслеживаемые выделенные ячейки
|
||||||
|
ObservableList<TablePosition<?, ?>> trackedCells = selectedCellsMap.get(tableView);
|
||||||
|
|
||||||
|
// Если список пуст или не синхронизирован, обновляем его
|
||||||
|
if (trackedCells == null || trackedCells.isEmpty()) {
|
||||||
|
if (trackedCells == null) {
|
||||||
|
trackedCells = FXCollections.observableArrayList();
|
||||||
|
selectedCellsMap.put(tableView, trackedCells);
|
||||||
|
}
|
||||||
|
updateTrackedCells(tableView, trackedCells);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Используем отслеживаемые ячейки для копирования
|
||||||
|
List<TablePosition<?, ?>> selectedCells = new ArrayList<>(trackedCells);
|
||||||
|
|
||||||
|
if (selectedCells.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортируем ячейки по строкам и колонкам
|
||||||
|
selectedCells.sort((a, b) -> {
|
||||||
|
int rowCompare = Integer.compare(a.getRow(), b.getRow());
|
||||||
|
if (rowCompare != 0) return rowCompare;
|
||||||
|
return Integer.compare(a.getColumn(), b.getColumn());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Группируем по строкам
|
||||||
|
StringBuilder clipboardText = new StringBuilder();
|
||||||
|
int currentRow = -1;
|
||||||
|
boolean firstCell = true;
|
||||||
|
|
||||||
|
for (TablePosition<?, ?> pos : selectedCells) {
|
||||||
|
if (currentRow != pos.getRow() && !firstCell) {
|
||||||
|
clipboardText.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentRow != pos.getRow()) {
|
||||||
|
currentRow = pos.getRow();
|
||||||
|
firstCell = true;
|
||||||
|
} else {
|
||||||
|
clipboardText.append("\t");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем значение ячейки
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
TablePosition<T, ?> typedPos = (TablePosition<T, ?>) pos;
|
||||||
|
Object cellValue = getCellValue(tableView, typedPos);
|
||||||
|
clipboardText.append(cellValue != null ? cellValue.toString() : "");
|
||||||
|
|
||||||
|
firstCell = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копируем в буфер обмена
|
||||||
|
Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||||
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
content.putString(clipboardText.toString());
|
||||||
|
clipboard.setContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает значение ячейки по позиции
|
||||||
|
*/
|
||||||
|
private static <T> Object getCellValue(TableView<T> tableView, TablePosition<T, ?> pos) {
|
||||||
|
if (pos.getRow() < 0 || pos.getRow() >= tableView.getItems().size()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
T rowItem = tableView.getItems().get(pos.getRow());
|
||||||
|
if (rowItem == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos.getColumn() < 0 || pos.getColumn() >= tableView.getColumns().size()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
TableColumn<T, ?> column = tableView.getColumns().get(pos.getColumn());
|
||||||
|
if (column == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем значение через cell value factory
|
||||||
|
Object cellValue = column.getCellData(rowItem);
|
||||||
|
return cellValue != null ? cellValue : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -12,6 +12,10 @@ public class AppConfig {
|
|||||||
public static final String KEY_WINDOW_HEIGHT = "window.height";
|
public static final String KEY_WINDOW_HEIGHT = "window.height";
|
||||||
public static final String KEY_WINDOW_X = "window.x";
|
public static final String KEY_WINDOW_X = "window.x";
|
||||||
public static final String KEY_WINDOW_Y = "window.y";
|
public static final String KEY_WINDOW_Y = "window.y";
|
||||||
|
public static final String KEY_HIDDEN_SUBMENUS = "submenu.hidden"; // comma-separated texts
|
||||||
|
public static final String KEY_PURCHASES_EXCEL_PATH = "purchases.excel.path";
|
||||||
|
public static final String KEY_MES_URL = "mes.url";
|
||||||
|
public static final String KEY_MES_MATCH_ERROR_URL = "mes.match_error.url";
|
||||||
|
|
||||||
// Значения по умолчанию
|
// Значения по умолчанию
|
||||||
public static final String DEFAULT_ADMIN_PASSWORD = "admin123";
|
public static final String DEFAULT_ADMIN_PASSWORD = "admin123";
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ package com.dsol.pki_management.controllers;
|
|||||||
import com.dsol.pki_management.components.MenuItem;
|
import com.dsol.pki_management.components.MenuItem;
|
||||||
import com.dsol.pki_management.components.SubMenuItem;
|
import com.dsol.pki_management.components.SubMenuItem;
|
||||||
import com.dsol.pki_management.modules.test1.Test1Controller;
|
import com.dsol.pki_management.modules.test1.Test1Controller;
|
||||||
|
import com.dsol.pki_management.config.ConfigManager;
|
||||||
|
import com.dsol.pki_management.config.AppConfig;
|
||||||
|
import com.dsol.pki_management.modules.purchases.PurchasesExcelValidator;
|
||||||
|
import com.dsol.pki_management.modules.purchases.PurchasesExcelParser;
|
||||||
|
import com.dsol.pki_management.modules.purchases.PurchasesDataStore;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
@@ -11,10 +16,18 @@ import javafx.scene.layout.StackPane;
|
|||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.Arrays;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Главный контроллер приложения
|
* Главный контроллер приложения
|
||||||
@@ -41,6 +54,8 @@ public class MainController {
|
|||||||
menuOverlay.setOnMouseClicked(e -> closeMenu());
|
menuOverlay.setOnMouseClicked(e -> closeMenu());
|
||||||
|
|
||||||
setupMenuItems();
|
setupMenuItems();
|
||||||
|
applyHiddenSubMenusFromConfig();
|
||||||
|
startPurchasesScanIfConfigured();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupMenuItems() {
|
private void setupMenuItems() {
|
||||||
@@ -59,6 +74,13 @@ public class MainController {
|
|||||||
createSubMenuItem("Отчеты", e -> System.out.println("Открыты отчеты"))
|
createSubMenuItem("Отчеты", e -> System.out.println("Открыты отчеты"))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
MenuItem purchases = createMenuItem("Закупки",
|
||||||
|
createSubMenuItem("Закупочный перечень", e -> {
|
||||||
|
loadPurchasesListModule();
|
||||||
|
closeMenu();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
MenuItem testMenu = createMenuItem("Test",
|
MenuItem testMenu = createMenuItem("Test",
|
||||||
createSubMenuItem("Тест 1", e -> {
|
createSubMenuItem("Тест 1", e -> {
|
||||||
loadTest1Module();
|
loadTest1Module();
|
||||||
@@ -68,10 +90,77 @@ public class MainController {
|
|||||||
|
|
||||||
// Добавляем блоки меню в контент меню
|
// Добавляем блоки меню в контент меню
|
||||||
if (menuContent != null) {
|
if (menuContent != null) {
|
||||||
menuContent.getChildren().addAll(mainFunctions, management, testMenu);
|
menuContent.getChildren().addAll(mainFunctions, management, purchases, testMenu);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Применяет скрытые подменю из конфигурации
|
||||||
|
*/
|
||||||
|
private void applyHiddenSubMenusFromConfig() {
|
||||||
|
ConfigManager config = ConfigManager.getInstance();
|
||||||
|
String raw = config.getProperty(AppConfig.KEY_HIDDEN_SUBMENUS, "");
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<String> hidden = Arrays.stream(raw.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.collect(Collectors.toCollection(HashSet::new));
|
||||||
|
|
||||||
|
for (SubMenuItem item : allSubMenuItems) {
|
||||||
|
if (hidden.contains(item.getText())) {
|
||||||
|
item.setItemVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (MenuItem menuItem : allMenuItems) {
|
||||||
|
menuItem.updateVisibility();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запускает фоновую проверку Excel закупок при старте приложения (если путь задан)
|
||||||
|
*/
|
||||||
|
private void startPurchasesScanIfConfigured() {
|
||||||
|
String pathStr = ConfigManager.getInstance().getProperty(AppConfig.KEY_PURCHASES_EXCEL_PATH, "");
|
||||||
|
if (pathStr == null || pathStr.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Path path = Paths.get(pathStr);
|
||||||
|
Task<Void> task = new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected Void call() {
|
||||||
|
try {
|
||||||
|
PurchasesDataStore.getInstance().setStatus("Сканирование файла...");
|
||||||
|
PurchasesExcelValidator validator = new PurchasesExcelValidator();
|
||||||
|
var results = validator.validate(path);
|
||||||
|
results.forEach(r -> {
|
||||||
|
if (r.valid) {
|
||||||
|
System.out.println("[OK] Лист '" + r.sheetName + "' — все обязательные заголовки найдены");
|
||||||
|
} else {
|
||||||
|
System.out.println("[WARN] Лист '" + r.sheetName + "' — отсутствуют заголовки: " + String.join(", ", r.missingHeaders));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Парсим данные и кладём в стор
|
||||||
|
PurchasesExcelParser parser = new PurchasesExcelParser();
|
||||||
|
var rows = parser.parse(path);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
PurchasesDataStore.getInstance().setRows(rows);
|
||||||
|
PurchasesDataStore.getInstance().setStatus("Загружено записей: " + rows.size());
|
||||||
|
});
|
||||||
|
} catch (Exception ex) {
|
||||||
|
System.err.println("[ERROR] Ошибка валидации/парсинга Excel при старте: " + ex.getMessage());
|
||||||
|
ex.printStackTrace();
|
||||||
|
Platform.runLater(() -> PurchasesDataStore.getInstance().setStatus("Ошибка: " + ex.getMessage()));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Thread t = new Thread(task, "startup-purchases-excel-validate");
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Создает MenuItem с указанным названием и подменю
|
* Создает MenuItem с указанным названием и подменю
|
||||||
*/
|
*/
|
||||||
@@ -143,6 +232,24 @@ public class MainController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загружает модуль "Закупочный перечень" в центральную область
|
||||||
|
*/
|
||||||
|
private void loadPurchasesListModule() {
|
||||||
|
try {
|
||||||
|
FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/dsol/pki_management/modules/purchases/purchases-list-view.fxml"));
|
||||||
|
VBox purchasesView = loader.load();
|
||||||
|
|
||||||
|
if (contentArea != null) {
|
||||||
|
contentArea.getChildren().clear();
|
||||||
|
contentArea.getChildren().add(purchasesView);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Ошибка загрузки модуля Закупочный перечень: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Загружает модуль настроек в центральную область
|
* Загружает модуль настроек в центральную область
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ import javafx.scene.layout.HBox;
|
|||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
|
import java.io.File;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Контроллер для модуля настроек
|
* Контроллер для модуля настроек
|
||||||
@@ -32,6 +37,12 @@ public class SettingsController {
|
|||||||
private Label statusLabel;
|
private Label statusLabel;
|
||||||
@FXML
|
@FXML
|
||||||
private VBox subMenuItemsContainer;
|
private VBox subMenuItemsContainer;
|
||||||
|
@FXML
|
||||||
|
private TextField purchasesExcelPathField;
|
||||||
|
@FXML
|
||||||
|
private TextField mesUrlField;
|
||||||
|
@FXML
|
||||||
|
private TextField mesMatchErrorUrlField;
|
||||||
|
|
||||||
private boolean isAdminMode = false;
|
private boolean isAdminMode = false;
|
||||||
private final ConfigManager configManager = ConfigManager.getInstance();
|
private final ConfigManager configManager = ConfigManager.getInstance();
|
||||||
@@ -54,6 +65,67 @@ public class SettingsController {
|
|||||||
if (exitAdminButton != null) {
|
if (exitAdminButton != null) {
|
||||||
exitAdminButton.setOnAction(e -> disableAdminMode());
|
exitAdminButton.setOnAction(e -> disableAdminMode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Инициализация поля пути к Excel файлу из конфига
|
||||||
|
if (purchasesExcelPathField != null) {
|
||||||
|
String path = configManager.getProperty(AppConfig.KEY_PURCHASES_EXCEL_PATH, "");
|
||||||
|
purchasesExcelPathField.setText(path);
|
||||||
|
purchasesExcelPathField.setOnAction(e -> savePurchasesExcelPath());
|
||||||
|
purchasesExcelPathField.focusedProperty().addListener((obs, oldV, newV) -> {
|
||||||
|
if (!newV) savePurchasesExcelPath();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация полей MES из конфига
|
||||||
|
if (mesUrlField != null) {
|
||||||
|
String url = configManager.getProperty(AppConfig.KEY_MES_URL, "");
|
||||||
|
mesUrlField.setText(url);
|
||||||
|
mesUrlField.setOnAction(e -> saveMesUrls());
|
||||||
|
mesUrlField.focusedProperty().addListener((obs, oldV, newV) -> {
|
||||||
|
if (!newV) saveMesUrls();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (mesMatchErrorUrlField != null) {
|
||||||
|
String url = configManager.getProperty(AppConfig.KEY_MES_MATCH_ERROR_URL, "");
|
||||||
|
mesMatchErrorUrlField.setText(url);
|
||||||
|
mesMatchErrorUrlField.setOnAction(e -> saveMesUrls());
|
||||||
|
mesMatchErrorUrlField.focusedProperty().addListener((obs, oldV, newV) -> {
|
||||||
|
if (!newV) saveMesUrls();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void savePurchasesExcelPath() {
|
||||||
|
if (purchasesExcelPathField == null) return;
|
||||||
|
String path = purchasesExcelPathField.getText() == null ? "" : purchasesExcelPathField.getText().trim();
|
||||||
|
configManager.setProperty(AppConfig.KEY_PURCHASES_EXCEL_PATH, path);
|
||||||
|
configManager.saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveMesUrls() {
|
||||||
|
if (mesUrlField != null) {
|
||||||
|
String url = mesUrlField.getText() == null ? "" : mesUrlField.getText().trim();
|
||||||
|
configManager.setProperty(AppConfig.KEY_MES_URL, url);
|
||||||
|
}
|
||||||
|
if (mesMatchErrorUrlField != null) {
|
||||||
|
String url = mesMatchErrorUrlField.getText() == null ? "" : mesMatchErrorUrlField.getText().trim();
|
||||||
|
configManager.setProperty(AppConfig.KEY_MES_MATCH_ERROR_URL, url);
|
||||||
|
}
|
||||||
|
configManager.saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void choosePurchasesExcelFile() {
|
||||||
|
FileChooser chooser = new FileChooser();
|
||||||
|
chooser.setTitle("Выберите Excel файл закупок");
|
||||||
|
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Excel (*.xlsx)", "*.xlsx"));
|
||||||
|
File initialDir = new File(System.getProperty("user.home"));
|
||||||
|
if (initialDir.exists()) chooser.setInitialDirectory(initialDir);
|
||||||
|
File file = chooser.showOpenDialog(null);
|
||||||
|
if (file != null) {
|
||||||
|
purchasesExcelPathField.setText(file.getAbsolutePath());
|
||||||
|
savePurchasesExcelPath();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,7 +202,7 @@ public class SettingsController {
|
|||||||
subMenuItemsContainer.setPadding(new Insets(15, 0, 15, 0));
|
subMenuItemsContainer.setPadding(new Insets(15, 0, 15, 0));
|
||||||
subMenuItemsContainer.setSpacing(8);
|
subMenuItemsContainer.setSpacing(8);
|
||||||
|
|
||||||
// Получаем все MenuItem и SubMenuItem из HelloController
|
// Получаем все MenuItem и SubMenuItem из MainController
|
||||||
List<MenuItem> menuItems = MainController.getAllMenuItems();
|
List<MenuItem> menuItems = MainController.getAllMenuItems();
|
||||||
|
|
||||||
if (menuItems.isEmpty()) {
|
if (menuItems.isEmpty()) {
|
||||||
@@ -194,7 +266,7 @@ public class SettingsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Переключает видимость SubMenuItem при клике
|
* Переключает видимость SubMenuItem при клике и сохраняет в конфиг
|
||||||
*/
|
*/
|
||||||
private void toggleSubMenuItemVisibility(SubMenuItem subMenuItem, Text text) {
|
private void toggleSubMenuItemVisibility(SubMenuItem subMenuItem, Text text) {
|
||||||
boolean newVisibility = !subMenuItem.isItemVisible();
|
boolean newVisibility = !subMenuItem.isItemVisible();
|
||||||
@@ -203,6 +275,27 @@ public class SettingsController {
|
|||||||
|
|
||||||
// Обновляем видимость всех MenuItem
|
// Обновляем видимость всех MenuItem
|
||||||
MainController.getAllMenuItems().forEach(MenuItem::updateVisibility);
|
MainController.getAllMenuItems().forEach(MenuItem::updateVisibility);
|
||||||
|
|
||||||
|
// Сохранить скрытые элементы в конфигурацию
|
||||||
|
persistHiddenSubMenusToConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохраняет список скрытых SubMenuItem в конфигурационный файл
|
||||||
|
*/
|
||||||
|
private void persistHiddenSubMenusToConfig() {
|
||||||
|
List<MenuItem> all = MainController.getAllMenuItems();
|
||||||
|
Set<String> hidden = new HashSet<>();
|
||||||
|
for (MenuItem menu : all) {
|
||||||
|
for (SubMenuItem sub : menu.getSubMenuItems()) {
|
||||||
|
if (!sub.isItemVisible()) {
|
||||||
|
hidden.add(sub.getText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String value = hidden.stream().sorted().collect(Collectors.joining(","));
|
||||||
|
configManager.setProperty(AppConfig.KEY_HIDDEN_SUBMENUS, value);
|
||||||
|
configManager.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package com.dsol.pki_management.modules.purchases;
|
||||||
|
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.TableCell;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Кастомная ячейка таблицы с подсветкой текста по регулярным выражениям
|
||||||
|
*/
|
||||||
|
public class HighlightedTableCell extends TableCell<PurchaseRow, String> {
|
||||||
|
private static final Color HIGHLIGHT_COLOR = Color.web("#ff4444"); // Красный цвет для подсветки
|
||||||
|
private static final Color TEXT_COLOR = Color.BLACK;
|
||||||
|
|
||||||
|
// Статический кэш паттернов для переиспользования
|
||||||
|
private static List<Pattern> cachedPatterns = List.of();
|
||||||
|
private static List<String> cachedStrings = List.of();
|
||||||
|
|
||||||
|
private List<Pattern> highlightPatterns = List.of();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает список строк для подсветки
|
||||||
|
* Из них будут сгенерированы регулярные выражения
|
||||||
|
* Оптимизировано: паттерны создаются один раз и кэшируются
|
||||||
|
*/
|
||||||
|
public void setHighlightStrings(List<String> highlightStrings) {
|
||||||
|
if (highlightStrings == null || highlightStrings.isEmpty()) {
|
||||||
|
this.highlightPatterns = List.of();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Нормализуем список (trim и фильтрация)
|
||||||
|
List<String> normalized = highlightStrings.stream()
|
||||||
|
.filter(s -> s != null && !s.trim().isEmpty())
|
||||||
|
.map(s -> s.trim())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Проверяем, совпадает ли список с закэшированным
|
||||||
|
if (normalized.equals(cachedStrings) && !cachedPatterns.isEmpty()) {
|
||||||
|
this.highlightPatterns = cachedPatterns;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем паттерны один раз и кэшируем их
|
||||||
|
this.highlightPatterns = normalized.stream()
|
||||||
|
.map(s -> Pattern.compile(Pattern.quote(s), Pattern.CASE_INSENSITIVE))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Сохраняем в статический кэш
|
||||||
|
cachedPatterns = this.highlightPatterns;
|
||||||
|
cachedStrings = normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(String item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
|
||||||
|
if (empty || item == null) {
|
||||||
|
setGraphic(null);
|
||||||
|
setText(null);
|
||||||
|
} else {
|
||||||
|
setGraphic(createHighlightedText(item));
|
||||||
|
setText(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает HBox с подсветкой совпадающих фрагментов
|
||||||
|
*/
|
||||||
|
private HBox createHighlightedText(String text) {
|
||||||
|
HBox hbox = new HBox();
|
||||||
|
hbox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
hbox.setSpacing(0);
|
||||||
|
|
||||||
|
if (highlightPatterns.isEmpty()) {
|
||||||
|
// Если нет паттернов для подсветки, просто показываем текст
|
||||||
|
Text normalText = new Text(text);
|
||||||
|
normalText.setFill(TEXT_COLOR);
|
||||||
|
hbox.getChildren().add(normalText);
|
||||||
|
return hbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Находим все совпадения для всех паттернов
|
||||||
|
List<MatchInfo> matches = findAllMatches(text);
|
||||||
|
|
||||||
|
if (matches.isEmpty()) {
|
||||||
|
// Нет совпадений - показываем обычный текст
|
||||||
|
Text normalText = new Text(text);
|
||||||
|
normalText.setFill(TEXT_COLOR);
|
||||||
|
hbox.getChildren().add(normalText);
|
||||||
|
return hbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортируем совпадения по позиции
|
||||||
|
matches.sort((a, b) -> Integer.compare(a.start, b.start));
|
||||||
|
|
||||||
|
// Строим HBox с подсветкой
|
||||||
|
int lastIndex = 0;
|
||||||
|
for (MatchInfo match : matches) {
|
||||||
|
// Добавляем текст до совпадения
|
||||||
|
if (match.start > lastIndex) {
|
||||||
|
Text beforeText = new Text(text.substring(lastIndex, match.start));
|
||||||
|
beforeText.setFill(TEXT_COLOR);
|
||||||
|
hbox.getChildren().add(beforeText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем подсвеченный текст с фоном
|
||||||
|
String highlightedText = text.substring(match.start, match.end);
|
||||||
|
Text textNode = new Text(highlightedText);
|
||||||
|
textNode.setFill(TEXT_COLOR);
|
||||||
|
|
||||||
|
// Создаем фон для подсветки
|
||||||
|
Rectangle background = new Rectangle();
|
||||||
|
background.setFill(HIGHLIGHT_COLOR);
|
||||||
|
background.widthProperty().bind(textNode.layoutBoundsProperty().map(bounds -> bounds.getWidth() + 4));
|
||||||
|
background.heightProperty().bind(textNode.layoutBoundsProperty().map(bounds -> bounds.getHeight()));
|
||||||
|
background.setArcWidth(2);
|
||||||
|
background.setArcHeight(2);
|
||||||
|
|
||||||
|
StackPane highlightedPane = new StackPane();
|
||||||
|
highlightedPane.getChildren().addAll(background, textNode);
|
||||||
|
hbox.getChildren().add(highlightedPane);
|
||||||
|
|
||||||
|
lastIndex = match.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем оставшийся текст
|
||||||
|
if (lastIndex < text.length()) {
|
||||||
|
Text afterText = new Text(text.substring(lastIndex));
|
||||||
|
afterText.setFill(TEXT_COLOR);
|
||||||
|
hbox.getChildren().add(afterText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Находит все совпадения всех паттернов в тексте
|
||||||
|
*/
|
||||||
|
private List<MatchInfo> findAllMatches(String text) {
|
||||||
|
List<MatchInfo> allMatches = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
if (text == null || text.isEmpty()) {
|
||||||
|
return allMatches;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Pattern pattern : highlightPatterns) {
|
||||||
|
Matcher matcher = pattern.matcher(text);
|
||||||
|
while (matcher.find()) {
|
||||||
|
allMatches.add(new MatchInfo(matcher.start(), matcher.end()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Объединяем перекрывающиеся совпадения
|
||||||
|
return mergeOverlappingMatches(allMatches);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Объединяет перекрывающиеся совпадения
|
||||||
|
*/
|
||||||
|
private List<MatchInfo> mergeOverlappingMatches(List<MatchInfo> matches) {
|
||||||
|
if (matches.isEmpty()) {
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MatchInfo> merged = new java.util.ArrayList<>();
|
||||||
|
matches.sort((a, b) -> Integer.compare(a.start, b.start));
|
||||||
|
|
||||||
|
MatchInfo current = matches.get(0);
|
||||||
|
for (int i = 1; i < matches.size(); i++) {
|
||||||
|
MatchInfo next = matches.get(i);
|
||||||
|
if (next.start <= current.end) {
|
||||||
|
// Перекрываются - объединяем
|
||||||
|
current = new MatchInfo(current.start, Math.max(current.end, next.end));
|
||||||
|
} else {
|
||||||
|
// Не перекрываются - сохраняем текущее и переходим к следующему
|
||||||
|
merged.add(current);
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
merged.add(current);
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Информация о совпадении
|
||||||
|
*/
|
||||||
|
private static class MatchInfo {
|
||||||
|
final int start;
|
||||||
|
final int end;
|
||||||
|
|
||||||
|
MatchInfo(int start, int end) {
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Конвертирует Color в hex строку
|
||||||
|
*/
|
||||||
|
private String toHexString(Color color) {
|
||||||
|
return String.format("#%02X%02X%02X",
|
||||||
|
(int) (color.getRed() * 255),
|
||||||
|
(int) (color.getGreen() * 255),
|
||||||
|
(int) (color.getBlue() * 255));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package com.dsol.pki_management.modules.purchases;
|
||||||
|
|
||||||
|
import com.dsol.pki_management.config.AppConfig;
|
||||||
|
import com.dsol.pki_management.config.ConfigManager;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Клиент для работы с API MES
|
||||||
|
*/
|
||||||
|
public class MesApiClient {
|
||||||
|
private static final Logger logger = Logger.getLogger(MesApiClient.class.getName());
|
||||||
|
private static final int TIMEOUT_SECONDS = 10;
|
||||||
|
|
||||||
|
private final ConfigManager configManager;
|
||||||
|
private final HttpClient httpClient;
|
||||||
|
|
||||||
|
public MesApiClient() {
|
||||||
|
this.configManager = ConfigManager.getInstance();
|
||||||
|
this.httpClient = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(TIMEOUT_SECONDS))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает список строк для регулярных выражений из API MES
|
||||||
|
*
|
||||||
|
* @return Список строк для подсветки, или пустой список в случае ошибки
|
||||||
|
*/
|
||||||
|
public List<String> fetchMatchErrorStrings() {
|
||||||
|
String mesUrl = configManager.getProperty(AppConfig.KEY_MES_URL, "");
|
||||||
|
String matchErrorUrl = configManager.getProperty(AppConfig.KEY_MES_MATCH_ERROR_URL, "");
|
||||||
|
|
||||||
|
if (mesUrl == null || mesUrl.trim().isEmpty() ||
|
||||||
|
matchErrorUrl == null || matchErrorUrl.trim().isEmpty()) {
|
||||||
|
logger.warning("URL MES или URL Match Error не настроены");
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Объединяем URL
|
||||||
|
String fullUrl = mesUrl.trim();
|
||||||
|
if (!fullUrl.endsWith("/") && !matchErrorUrl.startsWith("/")) {
|
||||||
|
fullUrl += "/";
|
||||||
|
}
|
||||||
|
fullUrl += matchErrorUrl.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(fullUrl))
|
||||||
|
.GET()
|
||||||
|
.timeout(Duration.ofSeconds(TIMEOUT_SECONDS))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
|
||||||
|
if (response.statusCode() == 200) {
|
||||||
|
return parseJsonResponse(response.body());
|
||||||
|
} else {
|
||||||
|
logger.warning("API MES вернул статус " + response.statusCode() + " для URL: " + fullUrl);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
logger.severe("Ошибка при запросе к API MES: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Парсит JSON ответ от API
|
||||||
|
* Ожидается массив строк: ["строка1", "строка2", ...]
|
||||||
|
*/
|
||||||
|
private List<String> parseJsonResponse(String jsonBody) {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info("Парсинг JSON ответа (длина: " + jsonBody.length() + "): " + jsonBody);
|
||||||
|
|
||||||
|
// Используем многострочный режим для обработки JSON с переносами строк
|
||||||
|
// Ищем строки в формате: "текст" (только двойные кавычки, как в стандартном JSON)
|
||||||
|
// Улучшенное регулярное выражение для обработки экранированных кавычек и многострочного режима
|
||||||
|
Pattern pattern = Pattern.compile("\"((?:[^\"\\\\]|\\\\.)*)\"", Pattern.MULTILINE | Pattern.DOTALL);
|
||||||
|
Matcher matcher = pattern.matcher(jsonBody);
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
String value = matcher.group(1);
|
||||||
|
if (value != null) {
|
||||||
|
// Убираем пробелы в начале и конце, но сохраняем внутренние пробелы
|
||||||
|
value = value.trim();
|
||||||
|
if (!value.isEmpty()) {
|
||||||
|
// Декодируем Unicode escape-последовательности (\\uXXXX) в обычные символы
|
||||||
|
value = decodeUnicodeEscapes(value);
|
||||||
|
result.add(value);
|
||||||
|
logger.info("Извлечена строка для подсветки: '" + value + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
logger.warning("Не удалось извлечь строки из JSON ответа. Исходный ответ: " + jsonBody);
|
||||||
|
// Попробуем альтернативный метод - простое извлечение строк между кавычками
|
||||||
|
Pattern simplePattern = Pattern.compile("\"([^\"]+)\"");
|
||||||
|
Matcher simpleMatcher = simplePattern.matcher(jsonBody);
|
||||||
|
while (simpleMatcher.find()) {
|
||||||
|
String value = simpleMatcher.group(1).trim();
|
||||||
|
if (!value.isEmpty()) {
|
||||||
|
// Декодируем Unicode escape-последовательности (\\uXXXX) в обычные символы
|
||||||
|
value = decodeUnicodeEscapes(value);
|
||||||
|
result.add(value);
|
||||||
|
logger.info("Извлечена строка альтернативным методом: '" + value + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info("Успешно извлечено строк для подсветки: " + result.size());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.severe("Ошибка при парсинге JSON ответа от API MES: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Декодирует Unicode escape-последовательности (\\uXXXX) в обычные символы
|
||||||
|
* Например: "\\u0437\\u0430\\u043c\\u0435\\u043d\\u0430" преобразуется в "замена"
|
||||||
|
*/
|
||||||
|
private String decodeUnicodeEscapes(String input) {
|
||||||
|
if (input == null || input.isEmpty()) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем паттерн \\uXXXX (4 шестнадцатеричные цифры)
|
||||||
|
// Используем конкатенацию строк, чтобы избежать интерпретации \\u компилятором
|
||||||
|
String backslash = "\\";
|
||||||
|
String unicodePatternStr = backslash + backslash + "u([0-9a-fA-F]{4})";
|
||||||
|
Pattern unicodePattern = Pattern.compile(unicodePatternStr);
|
||||||
|
Matcher matcher = unicodePattern.matcher(input);
|
||||||
|
|
||||||
|
StringBuffer result = new StringBuffer();
|
||||||
|
while (matcher.find()) {
|
||||||
|
// Извлекаем код символа из шестнадцатеричного представления
|
||||||
|
int codePoint = Integer.parseInt(matcher.group(1), 16);
|
||||||
|
// Заменяем escape-последовательность на реальный символ
|
||||||
|
matcher.appendReplacement(result, new String(Character.toChars(codePoint)));
|
||||||
|
}
|
||||||
|
matcher.appendTail(result);
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.dsol.pki_management.modules.purchases;
|
||||||
|
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
|
||||||
|
public class PurchaseRow {
|
||||||
|
private final StringProperty type = new SimpleStringProperty("");
|
||||||
|
private final StringProperty nameFull = new SimpleStringProperty("");
|
||||||
|
private final StringProperty nameUPD = new SimpleStringProperty("");
|
||||||
|
private final StringProperty quantity = new SimpleStringProperty("");
|
||||||
|
private final StringProperty onePiece = new SimpleStringProperty("");
|
||||||
|
private final StringProperty stockCode = new SimpleStringProperty("");
|
||||||
|
private final StringProperty expectedDate = new SimpleStringProperty("");
|
||||||
|
private final StringProperty actualDate = new SimpleStringProperty("");
|
||||||
|
private final StringProperty invoice = new SimpleStringProperty("");
|
||||||
|
private final StringProperty orderNumber = new SimpleStringProperty("");
|
||||||
|
private final StringProperty redmine = new SimpleStringProperty("");
|
||||||
|
private final StringProperty owner = new SimpleStringProperty("");
|
||||||
|
|
||||||
|
public StringProperty typeProperty() { return type; }
|
||||||
|
public StringProperty nameFullProperty() { return nameFull; }
|
||||||
|
public StringProperty nameUPDProperty() { return nameUPD; }
|
||||||
|
public StringProperty quantityProperty() { return quantity; }
|
||||||
|
public StringProperty onePieceProperty() { return onePiece; }
|
||||||
|
public StringProperty stockCodeProperty() { return stockCode; }
|
||||||
|
public StringProperty expectedDateProperty() { return expectedDate; }
|
||||||
|
public StringProperty actualDateProperty() { return actualDate; }
|
||||||
|
public StringProperty invoiceProperty() { return invoice; }
|
||||||
|
public StringProperty orderNumberProperty() { return orderNumber; }
|
||||||
|
public StringProperty redmineProperty() { return redmine; }
|
||||||
|
public StringProperty ownerProperty() { return owner; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package com.dsol.pki_management.modules.purchases;
|
||||||
|
|
||||||
|
import com.dsol.pki_management.components.TableViewEnhancer;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.collections.transformation.FilteredList;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TableColumn;
|
||||||
|
import javafx.scene.control.TableView;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.control.cell.PropertyValueFactory;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Контроллер для модуля "Закупочный перечень"
|
||||||
|
*/
|
||||||
|
public class PurchasesController {
|
||||||
|
@FXML
|
||||||
|
private VBox root;
|
||||||
|
@FXML
|
||||||
|
private Label titleLabel;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TableView<PurchaseRow> purchasesTable;
|
||||||
|
@FXML
|
||||||
|
private TableColumn<PurchaseRow, String> colType;
|
||||||
|
@FXML
|
||||||
|
private TableColumn<PurchaseRow, String> colNameFull;
|
||||||
|
@FXML
|
||||||
|
private TableColumn<PurchaseRow, String> colNameUPD;
|
||||||
|
@FXML
|
||||||
|
private TableColumn<PurchaseRow, String> colQty;
|
||||||
|
@FXML
|
||||||
|
private TableColumn<PurchaseRow, String> colOne;
|
||||||
|
@FXML
|
||||||
|
private TableColumn<PurchaseRow, String> colStockCode;
|
||||||
|
@FXML
|
||||||
|
private TableColumn<PurchaseRow, String> colExpectedDate;
|
||||||
|
@FXML
|
||||||
|
private TableColumn<PurchaseRow, String> colActualDate;
|
||||||
|
@FXML
|
||||||
|
private TableColumn<PurchaseRow, String> colInvoice;
|
||||||
|
@FXML
|
||||||
|
private TableColumn<PurchaseRow, String> colOrderNumber;
|
||||||
|
@FXML
|
||||||
|
private TableColumn<PurchaseRow, String> colRedmine;
|
||||||
|
@FXML
|
||||||
|
private TableColumn<PurchaseRow, String> colOwner;
|
||||||
|
|
||||||
|
// Фильтры
|
||||||
|
@FXML
|
||||||
|
private TextField filterType;
|
||||||
|
@FXML
|
||||||
|
private TextField filterNameFull;
|
||||||
|
@FXML
|
||||||
|
private TextField filterNameUPD;
|
||||||
|
@FXML
|
||||||
|
private TextField filterQty;
|
||||||
|
@FXML
|
||||||
|
private TextField filterOne;
|
||||||
|
@FXML
|
||||||
|
private TextField filterStockCode;
|
||||||
|
@FXML
|
||||||
|
private TextField filterExpectedDate;
|
||||||
|
@FXML
|
||||||
|
private TextField filterActualDate;
|
||||||
|
@FXML
|
||||||
|
private TextField filterInvoice;
|
||||||
|
@FXML
|
||||||
|
private TextField filterOrderNumber;
|
||||||
|
@FXML
|
||||||
|
private TextField filterRedmine;
|
||||||
|
@FXML
|
||||||
|
private TextField filterOwner;
|
||||||
|
|
||||||
|
private FilteredList<PurchaseRow> filteredData;
|
||||||
|
private MesApiClient mesApiClient;
|
||||||
|
private List<String> highlightStrings = List.of();
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
if (titleLabel != null) {
|
||||||
|
titleLabel.setText("Закупочный перечень");
|
||||||
|
}
|
||||||
|
|
||||||
|
mesApiClient = new MesApiClient();
|
||||||
|
initTable();
|
||||||
|
initFilters();
|
||||||
|
// Подключаем таблицу к общему стору, чтобы отразить данные, загруженные при старте
|
||||||
|
ObservableList<PurchaseRow> allData = PurchasesDataStore.getInstance().getRows();
|
||||||
|
filteredData = new FilteredList<>(allData, p -> true);
|
||||||
|
purchasesTable.setItems(filteredData);
|
||||||
|
// Настраиваем расширенные возможности таблицы (выделение ячеек, копирование)
|
||||||
|
TableViewEnhancer.enhance(purchasesTable);
|
||||||
|
|
||||||
|
// Загружаем данные для подсветки из API в фоновом режиме
|
||||||
|
loadHighlightData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initTable() {
|
||||||
|
colType.setCellValueFactory(new PropertyValueFactory<>("type"));
|
||||||
|
colNameFull.setCellValueFactory(new PropertyValueFactory<>("nameFull"));
|
||||||
|
colNameUPD.setCellValueFactory(new PropertyValueFactory<>("nameUPD"));
|
||||||
|
colQty.setCellValueFactory(new PropertyValueFactory<>("quantity"));
|
||||||
|
colOne.setCellValueFactory(new PropertyValueFactory<>("onePiece"));
|
||||||
|
colStockCode.setCellValueFactory(new PropertyValueFactory<>("stockCode"));
|
||||||
|
colExpectedDate.setCellValueFactory(new PropertyValueFactory<>("expectedDate"));
|
||||||
|
colActualDate.setCellValueFactory(new PropertyValueFactory<>("actualDate"));
|
||||||
|
colInvoice.setCellValueFactory(new PropertyValueFactory<>("invoice"));
|
||||||
|
colOrderNumber.setCellValueFactory(new PropertyValueFactory<>("orderNumber"));
|
||||||
|
colRedmine.setCellValueFactory(new PropertyValueFactory<>("redmine"));
|
||||||
|
colOwner.setCellValueFactory(new PropertyValueFactory<>("owner"));
|
||||||
|
purchasesTable.setPlaceholder(new Label("Нет данных"));
|
||||||
|
|
||||||
|
// Настраиваем кастомную ячейку с подсветкой для колонки "Наименование компонента (полное)"
|
||||||
|
// Данные для подсветки будут загружены асинхронно из API
|
||||||
|
updateNameFullCellFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет cellFactory для колонки "Наименование компонента (полное)" с текущими данными для подсветки
|
||||||
|
*/
|
||||||
|
private void updateNameFullCellFactory() {
|
||||||
|
colNameFull.setCellFactory(column -> {
|
||||||
|
HighlightedTableCell cell = new HighlightedTableCell();
|
||||||
|
cell.setHighlightStrings(highlightStrings);
|
||||||
|
return cell;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загружает данные для подсветки из API MES в фоновом режиме
|
||||||
|
*/
|
||||||
|
private void loadHighlightData() {
|
||||||
|
Task<List<String>> task = new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected List<String> call() {
|
||||||
|
return mesApiClient.fetchMatchErrorStrings();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
task.setOnSucceeded(e -> {
|
||||||
|
List<String> loadedStrings = task.getValue();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
// Сохраняем загруженные данные
|
||||||
|
highlightStrings = loadedStrings;
|
||||||
|
// Обновляем cellFactory с новыми данными
|
||||||
|
updateNameFullCellFactory();
|
||||||
|
// Принудительно обновляем колонку и таблицу
|
||||||
|
colNameFull.setVisible(false);
|
||||||
|
colNameFull.setVisible(true);
|
||||||
|
purchasesTable.refresh();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
task.setOnFailed(e -> {
|
||||||
|
// В случае ошибки просто логируем, подсветка не будет работать
|
||||||
|
System.err.println("Ошибка при загрузке данных для подсветки: " + task.getException().getMessage());
|
||||||
|
if (task.getException() != null) {
|
||||||
|
task.getException().printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Thread thread = new Thread(task);
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initFilters() {
|
||||||
|
// Настраиваем обработчики для всех фильтров
|
||||||
|
filterType.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||||
|
filterNameFull.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||||
|
filterNameUPD.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||||
|
filterQty.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||||
|
filterOne.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||||
|
filterStockCode.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||||
|
filterExpectedDate.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||||
|
filterActualDate.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||||
|
filterInvoice.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||||
|
filterOrderNumber.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||||
|
filterRedmine.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||||
|
filterOwner.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFilters() {
|
||||||
|
if (filteredData == null) return;
|
||||||
|
|
||||||
|
filteredData.setPredicate(row -> {
|
||||||
|
if (!matchesFilter(filterType.getText(), row.typeProperty().get())) return false;
|
||||||
|
if (!matchesFilter(filterNameFull.getText(), row.nameFullProperty().get())) return false;
|
||||||
|
if (!matchesFilter(filterNameUPD.getText(), row.nameUPDProperty().get())) return false;
|
||||||
|
if (!matchesFilter(filterQty.getText(), row.quantityProperty().get())) return false;
|
||||||
|
if (!matchesFilter(filterOne.getText(), row.onePieceProperty().get())) return false;
|
||||||
|
if (!matchesFilter(filterStockCode.getText(), row.stockCodeProperty().get())) return false;
|
||||||
|
if (!matchesFilter(filterExpectedDate.getText(), row.expectedDateProperty().get())) return false;
|
||||||
|
if (!matchesFilter(filterActualDate.getText(), row.actualDateProperty().get())) return false;
|
||||||
|
if (!matchesFilter(filterInvoice.getText(), row.invoiceProperty().get())) return false;
|
||||||
|
if (!matchesFilter(filterOrderNumber.getText(), row.orderNumberProperty().get())) return false;
|
||||||
|
if (!matchesFilter(filterRedmine.getText(), row.redmineProperty().get())) return false;
|
||||||
|
if (!matchesFilter(filterOwner.getText(), row.ownerProperty().get())) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, соответствует ли значение ячейки фильтру.
|
||||||
|
* Поддерживает множественные значения через запятую (например: "Резистор,Конденсатор").
|
||||||
|
*
|
||||||
|
* @param filterText Текст фильтра (может содержать несколько значений через запятую)
|
||||||
|
* @param cellValue Значение ячейки для проверки
|
||||||
|
* @return true, если значение соответствует фильтру
|
||||||
|
*/
|
||||||
|
private boolean matchesFilter(String filterText, String cellValue) {
|
||||||
|
if (filterText == null || filterText.trim().isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cellValue == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String lowerCellValue = cellValue.toLowerCase();
|
||||||
|
|
||||||
|
// Разбиваем фильтр по запятой и проверяем каждое значение
|
||||||
|
String[] filterValues = filterText.split(",");
|
||||||
|
for (String filterValue : filterValues) {
|
||||||
|
String trimmedFilter = filterValue.trim();
|
||||||
|
if (!trimmedFilter.isEmpty()) {
|
||||||
|
// Если хотя бы одно значение из списка найдено - возвращаем true
|
||||||
|
if (lowerCellValue.contains(trimmedFilter.toLowerCase())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если ни одно значение не найдено - возвращаем false
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.dsol.pki_management.modules.purchases;
|
||||||
|
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PurchasesDataStore {
|
||||||
|
private static final PurchasesDataStore INSTANCE = new PurchasesDataStore();
|
||||||
|
|
||||||
|
private final ObservableList<PurchaseRow> rows = FXCollections.observableArrayList();
|
||||||
|
private volatile String status = "";
|
||||||
|
private volatile Instant lastUpdated = null;
|
||||||
|
|
||||||
|
private PurchasesDataStore() {}
|
||||||
|
|
||||||
|
public static PurchasesDataStore getInstance() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableList<PurchaseRow> getRows() {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRows(List<PurchaseRow> newRows) {
|
||||||
|
rows.setAll(newRows);
|
||||||
|
lastUpdated = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getLastUpdated() {
|
||||||
|
return lastUpdated;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package com.dsol.pki_management.modules.purchases;
|
||||||
|
|
||||||
|
import org.apache.poi.ss.usermodel.*;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Парсер Excel для модуля закупок. Читает листы с корректными заголовками и
|
||||||
|
* формирует список PurchaseRow по колонкам.
|
||||||
|
*/
|
||||||
|
public class PurchasesExcelParser {
|
||||||
|
private static final int HEADER_ROW_INDEX = 3; // 4-я строка
|
||||||
|
private static final int FIRST_DATA_ROW_INDEX = 5; // 6-я строка (после пустой строки 5)
|
||||||
|
|
||||||
|
// Названия колонок, которые нужно загрузить
|
||||||
|
private static final List<String> NEEDED_HEADERS = List.of(
|
||||||
|
"Тип",
|
||||||
|
"Наименование компонента (полное)",
|
||||||
|
"Наименование в УПД",
|
||||||
|
"К-во",
|
||||||
|
"1 шт",
|
||||||
|
"Шифр Склад/Бухгалтерия",
|
||||||
|
"Ожидаемая дата",
|
||||||
|
"Фактическая дата",
|
||||||
|
"Накладная, №, дата",
|
||||||
|
"Номер заказа",
|
||||||
|
"Redmine",
|
||||||
|
"Ответственный"
|
||||||
|
);
|
||||||
|
|
||||||
|
public List<PurchaseRow> parse(Path xlsxPath) throws Exception {
|
||||||
|
if (xlsxPath == null || !Files.exists(xlsxPath)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<PurchaseRow> result = new ArrayList<>();
|
||||||
|
try (InputStream in = Files.newInputStream(xlsxPath); Workbook wb = new XSSFWorkbook(in)) {
|
||||||
|
for (int i = 0; i < wb.getNumberOfSheets(); i++) {
|
||||||
|
Sheet sheet = wb.getSheetAt(i);
|
||||||
|
Map<String, Integer> headerToIndex = readHeaderIndexes(sheet);
|
||||||
|
if (!headerToIndex.keySet().containsAll(NEEDED_HEADERS)) {
|
||||||
|
// Пропускаем листы без необходимых заголовков
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Читаем строки данных
|
||||||
|
int lastRow = sheet.getLastRowNum();
|
||||||
|
for (int r = FIRST_DATA_ROW_INDEX; r <= lastRow; r++) {
|
||||||
|
Row row = sheet.getRow(r);
|
||||||
|
if (row == null) continue;
|
||||||
|
// Признак пустой строки: все нужные ячейки пустые
|
||||||
|
if (isRowEmpty(row, headerToIndex)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PurchaseRow item = new PurchaseRow();
|
||||||
|
item.typeProperty().set(getCellString(row, headerToIndex.get("Тип")));
|
||||||
|
item.nameFullProperty().set(getCellString(row, headerToIndex.get("Наименование компонента (полное)")));
|
||||||
|
item.nameUPDProperty().set(getCellString(row, headerToIndex.get("Наименование в УПД")));
|
||||||
|
item.quantityProperty().set(getCellString(row, headerToIndex.get("К-во")));
|
||||||
|
item.onePieceProperty().set(getCellString(row, headerToIndex.get("1 шт")));
|
||||||
|
item.stockCodeProperty().set(getCellString(row, headerToIndex.get("Шифр Склад/Бухгалтерия")));
|
||||||
|
item.expectedDateProperty().set(getCellString(row, headerToIndex.get("Ожидаемая дата")));
|
||||||
|
item.actualDateProperty().set(getCellString(row, headerToIndex.get("Фактическая дата")));
|
||||||
|
item.invoiceProperty().set(getCellString(row, headerToIndex.get("Накладная, №, дата")));
|
||||||
|
item.orderNumberProperty().set(getCellString(row, headerToIndex.get("Номер заказа")));
|
||||||
|
item.redmineProperty().set(getCellString(row, headerToIndex.get("Redmine")));
|
||||||
|
item.ownerProperty().set(getCellString(row, headerToIndex.get("Ответственный")));
|
||||||
|
result.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Integer> readHeaderIndexes(Sheet sheet) {
|
||||||
|
Map<String, Integer> map = new HashMap<>();
|
||||||
|
if (sheet == null) return map;
|
||||||
|
Row headerRow = sheet.getRow(HEADER_ROW_INDEX);
|
||||||
|
if (headerRow == null) return map;
|
||||||
|
int lastCell = headerRow.getLastCellNum();
|
||||||
|
for (int c = 0; c < lastCell; c++) {
|
||||||
|
Cell cell = headerRow.getCell(c);
|
||||||
|
String text = getCellString(cell).trim();
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
map.put(text, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRowEmpty(Row row, Map<String, Integer> headerToIndex) {
|
||||||
|
for (String header : NEEDED_HEADERS) {
|
||||||
|
Integer idx = headerToIndex.get(header);
|
||||||
|
if (idx == null) continue;
|
||||||
|
String v = getCellString(row, idx).trim();
|
||||||
|
if (!v.isEmpty()) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCellString(Row row, int columnIndex) {
|
||||||
|
if (columnIndex < 0) return "";
|
||||||
|
Cell cell = row.getCell(columnIndex, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL);
|
||||||
|
return getCellString(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCellString(Cell cell) {
|
||||||
|
if (cell == null) return "";
|
||||||
|
return switch (cell.getCellType()) {
|
||||||
|
case STRING -> cell.getStringCellValue();
|
||||||
|
case NUMERIC -> {
|
||||||
|
if (DateUtil.isCellDateFormatted(cell)) {
|
||||||
|
// Преобразуем дату в строку по умолчанию (yyyy-MM-dd)
|
||||||
|
java.util.Date d = cell.getDateCellValue();
|
||||||
|
yield new java.text.SimpleDateFormat("yyyy-MM-dd").format(d);
|
||||||
|
}
|
||||||
|
double val = cell.getNumericCellValue();
|
||||||
|
// Убираем .0 у целых чисел
|
||||||
|
if (Math.floor(val) == val) {
|
||||||
|
yield String.valueOf((long) val);
|
||||||
|
}
|
||||||
|
// Ограничиваем до 2 знаков после запятой
|
||||||
|
java.text.DecimalFormat df = new java.text.DecimalFormat("#.##");
|
||||||
|
df.setMaximumFractionDigits(2);
|
||||||
|
df.setMinimumFractionDigits(0);
|
||||||
|
yield df.format(val);
|
||||||
|
}
|
||||||
|
case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
|
||||||
|
case FORMULA -> {
|
||||||
|
try {
|
||||||
|
yield cell.getStringCellValue();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
yield String.valueOf(cell.getNumericCellValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.dsol.pki_management.modules.purchases;
|
||||||
|
|
||||||
|
import org.apache.poi.ss.usermodel.Cell;
|
||||||
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валидатор Excel (XLSX) по шаблону закупок.
|
||||||
|
* Проверяет, что на каждой странице (Sheet) в строке 4 присутствуют необходимые заголовки.
|
||||||
|
*/
|
||||||
|
public class PurchasesExcelValidator {
|
||||||
|
// Строка с заголовками (0-based index)
|
||||||
|
private static final int HEADER_ROW_INDEX = 3; // 4-я строка
|
||||||
|
|
||||||
|
// Обязательные заголовки
|
||||||
|
private static final List<String> REQUIRED_HEADERS = List.of(
|
||||||
|
"Наименование компонента (полное)",
|
||||||
|
"Наименование в УПД",
|
||||||
|
"К-во",
|
||||||
|
"Шифр Склад/Бухгалтерия",
|
||||||
|
"Поставщик",
|
||||||
|
"Фактическая дата",
|
||||||
|
"Накладная, №, дата"
|
||||||
|
);
|
||||||
|
|
||||||
|
public static class SheetValidationResult {
|
||||||
|
public final String sheetName;
|
||||||
|
public final boolean valid;
|
||||||
|
public final List<String> missingHeaders;
|
||||||
|
|
||||||
|
public SheetValidationResult(String sheetName, boolean valid, List<String> missingHeaders) {
|
||||||
|
this.sheetName = sheetName;
|
||||||
|
this.valid = valid;
|
||||||
|
this.missingHeaders = missingHeaders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валидирует файл и возвращает результаты по всем листам
|
||||||
|
*/
|
||||||
|
public List<SheetValidationResult> validate(Path xlsxPath) throws IOException {
|
||||||
|
if (xlsxPath == null || !Files.exists(xlsxPath)) {
|
||||||
|
throw new IOException("Файл не найден: " + xlsxPath);
|
||||||
|
}
|
||||||
|
try (InputStream in = Files.newInputStream(xlsxPath); Workbook wb = new XSSFWorkbook(in)) {
|
||||||
|
List<SheetValidationResult> results = new ArrayList<>();
|
||||||
|
for (int i = 0; i < wb.getNumberOfSheets(); i++) {
|
||||||
|
Sheet sheet = wb.getSheetAt(i);
|
||||||
|
SheetValidationResult r = validateSheet(sheet);
|
||||||
|
results.add(r);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SheetValidationResult validateSheet(Sheet sheet) {
|
||||||
|
Set<String> headersInRow = readHeaderRow(sheet);
|
||||||
|
List<String> missing = new ArrayList<>();
|
||||||
|
for (String required : REQUIRED_HEADERS) {
|
||||||
|
if (!headersInRow.contains(required)) {
|
||||||
|
missing.add(required);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new SheetValidationResult(sheet.getSheetName(), missing.isEmpty(), missing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> readHeaderRow(Sheet sheet) {
|
||||||
|
Set<String> headers = new HashSet<>();
|
||||||
|
if (sheet == null) return headers;
|
||||||
|
Row headerRow = sheet.getRow(HEADER_ROW_INDEX);
|
||||||
|
if (headerRow == null) return headers;
|
||||||
|
int lastCell = headerRow.getLastCellNum();
|
||||||
|
for (int c = 0; c < lastCell; c++) {
|
||||||
|
Cell cell = headerRow.getCell(c);
|
||||||
|
if (cell == null) continue;
|
||||||
|
String value = cellToString(cell).trim();
|
||||||
|
if (!value.isEmpty()) headers.add(value);
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cellToString(Cell cell) {
|
||||||
|
return switch (cell.getCellType()) {
|
||||||
|
case STRING -> cell.getStringCellValue();
|
||||||
|
case NUMERIC -> String.valueOf(cell.getNumericCellValue());
|
||||||
|
case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
|
||||||
|
case FORMULA -> cell.getCellFormula();
|
||||||
|
default -> "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,21 +2,26 @@ module com.dsol.pki_management {
|
|||||||
requires javafx.controls;
|
requires javafx.controls;
|
||||||
requires javafx.fxml;
|
requires javafx.fxml;
|
||||||
requires javafx.web;
|
requires javafx.web;
|
||||||
|
requires java.logging;
|
||||||
|
requires java.net.http;
|
||||||
|
requires org.apache.poi.ooxml;
|
||||||
|
requires org.apache.poi.poi;
|
||||||
|
|
||||||
requires org.controlsfx.controls;
|
requires org.controlsfx.controls;
|
||||||
requires com.dlsc.formsfx;
|
requires com.dlsc.formsfx;
|
||||||
requires org.kordamp.ikonli.javafx;
|
requires org.kordamp.ikonli.javafx;
|
||||||
requires org.kordamp.bootstrapfx.core;
|
requires org.kordamp.bootstrapfx.core;
|
||||||
requires eu.hansolo.tilesfx;
|
|
||||||
|
|
||||||
opens com.dsol.pki_management.app to javafx.fxml;
|
opens com.dsol.pki_management.app to javafx.fxml;
|
||||||
opens com.dsol.pki_management.controllers to javafx.fxml;
|
opens com.dsol.pki_management.controllers to javafx.fxml;
|
||||||
opens com.dsol.pki_management.components to javafx.fxml;
|
opens com.dsol.pki_management.components to javafx.fxml;
|
||||||
opens com.dsol.pki_management.modules.test1 to javafx.fxml;
|
opens com.dsol.pki_management.modules.test1 to javafx.fxml;
|
||||||
|
opens com.dsol.pki_management.modules.purchases to javafx.fxml;
|
||||||
|
|
||||||
exports com.dsol.pki_management.app;
|
exports com.dsol.pki_management.app;
|
||||||
exports com.dsol.pki_management.controllers;
|
exports com.dsol.pki_management.controllers;
|
||||||
exports com.dsol.pki_management.components;
|
exports com.dsol.pki_management.components;
|
||||||
exports com.dsol.pki_management.modules.test1;
|
exports com.dsol.pki_management.modules.test1;
|
||||||
|
exports com.dsol.pki_management.modules.purchases;
|
||||||
exports com.dsol.pki_management.config;
|
exports com.dsol.pki_management.config;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.TableColumn?>
|
||||||
|
<?import javafx.scene.control.TableView?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
|
||||||
|
<VBox fx:controller="com.dsol.pki_management.modules.purchases.PurchasesController"
|
||||||
|
xmlns="http://javafx.com/javafx/21"
|
||||||
|
xmlns:fx="http://javafx.com/fxml/1"
|
||||||
|
alignment="TOP_LEFT"
|
||||||
|
spacing="16.0"
|
||||||
|
style="-fx-background-color: white; -fx-padding: 24;">
|
||||||
|
<children>
|
||||||
|
<Label fx:id="titleLabel"
|
||||||
|
style="-fx-font-size: 24px; -fx-font-weight: bold; -fx-text-fill: #0d6efd;"
|
||||||
|
text="Закупочный перечень"/>
|
||||||
|
|
||||||
|
<!-- Строка фильтров -->
|
||||||
|
<HBox spacing="2.0" style="-fx-background-color: #f8f9fa; -fx-padding: 10 0 10 0; -fx-border-color: #dee2e6; -fx-border-width: 0 0 1 0;">
|
||||||
|
<TextField fx:id="filterType" prefWidth="100.0" minWidth="100.0" maxWidth="100.0" promptText="Тип"
|
||||||
|
style="-fx-background-color: white; -fx-border-color: #ced4da; -fx-border-width: 1; -fx-border-radius: 4; -fx-padding: 6 10 6 10; -fx-font-size: 12px;"/>
|
||||||
|
<TextField fx:id="filterNameFull" prefWidth="260.0" minWidth="260.0" maxWidth="260.0" promptText="Наименование компонента (полное)"
|
||||||
|
style="-fx-background-color: white; -fx-border-color: #ced4da; -fx-border-width: 1; -fx-border-radius: 4; -fx-padding: 6 10 6 10; -fx-font-size: 12px;"/>
|
||||||
|
<TextField fx:id="filterNameUPD" prefWidth="220.0" minWidth="220.0" maxWidth="220.0" promptText="Наименование в УПД"
|
||||||
|
style="-fx-background-color: white; -fx-border-color: #ced4da; -fx-border-width: 1; -fx-border-radius: 4; -fx-padding: 6 10 6 10; -fx-font-size: 12px;"/>
|
||||||
|
<TextField fx:id="filterQty" prefWidth="70.0" minWidth="70.0" maxWidth="70.0" promptText="К-во"
|
||||||
|
style="-fx-background-color: white; -fx-border-color: #ced4da; -fx-border-width: 1; -fx-border-radius: 4; -fx-padding: 6 10 6 10; -fx-font-size: 12px;"/>
|
||||||
|
<TextField fx:id="filterOne" prefWidth="70.0" minWidth="70.0" maxWidth="70.0" promptText="1 шт"
|
||||||
|
style="-fx-background-color: white; -fx-border-color: #ced4da; -fx-border-width: 1; -fx-border-radius: 4; -fx-padding: 6 10 6 10; -fx-font-size: 12px;"/>
|
||||||
|
<TextField fx:id="filterStockCode" prefWidth="200.0" minWidth="200.0" maxWidth="200.0" promptText="Шифр Склад/Бухгалтерия"
|
||||||
|
style="-fx-background-color: white; -fx-border-color: #ced4da; -fx-border-width: 1; -fx-border-radius: 4; -fx-padding: 6 10 6 10; -fx-font-size: 12px;"/>
|
||||||
|
<TextField fx:id="filterExpectedDate" prefWidth="140.0" minWidth="140.0" maxWidth="140.0" promptText="Ожидаемая дата"
|
||||||
|
style="-fx-background-color: white; -fx-border-color: #ced4da; -fx-border-width: 1; -fx-border-radius: 4; -fx-padding: 6 10 6 10; -fx-font-size: 12px;"/>
|
||||||
|
<TextField fx:id="filterActualDate" prefWidth="140.0" minWidth="140.0" maxWidth="140.0" promptText="Фактическая дата"
|
||||||
|
style="-fx-background-color: white; -fx-border-color: #ced4da; -fx-border-width: 1; -fx-border-radius: 4; -fx-padding: 6 10 6 10; -fx-font-size: 12px;"/>
|
||||||
|
<TextField fx:id="filterInvoice" prefWidth="180.0" minWidth="180.0" maxWidth="180.0" promptText="Накладная, №, дата"
|
||||||
|
style="-fx-background-color: white; -fx-border-color: #ced4da; -fx-border-width: 1; -fx-border-radius: 4; -fx-padding: 6 10 6 10; -fx-font-size: 12px;"/>
|
||||||
|
<TextField fx:id="filterOrderNumber" prefWidth="140.0" minWidth="140.0" maxWidth="140.0" promptText="Номер заказа"
|
||||||
|
style="-fx-background-color: white; -fx-border-color: #ced4da; -fx-border-width: 1; -fx-border-radius: 4; -fx-padding: 6 10 6 10; -fx-font-size: 12px;"/>
|
||||||
|
<TextField fx:id="filterRedmine" prefWidth="120.0" minWidth="120.0" maxWidth="120.0" promptText="Redmine"
|
||||||
|
style="-fx-background-color: white; -fx-border-color: #ced4da; -fx-border-width: 1; -fx-border-radius: 4; -fx-padding: 6 10 6 10; -fx-font-size: 12px;"/>
|
||||||
|
<TextField fx:id="filterOwner" prefWidth="160.0" minWidth="160.0" maxWidth="160.0" promptText="Ответственный"
|
||||||
|
style="-fx-background-color: white; -fx-border-color: #ced4da; -fx-border-width: 1; -fx-border-radius: 4; -fx-padding: 6 10 6 10; -fx-font-size: 12px;"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<TableView fx:id="purchasesTable" prefHeight="600.0" prefWidth="1200.0">
|
||||||
|
<columns>
|
||||||
|
<TableColumn fx:id="colType" text="Тип" prefWidth="100.0"/>
|
||||||
|
<TableColumn fx:id="colNameFull" text="Наименование компонента (полное)" prefWidth="260.0"/>
|
||||||
|
<TableColumn fx:id="colNameUPD" text="Наименование в УПД" prefWidth="220.0"/>
|
||||||
|
<TableColumn fx:id="colQty" text="К-во" prefWidth="70.0"/>
|
||||||
|
<TableColumn fx:id="colOne" text="1 шт" prefWidth="70.0"/>
|
||||||
|
<TableColumn fx:id="colStockCode" text="Шифр Склад/Бухгалтерия" prefWidth="200.0"/>
|
||||||
|
<TableColumn fx:id="colExpectedDate" text="Ожидаемая дата" prefWidth="140.0"/>
|
||||||
|
<TableColumn fx:id="colActualDate" text="Фактическая дата" prefWidth="140.0"/>
|
||||||
|
<TableColumn fx:id="colInvoice" text="Накладная, №, дата" prefWidth="180.0"/>
|
||||||
|
<TableColumn fx:id="colOrderNumber" text="Номер заказа" prefWidth="140.0"/>
|
||||||
|
<TableColumn fx:id="colRedmine" text="Redmine" prefWidth="120.0"/>
|
||||||
|
<TableColumn fx:id="colOwner" text="Ответственный" prefWidth="160.0"/>
|
||||||
|
</columns>
|
||||||
|
</TableView>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
<?import javafx.scene.control.Button?>
|
<?import javafx.scene.control.Button?>
|
||||||
<?import javafx.scene.control.Label?>
|
<?import javafx.scene.control.Label?>
|
||||||
<?import javafx.scene.control.Separator?>
|
<?import javafx.scene.control.Separator?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
|
||||||
<VBox fx:controller="com.dsol.pki_management.controllers.SettingsController"
|
<VBox fx:controller="com.dsol.pki_management.controllers.SettingsController"
|
||||||
@@ -29,6 +31,21 @@
|
|||||||
<Label style="-fx-font-size: 14px; -fx-text-fill: #495057;"
|
<Label style="-fx-font-size: 14px; -fx-text-fill: #495057;"
|
||||||
text="• Уведомления"/>
|
text="• Уведомления"/>
|
||||||
|
|
||||||
|
<!-- Закупки (из общих настроек) -->
|
||||||
|
<Label style="-fx-font-size: 16px; -fx-font-weight: bold; -fx-text-fill: #495057;"
|
||||||
|
text="Закупки"/>
|
||||||
|
<VBox spacing="10.0">
|
||||||
|
<children>
|
||||||
|
<Label style="-fx-font-size: 14px; -fx-text-fill: #495057;" text="Excel файл"/>
|
||||||
|
<HBox spacing="8.0">
|
||||||
|
<children>
|
||||||
|
<TextField fx:id="purchasesExcelPathField" promptText="Путь к Excel файлу (*.xlsx)" prefWidth="600.0" HBox.hgrow="ALWAYS"/>
|
||||||
|
<Button text="Выбрать..." onAction="#choosePurchasesExcelFile"/>
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
<Separator/>
|
<Separator/>
|
||||||
|
|
||||||
<Button fx:id="adminAccessButton"
|
<Button fx:id="adminAccessButton"
|
||||||
@@ -56,6 +73,20 @@
|
|||||||
|
|
||||||
<Separator/>
|
<Separator/>
|
||||||
|
|
||||||
|
<!-- Блок MES -->
|
||||||
|
<Label style="-fx-font-size: 16px; -fx-font-weight: bold; -fx-text-fill: #495057;"
|
||||||
|
text="MES"/>
|
||||||
|
<VBox spacing="10.0">
|
||||||
|
<children>
|
||||||
|
<Label style="-fx-font-size: 14px; -fx-text-fill: #495057;" text="URL MES"/>
|
||||||
|
<TextField fx:id="mesUrlField" promptText="https://example.com/api" prefWidth="600.0" HBox.hgrow="ALWAYS"/>
|
||||||
|
<Label style="-fx-font-size: 14px; -fx-text-fill: #495057;" text="URL Match Error"/>
|
||||||
|
<TextField fx:id="mesMatchErrorUrlField" promptText="/match-error" prefWidth="600.0" HBox.hgrow="ALWAYS"/>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<Separator/>
|
||||||
|
|
||||||
<Label style="-fx-font-size: 16px; -fx-font-weight: bold; -fx-text-fill: #495057;"
|
<Label style="-fx-font-size: 16px; -fx-font-weight: bold; -fx-text-fill: #495057;"
|
||||||
text="Управление видимостью элементов меню"/>
|
text="Управление видимостью элементов меню"/>
|
||||||
<VBox fx:id="subMenuItemsContainer" spacing="8.0">
|
<VBox fx:id="subMenuItemsContainer" spacing="8.0">
|
||||||
|
|||||||
Reference in New Issue
Block a user