Add biggest update for UI. Create Ordering module
This commit is contained in:
24
pom.xml
24
pom.xml
@@ -12,6 +12,7 @@
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<junit.version>5.12.1</junit.version>
|
||||
<poi.version>5.2.5</poi.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -61,17 +62,19 @@
|
||||
<artifactId>bootstrapfx-core</artifactId>
|
||||
<version>0.4.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache POI for Excel (XLSX) -->
|
||||
<dependency>
|
||||
<groupId>eu.hansolo</groupId>
|
||||
<artifactId>tilesfx</artifactId>
|
||||
<version>21.0.9</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi</artifactId>
|
||||
<version>${poi.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>${poi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
@@ -103,10 +106,9 @@
|
||||
<version>0.0.8</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<!-- Default configuration for running with: mvn clean javafx:run -->
|
||||
<id>default-cli</id>
|
||||
<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>
|
||||
<jlinkZipName>app</jlinkZipName>
|
||||
<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_X = "window.x";
|
||||
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";
|
||||
|
||||
@@ -3,6 +3,11 @@ package com.dsol.pki_management.controllers;
|
||||
import com.dsol.pki_management.components.MenuItem;
|
||||
import com.dsol.pki_management.components.SubMenuItem;
|
||||
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.FXMLLoader;
|
||||
import javafx.scene.control.Button;
|
||||
@@ -11,10 +16,18 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Arrays;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.application.Platform;
|
||||
|
||||
/**
|
||||
* Главный контроллер приложения
|
||||
@@ -41,6 +54,8 @@ public class MainController {
|
||||
menuOverlay.setOnMouseClicked(e -> closeMenu());
|
||||
|
||||
setupMenuItems();
|
||||
applyHiddenSubMenusFromConfig();
|
||||
startPurchasesScanIfConfigured();
|
||||
}
|
||||
|
||||
private void setupMenuItems() {
|
||||
@@ -59,6 +74,13 @@ public class MainController {
|
||||
createSubMenuItem("Отчеты", e -> System.out.println("Открыты отчеты"))
|
||||
);
|
||||
|
||||
MenuItem purchases = createMenuItem("Закупки",
|
||||
createSubMenuItem("Закупочный перечень", e -> {
|
||||
loadPurchasesListModule();
|
||||
closeMenu();
|
||||
})
|
||||
);
|
||||
|
||||
MenuItem testMenu = createMenuItem("Test",
|
||||
createSubMenuItem("Тест 1", e -> {
|
||||
loadTest1Module();
|
||||
@@ -68,10 +90,77 @@ public class MainController {
|
||||
|
||||
// Добавляем блоки меню в контент меню
|
||||
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 с указанным названием и подменю
|
||||
*/
|
||||
@@ -142,6 +231,24 @@ public class MainController {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает модуль "Закупочный перечень" в центральную область
|
||||
*/
|
||||
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.text.Text;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.FileChooser;
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Контроллер для модуля настроек
|
||||
@@ -32,6 +37,12 @@ public class SettingsController {
|
||||
private Label statusLabel;
|
||||
@FXML
|
||||
private VBox subMenuItemsContainer;
|
||||
@FXML
|
||||
private TextField purchasesExcelPathField;
|
||||
@FXML
|
||||
private TextField mesUrlField;
|
||||
@FXML
|
||||
private TextField mesMatchErrorUrlField;
|
||||
|
||||
private boolean isAdminMode = false;
|
||||
private final ConfigManager configManager = ConfigManager.getInstance();
|
||||
@@ -54,8 +65,69 @@ public class SettingsController {
|
||||
if (exitAdminButton != null) {
|
||||
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.setSpacing(8);
|
||||
|
||||
// Получаем все MenuItem и SubMenuItem из HelloController
|
||||
// Получаем все MenuItem и SubMenuItem из MainController
|
||||
List<MenuItem> menuItems = MainController.getAllMenuItems();
|
||||
|
||||
if (menuItems.isEmpty()) {
|
||||
@@ -194,7 +266,7 @@ public class SettingsController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключает видимость SubMenuItem при клике
|
||||
* Переключает видимость SubMenuItem при клике и сохраняет в конфиг
|
||||
*/
|
||||
private void toggleSubMenuItemVisibility(SubMenuItem subMenuItem, Text text) {
|
||||
boolean newVisibility = !subMenuItem.isItemVisible();
|
||||
@@ -203,6 +275,27 @@ public class SettingsController {
|
||||
|
||||
// Обновляем видимость всех MenuItem
|
||||
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.fxml;
|
||||
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 com.dlsc.formsfx;
|
||||
requires org.kordamp.ikonli.javafx;
|
||||
requires org.kordamp.bootstrapfx.core;
|
||||
requires eu.hansolo.tilesfx;
|
||||
|
||||
opens com.dsol.pki_management.app 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.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.controllers;
|
||||
exports com.dsol.pki_management.components;
|
||||
exports com.dsol.pki_management.modules.test1;
|
||||
exports com.dsol.pki_management.modules.purchases;
|
||||
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.Label?>
|
||||
<?import javafx.scene.control.Separator?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<VBox fx:controller="com.dsol.pki_management.controllers.SettingsController"
|
||||
@@ -29,6 +31,21 @@
|
||||
<Label style="-fx-font-size: 14px; -fx-text-fill: #495057;"
|
||||
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/>
|
||||
|
||||
<Button fx:id="adminAccessButton"
|
||||
@@ -56,6 +73,20 @@
|
||||
|
||||
<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;"
|
||||
text="Управление видимостью элементов меню"/>
|
||||
<VBox fx:id="subMenuItemsContainer" spacing="8.0">
|
||||
|
||||
Reference in New Issue
Block a user