#86: Mehrfachauswahl im Verlauf-Tab (SelectionMode.MULTIPLE)

Strg+Klick, Shift+Klick und Strg+A (alle sichtbaren Eintraege) werden durch
JavaFX natuerlich unterstuetzt. Aktionsbuttons (Reset, Loeschen) arbeiten nun
auf allen selektierten Eintraegen. Bei Status-Reset wird ein Hinweis angezeigt,
wenn SUCCESS-Eintraege in der Auswahl enthalten sind (Partial-Success-Dialog).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 12:45:23 +02:00
parent 9f222208c0
commit 6c2e2efe22
@@ -31,6 +31,7 @@ import javafx.util.Duration;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@@ -209,8 +210,7 @@ public final class GuiHistoryTab {
* Muss auf dem JavaFX Application Thread aufgerufen werden. * Muss auf dem JavaFX Application Thread aufgerufen werden.
*/ */
public void notifyRunEnded() { public void notifyRunEnded() {
DocumentHistoryRow selected = overviewTable.getSelectionModel().getSelectedItem(); if (!overviewTable.getSelectionModel().getSelectedItems().isEmpty()) {
if (selected != null) {
resetButton.setDisable(false); resetButton.setDisable(false);
deleteButton.setDisable(false); deleteButton.setDisable(false);
} }
@@ -293,7 +293,7 @@ public final class GuiHistoryTab {
private void buildOverviewTable() { private void buildOverviewTable() {
overviewTable.setItems(overviewItems); overviewTable.setItems(overviewItems);
overviewTable.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); overviewTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
overviewTable.setPlaceholder(new Label(EMPTY_DB_TEXT)); overviewTable.setPlaceholder(new Label(EMPTY_DB_TEXT));
overviewTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); overviewTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
@@ -479,17 +479,26 @@ public final class GuiHistoryTab {
statusFilterBox.setOnAction(e -> loadOverview()); statusFilterBox.setOnAction(e -> loadOverview());
// Detailbereich bei Zeilenselektion // Detailbereich und Buttons bei Selektionsänderung aktualisieren
overviewTable.getSelectionModel().selectedItemProperty().addListener( overviewTable.getSelectionModel().getSelectedItems().addListener(
(obs, old, selected) -> { (ListChangeListener<DocumentHistoryRow>) change -> {
if (selected == null) { List<DocumentHistoryRow> sel =
List.copyOf(overviewTable.getSelectionModel().getSelectedItems());
boolean running = runningCheck.getAsBoolean();
if (sel.isEmpty()) {
clearDetailPane(); clearDetailPane();
resetButton.setDisable(true); resetButton.setDisable(true);
deleteButton.setDisable(true); deleteButton.setDisable(true);
} else if (sel.size() == 1) {
resetButton.setDisable(running);
deleteButton.setDisable(running);
loadDetails(sel.get(0).fingerprint());
} else { } else {
resetButton.setDisable(runningCheck.getAsBoolean()); // Mehrfachauswahl: Detail-Bereich löschen, Buttons aktivieren
deleteButton.setDisable(runningCheck.getAsBoolean()); clearDetailPane();
loadDetails(selected.fingerprint()); resetButton.setDisable(running);
deleteButton.setDisable(running);
statusBarLabel.setText(sel.size() + " Einträge ausgewählt.");
} }
}); });
@@ -598,47 +607,72 @@ public final class GuiHistoryTab {
return; return;
} }
DocumentHistoryRow selected = overviewTable.getSelectionModel().getSelectedItem(); List<DocumentHistoryRow> selectedItems =
if (selected == null) return; List.copyOf(overviewTable.getSelectionModel().getSelectedItems());
if (selectedItems.isEmpty()) return;
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
confirm.setTitle("Status zurücksetzen");
confirm.setHeaderText("Status zurücksetzen?");
confirm.setContentText(
"Setzt den Status des Dokuments auf READY_FOR_AI zurück.\n"
+ "Fehlerzähler und letzter Fehlerzeitpunkt werden gelöscht.\n"
+ "Die Versuchshistorie bleibt vollständig erhalten.\n\n"
+ "Das Dokument wird beim nächsten Verarbeitungslauf erneut verarbeitet.\n\n"
+ "Quelldatei: " + selected.sourceFileName());
Optional<ButtonType> choice = confirm.showAndWait();
if (choice.isEmpty() || choice.get() != ButtonType.OK) return;
DocumentFingerprint fp = selected.fingerprint();
Path configPath = configPathSupplier.get(); Path configPath = configPathSupplier.get();
if (configPath == null) { if (configPath == null) {
showInfo("Keine Konfiguration geladen."); showInfo("Keine Konfiguration geladen.");
return; return;
} }
long successCount = selectedItems.stream()
.filter(r -> r.overallStatus() == ProcessingStatus.SUCCESS)
.count();
StringBuilder sb = new StringBuilder();
sb.append("Setzt den Status auf READY_FOR_AI zurück.\n");
sb.append("Fehlerzähler und letzter Fehlerzeitpunkt werden gelöscht.\n");
sb.append("Die Versuchshistorie bleibt vollständig erhalten.\n\n");
if (selectedItems.size() == 1) {
sb.append("Quelldatei: ").append(selectedItems.get(0).sourceFileName());
} else {
sb.append(selectedItems.size()).append(" Einträge werden zurückgesetzt.");
}
if (successCount > 0) {
sb.append("\n\nHinweis: ").append(successCount)
.append(" der ausgewählten Einträge ")
.append(successCount == 1 ? "hat" : "haben")
.append(" Status \"Erfolgreich\". ")
.append(successCount == 1 ? "Dieser Eintrag wird" : "Diese Einträge werden")
.append(" erneut verarbeitet.");
}
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
confirm.setTitle("Status zurücksetzen");
confirm.setHeaderText("Status zurücksetzen?");
confirm.setContentText(sb.toString());
Optional<ButtonType> choice = confirm.showAndWait();
if (choice.isEmpty() || choice.get() != ButtonType.OK) return;
resetButton.setDisable(true); resetButton.setDisable(true);
deleteButton.setDisable(true); deleteButton.setDisable(true);
statusBarLabel.setText("Status wird zurückgesetzt …"); statusBarLabel.setText("Status wird zurückgesetzt …");
workerPool.submit(() -> { workerPool.submit(() -> {
try { int okCount = 0;
resetPort.resetStatus(configPath, fp); int errCount = 0;
LOG.info("Status-Reset durchgeführt für Fingerprint: {}", fp.sha256Hex()); for (DocumentHistoryRow row : selectedItems) {
Platform.runLater(() -> { try {
statusBarLabel.setText("Status erfolgreich zurückgesetzt."); resetPort.resetStatus(configPath, row.fingerprint());
loadOverview(); LOG.info("Status-Reset durchgeführt für Fingerprint: {}", row.fingerprint().sha256Hex());
}); okCount++;
} catch (Exception ex) { } catch (Exception ex) {
LOG.error("Status-Reset fehlgeschlagen für {}: {}", fp.sha256Hex(), ex.getMessage(), ex); LOG.error("Status-Reset fehlgeschlagen für {}: {}",
Platform.runLater(() -> { row.fingerprint().sha256Hex(), ex.getMessage(), ex);
statusBarLabel.setText("Fehler beim Status-Reset: " + ex.getMessage()); errCount++;
resetButton.setDisable(false); }
deleteButton.setDisable(false);
});
} }
final int ok = okCount, err = errCount;
Platform.runLater(() -> {
if (err == 0) {
statusBarLabel.setText("Status erfolgreich zurückgesetzt: " + ok + " Eintrag/Einträge.");
} else {
statusBarLabel.setText("Status zurückgesetzt: " + ok + " OK, " + err + " Fehler.");
}
loadOverview();
});
}); });
} }
@@ -648,47 +682,63 @@ public final class GuiHistoryTab {
return; return;
} }
DocumentHistoryRow selected = overviewTable.getSelectionModel().getSelectedItem(); List<DocumentHistoryRow> selectedItems =
if (selected == null) return; List.copyOf(overviewTable.getSelectionModel().getSelectedItems());
if (selectedItems.isEmpty()) return;
Alert confirm = new Alert(Alert.AlertType.WARNING);
confirm.setTitle("Eintrag löschen");
confirm.setHeaderText("Eintrag vollständig löschen?");
confirm.setContentText(
"Der Stammsatz und ALLE Verarbeitungsversuche werden unwiderruflich gelöscht.\n"
+ "Diese Aktion kann nicht rückgängig gemacht werden.\n\n"
+ "Quelldatei: " + selected.sourceFileName());
confirm.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
Optional<ButtonType> choice = confirm.showAndWait();
if (choice.isEmpty() || choice.get() != ButtonType.OK) return;
DocumentFingerprint fp = selected.fingerprint();
Path configPath = configPathSupplier.get(); Path configPath = configPathSupplier.get();
if (configPath == null) { if (configPath == null) {
showInfo("Keine Konfiguration geladen."); showInfo("Keine Konfiguration geladen.");
return; return;
} }
String contentText;
if (selectedItems.size() == 1) {
contentText = "Der Stammsatz und ALLE Verarbeitungsversuche werden unwiderruflich gelöscht.\n"
+ "Diese Aktion kann nicht rückgängig gemacht werden.\n\n"
+ "Quelldatei: " + selectedItems.get(0).sourceFileName();
} else {
contentText = selectedItems.size() + " Einträge werden mit allen Versuchen unwiderruflich gelöscht.\n"
+ "Diese Aktion kann nicht rückgängig gemacht werden.";
}
Alert confirm = new Alert(Alert.AlertType.WARNING);
confirm.setTitle("Eintrag löschen");
confirm.setHeaderText(selectedItems.size() == 1 ? "Eintrag vollständig löschen?"
: selectedItems.size() + " Einträge vollständig löschen?");
confirm.setContentText(contentText);
confirm.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
Optional<ButtonType> choice = confirm.showAndWait();
if (choice.isEmpty() || choice.get() != ButtonType.OK) return;
resetButton.setDisable(true); resetButton.setDisable(true);
deleteButton.setDisable(true); deleteButton.setDisable(true);
statusBarLabel.setText("Eintrag wird gelöscht …"); statusBarLabel.setText("Einträge werden gelöscht …");
workerPool.submit(() -> { workerPool.submit(() -> {
try { int okCount = 0;
deletePort.deleteHistory(configPath, fp); int errCount = 0;
LOG.info("Dokumenteintrag gelöscht für Fingerprint: {}", fp.sha256Hex()); for (DocumentHistoryRow row : selectedItems) {
Platform.runLater(() -> { try {
statusBarLabel.setText("Eintrag erfolgreich gelöscht."); deletePort.deleteHistory(configPath, row.fingerprint());
clearDetailPane(); LOG.info("Dokumenteintrag gelöscht für Fingerprint: {}", row.fingerprint().sha256Hex());
loadOverview(); okCount++;
}); } catch (Exception ex) {
} catch (Exception ex) { LOG.error("Löschen fehlgeschlagen für {}: {}",
LOG.error("Löschen fehlgeschlagen für {}: {}", fp.sha256Hex(), ex.getMessage(), ex); row.fingerprint().sha256Hex(), ex.getMessage(), ex);
Platform.runLater(() -> { errCount++;
statusBarLabel.setText("Fehler beim Löschen: " + ex.getMessage()); }
resetButton.setDisable(false);
deleteButton.setDisable(false);
});
} }
final int ok = okCount, err = errCount;
Platform.runLater(() -> {
if (err == 0) {
statusBarLabel.setText("Gelöscht: " + ok + " Eintrag/Einträge.");
} else {
statusBarLabel.setText("Gelöscht: " + ok + " OK, " + err + " Fehler.");
}
clearDetailPane();
loadOverview();
});
}); });
} }