This commit is contained in:
Christian Berger
2025-11-24 09:15:38 +01:00
commit fa7c019588
86 changed files with 3916 additions and 0 deletions

20
.classpath Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/classes" path="src">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

23
.project Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>schulungsstatistiktool</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,15 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.release=enabled
org.eclipse.jdt.core.compiler.source=17

View File

@@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

BIN
bin/main/Start.class Normal file

Binary file not shown.

BIN
bin/module-info.class Normal file

Binary file not shown.

170
pom.xml Normal file
View File

@@ -0,0 +1,170 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>schulungsstatistiktool</groupId>
<artifactId>schulungsstatistiktool</artifactId>
<version>0.1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<javafx.version>21.0.2</javafx.version>
<slf4j.version>2.0.13</slf4j.version>
<logback.version>1.5.6</logback.version>
<jackson.version>2.17.2</jackson.version>
<commons.lang.version>3.14.0</commons.lang.version>
<commons.io.version>2.16.1</commons.io.version>
</properties>
<build>
<sourceDirectory>src</sourceDirectory>
<resources>
<resource>
<directory>resources</directory>
<includes>
<include>**/*.fxml</include>
<include>**/*.properties</include>
</includes>
</resource>
</resources>
<plugins>
<!-- Compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
<!-- Manifest mit Main-Class und Lib-ClassPath -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>main.Start</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<!-- Dependencies in /lib kopieren -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- JavaFX Run Support -->
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>schulungsstatistiktool.ui.MainApp</mainClass>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- JavaFX -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- ControlsFX -->
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>controlsfx</artifactId>
<version>11.2.1</version>
</dependency>
<!-- SQLite -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.46.0.0</version>
</dependency>
<!-- OpenCSV -->
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.9</version>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons.lang.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons.io.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,68 @@
package db;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
public class DatabaseBackupRunner {
private static final String REQUIRED_USER = "cbu615";
private static final String BACKUP_DIR = "V:\\Schulungsstatistik_Backup";
private static final String DB_FILE = util.Config.getDbPath();
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/** Call this once on program start */
public static void runBackupCheck() {
if (!System.getProperty("user.name").equalsIgnoreCase(REQUIRED_USER)) return;
File backupFolder = new File(BACKUP_DIR);
if (!backupFolder.exists()) backupFolder.mkdirs();
LocalDate newest = findNewestBackupDate(backupFolder);
if (newest == null || newest.isBefore(LocalDate.now().minusDays(7))) {
System.out.println("creating backup...");
createBackup(backupFolder);
} else
{
System.out.println("no backup needed; last backup in " + BACKUP_DIR + " (" + newest.format(DateTimeFormatter.ISO_LOCAL_DATE) + ")");
}
}
private static LocalDate findNewestBackupDate(File folder) {
File[] files = folder.listFiles((dir, name) -> name.startsWith("backup_") && name.endsWith(".db"));
if (files == null || files.length == 0) return null;
return java.util.Arrays.stream(files)
.map(f -> {
try {
String datePart = f.getName().substring("backup_".length(), "backup_".length() + 10);
return LocalDate.parse(datePart, FMT);
} catch (Exception e) {
return null;
}
})
.filter(d -> d != null)
.max(Comparator.naturalOrder())
.orElse(null);
}
private static void createBackup(File folder) {
String date = LocalDate.now().format(FMT);
File target = new File(folder, "backup_" + date + ".db");
try {
Files.copy(
Paths.get(DB_FILE),
target.toPath(),
StandardCopyOption.REPLACE_EXISTING
);
} catch (IOException e) {
e.printStackTrace();
}
}
}

361
src/db/TrainingDAO.java Normal file
View File

@@ -0,0 +1,361 @@
package db;
import util.Config;
import java.sql.*;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.*;
import model.PersonMapping;
/**
* DAO für die Schulungs-Tabelle.
* Führt INSERT-, SELECT- und UPDATE-Operationen aus, öffnet/schließt die DB-Verbindung automatisch.
*/
public class TrainingDAO {
private static final String INSERT_SQL = """
INSERT INTO trainings (
datum, startzeit, dauer_minuten, modus,
teilnehmende_anzahl, teilnehmende_typ, durchfuehrende_person,
ausgefallen_grund, titel, inhaltstyp, veranstaltungstyp, kommentar,
geplant, evaluation
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
private Connection connect() throws SQLException {
return DriverManager.getConnection("jdbc:sqlite:" + Config.getDbPath());
}
// --------------------------------------------------------
// INSERT (Overload mit geplant)
// --------------------------------------------------------
public void insertTraining(
String datum,
String startzeit,
Integer dauerMinuten,
String modus,
Integer teilnehmendeAnzahl,
String teilnehmendeTyp,
String durchfuehrendePerson,
String ausgefallenGrund,
String titel,
String inhaltstyp,
String veranstaltungstyp,
String kommentar,
Integer geplant, // 0/1 (null -> 0)
Double evaluation
) {
try (Connection conn = connect();
PreparedStatement ps = conn.prepareStatement(INSERT_SQL)) {
ps.setString(1, datum);
ps.setString(2, startzeit);
if (dauerMinuten != null) ps.setInt(3, dauerMinuten); else ps.setNull(3, Types.INTEGER);
ps.setString(4, modus);
if (teilnehmendeAnzahl != null) ps.setInt(5, teilnehmendeAnzahl); else ps.setNull(5, Types.INTEGER);
ps.setString(6, teilnehmendeTyp);
ps.setString(7, PersonMapping.toKuerzel(durchfuehrendePerson));
ps.setString(8, ausgefallenGrund);
ps.setString(9, titel);
ps.setString(10, inhaltstyp);
ps.setString(11, veranstaltungstyp);
ps.setString(12, kommentar);
ps.setInt(13, (geplant != null && geplant != 0) ? 1 : 0);
if (evaluation != null) ps.setDouble(14, evaluation);
else ps.setNull(14, Types.REAL);
ps.executeUpdate();
} catch (SQLException e) {
System.err.println("Fehler beim Speichern des Trainingsdatensatzes: " + e.getMessage());
e.printStackTrace();
}
}
// Abwärtskompatibel: delegiert auf geplant=0
public void insertTraining(
String datum,
String startzeit,
Integer dauerMinuten,
String modus,
Integer teilnehmendeAnzahl,
String teilnehmendeTyp,
String durchfuehrendePerson,
String ausgefallenGrund,
String titel,
String inhaltstyp,
String veranstaltungstyp,
String kommentar
) {
insertTraining(datum, startzeit, dauerMinuten, modus, teilnehmendeAnzahl, teilnehmendeTyp,
durchfuehrendePerson, ausgefallenGrund, titel, inhaltstyp, veranstaltungstyp, kommentar, 0, null);
}
// --------------------------------------------------------
// UPDATE inkl. "geplant" (für Edit-Dialog mit Planungs-Flag)
// --------------------------------------------------------
public void updateTraining(
int id,
LocalDate datum,
LocalTime startzeit,
Integer dauerMinuten,
String modus,
Integer teilnehmendeAnzahl,
String teilnehmendeTyp,
String durchfuehrendePerson,
String ausgefallenGrund,
String titel,
String inhaltstyp,
String veranstaltungstyp,
String kommentar,
Integer geplant, // 0/1; null wird zu 0 normalisiert
Double evaluation
) {
final String sql = """
UPDATE trainings SET
datum = ?,
startzeit = ?,
dauer_minuten = ?,
modus = ?,
teilnehmende_anzahl = ?,
teilnehmende_typ = ?,
durchfuehrende_person = ?,
ausgefallen_grund = ?,
titel = ?,
inhaltstyp = ?,
veranstaltungstyp = ?,
kommentar = ?,
geplant = ?,
evaluation = ?
WHERE id = ?
""";
try (Connection conn = connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
if (datum != null) ps.setString(1, datum.toString()); else ps.setNull(1, Types.VARCHAR);
if (startzeit != null) ps.setString(2, startzeit.toString()); else ps.setNull(2, Types.VARCHAR);
if (dauerMinuten != null) ps.setInt(3, dauerMinuten); else ps.setNull(3, Types.INTEGER);
setNullableString(ps, 4, modus);
if (teilnehmendeAnzahl != null) ps.setInt(5, teilnehmendeAnzahl); else ps.setNull(5, Types.INTEGER);
setNullableString(ps, 6, teilnehmendeTyp);
setNullableString(ps, 7, durchfuehrendePerson);
setNullableString(ps, 8, ausgefallenGrund);
setNullableString(ps, 9, titel);
setNullableString(ps, 10, inhaltstyp);
setNullableString(ps, 11, veranstaltungstyp);
setNullableString(ps, 12, kommentar);
ps.setInt(13, (geplant != null && geplant != 0) ? 1 : 0);
if (evaluation != null) ps.setDouble(14, evaluation); else ps.setNull(14, Types.REAL);
ps.setInt(15, id);
ps.executeUpdate();
}
catch (SQLException e) {
System.err.println("Fehler beim Aktualisieren des Trainingsdatensatzes (id=" + id + "): " + e.getMessage());
throw new RuntimeException(e);
}
}
private static void setNullableString(PreparedStatement ps, int index, String value) throws SQLException {
if (value != null) ps.setString(index, value); else ps.setNull(index, Types.VARCHAR);
}
// --------------------------------------------------------
// SELECTS
// --------------------------------------------------------
public List<Map<String, Object>> getAllTrainings() {
List<Map<String, Object>> result = new ArrayList<>();
String sql = "SELECT * FROM trainings ORDER BY datum DESC, startzeit ASC";
try (Connection conn = connect();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
ResultSetMetaData meta = rs.getMetaData();
int colCount = meta.getColumnCount();
while (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>();
for (int i = 1; i <= colCount; i++) {
row.put(meta.getColumnName(i), rs.getObject(i));
}
result.add(row);
}
} catch (SQLException e) {
System.err.println("Fehler beim Laden der Trainingsdaten: " + e.getMessage());
e.printStackTrace();
}
return result;
}
public List<Map<String, Object>> getTrainingsWithFilters(
String titel,
String typ,
String inhaltstyp,
String modus,
String teilnehmendeTyp,
String durchfuehrendePerson,
boolean nurAusgefallene
) {
List<Map<String, Object>> result = new ArrayList<>();
StringBuilder sql = new StringBuilder("SELECT * FROM trainings WHERE 1=1");
List<Object> params = new ArrayList<>();
if (titel != null && !titel.isBlank()) {
sql.append(" AND titel LIKE ?");
params.add("%" + titel + "%");
}
if (typ != null && !typ.isBlank()) {
sql.append(" AND veranstaltungstyp = ?");
params.add(typ);
}
if (inhaltstyp != null && !inhaltstyp.isBlank()) {
sql.append(" AND inhaltstyp = ?");
params.add(inhaltstyp);
}
if (modus != null && !modus.isBlank()) {
sql.append(" AND modus = ?");
params.add(modus);
}
if (teilnehmendeTyp != null && !teilnehmendeTyp.isBlank()) {
sql.append(" AND teilnehmende_typ = ?");
params.add(teilnehmendeTyp);
}
if (durchfuehrendePerson != null && !durchfuehrendePerson.isBlank()) {
sql.append(" AND durchfuehrende_person = ?");
params.add(durchfuehrendePerson);
}
if (nurAusgefallene) {
sql.append(" AND (ausgefallen_grund IS NOT NULL AND ausgefallen_grund <> '')");
}
sql.append(" ORDER BY datum DESC, startzeit ASC");
try (Connection conn = connect();
PreparedStatement ps = conn.prepareStatement(sql.toString())) {
for (int i = 0; i < params.size(); i++) {
ps.setObject(i + 1, params.get(i));
}
ResultSet rs = ps.executeQuery();
ResultSetMetaData meta = rs.getMetaData();
int colCount = meta.getColumnCount();
while (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>();
for (int i = 1; i <= colCount; i++) {
row.put(meta.getColumnName(i), rs.getObject(i));
}
result.add(row);
}
} catch (SQLException e) {
System.err.println("Fehler beim Filtern der Trainings: " + e.getMessage());
e.printStackTrace();
}
return result;
}
// --------------------------------------------------------
// Hilfs-Select: distinct durchfuehrende_person (für Dropdown)
// --------------------------------------------------------
public List<String> getRecentDurchfuehrendePersonen(int monthsBack) {
String sql = """
SELECT DISTINCT durchfuehrende_person
FROM trainings
WHERE durchfuehrende_person IS NOT NULL
AND durchfuehrende_person <> ''
AND datum >= date('now', ?)
ORDER BY durchfuehrende_person COLLATE NOCASE
""";
List<String> list = new ArrayList<>();
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + util.Config.getDbPath());
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, "-" + monthsBack + " months");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
list.add(rs.getString(1));
}
} catch (SQLException e) {
throw new RuntimeException("Fehler beim Laden der Personen (letzte " + monthsBack + " Monate): " + e.getMessage(), e);
}
return list;
}
public boolean hasTrainingsWithoutParticipantsForCurrentUser() {
final String sql = """
SELECT COUNT(*) AS cnt
FROM trainings
WHERE durchfuehrende_person = ?
AND datum < date('now')
AND (teilnehmende_anzahl IS NULL OR teilnehmende_anzahl = 0)
AND (ausgefallen_grund IS NULL OR ausgefallen_grund = '')
""";
String currentUser = System.getProperty("user.name");
try (Connection conn = connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, currentUser);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
return rs.getInt("cnt") > 0;
}
} catch (SQLException e) {
System.err.println("Fehler bei Abfrage unvollständiger Schulungen für Benutzer '" + System.getProperty("user.name") + "': " + e.getMessage());
e.printStackTrace();
}
return false;
}
public List<String> getAllDurchfuehrendePersonen() {
final String sql = """
SELECT DISTINCT durchfuehrende_person
FROM trainings
WHERE durchfuehrende_person IS NOT NULL AND durchfuehrende_person <> ''
ORDER BY durchfuehrende_person
""";
List<String> list = new ArrayList<>();
try (Connection conn = connect();
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql)) {
while (rs.next()) {
list.add(rs.getString(1));
}
} catch (SQLException e) {
System.err.println("Fehler beim Laden der durchführenden Personen: " + e.getMessage());
e.printStackTrace();
}
if (list.isEmpty()) {
list.add(System.getProperty("user.name"));
}
return list;
}
public void deleteTraining(int id) {
final String sql = "DELETE FROM trainings WHERE id = ?";
try (Connection conn = connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, id);
ps.executeUpdate();
} catch (SQLException e) {
System.err.println("Fehler beim Löschen des Trainingsdatensatzes (id=" + id + "): " + e.getMessage());
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,41 @@
package db;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* Liest die Schulungstypen aus der SQLite-Datenbank.
* Öffnet eine Verbindung, führt SELECT aus, schließt sofort wieder.
*/
public class TrainingTypeDAO {
private final String dbPath;
public TrainingTypeDAO(String dbPath) {
this.dbPath = dbPath;
}
/**
* Ruft alle Schulungstypen aus der Tabelle 'training_types' ab.
* @return Liste der Typnamen (alphabetisch sortiert)
*/
public List<String> getAllTrainingTypes() {
List<String> types = new ArrayList<>();
String sql = "SELECT name FROM training_types ORDER BY id";
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
types.add(rs.getString("name"));
}
} catch (SQLException e) {
System.err.println("Fehler beim Laden der Schulungstypen: " + e.getMessage());
}
return types;
}
}

115
src/db/UserDAO.java Normal file
View File

@@ -0,0 +1,115 @@
package db;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* Verwaltet die Nutzer-Tabelle in der SQLite-Datenbank.
* Bietet Methoden, um alle Nutzer zu laden, neue anzulegen und einzelne zu löschen.
*/
public class UserDAO {
private final String dbPath;
public UserDAO(String dbPath) {
this.dbPath = dbPath;
}
public List<String[]> getAllUsers() {
List<String[]> users = new ArrayList<>();
String sql = "SELECT phaccount, name FROM users ORDER BY name ASC";
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
users.add(new String[] { rs.getString("phaccount"), rs.getString("name") });
}
} catch (SQLException e) {
System.err.println("Fehler beim Laden der Nutzer: " + e.getMessage());
}
return users;
}
/**
* Prüft, ob ein Nutzer mit diesem Kürzel existiert.
*/
public boolean userExists(String phaccount) {
String sql = "SELECT 1 FROM users WHERE phaccount = ? LIMIT 1";
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, phaccount);
try (ResultSet rs = stmt.executeQuery()) {
return rs.next();
}
} catch (SQLException e) {
System.err.println("Fehler bei userExists: " + e.getMessage());
return false;
}
}
/**
* Aktualisiert den Namen eines bestehenden Nutzers.
*/
public boolean updateUser(String phaccount, String name) {
String sql = "UPDATE users SET name = ? WHERE phaccount = ?";
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, name);
stmt.setString(2, phaccount);
int rows = stmt.executeUpdate();
return rows == 1;
} catch (SQLException e) {
System.err.println("Fehler beim Aktualisieren des Nutzers: " + e.getMessage());
return false;
}
}
/**
* Fügt einen neuen Nutzer ein.
*/
public boolean insertUser(String phaccount, String name) {
String sql = "INSERT INTO users (phaccount, name) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, phaccount);
stmt.setString(2, name);
stmt.executeUpdate();
return true;
} catch (SQLException e) {
System.err.println("Fehler beim Einfügen des Nutzers: " + e.getMessage());
return false;
}
}
/**
* Speichert Nutzer: Update, falls vorhanden, sonst Insert.
*/
public boolean saveUser(String phaccount, String name) {
if (userExists(phaccount)) {
return updateUser(phaccount, name);
} else {
return insertUser(phaccount, name);
}
}
/**
* Löscht einen Nutzer anhand seines Kürzels/IDs.
*/
public boolean deleteUser(String phaccount) {
String sql = "DELETE FROM users WHERE phaccount = ?";
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, phaccount);
int rowsAffected = stmt.executeUpdate();
return rowsAffected == 1;
} catch (SQLException e) {
System.err.println("Fehler beim Löschen des Nutzers: " + e.getMessage());
return false;
}
}
}

15
src/main/Start.java Normal file
View File

@@ -0,0 +1,15 @@
package main;
import db.DatabaseBackupRunner;
import javafx.application.Application;
import javafx.stage.Stage;
import ui.MainUI;
public class Start extends Application {
@Override
public void start(Stage stage) {
DatabaseBackupRunner.runBackupCheck();
new MainUI(stage);
}
public static void main(String[] args) { launch(); }
}

64
src/model/DataModels.java Normal file
View File

@@ -0,0 +1,64 @@
package model;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Zentrale Datenquelle für alle statischen Auswahl-Listen (ComboBox-Optionen)
* und standardisierte Mappings (z. B. CSV-Spalten zu SQLite-Feldern).
*/
public final class DataModels {
private DataModels() {
// Utility class: no instances
}
/** CSV → SQLite Spalten-Mapping (zentrale Definition, Reihenfolge garantiert) */
public static Map<String, String> getCsvToSqliteMapping() {
Map<String, String> map = new LinkedHashMap<>();
map.put("ID", "id");
map.put("Datum", "datum");
map.put("Startzeit", "startzeit");
map.put("Typ", "veranstaltungstyp");
map.put("Titel", "titel");
map.put("Thema", "inhaltstyp");
map.put("Modus", "modus");
map.put("Dauer (Min)", "dauer_minuten");
map.put("Anzahl TN", "teilnehmende_anzahl");
map.put("Typ TN", "teilnehmende_typ");
map.put("Von (PHID)", "durchfuehrende_person");
map.put("Ausfall", "ausgefallen_grund");
map.put("Kommentar", "kommentar");
map.put("Bewertung (16)", "evaluation"); // <-- added line
return map;
}
/** Veranstaltungstypen (z. B. Schulung, Führung) */
public static List<String> getVeranstaltungstypen() {
return Arrays.asList("schulung", "fuehrung", "coffeelecture", "selbstlerneinheit");
}
/** Modus der Veranstaltung (Präsenz, Online, Selbstlerneinheit) */
public static List<String> getModi() {
return Arrays.asList("online", "praesenz");
}
/** Typ der Teilnehmenden */
public static List<String> getTeilnehmendeTypen() {
return Arrays.asList("studierende", "mitarbeitende", "externe", "sonstige", "gemischt");
}
/** Gründe für ausgefallene Veranstaltungen */
public static List<String> getAusgefallenGruende() {
return Arrays.asList(
"",
"keine anmeldung",
"weniger als 3 anmeldungen",
"nicht erschienen",
"sonstiger grund"
);
}
}

View File

@@ -0,0 +1,79 @@
package model;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import db.UserDAO;
public class PersonMapping {
private static final Map<String, String> KUERZEL_TO_NAME = new LinkedHashMap<>();
private static final List<String> ADMINS = List.of("cbu615", "jbo733");
private static String dbPath = null;
private static boolean initialized = false;
/** Holt die Daten aus der DB, falls noch nicht erfolgt */
private static synchronized void loadPeople() {
dbPath = util.Config.getDbPath();
if (dbPath == null) throw new IllegalStateException("DB-Pfad nicht gesetzt!");
UserDAO dao = new UserDAO(dbPath);
List<String[]> users = dao.getAllUsers();
KUERZEL_TO_NAME.clear();
for (String[] user : users) {
KUERZEL_TO_NAME.put(user[0], user[1]);
}
initialized = true;
}
/** Prüft, ob geladen, und lädt ggf. */
private static void listLoadedCheck() {
if (!initialized) {
System.out.println("load");
loadPeople();
}
}
public static void forceReloadPeople() {
// Nur explizit von außen aufrufen!
loadPeople();
}
public static boolean isAdmin(String login) {
if (login == null) return false;
login = normalizeLogin(login);
return ADMINS.contains(login.toLowerCase());
}
/** Kürzel → Klarname */
public static String toName(String kuerzel) {
listLoadedCheck();
if (kuerzel == null || kuerzel.isBlank()) return "";
return KUERZEL_TO_NAME.getOrDefault(kuerzel, kuerzel);
}
/** Klarname → Kürzel */
public static String toKuerzel(String name) {
listLoadedCheck();
if (name == null || name.isBlank()) return "";
for (Map.Entry<String, String> e : KUERZEL_TO_NAME.entrySet()) {
if (e.getValue().equalsIgnoreCase(name.trim())) {
return e.getKey();
}
}
return name;
}
/** Für Dropdown-Menüs: alle Klarnamen (sortiert nach Eintragungsreihenfolge) */
public static java.util.List<String> getAllNames() {
listLoadedCheck();
return new java.util.ArrayList<>(KUERZEL_TO_NAME.values());
}
public static String normalizeLogin(String login) {
if (login == null) return "";
login = login.trim().toLowerCase();
return login;
}
}

13
src/module-info.java Normal file
View File

@@ -0,0 +1,13 @@
module schulungsstatistiktool {
requires javafx.controls;
requires javafx.fxml;
requires javafx.graphics;
requires java.sql;
requires java.desktop;
opens ui to javafx.fxml;
exports ui;
exports db;
exports util;
exports main;
}

213
src/ui/ConfigUI.java Normal file
View File

@@ -0,0 +1,213 @@
package ui;
import java.io.File;
import java.util.List;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import model.PersonMapping;
import db.UserDAO;
public class ConfigUI extends BorderPane {
private final TextField dbPathField = new TextField();
private final Button chooseDbButton = new Button("");
private final Button reloadButton = new Button("Neu laden");
private final MainUI mainUI;
// Userverwaltung-Elemente
private final TableView<UserEntry> userTable = new TableView<>();
private final TextField kuerzelField = new TextField();
private final TextField nameField = new TextField();
private final Button saveButton = new Button("Speichern");
private final Button deleteButton = new Button("Löschen");
private final UserDAO userDAO = new UserDAO(util.Config.getDbPath());
private final String currentLogin = System.getProperty("user.name").toLowerCase();
public ConfigUI(Stage stage, MainUI mainUI) {
this.mainUI = mainUI;
setPadding(new Insets(15));
// --------- DB-Pfad UI ---------
GridPane grid = new GridPane();
grid.setVgap(8);
grid.setHgap(10);
Label label = new Label("Pfad zur Datenbankdatei:");
dbPathField.setText(util.Config.getDbPath());
dbPathField.setPrefWidth(400);
chooseDbButton.setOnAction(e -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("SQLite-Datenbank auswählen");
fileChooser.getExtensionFilters().add(
new FileChooser.ExtensionFilter("SQLite DB", "*.db", "*.sqlite"));
File file = fileChooser.showOpenDialog(stage);
if (file != null) {
String newPath = file.getAbsolutePath();
dbPathField.setText(newPath);
util.Config.saveDbPathFromUI(newPath);
mainUI.reloadDatabaseDependentData();
}
});
reloadButton.setOnAction(e -> {
util.Config.load();
dbPathField.setText(util.Config.getDbPath());
mainUI.reloadDatabaseDependentData();
Alert alert = new Alert(Alert.AlertType.INFORMATION,
"Konfiguration neu geladen:\n" + util.Config.getDbPath());
alert.setHeaderText(null);
alert.showAndWait();
});
grid.add(label, 0, 0);
grid.add(new HBox(5, dbPathField, chooseDbButton, reloadButton), 1, 0);
// --------- User-Verwaltung ---------
VBox userBox = new VBox(15);
userBox.setPadding(new Insets(30, 0, 0, 0));
userBox.getChildren().add(new Separator());
boolean isAdmin = PersonMapping.isAdmin(currentLogin);
if (isAdmin) {
Label heading = new Label("Benutzerverwaltung (Admin)");
heading.setStyle("-fx-font-size: 15; -fx-font-weight: bold;");
userBox.getChildren().add(heading);
TableColumn<UserEntry, String> kuerzelCol = new TableColumn<>("Kürzel");
kuerzelCol.setCellValueFactory(data -> data.getValue().kuerzelProperty());
TableColumn<UserEntry, String> nameCol = new TableColumn<>("Name");
nameCol.setCellValueFactory(data -> data.getValue().nameProperty());
kuerzelCol.setPrefWidth(120);
nameCol.setPrefWidth(250);
userTable.getColumns().addAll(kuerzelCol, nameCol);
refreshUserList();
// Auswahl -> Felder füllen
userTable.getSelectionModel().selectedItemProperty().addListener((obs, old, sel) -> {
if (sel != null) {
kuerzelField.setText(sel.getKuerzel());
nameField.setText(sel.getName());
}
});
HBox editor = new HBox(8,
new Label("Kürzel:"), kuerzelField,
new Label("Name:"), nameField,
saveButton, deleteButton
);
editor.setPadding(new Insets(10, 0, 0, 0));
saveButton.setOnAction(e -> saveUser());
deleteButton.setOnAction(e -> deleteUser());
userBox.getChildren().addAll(userTable, editor);
} else {
Label heading = new Label("Eigener Eintrag");
heading.setStyle("-fx-font-size: 15; -fx-font-weight: bold;");
userBox.getChildren().add(heading);
kuerzelField.setText(currentLogin);
kuerzelField.setEditable(false);
String currentName = PersonMapping.toName(currentLogin);
if (currentName != null && !currentName.equals(currentLogin)) {
nameField.setText(currentName);
}
saveButton.setOnAction(e -> saveOwnUser());
GridPane userGrid = new GridPane();
userGrid.setVgap(8);
userGrid.setHgap(10);
userGrid.add(new Label("Kürzel:"), 0, 0); userGrid.add(kuerzelField, 1, 0);
userGrid.add(new Label("Name:"), 0, 1); userGrid.add(nameField, 1, 1);
userGrid.add(saveButton, 1, 2);
userBox.getChildren().add(userGrid);
}
VBox vbox = new VBox(15, grid, userBox);
setCenter(vbox);
}
private void refreshUserList() {
ObservableList<UserEntry> users = FXCollections.observableArrayList();
for (String[] entry : userDAO.getAllUsers()) {
users.add(new UserEntry(entry[0], entry[1]));
}
userTable.setItems(users);
}
private void saveUser() {
String k = kuerzelField.getText().trim().toLowerCase();
String n = nameField.getText().trim();
if (k.isEmpty() || n.isEmpty()) {
showAlert("Bitte Kürzel und Name eingeben.");
return;
}
boolean ok = userDAO.saveUser(k, n); // NEU: insert or update je nach Existenz
PersonMapping.forceReloadPeople();
refreshUserList();
showAlert(ok ? "Gespeichert. Änderung nach Neustart verfügbar." : "Fehler beim Speichern.");
PersonMapping.forceReloadPeople();
}
private void saveOwnUser() {
String k = kuerzelField.getText().trim().toLowerCase();
String n = nameField.getText().trim();
if (n.isEmpty()) {
showAlert("Bitte Namen eingeben.");
return;
}
boolean ok = userDAO.saveUser(k, n); // NEU: insert or update je nach Existenz
PersonMapping.forceReloadPeople();
refreshUserList();
showAlert(ok ? "Gespeichert. Änderung nach Neustart verfügbar." : "Fehler beim Speichern.");
}
private void deleteUser() {
String k = kuerzelField.getText().trim().toLowerCase();
if (k.isEmpty()) return;
userDAO.deleteUser(k);
refreshUserList();
showAlert("Gelöscht.");
}
private void showAlert(String text) {
Alert alert = new Alert(Alert.AlertType.INFORMATION, text);
alert.setHeaderText(null);
alert.showAndWait();
}
// --------- Hilfsklasse für TableView ---------
public static class UserEntry {
private final StringProperty kuerzel = new SimpleStringProperty();
private final StringProperty name = new SimpleStringProperty();
public UserEntry(String kuerzel, String name) {
this.kuerzel.set(kuerzel);
this.name.set(name);
}
public String getKuerzel() { return kuerzel.get(); }
public String getName() { return name.get(); }
public StringProperty kuerzelProperty() { return kuerzel; }
public StringProperty nameProperty() { return name; }
}
}

View File

@@ -0,0 +1,132 @@
package ui;
import javafx.animation.PauseTransition;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.ArrayList;
import java.util.List;
public class EvaluationPopupUI {
private final List<Double> votes = new ArrayList<>();
private double average = -1;
public double showAndWait() {
Stage stage = new Stage();
stage.initModality(Modality.APPLICATION_MODAL);
stage.setTitle("Schulungsevaluation");
VBox root = new VBox(30);
root.setPadding(new Insets(35));
root.setAlignment(Pos.TOP_CENTER);
root.setStyle("-fx-background-color: linear-gradient(to bottom right, #f8f9fa, #e9ecef);");
Label heading = new Label("Wie empfanden Sie diese Schulung in Schulnoten?");
heading.setStyle("-fx-font-size: 22; -fx-font-weight: bold; -fx-text-fill: #333;");
GridPane grid = new GridPane();
grid.setHgap(30);
grid.setVgap(30);
grid.setAlignment(Pos.CENTER);
for (int i = 1; i <= 6; i++) {
StackPane tile = createTile(i, stage, grid);
int row = (i - 1) / 3;
int col = (i - 1) % 3;
grid.add(tile, col, row);
}
Label thankYou = new Label("Vielen Dank!");
thankYou.setStyle("-fx-font-size: 26; -fx-text-fill: #2ecc71; -fx-font-weight: bold;");
thankYou.setVisible(false);
Button btnFinish = new Button("Evaluation beenden");
btnFinish.setStyle("""
-fx-font-size: 15;
-fx-padding: 10 25;
-fx-background-color: #007bff;
-fx-text-fill: white;
-fx-background-radius: 8;
-fx-font-weight: bold;
""");
btnFinish.setOnMouseEntered(e -> btnFinish.setStyle(btnFinish.getStyle() + "-fx-background-color: #3399ff;"));
btnFinish.setOnMouseExited(e -> btnFinish.setStyle(btnFinish.getStyle().replace("-fx-background-color: #3399ff;", "-fx-background-color: #007bff;")));
btnFinish.setOnAction(e -> {
if (!votes.isEmpty()) {
average = votes.stream().mapToDouble(Double::doubleValue).average().orElse(-1);
} else {
average = -1;
}
stage.close();
});
root.getChildren().addAll(heading, grid, thankYou, btnFinish);
Scene scene = new Scene(root, 800, 650);
stage.setScene(scene);
stage.showAndWait();
return average;
}
private StackPane createTile(int number, Stage stage, GridPane grid) {
String[] labels = {
"", "sehr gut", "gut", "befriedigend", "ausreichend", "mangelhaft", "ungenügend"
};
VBox tileBox = new VBox(5);
tileBox.setAlignment(Pos.CENTER);
Label numLabel = new Label(String.valueOf(number));
numLabel.setStyle("-fx-font-size: 54; -fx-font-weight: bold; -fx-text-fill: #333;");
Label textLabel = new Label(labels[number]);
textLabel.setStyle("-fx-font-size: 16; -fx-text-fill: #666;");
tileBox.getChildren().addAll(numLabel, textLabel);
StackPane tile = new StackPane(tileBox);
tile.setPrefSize(240, 240);
tile.setStyle("""
-fx-background-color: white;
-fx-border-color: #ccc;
-fx-border-radius: 20;
-fx-background-radius: 20;
-fx-border-width: 2;
""");
DropShadow shadow = new DropShadow(15, Color.rgb(0, 0, 0, 0.15));
tile.setEffect(shadow);
tile.setOnMouseEntered(e -> tile.setStyle(tile.getStyle().replace("-fx-background-color: white;", "-fx-background-color: #dff0ff;")));
tile.setOnMouseExited(e -> tile.setStyle(tile.getStyle().replace("-fx-background-color: #dff0ff;", "-fx-background-color: white;")));
tile.setOnMouseClicked(e -> {
votes.add((double) number);
grid.setVisible(false);
Label thankYou = (Label) ((VBox) stage.getScene().getRoot()).getChildren().get(2);
thankYou.setVisible(true);
PauseTransition pause = new PauseTransition(Duration.seconds(1.5));
pause.setOnFinished(ev -> {
thankYou.setVisible(false);
grid.setVisible(true);
});
pause.play();
});
return tile;
}
}

343
src/ui/EvaluationUI.java Normal file
View File

@@ -0,0 +1,343 @@
package ui;
import db.TrainingDAO;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import model.PersonMapping;
import java.util.*;
import java.util.stream.Collectors;
/**
* Zeigt Bewertungen pro Schulungstyp:
* - normale Nutzer: nur eigene
* - Admins: Tabs pro Person
* Mit Refresh-Button.
*/
public class EvaluationUI extends BorderPane {
private final TabPane tabs = new TabPane();
private final Button btnRefresh = new Button("Aktualisieren");
public EvaluationUI() {
// outer layout
VBox content = new VBox(10);
content.setPadding(new Insets(15));
Label heading = new Label("Meine Evaluationen nach Schulungstyp");
heading.setStyle("-fx-font-size: 18; -fx-font-weight: bold;");
// --- Refresh button ---
btnRefresh.setOnAction(e -> {
content.getChildren().remove(tabs); // clear old
loadEvaluations(); // reload data
content.getChildren().add(tabs); // re-add updated tabs
});
// --- Online Evaluation button ---
Button btnOnlineEval = new Button("Online-Evaluation starten");
btnOnlineEval.setStyle("-fx-background-color: #324B8C; -fx-text-fill: white; -fx-font-weight: bold;");
btnOnlineEval.setOnAction(e -> {
try {
java.awt.Desktop.getDesktop().browse(new java.net.URI("https://mrbs.ph-freiburg.de/evaluation.php"));
} catch (Exception ex) {
ex.printStackTrace();
}
});
HBox header = new HBox(15, heading, btnRefresh, btnOnlineEval);
header.setAlignment(Pos.CENTER_LEFT);
header.setPadding(new Insets(0, 0, 10, 0));
// add heading and tabs container
content.getChildren().addAll(header, tabs);
// put everything into scroll pane
ScrollPane scroll = new ScrollPane(content);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
setCenter(scroll);
// finally load data
loadEvaluations();
}
private void loadEvaluations() {
tabs.getTabs().clear();
String login = System.getProperty("user.name");
boolean isAdmin = PersonMapping.isAdmin(login);
TrainingDAO dao = new TrainingDAO();
List<Map<String, Object>> all = dao.getAllTrainings();
// --- Gruppieren nach Person (damit Admin Tabs bekommt) ---
Map<String, List<Map<String, Object>>> byPerson = all.stream()
.filter(r -> r.get("durchfuehrende_person") != null)
.collect(Collectors.groupingBy(r -> r.get("durchfuehrende_person").toString()));
if (!isAdmin) {
// nur eigener Datensatz
String loginNorm = PersonMapping.normalizeLogin(login);
List<Map<String, Object>> mine = byPerson.entrySet().stream()
.filter(e -> loginNorm.equalsIgnoreCase(PersonMapping.normalizeLogin(e.getKey())))
.flatMap(e -> e.getValue().stream())
.toList();
Tab ownTab = new Tab(PersonMapping.toName(loginNorm));
ownTab.setClosable(false);
ownTab.setContent(createEvaluationContent(mine));
tabs.getTabs().add(ownTab);
} else {
// Admin: Tab pro Person mit Daten
for (Map.Entry<String, List<Map<String, Object>>> entry : byPerson.entrySet()) {
String kuerzel = entry.getKey();
List<Map<String, Object>> rows = entry.getValue();
String name = PersonMapping.toName(kuerzel);
Tab t = new Tab(name != null && !name.isBlank() ? name : kuerzel);
t.setClosable(false);
t.setContent(createEvaluationContent(rows));
tabs.getTabs().add(t);
}
if (!tabs.getTabs().isEmpty()) {
tabs.getSelectionModel().select(0);
}
}
}
// --- Einzelansicht pro Person ---
private VBox createEvaluationContent(List<Map<String, Object>> rows) {
VBox box = new VBox(10);
box.setPadding(new Insets(10));
if (rows == null || rows.isEmpty()) {
Label none = new Label("Keine Schulungen vorhanden.");
none.setStyle("-fx-text-fill: gray; -fx-font-style: italic;");
box.getChildren().add(none);
return box;
}
Map<String, List<Double>> evalByType = new TreeMap<>();
Set<String> typesWithNoEval = new TreeSet<>();
for (Map<String, Object> row : rows) {
String type = Objects.toString(row.get("inhaltstyp"), "");
Object e = row.get("evaluation");
if (e != null) {
try {
double val = Double.parseDouble(String.valueOf(e));
if (val >= 1.0 && val <= 6.0)
evalByType.computeIfAbsent(type, k -> new ArrayList<>()).add(val);
} catch (NumberFormatException ignore) {}
} else {
typesWithNoEval.add(type);
}
}
Set<String> allTypes = new TreeSet<>();
allTypes.addAll(evalByType.keySet());
allTypes.addAll(typesWithNoEval);
for (String type : allTypes) {
List<Double> vals = evalByType.get(type);
Label title = new Label(type);
title.setStyle("-fx-font-size: 14; -fx-font-weight: bold;");
if (vals == null || vals.isEmpty()) {
Label none = new Label("Keine Bewertungen");
none.setStyle("-fx-text-fill: gray;");
box.getChildren().addAll(title, none);
continue;
}
double avg = vals.stream().mapToDouble(Double::doubleValue).average().orElse(0);
Label summary = new Label(String.format(Locale.US,
"Ø %.2f (%d Bewertung%s)", avg, vals.size(), vals.size() == 1 ? "" : "en"));
summary.setStyle("-fx-font-size: 12;");
box.getChildren().addAll(title, summary);
if (vals.size() >= 2) {
// filter only the rows that belong to this training type
List<Map<String, Object>> rowsForType = rows.stream()
.filter(r -> type.equals(Objects.toString(r.get("inhaltstyp"), "")))
.collect(Collectors.toList());
LineChart<Number, Number> chart = createTinyChart(rowsForType);
Label today = new Label("→ heute");
today.setStyle("-fx-font-size: 10; -fx-text-fill: #666;");
today.setPadding(new Insets(-5, 0, 5, 0));
today.setMaxWidth(Double.MAX_VALUE);
HBox h = new HBox(today);
h.setAlignment(Pos.CENTER_RIGHT);
box.getChildren().addAll(chart, h);
}
}
return box;
}
private LineChart<Number, Number> createTinyChart(List<Map<String, Object>> rows) {
// Nur valide Bewertungen behalten (1..6) Liste bleibt 1:1 zu vals ausgerichtet
// --- Sort chronologically: oldest → newest ---
List<Map<String, Object>> evalRows = rows.stream()
.filter(r -> {
Object e = r.get("evaluation");
try {
double v = (e == null) ? -1 : Double.parseDouble(e.toString());
return v >= 1.0 && v <= 6.0;
} catch (Exception ex) {
return false;
}
})
.sorted(Comparator.comparing(r -> Objects.toString(r.get("datum"), ""))) // sort by date ascending
.toList();
List<Double> vals = evalRows.stream()
.map(r -> Double.parseDouble(r.get("evaluation").toString()))
.toList();
NumberAxis xAxis = new NumberAxis(1, Math.max(1, vals.size()), 1);
xAxis.setTickLabelsVisible(false);
xAxis.setTickMarkVisible(false);
xAxis.setMinorTickVisible(false);
xAxis.setOpacity(0);
xAxis.setVisible(false);
// 1 oben (gut) → 6 unten (schlecht), ohne gespiegelten Text
NumberAxis yAxis = new NumberAxis(6, 1, -1);
yAxis.setAutoRanging(false);
yAxis.setTickLabelsVisible(true);
yAxis.setTickLabelFill(javafx.scene.paint.Color.GRAY);
yAxis.setMinorTickVisible(false);
yAxis.setTickMarkVisible(true);
yAxis.setLabel(null);
LineChart<Number, Number> chart = new LineChart<>(xAxis, yAxis);
chart.setCreateSymbols(true); // wir setzen eigene Symbole, aber das kann an bleiben
chart.setLegendVisible(false);
chart.setHorizontalGridLinesVisible(true);
chart.setVerticalGridLinesVisible(false);
chart.setAlternativeColumnFillVisible(false);
chart.setAlternativeRowFillVisible(false);
chart.setAnimated(false);
chart.setPrefHeight(100);
chart.setMinHeight(80);
chart.setMaxHeight(120);
chart.setStyle("-fx-background-color: transparent; -fx-border-color: #ccc;");
// --- Rohwerte (mit eigenen Symbol-Nodes + Tooltip) ---
XYChart.Series<Number, Number> seriesRaw = new XYChart.Series<>();
for (int i = 0; i < vals.size(); i++) {
double y = vals.get(i);
XYChart.Data<Number, Number> data = new XYChart.Data<>(i + 1, y);
StackPane symbol = new StackPane();
symbol.getStyleClass().add("chart-line-symbol");
symbol.setPrefSize(8, 8);
symbol.setMouseTransparent(false);
Map<String, Object> row = evalRows.get(i);
String title = Objects.toString(row.get("titel"), "");
String date = Objects.toString(row.get("datum"), "");
String inhalt = Objects.toString(row.get("inhaltstyp"), "");
String modus = Objects.toString(row.get("modus"), "");
String eval = String.format(Locale.US, "%.2f", y);
Tooltip.install(symbol, new Tooltip(title + "\n" + date + "\nBewertung: " + eval));
// --- On click: show small popup window with details ---
symbol.setOnMouseClicked(evt -> {
Stage popup = new Stage();
popup.initModality(javafx.stage.Modality.APPLICATION_MODAL);
popup.setTitle("Details zur Schulung");
VBox box = new VBox(8);
box.setPadding(new Insets(15));
box.getChildren().addAll(
new Label("Titel: " + title),
new Label("Datum: " + date),
new Label("Inhaltstyp: " + inhalt),
new Label("Modus: " + modus),
new Label("Bewertung: " + eval)
);
Scene scene = new Scene(box);
popup.setScene(scene);
popup.setWidth(300);
popup.setHeight(200);
popup.showAndWait();
});
data.setNode(symbol);
seriesRaw.getData().add(data);
}
// --- Laufender Durchschnitt (ohne Symbole) ---
XYChart.Series<Number, Number> seriesAvg = new XYChart.Series<>();
double sum = 0;
for (int i = 0; i < vals.size(); i++) {
sum += vals.get(i);
double avg = sum / (i + 1);
XYChart.Data<Number, Number> d = new XYChart.Data<>(i + 1, avg);
d.setNode(null);
seriesAvg.getData().add(d);
}
// --- Global average line (horizontal across entire chart) ---
double globalAvg = vals.stream().mapToDouble(Double::doubleValue).average().orElse(0);
XYChart.Series<Number, Number> seriesGlobal = new XYChart.Series<>();
seriesGlobal.getData().add(new XYChart.Data<>(1, globalAvg));
seriesGlobal.getData().add(new XYChart.Data<>(vals.size(), globalAvg));
chart.getData().addAll(seriesRaw, seriesAvg, seriesGlobal);
// Style: blue (raw), orange (running avg), gray dashed (global avg)
var rawLine = chart.lookup(".chart-series-line.series0");
if (rawLine != null)
rawLine.setStyle("-fx-stroke: #2a7fff; -fx-stroke-width: 2px;");
var avgLine = chart.lookup(".chart-series-line.series1");
if (avgLine != null)
avgLine.setStyle("-fx-stroke: #ff7f27; -fx-stroke-width: 1.5px; -fx-stroke-dash-array: 4 4;");
var globalLine = chart.lookup(".chart-series-line.series2");
if (globalLine != null)
globalLine.setStyle("-fx-stroke: #999999; -fx-stroke-width: 1.2px; -fx-stroke-dash-array: 8 6;");
return chart;
}
}

98
src/ui/MainUI.java Normal file
View File

@@ -0,0 +1,98 @@
package ui;
import db.TrainingDAO;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import util.Config;
public class MainUI {
private final TabPane tabPane = new TabPane();
private final TrainingFormUI trainingFormUI;
private final ConfigUI configUI;
private final Label lblWarning = new Label(); // Warnhinweis oben
private final Button btnRefreshWarning = new Button("Habe ich alles ausgefüllt?");
public MainUI(Stage stage) {
trainingFormUI = new TrainingFormUI();
configUI = new ConfigUI(stage, this); // <— MainUI wird an ConfigUI übergeben!
tabPane.getTabs().addAll(
createTab("Erfassung", trainingFormUI),
createTab("Daten", new TrainingOverviewUI()),
createTab("Evaluation", new EvaluationUI()), // <--- new tab here
createTab("Config", configUI)
);
// ------------------------------------------------------------
// Obere Leiste mit Warnhinweis + Button
// ------------------------------------------------------------
HBox warningBox = new HBox(10);
warningBox.setPadding(new Insets(5, 10, 5, 10));
lblWarning.setWrapText(true);
lblWarning.setVisible(true);
btnRefreshWarning.setOnAction(e -> checkForIncompleteTrainings());
btnRefreshWarning.setVisible(true);
warningBox.getChildren().addAll(lblWarning, btnRefreshWarning);
BorderPane root = new BorderPane();
root.setTop(warningBox);
root.setCenter(tabPane);
Scene scene = new Scene(root, 900, 600);
stage.setTitle("Schulungsstatistik Tool");
stage.setScene(scene);
stage.show();
// Erste Prüfung beim Start
checkForIncompleteTrainings();
}
private Tab createTab(String name, javafx.scene.Node content) {
Tab tab = new Tab(name);
tab.setClosable(false);
tab.setContent(content);
return tab;
}
public String getDbPath() {
return Config.getDbPath();
}
// ------------------------------------------------------------
// Neue Methode: rotes Label, wenn unvollständige Schulungen existieren
// ------------------------------------------------------------
private void checkForIncompleteTrainings() {
TrainingDAO dao = new TrainingDAO();
boolean hasIncomplete = dao.hasTrainingsWithoutParticipantsForCurrentUser();
if (hasIncomplete) {
lblWarning.setText(
"Es existieren Schulungen in der Vergangenheit ohne Teilnehmerzahl oder Ausfallgrund. Bitte prüfen Sie die Daten."
);
lblWarning.setTextFill(Color.RED);
lblWarning.setStyle("-fx-font-weight: bold;");
} else {
lblWarning.setText(
"All Ihre eingetragenen Termine haben entweder eine Teilnehmerzahl oder einen Ausfallgrund. Perfekt!"
);
lblWarning.setTextFill(Color.GREEN);
lblWarning.setStyle("-fx-font-weight: bold;");
lblWarning.setVisible(false);
btnRefreshWarning.setVisible(false);
}
}
/** Wird von ConfigUI aufgerufen, wenn Config neu geladen wurde */
public void reloadDatabaseDependentData() {
trainingFormUI.refreshTrainingTypes(); // <— ruft DAO erneut auf
}
}

80
src/ui/TimeSpinner.java Normal file
View File

@@ -0,0 +1,80 @@
package ui;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.util.StringConverter;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
/**
* Ein einfacher Spinner für Uhrzeiten im Format HH:mm.
*/
public class TimeSpinner extends Spinner<LocalTime> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
public TimeSpinner() {
this(LocalTime.of(10, 0));
}
public TimeSpinner(LocalTime defaultTime) {
setEditable(true);
SpinnerValueFactory<LocalTime> factory = new SpinnerValueFactory<>() {
{
setValue(defaultTime);
}
@Override
public void decrement(int steps) {
LocalTime time = getValue() != null ? getValue() : LocalTime.of(10, 0);
setValue(time.minusMinutes(steps * 5));
}
@Override
public void increment(int steps) {
LocalTime time = getValue() != null ? getValue() : LocalTime.of(10, 0);
setValue(time.plusMinutes(steps * 5));
}
};
factory.setConverter(new StringConverter<>() {
@Override
public String toString(LocalTime time) {
return time == null ? "" : time.format(FORMATTER);
}
@Override
public LocalTime fromString(String text) {
try {
if (text == null || text.isBlank()) {
return getValue();
}
// Wenn Nutzer nur "10" oder "9" eintippt → ":00" ergänzen
String cleaned = text.trim();
if (cleaned.matches("^\\d{1,2}$")) {
cleaned = cleaned + ":00";
}
// Wenn z.B. "9:5" eingegeben wird → auf "09:05" normalisieren
if (cleaned.matches("^\\d{1,2}:\\d{1}$")) {
String[] parts = cleaned.split(":");
cleaned = String.format("%02d:%02d",
Integer.parseInt(parts[0]),
Integer.parseInt(parts[1]));
}
return LocalTime.parse(cleaned, FORMATTER);
} catch (Exception e) {
return getValue();
}
}
});
setValueFactory(factory);
getEditor().setText(defaultTime.format(FORMATTER));
}
}

View File

@@ -0,0 +1,332 @@
// File: ui/TrainingEditDialog.java
package ui;
import db.TrainingDAO;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import model.PersonMapping;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
/**
* Modaler Bearbeitungsdialog für einen Trainings-Datensatz.
* Rechteprüfung erfolgt VOR dem Öffnen im aufrufenden UI.
*/
public class TrainingEditDialog extends Dialog<Boolean> {
private static final DateTimeFormatter TF = DateTimeFormatter.ofPattern("HH:mm");
private final DatePicker dpDatum = new DatePicker();
private final TimeSpinner spZeit = new TimeSpinner();
private final TextField tfDauer = new TextField();
private final ComboBox<String> cbModus = new ComboBox<>();
private final TextField tfTeilnAnz = new TextField();
private final ComboBox<String> cbTeilnTyp = new ComboBox<>();
private final ComboBox<String> cbPerson = new ComboBox<>();
private final ComboBox<String> tfAusgefallen = new ComboBox<>();
private final TextField tfTitel = new TextField();
private final ComboBox<String> cbInhalt = new ComboBox<>();
private final ComboBox<String> cbVeranst = new ComboBox<>();
private final TextArea taKommentar = new TextArea();
private final TextField tfEvaluation = new TextField();
public TrainingEditDialog(Map<String, Object> row,
List<String> modi,
List<String> teilnTypen,
List<String> veranstaltungstypen,
List<String> inhaltstypen,
List<String> personen) {
setTitle("Datensatz bearbeiten");
getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
ButtonType deleteType = new ButtonType("Löschen", ButtonBar.ButtonData.LEFT);
getDialogPane().getButtonTypes().add(deleteType);
// Dropdown-Optionen
cbModus.getItems().setAll(modi);
cbTeilnTyp.getItems().setAll(teilnTypen);
cbVeranst.getItems().setAll(veranstaltungstypen);
cbInhalt.getItems().setAll(inhaltstypen);
cbPerson.getItems().setAll(PersonMapping.getAllNames());
// Standardwerte für "Ausgefallen Grund"
tfAusgefallen.getItems().setAll(model.DataModels.getAusgefallenGruende());
tfAusgefallen.setEditable(false); // kein Freitext
tfAusgefallen.setPromptText("— bitte auswählen —");
tfAusgefallen.setButtonCell(new ListCell<>() {
@Override protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
setText(empty || item == null || item.isBlank() ? "optional" : item);
}
});
// nur Zahlen
tfDauer.textProperty().addListener((o, ov, nv) -> {
if (!nv.matches("\\d*")) tfDauer.setText(nv.replaceAll("[^\\d]", ""));
});
tfTeilnAnz.textProperty().addListener((o, ov, nv) -> {
if (!nv.matches("\\d*")) tfTeilnAnz.setText(nv.replaceAll("[^\\d]", ""));
});
// Prefill
dpDatum.setValue(parseDate(row.get("datum")));
spZeit.getValueFactory().setValue(parseTime(row.get("startzeit")));
tfDauer.setText(nz(row.get("dauer_minuten")));
cbModus.setValue(nz(row.get("modus")));
tfTeilnAnz.setText(nz(row.get("teilnehmende_anzahl")));
cbTeilnTyp.setValue(nz(row.get("teilnehmende_typ")));
cbPerson.setValue(PersonMapping.toName(nz(row.get("durchfuehrende_person"))));
tfAusgefallen.setValue(nz(row.get("ausgefallen_grund")));
tfTitel.setText(nz(row.get("titel")));
cbInhalt.setValue(nz(row.get("inhaltstyp")));
cbVeranst.setValue(nz(row.get("veranstaltungstyp")));
taKommentar.setText(nz(row.get("kommentar")));
taKommentar.setPrefRowCount(3);
taKommentar.setWrapText(true);
tfEvaluation.setText(nz(row.get("evaluation")));
tfEvaluation.textProperty().addListener((o, ov, nv) -> {
if (!nv.matches("\\d*(\\.\\d*)?")) tfEvaluation.setText(nv.replaceAll("[^\\d.]", ""));
});
// Layout
GridPane g = new GridPane();
g.setHgap(10);
g.setVgap(8);
g.setPadding(new Insets(12));
int r = 0;
g.add(new Label("Datum"), 0, r); g.add(dpDatum, 1, r++);
g.add(new Label("Startzeit"), 0, r); g.add(spZeit, 1, r++);
g.add(new Label("Dauer (Min)"), 0, r); g.add(tfDauer, 1, r++);
g.add(new Label("Modus"), 0, r); g.add(cbModus, 1, r++);
g.add(new Label("Teilnehmende Anzahl"), 0, r); g.add(tfTeilnAnz, 1, r++);
g.add(new Label("Teilnehmende Typ"), 0, r); g.add(cbTeilnTyp, 1, r++);
g.add(new Label("Durchführende Person"), 0, r); g.add(cbPerson, 1, r++);
g.add(new Label("Ausgefallen Grund"), 0, r); g.add(tfAusgefallen, 1, r++);
g.add(new Label("Titel"), 0, r); g.add(tfTitel, 1, r++);
g.add(new Label("Inhaltstyp"), 0, r); g.add(cbInhalt, 1, r++);
g.add(new Label("Veranstaltungstyp"), 0, r); g.add(cbVeranst, 1, r++);
// --- Bewertung (16) mit optionaler Live-Evaluation ---
g.add(new Label("Bewertung (16)"), 0, r);
HBox evalBox = new HBox(10);
evalBox.setPadding(new Insets(0));
tfEvaluation.setPromptText("16");
tfEvaluation.setEditable(true);
tfEvaluation.setPrefWidth(80);
Button liveEvalButton = new Button("Live-Evaluation starten");
liveEvalButton.setStyle("-fx-font-size: 12;");
LocalDate datum = parseDate(row.get("datum"));
if (datum != null && datum.equals(LocalDate.now())) {
// Nur anzeigen, wenn Schulung heute ist
liveEvalButton.setVisible(true);
liveEvalButton.setManaged(true);
} else {
liveEvalButton.setVisible(false);
liveEvalButton.setManaged(false);
}
liveEvalButton.setOnAction(e -> {
EvaluationPopupUI popup = new EvaluationPopupUI();
double avg = popup.showAndWait();
if (avg > 0) {
tfEvaluation.setText(String.valueOf(avg));
}
});
evalBox.getChildren().addAll(tfEvaluation, liveEvalButton);
g.add(evalBox, 1, r++);
g.add(new Label("Kommentar"), 0, r); g.add(taKommentar, 1, r++);
getDialogPane().setContent(g);
// --- Sichtbarkeit Bewertung nur für eigene oder Admin ---
String login = System.getProperty("user.name");
String loginKuerzel = PersonMapping.normalizeLogin(login);
String personKuerzel = PersonMapping.normalizeLogin(nz(row.get("durchfuehrende_person")));
boolean isOwn = loginKuerzel != null && loginKuerzel.equalsIgnoreCase(personKuerzel);
if (!isOwn && !PersonMapping.isAdmin(login)) {
// Completely remove both label and field from layout
g.getChildren().removeIf(node -> GridPane.getColumnIndex(node) != null && GridPane.getColumnIndex(node) == 1 && node == tfEvaluation);
g.getChildren().removeIf(node -> node instanceof Label && ((Label) node).getText().contains("Bewertung"));
}
// --- Delete-Handler ---
final Button deleteBtn = (Button) getDialogPane().lookupButton(deleteType);
deleteBtn.addEventFilter(javafx.event.ActionEvent.ACTION, ev -> {
final int id = Integer.parseInt(String.valueOf(row.get("id")));
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
"Datensatz wirklich löschen?\n(ID: " + id + ")",
ButtonType.YES, ButtonType.NO);
confirm.setHeaderText("Löschbestätigung");
confirm.showAndWait().ifPresent(bt -> {
if (bt == ButtonType.YES) {
try {
new TrainingDAO().deleteTraining(id);
// UI-Markierung: Zeile visuell „gelöscht“
row.put("_deleted", Boolean.TRUE);
setResult(Boolean.TRUE);
} catch (Exception ex) {
new Alert(Alert.AlertType.ERROR,
"Fehler beim Löschen: " + ex.getMessage()).showAndWait();
}
}
});
ev.consume(); // Dialog offen lassen / Ergebnis bereits gesetzt
});
// OK-Handler (validieren + speichern)
final Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK);
okBtn.addEventFilter(javafx.event.ActionEvent.ACTION, ev -> {
if (!validateRequired()) {
new Alert(Alert.AlertType.WARNING, "Pflichtfelder fehlen oder ungültig.").showAndWait();
ev.consume();
return;
}
Double evaluation = parseDoubleOrNull(tfEvaluation.getText());
if (evaluation != null && (evaluation < 1.0 || evaluation > 6.0)) {
new Alert(Alert.AlertType.WARNING, "Bewertung muss zwischen 1.0 und 6.0 liegen.").showAndWait();
ev.consume();
return;
}
try {
final int id = Integer.parseInt(String.valueOf(row.get("id")));
// ggf. Inhalt für Führung erzwingen
String veranst = cbVeranst.getValue();
String inhalt = cbInhalt.getValue();
if ("fuehrung".equals(veranst)) {
inhalt = "bibliotheksnutzung";
}
// bisherigen planned-Wert lesen
int plannedOld = 0;
Object pv = row.get("geplant");
if (pv instanceof Number n) {
plannedOld = n.intValue();
} else if (pv != null) {
try { plannedOld = Integer.parseInt(String.valueOf(pv)); } catch (Exception ignore) {}
}
// Teilnehmerzahl: jede eingetragene Zahl (auch "0") -> nicht mehr geplant
String tnText = tfTeilnAnz.getText();
boolean anyTnEntered = tnText != null && !tnText.trim().isEmpty();
Integer tn = parseIntOrNull(tnText);
int plannedNew = anyTnEntered ? 0 : plannedOld;
String personKuerzelNew = PersonMapping.toKuerzel(cbPerson.getValue());
new TrainingDAO().updateTraining(
id,
dpDatum.getValue(),
spZeit.getValueFactory().getValue(),
parseIntOrNull(tfDauer.getText()),
cbModus.getValue(),
tn,
cbTeilnTyp.getValue(),
personKuerzelNew,
tfAusgefallen.getValue(),
tfTitel.getText(),
inhalt,
veranst,
taKommentar.getText(),
plannedNew,
evaluation
);
// update in-memory map for table
row.put("datum", dpDatum.getValue() != null ? dpDatum.getValue().toString() : null);
row.put("startzeit", spZeit.getValueFactory().getValue() != null
? spZeit.getValueFactory().getValue().format(TF)
: null);
row.put("dauer_minuten", tfDauer.getText());
row.put("modus", cbModus.getValue());
row.put("teilnehmende_anzahl", tfTeilnAnz.getText());
row.put("teilnehmende_typ", cbTeilnTyp.getValue());
row.put("durchfuehrende_person", PersonMapping.toKuerzel(cbPerson.getValue()));
row.put("ausgefallen_grund", tfAusgefallen.getValue());
row.put("titel", tfTitel.getText());
row.put("inhaltstyp", cbInhalt.getValue());
row.put("veranstaltungstyp", cbVeranst.getValue());
row.put("kommentar", taKommentar.getText());
row.put("geplant", plannedNew);
row.put("evaluation", tfEvaluation.getText());
setResult(Boolean.TRUE);
} catch (Exception ex) {
new Alert(Alert.AlertType.ERROR, "Fehler beim Speichern: " + ex.getMessage()).showAndWait();
ev.consume();
}
});
setResultConverter(dialogButton -> {
if (dialogButton == ButtonType.OK) {
return Boolean.TRUE;
} else {
return null;
}
});
}
// --- Helpers ---
private boolean validateRequired() {
if (dpDatum.getValue() == null) return false;
if (cbModus.getValue() == null || cbModus.getValue().isBlank()) return false;
if (cbTeilnTyp.getValue() == null || cbTeilnTyp.getValue().isBlank()) return false;
if (cbVeranst.getValue() == null || cbVeranst.getValue().isBlank()) return false;
if (cbInhalt.getValue() == null || cbInhalt.getValue().isBlank()) return false;
// Startzeit muss HH:mm sein
try {
LocalTime.parse(spZeit.getEditor().getText(), TF);
} catch (Exception e) {
return false;
}
return true;
}
private static String nz(Object o) {
return o == null ? "" : String.valueOf(o);
}
private static LocalDate parseDate(Object o) {
try { return LocalDate.parse(String.valueOf(o)); } catch (Exception e) { return null; }
}
private static LocalTime parseTime(Object o) {
try { return LocalTime.parse(String.valueOf(o), TF); } catch (Exception e) { return LocalTime.of(10, 0); }
}
private static Integer parseIntOrNull(String s) {
if (s == null || s.isBlank()) return null;
return Integer.parseInt(s);
}
private static Double parseDoubleOrNull(String s) {
if (s == null || s.isBlank()) return null;
try { return Double.parseDouble(s); } catch (Exception e) { return null; }
}
}

View File

@@ -0,0 +1,32 @@
// File: ui/TrainingFormUI.java
package ui;
import javafx.geometry.Insets;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.BorderPane;
public class TrainingFormUI extends BorderPane {
private final TrainingFormUIEnterEvent enterTab = new TrainingFormUIEnterEvent();
private final TrainingFormUIPlanner plannerTab = new TrainingFormUIPlanner();
public TrainingFormUI() {
setPadding(new Insets(15));
TabPane tabs = new TabPane();
tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
Tab tabEnter = new Tab("Neue Schulung anlegen", enterTab);
Tab tabPlan = new Tab("Schulungen vorausplanen", plannerTab);
tabs.getTabs().addAll(tabEnter, tabPlan);
setCenter(tabs);
}
/** Delegiert DB-abhängige Reloads an die Tabs */
public void refreshTrainingTypes() {
enterTab.refreshTrainingTypes();
plannerTab.refreshTrainingTypes(); // derzeit No-Op
}
}

View File

@@ -0,0 +1,531 @@
// File: ui/TrainingFormUIEnterEvent.java
package ui;
import db.TrainingDAO;
import db.TrainingTypeDAO;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import model.PersonMapping;
import util.Config;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;
/**
* Tab 1: Formular zur Erfassung einer neuen Schulung oder Führung.
* Inhalt aus der bisherigen TrainingFormUI übernommen.
*/
public class TrainingFormUIEnterEvent extends BorderPane {
private final ComboBox<String> veranstaltungstypBox = new ComboBox<>();
private final ComboBox<String> inhaltstypBox = new ComboBox<>();
private final TextField titelField = new TextField();
private final DatePicker dateField = new DatePicker();
private final TimeSpinner startzeitSpinner = new TimeSpinner();
private final TextField dauerField = new TextField();
private final ComboBox<String> modusBox = new ComboBox<>();
private final TextField teilnehmerAnzahlField = new TextField();
private final ComboBox<String> teilnehmerTypBox = new ComboBox<>();
//private final TextField durchfuehrendePersonField = new TextField();
private final ComboBox<String> durchfuehrendePersonBox = new ComboBox<>();
private final ComboBox<String> ausgefallenBox = new ComboBox<>();
private final TextArea kommentarField = new TextArea();
private final TextField evaluationField = new TextField();
private final Button speichernButton = new Button("Speichern");
private final Button resetButton = new Button("Zurücksetzen");
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
public TrainingFormUIEnterEvent() {
setPadding(new Insets(15));
Label heading = new Label("Neue Schulung anlegen");
heading.setStyle("-fx-font-size: 18; -fx-font-weight: bold;");
Label hint = new Label("Hinweis: Bereits geplante Veranstaltungen bitte im Tab 'Daten' bearbeiten.");
hint.setStyle("-fx-font-size: 12; -fx-text-fill: #555555; -fx-font-style: italic;");
VBox headingBox = new VBox(4, heading, hint);
setTop(headingBox);
GridPane grid = new GridPane();
grid.setPadding(new Insets(15, 0, 0, 0));
grid.setVgap(8);
grid.setHgap(10);
int row = 0;
grid.add(new Label("Veranstaltungstyp:"), 0, row); grid.add(veranstaltungstypBox, 1, row++);
grid.add(new Label("Inhaltstyp:"), 0, row); grid.add(inhaltstypBox, 1, row++);
grid.add(new Label("Titel Veranstaltung:"), 0, row); grid.add(titelField, 1, row++);
grid.add(new Label("Datum:"), 0, row); grid.add(dateField, 1, row++);
grid.add(new Label("Startzeit:"), 0, row); grid.add(startzeitSpinner, 1, row++);
grid.add(new Label("Dauer (Min):"), 0, row); grid.add(dauerField, 1, row++);
grid.add(new Label("Modus:"), 0, row); grid.add(modusBox, 1, row++);
grid.add(new Label("Teilnehmende:"), 0, row); grid.add(teilnehmerAnzahlField, 1, row++);
grid.add(new Label("Ausgefallen (Grund):"), 0, row); grid.add(ausgefallenBox, 1, row++);
grid.add(new Label("Typ Teilnehmende:"), 0, row); grid.add(teilnehmerTypBox, 1, row++);
//grid.add(new Label("Durchführende Person:"), 0, row); grid.add(durchfuehrendePersonField, 1, row++);
grid.add(new Label("Durchführende Person:"), 0, row);
grid.add(durchfuehrendePersonBox, 1, row++);
// --- Evaluation (direkte Eingabe ODER Live-Evaluation starten) ---
grid.add(new Label("Bewertung (16):"), 0, row);
HBox evalBox = new HBox(10);
evalBox.setPadding(new Insets(0, 0, 0, 0));
evaluationField.setPromptText("16");
evaluationField.setEditable(true);
evaluationField.setPrefWidth(80);
Button liveEvalButton = new Button("Live-Evaluation starten");
liveEvalButton.setStyle("-fx-font-size: 12;");
liveEvalButton.setOnAction(e -> {
EvaluationPopupUI popup = new EvaluationPopupUI();
double avg = popup.showAndWait();
if (avg > 0) {
evaluationField.setText(String.valueOf(avg));
}
});
evalBox.getChildren().addAll(evaluationField, liveEvalButton);
grid.add(evalBox, 1, row++);
grid.add(new Label("Kommentar:"), 0, row); grid.add(kommentarField, 1, row++);
grid.add(new HBox(10, speichernButton, resetButton), 1, ++row);
veranstaltungstypBox.getItems().setAll(model.DataModels.getVeranstaltungstypen());
modusBox.getItems().setAll(model.DataModels.getModi());
teilnehmerTypBox.getItems().setAll(model.DataModels.getTeilnehmendeTypen());
ausgefallenBox.getItems().setAll(model.DataModels.getAusgefallenGruende());
// Durchführende Personen: vordefinierte Namen + frei editierbar
durchfuehrendePersonBox.getItems().setAll(PersonMapping.getAllNames());
durchfuehrendePersonBox.setEditable(true);
// --- Durchführende Person: Dropdown + Standardwert ---
List<String> bekanntePersonen = new java.util.ArrayList<>(model.PersonMapping.getAllNames());
durchfuehrendePersonBox.getItems().setAll(bekanntePersonen);
durchfuehrendePersonBox.setEditable(true);
durchfuehrendePersonBox.setPromptText("Name oder Kürzel eingeben");
String currentUser = System.getProperty("user.name");
String displayName = model.PersonMapping.toName(currentUser);
durchfuehrendePersonBox.setValue(displayName);
fillInhaltstypBoxFromDatabase();
kommentarField.setPrefRowCount(3);
kommentarField.setWrapText(true);
teilnehmerAnzahlField.textProperty().addListener((obs, oldVal, newVal) -> {
Integer parsed = parseIntSafe(newVal);
int anzahl = parsed == null ? 0 : parsed;
if (anzahl <= 0) {
ausgefallenBox.setDisable(false);
} else {
ausgefallenBox.setValue("");
ausgefallenBox.setDisable(true);
}
validateFields();
});
ausgefallenBox.valueProperty().addListener((obs, oldVal, newVal) -> {
if (newVal != null && !newVal.isBlank()) {
teilnehmerAnzahlField.setText("0");
teilnehmerAnzahlField.setDisable(true);
} else {
teilnehmerAnzahlField.setDisable(false);
}
validateFields();
});
String username = System.getProperty("user.name");
durchfuehrendePersonBox.getItems().setAll(model.PersonMapping.getAllNames());
durchfuehrendePersonBox.setEditable(true);
durchfuehrendePersonBox.setPromptText("Name oder Kürzel eingeben");
durchfuehrendePersonBox.setValue(model.PersonMapping.toName(username));
veranstaltungstypBox.setOnAction(e -> updateFieldsForVeranstaltungstyp());
inhaltstypBox.setOnAction(e -> updateTitleFromInhaltstyp());
applyIntegerFilter(dauerField);
applyIntegerFilter(teilnehmerAnzahlField);
applyDecimalFilter(evaluationField);
ChangeListener<Object> validationListener = (obs, o, n) -> validateFields();
veranstaltungstypBox.valueProperty().addListener(validationListener);
inhaltstypBox.valueProperty().addListener(validationListener);
titelField.textProperty().addListener(validationListener);
dateField.valueProperty().addListener(validationListener);
dauerField.textProperty().addListener(validationListener);
modusBox.valueProperty().addListener(validationListener);
teilnehmerAnzahlField.textProperty().addListener(validationListener);
teilnehmerTypBox.valueProperty().addListener(validationListener);
startzeitSpinner.valueProperty().addListener(validationListener);
startzeitSpinner.getEditor().textProperty().addListener(validationListener);
speichernButton.setOnAction(e -> saveTraining());
speichernButton.setDisable(true);
setCenter(new ScrollPane(grid));
}
private void fillInhaltstypBoxFromDatabase() {
TrainingTypeDAO dao = new TrainingTypeDAO(Config.getDbPath());
List<String> types = dao.getAllTrainingTypes();
inhaltstypBox.getItems().setAll(types);
}
private static class TypeRule {
final String forcedInhalt;
final boolean inhaltLocked;
final LocalDate forcedDate;
final boolean dateLocked;
final Integer forcedDauer;
final boolean dauerLocked;
final String forcedModus;
final boolean modusLocked;
final LocalTime forcedStart;
final boolean startLocked;
TypeRule(String forcedInhalt, boolean inhaltLocked,
LocalDate forcedDate, boolean dateLocked,
Integer forcedDauer, boolean dauerLocked,
String forcedModus, boolean modusLocked,
LocalTime forcedStart, boolean startLocked) {
this.forcedInhalt = forcedInhalt;
this.inhaltLocked = inhaltLocked;
this.forcedDate = forcedDate;
this.dateLocked = dateLocked;
this.forcedDauer = forcedDauer;
this.dauerLocked = dauerLocked;
this.forcedModus = forcedModus;
this.modusLocked = modusLocked;
this.forcedStart = forcedStart;
this.startLocked = startLocked;
}
}
private static final java.util.Map<String, TypeRule> RULES = java.util.Map.of(
"fuehrung",
new TypeRule(
"Bibliotheksnutzung", true, // forcedInhalt + locked
null, false, // date unchanged
45, true, // Dauer
"praesenz", true, // Modus
null, false // Startzeit unchanged
),
"schulung",
new TypeRule(
null, false, // Inhalt unchanged
null, false, // Date unchanged
null, false, // Dauer unchanged
null, false, // Modus unchanged
null, false // Startzeit unchanged
),
"coffeelecture",
new TypeRule(
null, false, // identical to schulung
null, false,
null, false,
null, false,
null, false
),
"selbstlerneinheit",
new TypeRule(
null, false, // Inhalt permitted
java.time.LocalDate.now(), true, // date forced to today + locked
0, true, // Dauer = 0 + locked
"online", true, // Modus = online + locked
java.time.LocalTime.of(0, 0), true // Start = 00:00 + locked
)
);
private void applyTypeRule(String typ) {
TypeRule r = RULES.get(typ);
if (r == null) return;
// Inhalt
if (r.forcedInhalt != null) inhaltstypBox.setValue(r.forcedInhalt);
inhaltstypBox.setDisable(r.inhaltLocked);
// Date
if (r.forcedDate != null) dateField.setValue(r.forcedDate);
dateField.setDisable(r.dateLocked);
// Dauer
if (r.forcedDauer != null) dauerField.setText(String.valueOf(r.forcedDauer));
dauerField.setDisable(r.dauerLocked);
// Modus
if (r.forcedModus != null) modusBox.setValue(r.forcedModus);
modusBox.setDisable(r.modusLocked);
// Startzeit
if (r.forcedStart != null) startzeitSpinner.getValueFactory().setValue(r.forcedStart);
startzeitSpinner.setDisable(r.startLocked);
}
private void updateFieldsForVeranstaltungstyp() {
String typ = veranstaltungstypBox.getValue();
if (typ == null) return;
// FULL reset
resetDefaultsForTypeChange();
// Unlock EVERYTHING before applying rules (important!)
inhaltstypBox.setDisable(false);
dateField.setDisable(false);
dauerField.setDisable(false);
modusBox.setDisable(false);
startzeitSpinner.setDisable(false);
// Apply rule set
applyTypeRule(typ);
// Generate title based on NEW values
titelField.setText(generateTitle(
typ,
inhaltstypBox.getValue()
));
validateFields();
}
private String generateTitle(String typ, String inhalt) {
if (typ == null) return "";
// Führung: fixed title
if (typ.equals("fuehrung")) {
return "Bibliotheksführung";
}
// If no topic chosen yet → no title
if (inhalt == null || inhalt.isBlank()) {
return "";
}
switch (typ) {
case "schulung":
return "Schulung: " + inhalt;
case "coffeelecture":
return "Coffee Lecture: " + inhalt;
case "selbstlerneinheit":
return "Selbstlerneinheit: " + inhalt;
default:
return "";
}
}
private void resetDefaultsForTypeChange() {
// Reset ONLY the fields the user requested:
inhaltstypBox.setValue(null);
inhaltstypBox.setDisable(false);
titelField.clear();
modusBox.setValue(null);
modusBox.setDisable(false);
}
private void updateTitleFromInhaltstyp() {
String typ = veranstaltungstypBox.getValue();
String inhalt = inhaltstypBox.getValue();
titelField.setText(generateTitle(typ, inhalt));
validateFields();
}
public void refreshTrainingTypes() {
fillInhaltstypBoxFromDatabase();
}
private void saveTraining() {
try {
if (!validateFields()) return;
String veranstaltungstyp = veranstaltungstypBox.getValue();
String inhaltstyp = inhaltstypBox.getValue();
String titel = titelField.getText();
LocalDate datum = dateField.getValue();
LocalTime startzeit;
try {
startzeit = LocalTime.parse(startzeitSpinner.getEditor().getText(), TIME_FORMAT);
} catch (DateTimeParseException e) {
markField(startzeitSpinner.getEditor(), false);
validateFields();
return;
}
Integer dauer = parseIntSafe(dauerField.getText());
String modus = modusBox.getValue();
Integer teilnehmende = parseIntSafe(teilnehmerAnzahlField.getText());
String teilnehmendeTyp = teilnehmerTypBox.getValue();
String personInput = durchfuehrendePersonBox.getEditor().getText();
if (personInput == null || personInput.isBlank())
personInput = durchfuehrendePersonBox.getValue();
String person = PersonMapping.toKuerzel(personInput);
String grund = ausgefallenBox.getValue();
String kommentar = kommentarField.getText();
String datumStr = datum.format(DateTimeFormatter.ISO_LOCAL_DATE);
String startzeitStr = startzeit.format(TIME_FORMAT);
Double evaluation = parseDoubleSafe(evaluationField.getText());
if (evaluation != null && (evaluation < 1.0 || evaluation > 6.0)) {
new Alert(Alert.AlertType.WARNING, "Bewertung muss zwischen 1.0 und 6.0 liegen.").showAndWait();
return;
}
TrainingDAO dao = new TrainingDAO();
dao.insertTraining(
datumStr,
startzeitStr,
dauer,
modus,
teilnehmende,
teilnehmendeTyp,
person,
grund,
titel,
inhaltstyp,
veranstaltungstyp,
kommentar,
0,
evaluation
);
new Alert(Alert.AlertType.INFORMATION, "Datensatz erfolgreich gespeichert.").showAndWait();
resetForm();
} catch (Exception ex) {
new Alert(Alert.AlertType.ERROR, "Fehler beim Speichern: " + ex.getMessage()).showAndWait();
ex.printStackTrace();
}
}
// Hilfsmethoden ------------------------------------------------------------------
private void applyIntegerFilter(TextField field) {
field.textProperty().addListener((obs, old, neu) -> {
if (!neu.matches("\\d*")) field.setText(neu.replaceAll("[^\\d]", ""));
});
}
private void applyDecimalFilter(TextField field) {
field.textProperty().addListener((obs, old, neu) -> {
if (!neu.matches("\\d*(\\.\\d*)?")) field.setText(neu.replaceAll("[^\\d.]", ""));
});
}
private Integer parseIntSafe(String text) {
if (text == null || text.isBlank()) return null;
try {
return Integer.parseInt(text.trim());
} catch (NumberFormatException e) {
return null;
}
}
private Double parseDoubleSafe(String text) {
if (text == null || text.isBlank()) return null;
try {
return Double.parseDouble(text.trim());
} catch (NumberFormatException e) {
return null;
}
}
private boolean validateFields() {
boolean valid = true;
valid &= markField(veranstaltungstypBox, veranstaltungstypBox.getValue() != null);
valid &= markField(inhaltstypBox, inhaltstypBox.getValue() != null && !inhaltstypBox.getValue().isBlank());
valid &= markField(titelField, !titelField.getText().isBlank());
valid &= markField(dateField, dateField.getValue() != null);
valid &= markField(dauerField, !dauerField.getText().isBlank());
valid &= markField(modusBox, modusBox.getValue() != null);
valid &= markField(teilnehmerAnzahlField, !teilnehmerAnzahlField.getText().isBlank());
valid &= markField(teilnehmerTypBox, teilnehmerTypBox.getValue() != null);
String timeText = startzeitSpinner.getEditor().getText();
boolean validTime = false;
if (timeText != null && !timeText.isBlank()) {
try {
LocalTime.parse(timeText, TIME_FORMAT);
validTime = true;
} catch (DateTimeParseException e) {
validTime = false;
}
}
valid &= markField(startzeitSpinner.getEditor(), validTime);
speichernButton.setDisable(!valid);
return valid;
}
private boolean markField(Control field, boolean ok) {
if (ok) {
field.setStyle("");
} else {
field.setStyle("-fx-border-color: red; -fx-border-width: 2;");
}
return ok;
}
private void resetForm() {
veranstaltungstypBox.setValue(null);
inhaltstypBox.setDisable(false);
inhaltstypBox.setValue(null);
titelField.clear();
dateField.setValue(null);
startzeitSpinner.getValueFactory().setValue(LocalTime.of(10, 0));
dauerField.clear();
modusBox.setValue(null);
teilnehmerAnzahlField.clear();
teilnehmerTypBox.setValue(null);
ausgefallenBox.setValue("");
kommentarField.clear();
validateFields();
}
}

View File

@@ -0,0 +1,475 @@
// File: ui/TrainingFormUIPlanner.java
package ui;
import db.TrainingDAO;
import db.TrainingTypeDAO;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import util.Config;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.*;
/**
* Tab 2: Schulungen vorausplanen (bis zu 100 Termine).
* Unterschiede zu EnterEvent:
* - Keine Felder „Ausgefallen“ und „Teilnehmende“.
* - Pro Zeile: Datum, Startzeit, Durchführende Person (optional, jetzt auch Freitext).
* - 100 Zeilen, 1 sichtbar; jede befüllte Zeile schaltet die nächste frei.
* Beim Speichern werden alle befüllten Zeilen als geplant=1 gespeichert.
*/
public class TrainingFormUIPlanner extends BorderPane {
private static final int MAX_ROWS = 100;
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
// Oberer Block
private final ComboBox<String> veranstaltungstypBox = new ComboBox<>();
private final ComboBox<String> inhaltstypBox = new ComboBox<>();
private final TextField titelField = new TextField();
private final TextField dauerField = new TextField();
private final ComboBox<String> modusBox = new ComboBox<>();
private final ComboBox<String> teilnehmerTypBox = new ComboBox<>();
private final TextArea kommentarField = new TextArea();
private final Button speichernButton = new Button("Alle geplanten speichern");
private final Button resetButton = new Button("Zurücksetzen");
// Zeilenbereich
private final VBox rowsBox = new VBox(6);
private final List<RowControls> rows = new ArrayList<>(MAX_ROWS);
// Datenquellen (Personenliste aus DB)
private List<String> personen = new ArrayList<>();
public TrainingFormUIPlanner() {
setPadding(new Insets(15));
Label heading = new Label("Schulungen vorausplanen");
heading.setStyle("-fx-font-size: 18; -fx-font-weight: bold;");
setTop(heading);
// Oberes Formular
GridPane topGrid = new GridPane();
topGrid.setHgap(10);
topGrid.setVgap(8);
topGrid.setPadding(new Insets(15, 0, 10, 0));
int r = 0;
topGrid.add(new Label("Veranstaltungstyp:"), 0, r); topGrid.add(veranstaltungstypBox, 1, r++);
topGrid.add(new Label("Inhaltstyp:"), 0, r); topGrid.add(inhaltstypBox, 1, r++);
topGrid.add(new Label("Titel Veranstaltung:"), 0, r); topGrid.add(titelField, 1, r++);
topGrid.add(new Label("Dauer (Min):"), 0, r); topGrid.add(dauerField, 1, r++);
topGrid.add(new Label("Modus:"), 0, r); topGrid.add(modusBox, 1, r++);
topGrid.add(new Label("Typ Teilnehmende:"), 0, r); topGrid.add(teilnehmerTypBox, 1, r++);
topGrid.add(new Label("Kommentar:"), 0, r); topGrid.add(kommentarField, 1, r++);
veranstaltungstypBox.getItems().setAll(model.DataModels.getVeranstaltungstypen());
modusBox.getItems().setAll(model.DataModels.getModi());
teilnehmerTypBox.getItems().setAll(model.DataModels.getTeilnehmendeTypen());
kommentarField.setPrefRowCount(2);
kommentarField.setWrapText(true);
fillInhaltstypBoxFromDatabase();
// Titel-Autofill wie im EnterEvent
veranstaltungstypBox.setOnAction(e -> updateFieldsForVeranstaltungstyp());
inhaltstypBox.setOnAction(e -> updateTitleFromInhaltstyp());
applyIntegerFilter(dauerField);
// Zeilenbereich: 100 Zeilen vorbereiten
buildRows();
// Buttons
HBox actions = new HBox(10, speichernButton, resetButton);
speichernButton.setOnAction(e -> savePlanned());
resetButton.setOnAction(e -> resetForm());
VBox center = new VBox(12, topGrid, new Separator(), new Label("Termine (max. 100):"), rowsBox, actions);
setCenter(new ScrollPane(center));
// Defaults
modusBox.setValue("praesenz");
teilnehmerTypBox.setValue("studierende");
if (!inhaltstypBox.getItems().isEmpty()) inhaltstypBox.setValue(inhaltstypBox.getItems().get(0));
validateForm(); // initial Button-Status
}
// ---------- Rows ----------
private void buildRows() {
rowsBox.getChildren().clear();
rows.clear();
reloadPersonen();
for (int i = 0; i < MAX_ROWS; i++) {
RowControls rc = new RowControls(i + 1, personen);
rows.add(rc);
rowsBox.getChildren().add(rc.container);
}
// Nur erste Zeile sichtbar
for (int i = 0; i < MAX_ROWS; i++) {
setRowVisible(i, i == 0);
}
}
private void setRowVisible(int index, boolean visible) {
RowControls rc = rows.get(index);
rc.container.setManaged(visible);
rc.container.setVisible(visible);
}
private void maybeRevealNext(int index) {
if (index + 1 >= MAX_ROWS) return;
if (rows.get(index).isFilled()) {
setRowVisible(index + 1, true);
}
validateForm();
}
// ---------- DB-Backed dropdowns ----------
private void fillInhaltstypBoxFromDatabase() {
TrainingTypeDAO dao = new TrainingTypeDAO(Config.getDbPath());
List<String> types = dao.getAllTrainingTypes();
inhaltstypBox.getItems().setAll(types);
}
private void reloadPersonen() {
try {
// 1. Hole alle Personen der letzten 12 Monate aus der DB
List<String> rawPersonen = new TrainingDAO().getRecentDurchfuehrendePersonen(12);
// 2. Wandle bekannte Kürzel in Anzeigenamen um
personen = new ArrayList<>();
for (String raw : rawPersonen) {
if (raw == null || raw.isBlank()) continue;
String display = model.PersonMapping.toName(raw);
if (!personen.contains(display)) personen.add(display);
}
// 3. Sortiere alphabetisch
Collections.sort(personen);
} catch (Exception e) {
personen = new ArrayList<>();
}
// 4. Leer-Option vorne (optional)
if (personen.isEmpty() || !"".equals(personen.get(0))) {
personen.add(0, "");
}
// 5. Aktuellen Nutzer ganz oben einfügen
String username = System.getProperty("user.name");
String displayName = model.PersonMapping.toName(username);
if (!personen.contains(displayName)) personen.add(1, displayName); // direkt hinter Leerzeile
}
public void refreshTrainingTypes() {
fillInhaltstypBoxFromDatabase();
reloadPersonen();
// Personen-Combos aktualisieren, aktuelle Eingaben möglichst bewahren
for (RowControls rc : rows) {
rc.updatePersonItems(personen);
}
}
// ---------- Titel-Autofill ----------
private void updateFieldsForVeranstaltungstyp() {
String typ = veranstaltungstypBox.getValue();
if (typ == null) return;
if (typ.equals("fuehrung")) {
inhaltstypBox.setValue("bibliotheksnutzung");
inhaltstypBox.setDisable(true);
titelField.setText("bibliotheksführung");
if (dauerField.getText().isBlank()) dauerField.setText("45");
modusBox.setValue("praesenz");
teilnehmerTypBox.setValue("studierende");
} else if (typ.equals("schulung")) {
inhaltstypBox.setDisable(false);
teilnehmerTypBox.setValue("studierende");
if (inhaltstypBox.getValue() != null && !inhaltstypBox.getValue().isBlank()) {
titelField.setText("einführung in die " + inhaltstypBox.getValue());
} else {
titelField.clear();
}
}
validateForm();
}
private void updateTitleFromInhaltstyp() {
String inhalt = inhaltstypBox.getValue();
if (inhalt != null && !inhalt.isBlank()) {
if (inhalt.equalsIgnoreCase("sonstiges")) {
titelField.clear();
} else {
titelField.setText("einführung in die " + inhalt);
}
}
validateForm();
}
// ---------- Save ----------
private void savePlanned() {
try {
if (!validateForm()) return;
String veranstaltungstyp = veranstaltungstypBox.getValue();
String inhaltstyp = inhaltstypBox.getValue();
String titel = titelField.getText();
Integer dauer = parseIntSafe(dauerField.getText());
String modus = modusBox.getValue();
String teilnehmendeTyp = teilnehmerTypBox.getValue();
String kommentar = kommentarField.getText();
// Führung -> Inhalt sicherstellen
if ("fuehrung".equals(veranstaltungstyp)) {
inhaltstyp = "bibliotheksnutzung";
if (titel == null || titel.isBlank()) titel = "bibliotheksführung";
}
TrainingDAO dao = new TrainingDAO();
int count = 0;
for (RowControls rc : rows) {
if (!rc.container.isVisible()) break;
if (!rc.isFilled()) break;
LocalDate datum = rc.datePicker.getValue();
LocalTime start = rc.getStartTimeOrNull();
String person = rc.getPersonOrNull(); // <-- nutzt Editor-Text oder Auswahl
String datumStr = datum.toString();
String startStr = (start != null) ? start.format(TIME_FORMAT) : null;
dao.insertTraining(
datumStr,
startStr,
dauer,
modus,
null, // teilnehmende_anzahl -> immer NULL bei geplant
teilnehmendeTyp,
person,
"", // ausgefallen_grund -> leer
titel,
inhaltstyp,
veranstaltungstyp,
kommentar,
1,
null// geplant = 1
);
count++;
}
new Alert(Alert.AlertType.INFORMATION, count + " geplante Termine gespeichert.").showAndWait();
resetForm();
} catch (Exception ex) {
new Alert(Alert.AlertType.ERROR, "Fehler beim Speichern: " + ex.getMessage()).showAndWait();
ex.printStackTrace();
}
}
private void resetForm() {
veranstaltungstypBox.setValue(null);
inhaltstypBox.setDisable(false);
inhaltstypBox.setValue(null);
titelField.clear();
dauerField.clear();
modusBox.setValue(null);
teilnehmerTypBox.setValue(null);
kommentarField.clear();
for (int i = 0; i < rows.size(); i++) {
rows.get(i).reset();
setRowVisible(i, i == 0);
}
validateForm();
}
// ---------- Validation / Helpers ----------
private void applyIntegerFilter(TextField field) {
field.textProperty().addListener((obs, old, neu) -> {
if (!neu.matches("\\d*")) field.setText(neu.replaceAll("[^\\d]", ""));
});
}
private Integer parseIntSafe(String text) {
if (text == null || text.isBlank()) return null;
try { return Integer.parseInt(text.trim()); } catch (NumberFormatException e) { return null; }
}
/** Gesamtvalidierung: Top-Felder + mindestens eine befüllte Zeile */
private boolean validateForm() {
boolean valid = true;
valid &= mark(veranstaltungstypBox, veranstaltungstypBox.getValue() != null);
valid &= mark(inhaltstypBox, inhaltstypBox.getValue() != null && !inhaltstypBox.getValue().isBlank());
valid &= mark(titelField, !titelField.getText().isBlank());
valid &= mark(dauerField, !dauerField.getText().isBlank());
valid &= mark(modusBox, modusBox.getValue() != null);
valid &= mark(teilnehmerTypBox, teilnehmerTypBox.getValue() != null);
boolean anyRow = rows.get(0).isFilled();
speichernButton.setDisable(!(valid && anyRow));
return valid && anyRow;
}
private boolean mark(Control c, boolean ok) {
if (ok) c.setStyle("");
else c.setStyle("-fx-border-color: red; -fx-border-width: 2;");
return ok;
}
// ---------- RowControls (eine Terminzeile) ----------
private final class RowControls {
final HBox container = new HBox(8);
final Label nrLabel = new Label();
final DatePicker datePicker = new DatePicker();
final TimeSpinner timeSpinner = new TimeSpinner();
final ComboBox<String> personBox = new ComboBox<>();
RowControls(int nr, List<String> personen) {
nrLabel.setText(String.format("#%02d", nr));
// Personen-Combo: jetzt EDITIERBAR (Freitext)
personBox.getItems().setAll(personen);
personBox.setEditable(true);
personBox.setPromptText("Kürzel (abc123)");
// Standardwert = aktueller Nutzer
String username = System.getProperty("user.name");
String displayName = model.PersonMapping.toName(username);
personBox.setValue(displayName);
personBox.getEditor().setText(displayName);
timeSpinner.getValueFactory().setValue(LocalTime.of(10, 0));
container.getChildren().addAll(
nrLabel,
new Label("Datum:"), datePicker,
new Label("Startzeit:"), timeSpinner,
new Label("Durchführende Person:"), personBox
);
// Editor-Commit: ENTER/Fokusverlust -> Text ggf. in Items aufnehmen
personBox.getEditor().setOnAction(e -> commitPersonEditor());
personBox.focusedProperty().addListener((o, was, isNow) -> {
if (!isNow) commitPersonEditor();
});
// Reveal-Mechanik
ChangeListener<Object> reveal = (obs, o, n) -> {
if (isFilled()) {
maybeRevealNext(nr - 1);
}
validateForm();
};
datePicker.valueProperty().addListener(reveal);
timeSpinner.getEditor().textProperty().addListener(reveal);
}
/** Editor-Text in Items übernehmen, falls neu */
/** Editor-Text in Items übernehmen, falls neu */
private void commitPersonEditor() {
String text = personBox.getEditor().getText();
if (text == null) return;
String val = text.trim();
// Leere Eingabe -> leer setzen
if (val.isEmpty()) {
personBox.setValue("");
return;
}
// Wenn bereits in der Liste -> akzeptieren
if (personBox.getItems().contains(val)) {
personBox.setValue(val);
return;
}
// Wenn neu -> prüfen, ob Kürzel-Format stimmt
if (!val.matches("^[a-z]{3}\\d{3}$")) {
new Alert(Alert.AlertType.WARNING,
"Ungültiges Kürzel.\nBitte geben Sie ein Kürzel im Format 'abc123' ein (3 Buchstaben + 3 Ziffern).")
.showAndWait();
personBox.getEditor().setText("");
personBox.setValue("");
return;
}
// Wenn gültig -> hinzufügen
personBox.getItems().add(val);
personBox.setValue(val);
}
/** Von außen aufrufbar, um Items zu erneuern und aktuelle Eingabe zu bewahren */
void updatePersonItems(List<String> newItems) {
// aktuellen Wert (aus Editor oder Auswahl) sichern
String current = getPersonRaw();
personBox.getItems().setAll(newItems);
if (current != null && !current.isBlank() && !personBox.getItems().contains(current)) {
personBox.getItems().add(current);
}
// Wert zurücksetzen (Editor und Selection)
if (current == null) {
personBox.setValue("");
personBox.getEditor().setText("");
} else {
personBox.setValue(current);
personBox.getEditor().setText(current);
}
}
/** true, wenn Datum gesetzt und Startzeit parsebar; Person optional */
boolean isFilled() {
if (datePicker.getValue() == null) return false;
String t = timeSpinner.getEditor().getText();
if (t == null || t.isBlank()) return false;
try { LocalTime.parse(t, TIME_FORMAT); } catch (DateTimeParseException e) { return false; }
return true;
}
LocalTime getStartTimeOrNull() {
try { return LocalTime.parse(timeSpinner.getEditor().getText(), TIME_FORMAT); }
catch (Exception e) { return null; }
}
/** Liefert den aktuellen Personenwert (Editor-Text hat Vorrang), oder null/leer -> null */
String getPersonOrNull() {
String raw = getPersonRaw();
if (raw == null) return null;
String v = raw.trim();
return v.isEmpty() ? null : v;
}
private String getPersonRaw() {
String editor = personBox.getEditor().getText();
if (editor != null && !editor.trim().isEmpty()) return editor.trim();
String selected = personBox.getValue();
return selected != null ? selected : null;
}
void reset() {
datePicker.setValue(null);
timeSpinner.getValueFactory().setValue(LocalTime.of(10, 0));
personBox.setValue("");
personBox.getEditor().setText("");
}
}
}

View File

@@ -0,0 +1,539 @@
package ui;
import db.TrainingDAO;
import db.TrainingTypeDAO;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.*;
import model.DataModels;
import model.PersonMapping;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Übersicht über alle gespeicherten Trainingsdatensätze.
* - Filterzeile oben
* - Tabs pro Jahr mit Summen (Teilnehmende / Termine)
* - Export-Button pro Jahr (Tabulator-getrennt in Zwischenablage)
* - Doppelklick auf Zeile -> Edit-Dialog (mit Rechteprüfung)
* - Zeilen mit geplant=1 sind dezent orange hinterlegt
*/
public class TrainingOverviewUI extends BorderPane {
private final TabPane yearTabs = new TabPane();
// --- Filter-Felder ---
private final TextField titelFilter = new TextField();
private final ComboBox<String> typFilter = new ComboBox<>();
private final ComboBox<String> durchfuehrendeFilter = new ComboBox<>();
private final ComboBox<String> inhaltstypFilter = new ComboBox<>();
private final ComboBox<String> modusFilter = new ComboBox<>();
private final ComboBox<String> teilnehmendeTypFilter = new ComboBox<>();
private final CheckBox ausgefallenCheck = new CheckBox("Nur ausgefallene");
private final CheckBox ownOnlyCheck = new CheckBox("Nur eigene anzeigen");
private final Button filterButton = new Button("Filtern");
private final Button resetButton = new Button("Zurücksetzen");
public TrainingOverviewUI() {
setPadding(new Insets(15));
Label heading = new Label("Schulungs- und Führungsübersicht");
heading.setStyle("-fx-font-size: 18; -fx-font-weight: bold;");
// --- Filterzeile ---
GridPane filterGrid = new GridPane();
filterGrid.setHgap(10);
filterGrid.setVgap(5);
filterGrid.setPadding(new Insets(10, 0, 10, 0));
int col = 0;
filterGrid.add(new Label("Titel:"), col++, 0);
filterGrid.add(titelFilter, col++, 0);
filterGrid.add(new Label("Typ:"), col++, 0);
typFilter.getItems().addAll(Stream.concat(
Stream.of(""),
DataModels.getVeranstaltungstypen().stream()
).toList());
filterGrid.add(typFilter, col++, 0);
filterGrid.add(new Label("Thema:"), col++, 0);
inhaltstypFilter.setPromptText("alle");
fillInhaltstypFilter();
filterGrid.add(inhaltstypFilter, col++, 0);
filterGrid.add(new Label("Modus:"), col++, 0);
modusFilter.getItems().addAll(DataModels.getModi());
filterGrid.add(modusFilter, col++, 0);
filterGrid.add(new Label("Teilnehmende:"), col++, 0);
teilnehmendeTypFilter.getItems().addAll(DataModels.getTeilnehmendeTypen());
filterGrid.add(teilnehmendeTypFilter, col++, 0);
// Durchführende (Mapping)
filterGrid.add(new Label("Durchführende:"), col++, 0);
List<String> personenRaw = new TrainingDAO().getAllDurchfuehrendePersonen();
List<String> personenMapped = new ArrayList<>();
personenMapped.add(""); // "alle"
for (String p : personenRaw) {
if (p == null || p.isBlank()) continue;
String display = model.PersonMapping.toName(p);
if (!personenMapped.contains(display)) personenMapped.add(display);
}
Collections.sort(personenMapped);
durchfuehrendeFilter.getItems().setAll(personenMapped);
filterGrid.add(durchfuehrendeFilter, col++, 0);
filterGrid.add(ausgefallenCheck, col++, 0);
filterGrid.add(ownOnlyCheck, col++, 0);
HBox buttonBox = new HBox(10, filterButton, resetButton);
filterGrid.add(buttonBox, col, 0);
VBox topBox = new VBox(10, heading, filterGrid);
topBox.setPadding(new Insets(10, 0, 10, 0));
setTop(topBox);
yearTabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
setCenter(yearTabs);
filterButton.setOnAction(e -> loadData());
resetButton.setOnAction(e -> resetFilters());
// ENTER -> Filtern: TextField & ComboBox feuern Action bei ENTER,
// CheckBox nicht: daher dort explizit filtern.
titelFilter.setOnAction(e -> filterButton.fire());
typFilter.setOnAction(e -> filterButton.fire());
inhaltstypFilter.setOnAction(e -> filterButton.fire());
modusFilter.setOnAction(e -> filterButton.fire());
teilnehmendeTypFilter.setOnAction(e -> filterButton.fire());
durchfuehrendeFilter.setOnAction(e -> filterButton.fire());
ausgefallenCheck.setOnAction(e -> filterButton.fire());
ownOnlyCheck.setOnAction(e -> filterButton.fire());
loadData();
}
// --- DB-Ladevorgänge ------------------------------------------------------
private void fillInhaltstypFilter() {
TrainingTypeDAO dao = new TrainingTypeDAO(util.Config.getDbPath());
List<String> types = dao.getAllTrainingTypes();
inhaltstypFilter.getItems().add(""); // für "alle"
inhaltstypFilter.getItems().addAll(types);
}
/**
* Lädt die Daten aus der DB, filtert sie und zeigt sie in Tabs nach Jahr an.
* Bewahrt (wenn möglich) den zuvor ausgewählten Jahr-Tab.
*/
private void loadData() {
// aktuellen Jahr-Tab merken (Jahreszahl vom Tab-Titel extrahieren)
String selectedYear = null;
Tab sel = yearTabs.getSelectionModel().getSelectedItem();
if (sel != null && sel.getText() != null) {
String t = sel.getText();
int sp = t.indexOf(' ');
selectedYear = sp > 0 ? t.substring(0, sp) : t;
}
yearTabs.getTabs().clear();
TrainingDAO dao = new TrainingDAO();
String selectedDurchfuehrende = durchfuehrendeFilter.getValue();
if (selectedDurchfuehrende != null && !selectedDurchfuehrende.isBlank()) {
selectedDurchfuehrende = model.PersonMapping.toKuerzel(selectedDurchfuehrende);
}
List<Map<String, Object>> trainings = dao.getTrainingsWithFilters(
titelFilter.getText(),
typFilter.getValue(),
inhaltstypFilter.getValue(),
modusFilter.getValue(),
teilnehmendeTypFilter.getValue(),
selectedDurchfuehrende,
ausgefallenCheck.isSelected()
);
// --- zusätzlicher Filter: Nur eigene anzeigen ---
if (ownOnlyCheck.isSelected()) {
final String login = System.getProperty("user.name");
trainings = trainings.stream()
.filter(t -> {
Object v = t.get("durchfuehrende_person");
return v != null && login.equalsIgnoreCase(String.valueOf(v));
})
.collect(Collectors.toList());
}
if (trainings.isEmpty()) {
yearTabs.getTabs().add(new Tab("Keine Daten", new Label("Keine passenden Datensätze gefunden.")));
return;
}
// Gruppiere Datensätze nach Jahr
Map<String, List<Map<String, Object>>> byYear = trainings.stream()
.filter(t -> t.get("datum") != null)
.collect(Collectors.groupingBy(t -> t.get("datum").toString().substring(0, 4)));
// Jahre absteigend sortieren (aktuelles Jahr links)
List<String> years = new ArrayList<>(byYear.keySet());
years.sort(Comparator.comparingInt((String y) -> Integer.parseInt(y)).reversed());
for (String year : years) {
List<Map<String, Object>> rows = byYear.get(year);
// --- Nur aktive (nicht ausgefallene) Zeilen berücksichtigen ---
List<Map<String, Object>> aktive = rows.stream()
.filter(r -> {
Object g = r.get("ausgefallen_grund");
return g == null || String.valueOf(g).isBlank();
})
.toList();
// --- Statistiken ---
int totalTeilnehmer = aktive.stream()
.mapToInt(r -> {
Object val = r.get("teilnehmende_anzahl");
try { return Integer.parseInt(String.valueOf(val)); } catch (Exception e) { return 0; }
})
.sum();
int aktiveTermine = aktive.size();
int aktiveDauerMinuten = aktive.stream()
.mapToInt(r -> {
Object v = r.get("dauer_minuten");
try { return Integer.parseInt(String.valueOf(v)); } catch (Exception e) { return 0; }
})
.sum();
double aktiveDauerStunden = Math.round((aktiveDauerMinuten / 60.0) * 10.0) / 10.0;
long countOnline = aktive.stream()
.filter(r -> "online".equalsIgnoreCase(String.valueOf(r.get("modus"))))
.count();
long countPraesenz = aktive.stream()
.filter(r -> "praesenz".equalsIgnoreCase(String.valueOf(r.get("modus"))))
.count();
long countSelbstlern = aktive.stream()
.filter(r -> "selbstlerneinheit".equalsIgnoreCase(String.valueOf(r.get("modus"))))
.count();
int sumZugriffeSelbstlern = aktive.stream()
.filter(r -> "selbstlerneinheit".equalsIgnoreCase(String.valueOf(r.get("modus"))))
.mapToInt(r -> {
Object val = r.get("teilnehmende_anzahl");
try { return Integer.parseInt(String.valueOf(val)); } catch (Exception e) { return 0; }
})
.sum();
TableView<Map<String, Object>> table = createTable(rows);
Button exportButton = new Button("In Zwischenablage (Excel)");
exportButton.setOnAction(e -> copyTableToClipboard(table));
Label stats = new Label(String.format(
"DBS-Werte für aktuelle Auswahl [BITTE AKTUELLE FILTER BEACHTEN!]: Teilnehmende: %d | Termine: %d | Dauer (Std): %.1f | Präsenz: %d | Online: %d | Selbstlern: %d | Zugriffe (Selbstlern): %d",
totalTeilnehmer, aktiveTermine, aktiveDauerStunden, countPraesenz, countOnline, countSelbstlern, sumZugriffeSelbstlern
));
stats.setStyle("-fx-font-size: 12; -fx-text-fill: #444;");
ScrollPane scroll = new ScrollPane(table);
scroll.setFitToWidth(true);
scroll.setFitToHeight(true);
VBox content = new VBox(10, exportButton, stats, scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
content.setPadding(new Insets(10));
// --- Tab-Titel: nur durchgeführte Termine + Teilnehmende anzeigen ---
String tabTitle = String.format("%s (%d / %d)", year, totalTeilnehmer, aktiveTermine);
Tab yearTab = new Tab(tabTitle, content);
yearTabs.getTabs().add(yearTab);
}
// nach dem Neuaufbau versuchen, den vorher gewählten Tab wiederherzustellen
if (selectedYear != null) {
for (Tab t : yearTabs.getTabs()) {
if (t.getText() != null && t.getText().startsWith(selectedYear + " ")) {
yearTabs.getSelectionModel().select(t);
break;
}
}
} else if (!yearTabs.getTabs().isEmpty()) {
yearTabs.getSelectionModel().select(0);
}
}
private void resetFilters() {
titelFilter.clear();
typFilter.setValue("");
inhaltstypFilter.setValue("");
modusFilter.setValue("");
teilnehmendeTypFilter.setValue("");
durchfuehrendeFilter.setValue("");
ausgefallenCheck.setSelected(false);
ownOnlyCheck.setSelected(false);
loadData();
}
// --- Tabellenaufbau -------------------------------------------------------
private TableView<Map<String, Object>> createTable(List<Map<String, Object>> data) {
TableView<Map<String, Object>> table = new TableView<>();
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_SUBSEQUENT_COLUMNS);
Map<String, String> headerToKey = new LinkedHashMap<>(model.DataModels.getCsvToSqliteMapping());
for (Map.Entry<String, String> e : headerToKey.entrySet()) {
String header = e.getKey();
String col = e.getValue();
TableColumn<Map<String, Object>, String> column = new TableColumn<>(header);
column.setCellValueFactory(cd -> {
Map<String, Object> r = cd.getValue();
Object o = (r != null) ? r.get(col) : null;
String value = "";
if (o != null) {
if ("durchfuehrende_person".equals(col)) {
String mapped = model.PersonMapping.toName(o.toString());
value = (mapped != null && !mapped.isBlank()) ? mapped : o.toString();
}
else if ("evaluation".equals(col)) {
// --- Hide evaluation if not own dataset ---
String login = System.getProperty("user.name");
String loginKuerzel = model.PersonMapping.normalizeLogin(login);
Object personObj = r.get("durchfuehrende_person");
String personKuerzel = (personObj != null)
? model.PersonMapping.normalizeLogin(personObj.toString())
: null;
boolean isOwn = personKuerzel != null && loginKuerzel.equalsIgnoreCase(personKuerzel);
if (isOwn || model.PersonMapping.isAdmin(login)) {
value = o.toString(); // show
} else {
value = ""; // hide
}
}
else {
value = o.toString();
}
}
return new javafx.beans.property.ReadOnlyStringWrapper(value);
});
// ID-Spalte einfach rendern
if (col.equals("id")) {
column.setCellFactory(colParam -> new TableCell<>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setStyle("");
return;
}
setText(item);
}
});
}
// Spaltenbreiten
double weight = switch (col) {
case "id" -> 0.5;
case "datum" -> 0.8;
case "startzeit" -> 0.6;
case "titel" -> 2.0;
case "dauer_minuten" -> 0.6;
case "inhaltstyp" -> 1.0;
case "modus" -> 0.7;
case "teilnehmende_typ" -> 1.0;
case "teilnehmende_anzahl" -> 0.7;
case "veranstaltungstyp" -> 0.8;
case "durchfuehrende_person" -> 1.0;
case "ausgefallen_grund" -> 0.8;
case "kommentar" -> 2.0;
default -> 1.0;
};
column.setUserData(weight);
table.getColumns().add(column);
}
// Dynamische Spaltenbreiten
table.widthProperty().addListener((obs, oldW, newW) -> {
double totalWeight = table.getColumns().stream()
.mapToDouble(c -> (double) c.getUserData())
.sum();
double available = newW.doubleValue() - 20;
for (TableColumn<Map<String, Object>, ?> c : table.getColumns()) {
double w = (double) c.getUserData() / totalWeight * available;
c.setPrefWidth(w);
}
});
ObservableList<Map<String, Object>> observableData = FXCollections.observableArrayList(data);
table.setItems(observableData);
// --- Zeilenaufbau ---
table.setRowFactory(tv -> {
TableRow<Map<String, Object>> r = new TableRow<>();
// Doppelklick -> Edit-Dialog mit Rechteprüfung
r.setOnMouseClicked(ev -> {
if (ev.getClickCount() == 2 && !r.isEmpty()) {
Map<String, Object> row = r.getItem();
if (!canEdit(row)) {
new Alert(Alert.AlertType.INFORMATION,
"Keine Berechtigung zum Bearbeiten.").showAndWait();
return;
}
List<String> modi = model.DataModels.getModi();
List<String> teilnTypen = model.DataModels.getTeilnehmendeTypen();
List<String> veranstaltungstypen = model.DataModels.getVeranstaltungstypen();
List<String> inhaltstypen = new db.TrainingTypeDAO(util.Config.getDbPath()).getAllTrainingTypes();
List<String> personen = new db.TrainingDAO().getAllDurchfuehrendePersonen();
ui.TrainingEditDialog dlg = new ui.TrainingEditDialog(
row, modi, teilnTypen, veranstaltungstypen, inhaltstypen, personen
);
Optional<Boolean> ok = dlg.showAndWait();
if (ok.isPresent() && ok.get()) {
int idx = table.getItems().indexOf(row);
if (idx >= 0) {
Map<String, Object> updated = new LinkedHashMap<>(row);
table.getItems().set(idx, updated);
}
table.refresh();
}
}
});
// Farb-Logik
r.itemProperty().addListener((obs, oldItem, newItem) -> updateRowStyle(r, newItem));
r.selectedProperty().addListener((obs, oldSel, newSel) -> updateRowStyle(r, r.getItem()));
return r;
});
table.refresh();
return table;
}
// ------------------------------------------------------------
// Zeilenstil-Logik
// ------------------------------------------------------------
private void updateRowStyle(TableRow<Map<String, Object>> r, Map<String, Object> newItem) {
if (newItem == null) {
r.setStyle("");
return;
}
Object dateObj = newItem.get("datum");
Object teiln = newItem.get("teilnehmende_anzahl");
Object ausgefallen = newItem.get("ausgefallen_grund");
boolean noParticipants = (teiln == null || teiln.toString().isBlank() || teiln.toString().equals("0"));
boolean notCancelled = (ausgefallen == null || ausgefallen.toString().isBlank());
String baseStyle = "";
if (noParticipants && notCancelled && dateObj != null) {
try {
java.time.LocalDate date = java.time.LocalDate.parse(dateObj.toString());
java.time.LocalDate today = java.time.LocalDate.now();
if (date.isAfter(today)) {
baseStyle = "-fx-background-color: #FFE5CC; -fx-font-weight: bold;";
} else if (date.isBefore(today)) {
baseStyle = "-fx-background-color: #FFCCCC; -fx-font-weight: bold;";
}
} catch (Exception ignored) {}
}
// Bei Selektion standardmäßig blau
if (r.isSelected()) {
r.setStyle("-fx-background-color: -fx-focus-color; -fx-text-fill: white; -fx-font-weight: bold;");
} else {
r.setStyle(baseStyle);
}
}
// ------------------------------------------------------------
// Rechteprüfung
// ------------------------------------------------------------
private boolean canEdit(Map<String, Object> row) {
String login = System.getProperty("user.name");
if (model.PersonMapping.isAdmin(login)) return true;
Object p = row.get("durchfuehrende_person");
return p != null && model.PersonMapping.normalizeLogin(login)
.equalsIgnoreCase(String.valueOf(p));
}
public void reloadAllData() {
loadData();
}
private boolean isPlanned(Map<String, Object> row) {
Object v = row.get("geplant");
if (v == null) return false;
try {
if (v instanceof Number n) return n.intValue() == 1;
return Integer.parseInt(String.valueOf(v)) == 1;
} catch (Exception e) {
return false;
}
}
// --- Exportfunktion -------------------------------------------------------
private void copyTableToClipboard(TableView<Map<String, Object>> table) {
if (table.getItems().isEmpty()) {
new Alert(Alert.AlertType.INFORMATION, "Keine Daten zum Kopieren vorhanden.").showAndWait();
return;
}
StringBuilder sb = new StringBuilder();
// Mapping zwischen sichtbaren Headern und internen Keys
Map<String, String> headerToKey = model.DataModels.getCsvToSqliteMapping();
// Überschriften
List<String> headers = table.getColumns().stream()
.map(TableColumn::getText)
.toList();
sb.append(String.join("\t", headers)).append("\n");
// Zeilen exportieren
for (Map<String, Object> row : table.getItems()) {
List<String> cells = new ArrayList<>();
for (String header : headers) {
String key = headerToKey.get(header);
Object v = key != null ? row.get(key) : null;
cells.add(v != null ? v.toString() : "");
}
sb.append(String.join("\t", cells)).append("\n");
}
final javafx.scene.input.ClipboardContent content = new javafx.scene.input.ClipboardContent();
content.putString(sb.toString());
javafx.scene.input.Clipboard.getSystemClipboard().setContent(content);
new Alert(Alert.AlertType.INFORMATION, "Tabelleninhalt in Zwischenablage kopiert.").showAndWait();
}
}

105
src/util/Config.java Normal file
View File

@@ -0,0 +1,105 @@
package util;
import java.io.*;
import java.nio.file.*;
import java.util.Properties;
/**
* Globale Konfiguration des Schulungsstatistik-Tools.
* - Erstellt automatisch ~/.schulungsstatistik/config.properties
* - Lädt vorhandene Werte oder setzt Standardwerte
* - Speichert Änderungen automatisch
*/
public class Config {
private static final Properties props = new Properties();
// Standardpfad für die Datenbank
private static final String dbPath_default = "Y:\\Gruppen\\Bibliothek-Oeffentliches\\14. Information\\14.1 Informations- und Schulungsstatistik\\system\\schulungsstatistik.db";
private static String dbPath = dbPath_default;
// Speicherort der Config-Datei
private static final Path CONFIG_DIR =
Paths.get(System.getProperty("user.home"), ".schulungsstatistik");
private static final Path CONFIG_FILE = CONFIG_DIR.resolve("config.properties");
// Wird beim Laden der Klasse ausgeführt
static {
load();
}
/** Setzt den Pfad zur Datenbank und speichert ihn sofort */
public static void setDbPath(String path) {
if (path != null && !path.isBlank()) {
dbPath = path;
props.setProperty("db.path", path);
save();
}
}
/** Wird von der Config-UI verwendet, um neuen Pfad zu speichern */
public static void saveDbPathFromUI(String newPath) {
if (newPath == null || newPath.isBlank()) return;
setDbPath(newPath);
System.out.println("Datenbankpfad aktualisiert: " + newPath);
}
public static String getDbPath() {
if (dbPath == null || dbPath.isBlank() || !Files.exists(Paths.get(dbPath))) {
System.err.println("Warnung: Ungültiger Datenbankpfad, Standardwert wird verwendet.");
dbPath = dbPath_default;
props.setProperty("db.path", dbPath_default);
save();
}
return dbPath;
}
public static void load() {
try {
if (!Files.exists(CONFIG_DIR)) {
Files.createDirectories(CONFIG_DIR);
}
if (Files.exists(CONFIG_FILE)) {
try (InputStream in = Files.newInputStream(CONFIG_FILE)) {
props.load(in);
String loaded = props.getProperty("db.path");
if (loaded == null || loaded.isBlank() || !Files.exists(Paths.get(loaded))) {
dbPath = dbPath_default;
props.setProperty("db.path", dbPath_default);
save();
} else {
dbPath = loaded;
}
}
} else {
dbPath = dbPath_default;
props.setProperty("db.path", dbPath_default);
save();
}
} catch (IOException e) {
System.err.println("Fehler beim Laden der Konfiguration: " + e.getMessage());
dbPath = dbPath_default; // Fallback
}
}
/** Speichert aktuelle Werte in die Config-Datei */
public static void save() {
try {
if (!Files.exists(CONFIG_DIR)) {
Files.createDirectories(CONFIG_DIR);
}
try (OutputStream out = Files.newOutputStream(CONFIG_FILE)) {
props.store(out, "Schulungsstatistik Tool Konfiguration");
}
} catch (IOException e) {
System.err.println("Fehler beim Speichern der Konfiguration: " + e.getMessage());
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
target/lib/opencsv-5.9.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
artifactId=schulungsstatistiktool
groupId=schulungsstatistiktool
version=0.1.0-SNAPSHOT

View File

@@ -0,0 +1,26 @@
ui\ConfigUI$UserEntry.class
ui\TimeSpinner$1.class
model\PersonMapping.class
ui\TrainingFormUIEnterEvent.class
ui\TrainingFormUIPlanner.class
ui\TimeSpinner.class
db\DatabaseBackupRunner.class
db\TrainingDAO.class
module-info.class
ui\EvaluationUI.class
ui\TrainingFormUIPlanner$RowControls.class
model\DataModels.class
main\Start.class
ui\TrainingFormUIEnterEvent$TypeRule.class
ui\ConfigUI.class
db\TrainingTypeDAO.class
ui\TimeSpinner$2.class
ui\TrainingOverviewUI$1.class
db\UserDAO.class
ui\TrainingEditDialog$1.class
ui\TrainingEditDialog.class
ui\EvaluationPopupUI.class
ui\TrainingOverviewUI.class
ui\TrainingFormUI.class
util\Config.class
ui\MainUI.class

View File

@@ -0,0 +1,19 @@
C:\java\workspace\schulungsstatistiktool\src\ui\EvaluationUI.java
C:\java\workspace\schulungsstatistiktool\src\ui\TrainingFormUIEnterEvent.java
C:\java\workspace\schulungsstatistiktool\src\ui\MainUI.java
C:\java\workspace\schulungsstatistiktool\src\ui\EvaluationPopupUI.java
C:\java\workspace\schulungsstatistiktool\src\ui\ConfigUI.java
C:\java\workspace\schulungsstatistiktool\src\model\DataModels.java
C:\java\workspace\schulungsstatistiktool\src\module-info.java
C:\java\workspace\schulungsstatistiktool\src\ui\TrainingFormUI.java
C:\java\workspace\schulungsstatistiktool\src\db\UserDAO.java
C:\java\workspace\schulungsstatistiktool\src\ui\TrainingFormUIPlanner.java
C:\java\workspace\schulungsstatistiktool\src\util\Config.java
C:\java\workspace\schulungsstatistiktool\src\db\DatabaseBackupRunner.java
C:\java\workspace\schulungsstatistiktool\src\db\TrainingDAO.java
C:\java\workspace\schulungsstatistiktool\src\model\PersonMapping.java
C:\java\workspace\schulungsstatistiktool\src\ui\TimeSpinner.java
C:\java\workspace\schulungsstatistiktool\src\main\Start.java
C:\java\workspace\schulungsstatistiktool\src\db\TrainingTypeDAO.java
C:\java\workspace\schulungsstatistiktool\src\ui\TrainingEditDialog.java
C:\java\workspace\schulungsstatistiktool\src\ui\TrainingOverviewUI.java

Binary file not shown.