Add update for Ordering. New TableView. Build MSI
This commit is contained in:
96
pom.xml
96
pom.xml
@@ -13,6 +13,8 @@
|
|||||||
<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>
|
<poi.version>5.2.5</poi.version>
|
||||||
|
<!-- Версия для Windows Installer (формат: major.minor.build) -->
|
||||||
|
<app.version>${project.version}</app.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -104,17 +106,95 @@
|
|||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-maven-plugin</artifactId>
|
<artifactId>javafx-maven-plugin</artifactId>
|
||||||
<version>0.0.8</version>
|
<version>0.0.8</version>
|
||||||
|
<configuration>
|
||||||
|
<mainClass>com.dsol.pki_management.app/com.dsol.pki_management.app.PKIApplication</mainClass>
|
||||||
|
<launcher>app</launcher>
|
||||||
|
<jlinkZipName>app</jlinkZipName>
|
||||||
|
<jlinkImageName>app</jlinkImageName>
|
||||||
|
<noManPages>true</noManPages>
|
||||||
|
<stripDebug>true</stripDebug>
|
||||||
|
<noHeaderFiles>true</noHeaderFiles>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-dependency-plugin</artifactId>
|
||||||
|
<version>3.6.1</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<id>default-cli</id>
|
<id>copy-dependencies</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>copy-dependencies</goal>
|
||||||
|
</goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<mainClass>com.dsol.pki_management.app/com.dsol.pki_management.app.PKIApplication</mainClass>
|
<outputDirectory>${project.build.directory}/jpackage-input/lib</outputDirectory>
|
||||||
<launcher>app</launcher>
|
<includeScope>runtime</includeScope>
|
||||||
<jlinkZipName>app</jlinkZipName>
|
</configuration>
|
||||||
<jlinkImageName>app</jlinkImageName>
|
</execution>
|
||||||
<noManPages>true</noManPages>
|
</executions>
|
||||||
<stripDebug>true</stripDebug>
|
</plugin>
|
||||||
<noHeaderFiles>true</noHeaderFiles>
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-antrun-plugin</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>prepare-jpackage-input</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>run</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<target>
|
||||||
|
<copy file="${project.build.directory}/${project.build.finalName}.jar"
|
||||||
|
tofile="${project.build.directory}/jpackage-input/${project.build.finalName}.jar"/>
|
||||||
|
</target>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<artifactId>exec-maven-plugin</artifactId>
|
||||||
|
<version>3.1.1</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>jpackage</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>exec</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<executable>jpackage</executable>
|
||||||
|
<workingDirectory>${project.build.directory}</workingDirectory>
|
||||||
|
<arguments>
|
||||||
|
<argument>--input</argument>
|
||||||
|
<argument>${project.build.directory}/jpackage-input</argument>
|
||||||
|
<argument>--name</argument>
|
||||||
|
<argument>PKI Management</argument>
|
||||||
|
<argument>--main-jar</argument>
|
||||||
|
<argument>${project.build.finalName}.jar</argument>
|
||||||
|
<argument>--main-class</argument>
|
||||||
|
<argument>com.dsol.pki_management.app.Launcher</argument>
|
||||||
|
<argument>--type</argument>
|
||||||
|
<argument>msi</argument>
|
||||||
|
<argument>--dest</argument>
|
||||||
|
<argument>${project.build.directory}</argument>
|
||||||
|
<argument>--app-version</argument>
|
||||||
|
<argument>1.0.0</argument>
|
||||||
|
<argument>--vendor</argument>
|
||||||
|
<argument>DSOL</argument>
|
||||||
|
<argument>--description</argument>
|
||||||
|
<argument>PKI Management System</argument>
|
||||||
|
<argument>--win-dir-chooser</argument>
|
||||||
|
<argument>--win-menu</argument>
|
||||||
|
<argument>--win-shortcut</argument>
|
||||||
|
<argument>--win-menu-group</argument>
|
||||||
|
<argument>PKI Management</argument>
|
||||||
|
<argument>--java-options</argument>
|
||||||
|
<argument>-Dfile.encoding=UTF-8</argument>
|
||||||
|
</arguments>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
package com.dsol.pki_management.app;
|
package com.dsol.pki_management.app;
|
||||||
|
|
||||||
|
import com.dsol.pki_management.config.LoggerConfig;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
|
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Точка входа в приложение
|
* Точка входа в приложение
|
||||||
*/
|
*/
|
||||||
public class Launcher {
|
public class Launcher {
|
||||||
|
private static final Logger logger = Logger.getLogger(Launcher.class.getName());
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
Application.launch(PKIApplication.class, args);
|
// Инициализируем логирование как можно раньше
|
||||||
|
LoggerConfig.initialize();
|
||||||
|
|
||||||
|
logger.info("Запуск приложения PKI Management");
|
||||||
|
logger.info("Аргументы командной строки: " + java.util.Arrays.toString(args));
|
||||||
|
|
||||||
|
try {
|
||||||
|
Application.launch(PKIApplication.class, args);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.severe("КРИТИЧЕСКАЯ ОШИБКА при запуске приложения: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,19 +7,56 @@ import javafx.scene.Scene;
|
|||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Главный класс приложения PKI Management
|
* Главный класс приложения PKI Management
|
||||||
*/
|
*/
|
||||||
public class PKIApplication extends Application {
|
public class PKIApplication extends Application {
|
||||||
|
private static final Logger logger = Logger.getLogger(PKIApplication.class.getName());
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(Stage stage) throws IOException {
|
public void init() throws Exception {
|
||||||
FXMLLoader fxmlLoader = new FXMLLoader(PKIApplication.class.getResource("/com/dsol/pki_management/views/main.fxml"));
|
super.init();
|
||||||
fxmlLoader.setController(new MainController());
|
logger.info("Инициализация JavaFX Application");
|
||||||
Scene scene = new Scene(fxmlLoader.load());
|
}
|
||||||
stage.setTitle("PKI Management");
|
|
||||||
stage.setScene(scene);
|
@Override
|
||||||
stage.show();
|
public void start(Stage stage) {
|
||||||
|
logger.info("Запуск метода start() JavaFX Application");
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info("Загрузка FXML файла: /com/dsol/pki_management/views/main.fxml");
|
||||||
|
FXMLLoader fxmlLoader = new FXMLLoader(PKIApplication.class.getResource("/com/dsol/pki_management/views/main.fxml"));
|
||||||
|
fxmlLoader.setController(new MainController());
|
||||||
|
|
||||||
|
logger.info("Создание сцены");
|
||||||
|
Scene scene = new Scene(fxmlLoader.load());
|
||||||
|
|
||||||
|
logger.info("Настройка окна");
|
||||||
|
stage.setTitle("PKI Management");
|
||||||
|
stage.setScene(scene);
|
||||||
|
|
||||||
|
logger.info("Отображение окна");
|
||||||
|
stage.show();
|
||||||
|
|
||||||
|
logger.info("Приложение успешно запущено");
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.severe("ОШИБКА при загрузке FXML: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException("Не удалось загрузить главное окно", e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.severe("ОШИБКА при запуске приложения: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException("Критическая ошибка при запуске", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() throws Exception {
|
||||||
|
super.stop();
|
||||||
|
logger.info("Остановка приложения");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ public class TableViewEnhancer {
|
|||||||
|
|
||||||
setupCellSelection(tableView);
|
setupCellSelection(tableView);
|
||||||
setupCopyHandler(tableView);
|
setupCopyHandler(tableView);
|
||||||
|
setupDragSelectionPerCell(tableView);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,81 +125,6 @@ public class TableViewEnhancer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обработчик перетаскивания мыши для выделения диапазона (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);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Сброс выделения при клике вне таблицы
|
// Сброс выделения при клике вне таблицы
|
||||||
// Устанавливаем обработчик на сцену, если она доступна
|
// Устанавливаем обработчик на сцену, если она доступна
|
||||||
@@ -279,6 +205,122 @@ public class TableViewEnhancer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Настраивает drag-селекцию на уровне каждой ячейки
|
||||||
|
* Это работает надежно даже когда внутри ячейки есть HBox, Text и т.д.
|
||||||
|
* Метод можно вызывать повторно - он обновит существующие cellFactory, сохранив обработчики drag
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
|
private static <T> void setupDragSelectionPerCell(TableView<T> tableView) {
|
||||||
|
TableSelectionModel<T> selectionModel = tableView.getSelectionModel();
|
||||||
|
ObservableList<TablePosition<?, ?>> trackedCells = selectedCellsMap.get(tableView);
|
||||||
|
|
||||||
|
for (TableColumn<T, ?> col : tableView.getColumns()) {
|
||||||
|
// Сохраняем существующий cellFactory (используем raw type для совместимости)
|
||||||
|
javafx.util.Callback existingFactory = col.getCellFactory();
|
||||||
|
|
||||||
|
// Создаем обертку, которая добавляет обработчики drag к любой ячейке
|
||||||
|
// Используем raw type Callback и явное приведение для избежания проблем с wildcard capture
|
||||||
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
|
javafx.util.Callback wrapperFactory = (javafx.util.Callback) (javafx.util.Callback<TableColumn, TableCell>) (TableColumn column) -> {
|
||||||
|
// Создаем ячейку через существующий factory или стандартную
|
||||||
|
TableCell<T, ?> cell;
|
||||||
|
if (existingFactory != null) {
|
||||||
|
cell = (TableCell<T, ?>) existingFactory.call(column);
|
||||||
|
} else {
|
||||||
|
// Стандартная ячейка, если factory нет
|
||||||
|
cell = new TableCell<T, Object>() {
|
||||||
|
@Override
|
||||||
|
protected void updateItem(Object item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
setText(empty || item == null ? "" : item.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем обработчики drag к ячейке
|
||||||
|
addDragHandlersToCell(tableView, cell, (TableColumn<T, ?>) column, selectionModel, trackedCells);
|
||||||
|
|
||||||
|
return cell;
|
||||||
|
};
|
||||||
|
|
||||||
|
col.setCellFactory(wrapperFactory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавляет обработчики drag к ячейке
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> void addDragHandlersToCell(
|
||||||
|
TableView<T> tableView,
|
||||||
|
TableCell<T, ?> cell,
|
||||||
|
TableColumn<T, ?> column,
|
||||||
|
TableSelectionModel<T> selectionModel,
|
||||||
|
ObservableList<TablePosition<?, ?>> trackedCells) {
|
||||||
|
|
||||||
|
// Начало перетаскивания
|
||||||
|
cell.setOnDragDetected(e -> {
|
||||||
|
TableRow<T> row = cell.getTableRow();
|
||||||
|
if (row != null && !cell.isEmpty() && row.getIndex() >= 0) {
|
||||||
|
tableView.startFullDrag();
|
||||||
|
TablePosition<T, ?> startPos = new TablePosition<>(tableView, row.getIndex(), column);
|
||||||
|
dragStartMap.put(tableView, startPos);
|
||||||
|
|
||||||
|
// Если не Shift+Click и не Ctrl+Click, очищаем выделение
|
||||||
|
if (!e.isShiftDown() && !e.isControlDown()) {
|
||||||
|
selectionModel.clearSelection();
|
||||||
|
trackedCells.clear();
|
||||||
|
selectionModel.select(row.getIndex(), column);
|
||||||
|
trackedCells.add(startPos);
|
||||||
|
}
|
||||||
|
e.consume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// При входе курсора в другую ячейку во время drag
|
||||||
|
cell.setOnMouseDragEntered(e -> {
|
||||||
|
TablePosition<?, ?> startPos = dragStartMap.get(tableView);
|
||||||
|
if (startPos == null) return;
|
||||||
|
|
||||||
|
TableRow<T> row = cell.getTableRow();
|
||||||
|
if (row == null || cell.isEmpty() || row.getIndex() < 0) return;
|
||||||
|
|
||||||
|
int startRow = Math.min(startPos.getRow(), row.getIndex());
|
||||||
|
int endRow = Math.max(startPos.getRow(), row.getIndex());
|
||||||
|
int startCol = Math.min(startPos.getColumn(), findColumnIndex(tableView, column));
|
||||||
|
int endCol = Math.max(startPos.getColumn(), findColumnIndex(tableView, column));
|
||||||
|
|
||||||
|
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, ?> tc = tableView.getColumns().get(c);
|
||||||
|
selectionModel.select(r, tc);
|
||||||
|
trackedCells.add(new TablePosition<>(tableView, r, tc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.consume();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Завершение drag
|
||||||
|
cell.setOnMouseDragReleased(e -> {
|
||||||
|
dragStartMap.remove(tableView);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Публичный метод для обновления drag-обработчиков после изменения cellFactory
|
||||||
|
* Можно вызывать после установки кастомных cellFactory
|
||||||
|
*/
|
||||||
|
public static <T> void refreshDragHandlers(TableView<T> tableView) {
|
||||||
|
setupDragSelectionPerCell(tableView);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Настраивает обработчик копирования (Ctrl+C)
|
* Настраивает обработчик копирования (Ctrl+C)
|
||||||
*/
|
*/
|
||||||
@@ -363,6 +405,134 @@ public class TableViewEnhancer {
|
|||||||
clipboard.setContent(content);
|
clipboard.setContent(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Находит TableCell, поднимаясь по иерархии узлов
|
||||||
|
* Надежный метод, который работает даже когда внутри ячейки есть HBox, Text и т.д.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> TableCell<T, ?> findTableCell(javafx.scene.Node node) {
|
||||||
|
javafx.scene.Node current = node;
|
||||||
|
int maxDepth = 30; // Чуть глубже для надежности
|
||||||
|
|
||||||
|
// Проходим вверх по иерархии узлов
|
||||||
|
while (current != null && maxDepth-- > 0) {
|
||||||
|
if (current instanceof TableCell<?, ?> tc) {
|
||||||
|
return (TableCell<T, ?>) tc;
|
||||||
|
}
|
||||||
|
// Останавливаемся, если дошли до TableView
|
||||||
|
if (current instanceof TableView) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback — пытаемся через lookup по стилю (некоторые обёртки теряют parent-ссылку)
|
||||||
|
if (node != null && node.getScene() != null) {
|
||||||
|
javafx.scene.Node root = node.getScene().getRoot();
|
||||||
|
if (root != null) {
|
||||||
|
// Ищем все TableCell на сцене
|
||||||
|
java.util.Set<javafx.scene.Node> cells = root.lookupAll(".table-cell");
|
||||||
|
javafx.geometry.Bounds nodeScreenBounds = node.localToScreen(node.getBoundsInLocal());
|
||||||
|
|
||||||
|
for (javafx.scene.Node n : cells) {
|
||||||
|
if (n instanceof TableCell<?, ?> tc) {
|
||||||
|
javafx.geometry.Bounds cellScreenBounds = tc.localToScreen(tc.getBoundsInLocal());
|
||||||
|
// Проверяем, попадает ли узел в границы ячейки
|
||||||
|
if (cellScreenBounds.contains(nodeScreenBounds.getMinX(), nodeScreenBounds.getMinY())) {
|
||||||
|
return (TableCell<T, ?>) tc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Находит индекс колонки в таблице (включая вложенные колонки)
|
||||||
|
*/
|
||||||
|
private static <T> int findColumnIndex(TableView<T> tableView, TableColumn<T, ?> targetColumn) {
|
||||||
|
// Сначала ищем в основных колонках
|
||||||
|
int index = tableView.getColumns().indexOf(targetColumn);
|
||||||
|
if (index >= 0) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не нашли, ищем рекурсивно во вложенных колонках
|
||||||
|
for (int i = 0; i < tableView.getColumns().size(); i++) {
|
||||||
|
TableColumn<T, ?> col = tableView.getColumns().get(i);
|
||||||
|
if (col.getColumns().contains(targetColumn)) {
|
||||||
|
// Если колонка вложена, возвращаем индекс родительской колонки
|
||||||
|
// В этом случае точный индекс вложенной колонки сложнее определить
|
||||||
|
// Для простоты возвращаем индекс родительской
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Находит индекс колонки по X координате
|
||||||
|
*/
|
||||||
|
private static <T> int findColumnByX(TableView<T> tableView, double x) {
|
||||||
|
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) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
currentX += colWidth;
|
||||||
|
}
|
||||||
|
// Если не нашли, возвращаем последнюю колонку или первую
|
||||||
|
return tableView.getColumns().isEmpty() ? -1 : tableView.getColumns().size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Находит индекс строки по Y координате с учетом прокрутки
|
||||||
|
*/
|
||||||
|
private static <T> int findRowByY(TableView<T> tableView, double y, double headerHeight, double tableHeight) {
|
||||||
|
if (y < 0 || tableView.getItems().isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем высоту строки
|
||||||
|
double rowHeight = tableView.getFixedCellSize() > 0
|
||||||
|
? tableView.getFixedCellSize()
|
||||||
|
: 25.0; // примерная высота по умолчанию
|
||||||
|
|
||||||
|
ScrollBar vScrollBar = (ScrollBar) tableView.lookup(".scroll-bar:vertical");
|
||||||
|
int totalRows = tableView.getItems().size();
|
||||||
|
|
||||||
|
if (vScrollBar != null && vScrollBar.isVisible() && totalRows > 0) {
|
||||||
|
int visibleRows = Math.max(1, (int) (tableHeight / rowHeight));
|
||||||
|
|
||||||
|
if (totalRows > visibleRows) {
|
||||||
|
double scrollValue = vScrollBar.getValue();
|
||||||
|
double maxScroll = vScrollBar.getMax();
|
||||||
|
|
||||||
|
if (maxScroll > 0) {
|
||||||
|
// Более точный расчет первой видимой строки
|
||||||
|
// scrollValue от 0 до 1, где 1 = полностью прокручено вниз
|
||||||
|
int firstVisibleRow = (int) (scrollValue * (totalRows - visibleRows));
|
||||||
|
int visibleRowIndex = (int) (y / rowHeight);
|
||||||
|
int calculatedRow = firstVisibleRow + visibleRowIndex;
|
||||||
|
return Math.max(0, Math.min(calculatedRow, totalRows - 1));
|
||||||
|
} else {
|
||||||
|
return Math.min((int)(y / rowHeight), totalRows - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Все строки видны, прокрутки нет
|
||||||
|
return Math.min((int)(y / rowHeight), totalRows - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Нет прокрутки или прокрутка не видна
|
||||||
|
return Math.min((int)(y / rowHeight), totalRows - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получает значение ячейки по позиции
|
* Получает значение ячейки по позиции
|
||||||
*/
|
*/
|
||||||
@@ -387,6 +557,15 @@ public class TableViewEnhancer {
|
|||||||
|
|
||||||
// Получаем значение через cell value factory
|
// Получаем значение через cell value factory
|
||||||
Object cellValue = column.getCellData(rowItem);
|
Object cellValue = column.getCellData(rowItem);
|
||||||
|
|
||||||
|
// Если значение является ObservableValue (например, StringProperty), получаем его значение
|
||||||
|
if (cellValue instanceof javafx.beans.value.ObservableValue) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
javafx.beans.value.ObservableValue<?> observableValue = (javafx.beans.value.ObservableValue<?>) cellValue;
|
||||||
|
Object value = observableValue.getValue();
|
||||||
|
return value != null ? value : "";
|
||||||
|
}
|
||||||
|
|
||||||
return cellValue != null ? cellValue : "";
|
return cellValue != null ? cellValue : "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.dsol.pki_management.config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.logging.FileHandler;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.logging.SimpleFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Конфигурация логирования приложения
|
||||||
|
* Настраивает запись логов в файл в директории конфигурации
|
||||||
|
*/
|
||||||
|
public class LoggerConfig {
|
||||||
|
private static final String LOG_FILE_NAME = "application.log";
|
||||||
|
private static final int MAX_LOG_SIZE = 5 * 1024 * 1024; // 5 MB
|
||||||
|
private static final int LOG_FILE_COUNT = 2; // Только текущий и один архивный
|
||||||
|
private static boolean initialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализирует систему логирования
|
||||||
|
* Должен быть вызван как можно раньше при запуске приложения
|
||||||
|
*/
|
||||||
|
public static void initialize() {
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем директорию конфигурации
|
||||||
|
ConfigManager configManager = ConfigManager.getInstance();
|
||||||
|
Path configDir = configManager.getConfigDir();
|
||||||
|
|
||||||
|
// Создаем директорию, если её нет
|
||||||
|
if (!Files.exists(configDir)) {
|
||||||
|
Files.createDirectories(configDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Путь к файлу логов (не скрытый)
|
||||||
|
Path logFile = configDir.resolve(LOG_FILE_NAME);
|
||||||
|
|
||||||
|
// Настраиваем FileHandler с ротацией логов
|
||||||
|
FileHandler fileHandler = new FileHandler(
|
||||||
|
logFile.toString(),
|
||||||
|
MAX_LOG_SIZE,
|
||||||
|
LOG_FILE_COUNT,
|
||||||
|
true // append
|
||||||
|
);
|
||||||
|
|
||||||
|
// Устанавливаем простой форматтер
|
||||||
|
fileHandler.setFormatter(new SimpleFormatter());
|
||||||
|
fileHandler.setLevel(Level.INFO); // INFO и выше (INFO, WARNING, SEVERE)
|
||||||
|
|
||||||
|
// Получаем root logger и настраиваем его
|
||||||
|
Logger rootLogger = Logger.getLogger("");
|
||||||
|
rootLogger.addHandler(fileHandler);
|
||||||
|
rootLogger.setLevel(Level.INFO); // INFO и выше (INFO, WARNING, SEVERE)
|
||||||
|
|
||||||
|
// Логируем начало работы
|
||||||
|
Logger logger = Logger.getLogger(LoggerConfig.class.getName());
|
||||||
|
logger.info("=========================================");
|
||||||
|
logger.info("Логирование инициализировано");
|
||||||
|
logger.info("Файл логов: " + logFile);
|
||||||
|
logger.info("Версия Java: " + System.getProperty("java.version"));
|
||||||
|
logger.info("ОС: " + System.getProperty("os.name") + " " + System.getProperty("os.version"));
|
||||||
|
logger.info("Пользователь: " + System.getProperty("user.name"));
|
||||||
|
logger.info("=========================================");
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Если не удалось создать файл логов, выводим в консоль
|
||||||
|
System.err.println("ОШИБКА: Не удалось инициализировать логирование в файл: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("ОШИБКА: Неожиданная ошибка при инициализации логирования: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает путь к файлу логов
|
||||||
|
*/
|
||||||
|
public static Path getLogFile() {
|
||||||
|
ConfigManager configManager = ConfigManager.getInstance();
|
||||||
|
return configManager.getConfigDir().resolve(LOG_FILE_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -11,6 +11,8 @@ 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;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Pane;
|
import javafx.scene.layout.Pane;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
@@ -43,6 +45,10 @@ public class MainController {
|
|||||||
private VBox menuContent;
|
private VBox menuContent;
|
||||||
@FXML
|
@FXML
|
||||||
private StackPane contentArea;
|
private StackPane contentArea;
|
||||||
|
@FXML
|
||||||
|
private HBox excelParsingStatusBar;
|
||||||
|
@FXML
|
||||||
|
private Label excelParsingStatusLabel;
|
||||||
|
|
||||||
// Статический список всех SubMenuItem для доступа из других контроллеров
|
// Статический список всех SubMenuItem для доступа из других контроллеров
|
||||||
private static final List<SubMenuItem> allSubMenuItems = new ArrayList<>();
|
private static final List<SubMenuItem> allSubMenuItems = new ArrayList<>();
|
||||||
@@ -131,6 +137,11 @@ public class MainController {
|
|||||||
@Override
|
@Override
|
||||||
protected Void call() {
|
protected Void call() {
|
||||||
try {
|
try {
|
||||||
|
// Показываем строку состояния
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
showParsingStatus("Сканирование файла...");
|
||||||
|
});
|
||||||
|
|
||||||
PurchasesDataStore.getInstance().setStatus("Сканирование файла...");
|
PurchasesDataStore.getInstance().setStatus("Сканирование файла...");
|
||||||
PurchasesExcelValidator validator = new PurchasesExcelValidator();
|
PurchasesExcelValidator validator = new PurchasesExcelValidator();
|
||||||
var results = validator.validate(path);
|
var results = validator.validate(path);
|
||||||
@@ -141,17 +152,42 @@ public class MainController {
|
|||||||
System.out.println("[WARN] Лист '" + r.sheetName + "' — отсутствуют заголовки: " + String.join(", ", r.missingHeaders));
|
System.out.println("[WARN] Лист '" + r.sheetName + "' — отсутствуют заголовки: " + String.join(", ", r.missingHeaders));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Обновляем статус перед парсингом
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
showParsingStatus("Парсинг данных...");
|
||||||
|
});
|
||||||
|
|
||||||
// Парсим данные и кладём в стор
|
// Парсим данные и кладём в стор
|
||||||
PurchasesExcelParser parser = new PurchasesExcelParser();
|
PurchasesExcelParser parser = new PurchasesExcelParser();
|
||||||
var rows = parser.parse(path);
|
var rows = parser.parse(path);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
PurchasesDataStore.getInstance().setRows(rows);
|
PurchasesDataStore.getInstance().setRows(rows);
|
||||||
PurchasesDataStore.getInstance().setStatus("Загружено записей: " + rows.size());
|
PurchasesDataStore.getInstance().setStatus("Загружено записей: " + rows.size());
|
||||||
|
hideParsingStatus();
|
||||||
});
|
});
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
System.err.println("[ERROR] Ошибка валидации/парсинга Excel при старте: " + ex.getMessage());
|
System.err.println("[ERROR] Ошибка валидации/парсинга Excel при старте: " + ex.getMessage());
|
||||||
ex.printStackTrace();
|
ex.printStackTrace();
|
||||||
Platform.runLater(() -> PurchasesDataStore.getInstance().setStatus("Ошибка: " + ex.getMessage()));
|
Platform.runLater(() -> {
|
||||||
|
PurchasesDataStore.getInstance().setStatus("Ошибка: " + ex.getMessage());
|
||||||
|
showParsingStatus("Ошибка: " + ex.getMessage());
|
||||||
|
// Скрываем строку состояния через 3 секунды после ошибки
|
||||||
|
javafx.concurrent.Service<Void> delayService = new javafx.concurrent.Service<>() {
|
||||||
|
@Override
|
||||||
|
protected javafx.concurrent.Task<Void> createTask() {
|
||||||
|
return new javafx.concurrent.Task<>() {
|
||||||
|
@Override
|
||||||
|
protected Void call() throws Exception {
|
||||||
|
Thread.sleep(3000);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
delayService.setOnSucceeded(e -> hideParsingStatus());
|
||||||
|
delayService.start();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -270,5 +306,26 @@ public class MainController {
|
|||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Показывает строку состояния парсинга Excel
|
||||||
|
*/
|
||||||
|
private void showParsingStatus(String message) {
|
||||||
|
if (excelParsingStatusBar != null && excelParsingStatusLabel != null) {
|
||||||
|
excelParsingStatusLabel.setText(message);
|
||||||
|
excelParsingStatusBar.setVisible(true);
|
||||||
|
excelParsingStatusBar.setManaged(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скрывает строку состояния парсинга Excel
|
||||||
|
*/
|
||||||
|
private void hideParsingStatus() {
|
||||||
|
if (excelParsingStatusBar != null) {
|
||||||
|
excelParsingStatusBar.setVisible(false);
|
||||||
|
excelParsingStatusBar.setManaged(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,11 @@ public class SettingsController {
|
|||||||
private void choosePurchasesExcelFile() {
|
private void choosePurchasesExcelFile() {
|
||||||
FileChooser chooser = new FileChooser();
|
FileChooser chooser = new FileChooser();
|
||||||
chooser.setTitle("Выберите Excel файл закупок");
|
chooser.setTitle("Выберите Excel файл закупок");
|
||||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Excel (*.xlsx)", "*.xlsx"));
|
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
|
||||||
|
"Excel файлы (*.xlsx, *.xlsm, *.xlsb, *.xltx, *.xltm, *.xls)",
|
||||||
|
"*.xlsx", "*.xlsm", "*.xlsb", "*.xltx", "*.xltm", "*.xls"
|
||||||
|
));
|
||||||
|
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Все файлы", "*.*"));
|
||||||
File initialDir = new File(System.getProperty("user.home"));
|
File initialDir = new File(System.getProperty("user.home"));
|
||||||
if (initialDir.exists()) chooser.setInitialDirectory(initialDir);
|
if (initialDir.exists()) chooser.setInitialDirectory(initialDir);
|
||||||
File file = chooser.showOpenDialog(null);
|
File file = chooser.showOpenDialog(null);
|
||||||
|
|||||||
@@ -36,14 +36,17 @@ public class MesApiClient {
|
|||||||
* Получает список строк для регулярных выражений из API MES
|
* Получает список строк для регулярных выражений из API MES
|
||||||
*
|
*
|
||||||
* @return Список строк для подсветки, или пустой список в случае ошибки
|
* @return Список строк для подсветки, или пустой список в случае ошибки
|
||||||
|
* @throws IOException если сервер недоступен или произошла ошибка сети
|
||||||
|
* @throws RuntimeException если URL не настроены (не критическая ошибка)
|
||||||
*/
|
*/
|
||||||
public List<String> fetchMatchErrorStrings() {
|
public List<String> fetchMatchErrorStrings() throws IOException {
|
||||||
String mesUrl = configManager.getProperty(AppConfig.KEY_MES_URL, "");
|
String mesUrl = configManager.getProperty(AppConfig.KEY_MES_URL, "");
|
||||||
String matchErrorUrl = configManager.getProperty(AppConfig.KEY_MES_MATCH_ERROR_URL, "");
|
String matchErrorUrl = configManager.getProperty(AppConfig.KEY_MES_MATCH_ERROR_URL, "");
|
||||||
|
|
||||||
if (mesUrl == null || mesUrl.trim().isEmpty() ||
|
if (mesUrl == null || mesUrl.trim().isEmpty() ||
|
||||||
matchErrorUrl == null || matchErrorUrl.trim().isEmpty()) {
|
matchErrorUrl == null || matchErrorUrl.trim().isEmpty()) {
|
||||||
logger.warning("URL MES или URL Match Error не настроены");
|
logger.info("URL MES или URL Match Error не настроены - подсветка отключена");
|
||||||
|
// Не бросаем исключение, если URL не настроены - это нормальная ситуация
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,13 +69,25 @@ public class MesApiClient {
|
|||||||
if (response.statusCode() == 200) {
|
if (response.statusCode() == 200) {
|
||||||
return parseJsonResponse(response.body());
|
return parseJsonResponse(response.body());
|
||||||
} else {
|
} else {
|
||||||
logger.warning("API MES вернул статус " + response.statusCode() + " для URL: " + fullUrl);
|
String errorMsg = "API MES вернул статус " + response.statusCode() + " для URL: " + fullUrl;
|
||||||
return new ArrayList<>();
|
logger.warning(errorMsg);
|
||||||
|
throw new IOException(errorMsg);
|
||||||
}
|
}
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
logger.severe("Ошибка при запросе к API MES: " + e.getMessage());
|
Thread.currentThread().interrupt();
|
||||||
e.printStackTrace();
|
throw new IOException("Запрос к API MES был прерван: " + e.getMessage(), e);
|
||||||
return new ArrayList<>();
|
} catch (java.net.http.HttpTimeoutException e) {
|
||||||
|
String errorMsg = "Таймаут при подключении к серверу MES: " + fullUrl;
|
||||||
|
logger.severe(errorMsg);
|
||||||
|
throw new IOException(errorMsg, e);
|
||||||
|
} catch (java.net.ConnectException e) {
|
||||||
|
String errorMsg = "Не удалось подключиться к серверу MES: " + fullUrl;
|
||||||
|
logger.severe(errorMsg);
|
||||||
|
throw new IOException(errorMsg, e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
String errorMsg = "Ошибка при запросе к API MES (" + fullUrl + "): " + e.getMessage();
|
||||||
|
logger.severe(errorMsg);
|
||||||
|
throw new IOException(errorMsg, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ public class PurchasesController {
|
|||||||
cell.setHighlightStrings(highlightStrings);
|
cell.setHighlightStrings(highlightStrings);
|
||||||
return cell;
|
return cell;
|
||||||
});
|
});
|
||||||
|
// Обновляем drag-обработчики после изменения cellFactory
|
||||||
|
TableViewEnhancer.refreshDragHandlers(purchasesTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,7 +140,7 @@ public class PurchasesController {
|
|||||||
private void loadHighlightData() {
|
private void loadHighlightData() {
|
||||||
Task<List<String>> task = new Task<>() {
|
Task<List<String>> task = new Task<>() {
|
||||||
@Override
|
@Override
|
||||||
protected List<String> call() {
|
protected List<String> call() throws Exception {
|
||||||
return mesApiClient.fetchMatchErrorStrings();
|
return mesApiClient.fetchMatchErrorStrings();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -158,18 +160,44 @@ public class PurchasesController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
task.setOnFailed(e -> {
|
task.setOnFailed(e -> {
|
||||||
// В случае ошибки просто логируем, подсветка не будет работать
|
// В случае ошибки логируем и показываем уведомление пользователю
|
||||||
System.err.println("Ошибка при загрузке данных для подсветки: " + task.getException().getMessage());
|
Throwable exception = task.getException();
|
||||||
if (task.getException() != null) {
|
String errorMessage = exception != null ? exception.getMessage() : "Неизвестная ошибка";
|
||||||
task.getException().printStackTrace();
|
|
||||||
|
System.err.println("Ошибка при загрузке данных для подсветки: " + errorMessage);
|
||||||
|
if (exception != null) {
|
||||||
|
exception.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем уведомление пользователю только если это ошибка подключения
|
||||||
|
// (не показываем, если URL просто не настроены)
|
||||||
|
if (exception instanceof java.io.IOException) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
showMesServerErrorNotification(errorMessage);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Запускаем задачу в фоновом потоке
|
||||||
Thread thread = new Thread(task);
|
Thread thread = new Thread(task);
|
||||||
thread.setDaemon(true);
|
thread.setDaemon(true);
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Показывает уведомление об ошибке подключения к серверу MES
|
||||||
|
*/
|
||||||
|
private void showMesServerErrorNotification(String errorMessage) {
|
||||||
|
javafx.scene.control.Alert alert = new javafx.scene.control.Alert(javafx.scene.control.Alert.AlertType.WARNING);
|
||||||
|
alert.setTitle("MES сервер недоступен");
|
||||||
|
alert.setHeaderText("Не удалось подключиться к серверу MES");
|
||||||
|
alert.setContentText("Сервер MES системы недоступен или не отвечает.\n\n" +
|
||||||
|
"Детали ошибки: " + errorMessage + "\n\n" +
|
||||||
|
"Подсветка ошибочных наименований будет недоступна до восстановления связи с сервером.\n" +
|
||||||
|
"Проверьте настройки подключения в разделе \"Настройки\" → \"MES\".");
|
||||||
|
alert.showAndWait();
|
||||||
|
}
|
||||||
|
|
||||||
private void initFilters() {
|
private void initFilters() {
|
||||||
// Настраиваем обработчики для всех фильтров
|
// Настраиваем обработчики для всех фильтров
|
||||||
filterType.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
filterType.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.dsol.pki_management.modules.purchases;
|
package com.dsol.pki_management.modules.purchases;
|
||||||
|
|
||||||
import org.apache.poi.ss.usermodel.*;
|
import org.apache.poi.ss.usermodel.*;
|
||||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -37,7 +36,7 @@ public class PurchasesExcelParser {
|
|||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
List<PurchaseRow> result = new ArrayList<>();
|
List<PurchaseRow> result = new ArrayList<>();
|
||||||
try (InputStream in = Files.newInputStream(xlsxPath); Workbook wb = new XSSFWorkbook(in)) {
|
try (InputStream in = Files.newInputStream(xlsxPath); Workbook wb = WorkbookFactory.create(in)) {
|
||||||
for (int i = 0; i < wb.getNumberOfSheets(); i++) {
|
for (int i = 0; i < wb.getNumberOfSheets(); i++) {
|
||||||
Sheet sheet = wb.getSheetAt(i);
|
Sheet sheet = wb.getSheetAt(i);
|
||||||
Map<String, Integer> headerToIndex = readHeaderIndexes(sheet);
|
Map<String, Integer> headerToIndex = readHeaderIndexes(sheet);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import org.apache.poi.ss.usermodel.Cell;
|
|||||||
import org.apache.poi.ss.usermodel.Row;
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
import org.apache.poi.ss.usermodel.Sheet;
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
import org.apache.poi.ss.usermodel.Workbook;
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -13,7 +13,7 @@ import java.nio.file.Path;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Валидатор Excel (XLSX) по шаблону закупок.
|
* Валидатор Excel файлов (XLSX, XLSM, XLSB, XLTX, XLTM, XLS) по шаблону закупок.
|
||||||
* Проверяет, что на каждой странице (Sheet) в строке 4 присутствуют необходимые заголовки.
|
* Проверяет, что на каждой странице (Sheet) в строке 4 присутствуют необходимые заголовки.
|
||||||
*/
|
*/
|
||||||
public class PurchasesExcelValidator {
|
public class PurchasesExcelValidator {
|
||||||
@@ -50,7 +50,7 @@ public class PurchasesExcelValidator {
|
|||||||
if (xlsxPath == null || !Files.exists(xlsxPath)) {
|
if (xlsxPath == null || !Files.exists(xlsxPath)) {
|
||||||
throw new IOException("Файл не найден: " + xlsxPath);
|
throw new IOException("Файл не найден: " + xlsxPath);
|
||||||
}
|
}
|
||||||
try (InputStream in = Files.newInputStream(xlsxPath); Workbook wb = new XSSFWorkbook(in)) {
|
try (InputStream in = Files.newInputStream(xlsxPath); Workbook wb = WorkbookFactory.create(in)) {
|
||||||
List<SheetValidationResult> results = new ArrayList<>();
|
List<SheetValidationResult> results = new ArrayList<>();
|
||||||
for (int i = 0; i < wb.getNumberOfSheets(); i++) {
|
for (int i = 0; i < wb.getNumberOfSheets(); i++) {
|
||||||
Sheet sheet = wb.getSheetAt(i);
|
Sheet sheet = wb.getSheetAt(i);
|
||||||
|
|||||||
@@ -40,6 +40,19 @@
|
|||||||
</children>
|
</children>
|
||||||
</StackPane>
|
</StackPane>
|
||||||
</center>
|
</center>
|
||||||
|
<bottom>
|
||||||
|
<HBox fx:id="excelParsingStatusBar"
|
||||||
|
alignment="CENTER_LEFT"
|
||||||
|
style="-fx-background-color: #0d6efd; -fx-padding: 8 16 8 16;"
|
||||||
|
visible="false"
|
||||||
|
managed="false">
|
||||||
|
<children>
|
||||||
|
<Label fx:id="excelParsingStatusLabel"
|
||||||
|
style="-fx-text-fill: white; -fx-font-size: 14px;"
|
||||||
|
text="Парсинг Excel файла..."/>
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
</bottom>
|
||||||
</BorderPane>
|
</BorderPane>
|
||||||
|
|
||||||
<Pane fx:id="menuOverlay"
|
<Pane fx:id="menuOverlay"
|
||||||
|
|||||||
Reference in New Issue
Block a user