Add update for Ordering. New TableView. Build MSI

This commit is contained in:
2025-11-11 21:52:45 +03:00
parent 38a8b06af2
commit a9db9fcd98
12 changed files with 631 additions and 111 deletions

96
pom.xml
View File

@@ -13,6 +13,8 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.12.1</junit.version>
<poi.version>5.2.5</poi.version>
<!-- Версия для Windows Installer (формат: major.minor.build) -->
<app.version>${project.version}</app.version>
</properties>
<dependencies>
@@ -104,17 +106,95 @@
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<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>
<execution>
<id>default-cli</id>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<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>
<outputDirectory>${project.build.directory}/jpackage-input/lib</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
<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>
</execution>
</executions>

View File

@@ -1,13 +1,30 @@
package com.dsol.pki_management.app;
import com.dsol.pki_management.config.LoggerConfig;
import javafx.application.Application;
import java.util.logging.Logger;
/**
* Точка входа в приложение
*/
public class Launcher {
private static final Logger logger = Logger.getLogger(Launcher.class.getName());
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);
}
}
}

View File

@@ -7,19 +7,56 @@ import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.logging.Logger;
/**
* Главный класс приложения PKI Management
*/
public class PKIApplication extends Application {
private static final Logger logger = Logger.getLogger(PKIApplication.class.getName());
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(PKIApplication.class.getResource("/com/dsol/pki_management/views/main.fxml"));
fxmlLoader.setController(new MainController());
Scene scene = new Scene(fxmlLoader.load());
stage.setTitle("PKI Management");
stage.setScene(scene);
stage.show();
public void init() throws Exception {
super.init();
logger.info("Инициализация JavaFX Application");
}
@Override
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("Остановка приложения");
}
}

View File

@@ -40,6 +40,7 @@ public class TableViewEnhancer {
setupCellSelection(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)
*/
@@ -363,6 +405,134 @@ public class TableViewEnhancer {
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
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 : "";
}
}

View File

@@ -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);
}
}

View File

@@ -11,6 +11,8 @@ import com.dsol.pki_management.modules.purchases.PurchasesDataStore;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@@ -43,6 +45,10 @@ public class MainController {
private VBox menuContent;
@FXML
private StackPane contentArea;
@FXML
private HBox excelParsingStatusBar;
@FXML
private Label excelParsingStatusLabel;
// Статический список всех SubMenuItem для доступа из других контроллеров
private static final List<SubMenuItem> allSubMenuItems = new ArrayList<>();
@@ -131,6 +137,11 @@ public class MainController {
@Override
protected Void call() {
try {
// Показываем строку состояния
Platform.runLater(() -> {
showParsingStatus("Сканирование файла...");
});
PurchasesDataStore.getInstance().setStatus("Сканирование файла...");
PurchasesExcelValidator validator = new PurchasesExcelValidator();
var results = validator.validate(path);
@@ -141,17 +152,42 @@ public class MainController {
System.out.println("[WARN] Лист '" + r.sheetName + "' — отсутствуют заголовки: " + String.join(", ", r.missingHeaders));
}
});
// Обновляем статус перед парсингом
Platform.runLater(() -> {
showParsingStatus("Парсинг данных...");
});
// Парсим данные и кладём в стор
PurchasesExcelParser parser = new PurchasesExcelParser();
var rows = parser.parse(path);
Platform.runLater(() -> {
PurchasesDataStore.getInstance().setRows(rows);
PurchasesDataStore.getInstance().setStatus("Загружено записей: " + rows.size());
hideParsingStatus();
});
} catch (Exception ex) {
System.err.println("[ERROR] Ошибка валидации/парсинга Excel при старте: " + ex.getMessage());
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;
}
@@ -270,5 +306,26 @@ public class MainController {
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);
}
}
}

View File

@@ -118,7 +118,11 @@ public class SettingsController {
private void choosePurchasesExcelFile() {
FileChooser chooser = new FileChooser();
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"));
if (initialDir.exists()) chooser.setInitialDirectory(initialDir);
File file = chooser.showOpenDialog(null);

View File

@@ -36,14 +36,17 @@ public class MesApiClient {
* Получает список строк для регулярных выражений из API MES
*
* @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 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 не настроены");
logger.info("URL MES или URL Match Error не настроены - подсветка отключена");
// Не бросаем исключение, если URL не настроены - это нормальная ситуация
return new ArrayList<>();
}
@@ -66,13 +69,25 @@ public class MesApiClient {
if (response.statusCode() == 200) {
return parseJsonResponse(response.body());
} else {
logger.warning("API MES вернул статус " + response.statusCode() + " для URL: " + fullUrl);
return new ArrayList<>();
String errorMsg = "API MES вернул статус " + response.statusCode() + " для URL: " + fullUrl;
logger.warning(errorMsg);
throw new IOException(errorMsg);
}
} catch (IOException | InterruptedException e) {
logger.severe("Ошибка при запросе к API MES: " + e.getMessage());
e.printStackTrace();
return new ArrayList<>();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Запрос к API MES был прерван: " + e.getMessage(), e);
} 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);
}
}

View File

@@ -130,6 +130,8 @@ public class PurchasesController {
cell.setHighlightStrings(highlightStrings);
return cell;
});
// Обновляем drag-обработчики после изменения cellFactory
TableViewEnhancer.refreshDragHandlers(purchasesTable);
}
/**
@@ -138,7 +140,7 @@ public class PurchasesController {
private void loadHighlightData() {
Task<List<String>> task = new Task<>() {
@Override
protected List<String> call() {
protected List<String> call() throws Exception {
return mesApiClient.fetchMatchErrorStrings();
}
};
@@ -158,18 +160,44 @@ public class PurchasesController {
});
task.setOnFailed(e -> {
// В случае ошибки просто логируем, подсветка не будет работать
System.err.println("Ошибка при загрузке данных для подсветки: " + task.getException().getMessage());
if (task.getException() != null) {
task.getException().printStackTrace();
// В случае ошибки логируем и показываем уведомление пользователю
Throwable exception = task.getException();
String errorMessage = exception != null ? exception.getMessage() : "Неизвестная ошибка";
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.setDaemon(true);
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() {
// Настраиваем обработчики для всех фильтров
filterType.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());

View File

@@ -1,7 +1,6 @@
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;
@@ -37,7 +36,7 @@ public class PurchasesExcelParser {
return List.of();
}
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++) {
Sheet sheet = wb.getSheetAt(i);
Map<String, Integer> headerToIndex = readHeaderIndexes(sheet);

View File

@@ -4,7 +4,7 @@ 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 org.apache.poi.ss.usermodel.WorkbookFactory;
import java.io.IOException;
import java.io.InputStream;
@@ -13,7 +13,7 @@ import java.nio.file.Path;
import java.util.*;
/**
* Валидатор Excel (XLSX) по шаблону закупок.
* Валидатор Excel файлов (XLSX, XLSM, XLSB, XLTX, XLTM, XLS) по шаблону закупок.
* Проверяет, что на каждой странице (Sheet) в строке 4 присутствуют необходимые заголовки.
*/
public class PurchasesExcelValidator {
@@ -50,7 +50,7 @@ public class PurchasesExcelValidator {
if (xlsxPath == null || !Files.exists(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<>();
for (int i = 0; i < wb.getNumberOfSheets(); i++) {
Sheet sheet = wb.getSheetAt(i);

View File

@@ -40,6 +40,19 @@
</children>
</StackPane>
</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>
<Pane fx:id="menuOverlay"