init
This commit is contained in:
20
.classpath
Normal file
20
.classpath
Normal 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
23
.project
Normal 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>
|
||||||
15
.settings/org.eclipse.jdt.core.prefs
Normal file
15
.settings/org.eclipse.jdt.core.prefs
Normal 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
|
||||||
4
.settings/org.eclipse.m2e.core.prefs
Normal file
4
.settings/org.eclipse.m2e.core.prefs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
activeProfiles=
|
||||||
|
eclipse.preferences.version=1
|
||||||
|
resolveWorkspaceProjects=true
|
||||||
|
version=1
|
||||||
BIN
bin/main/Start.class
Normal file
BIN
bin/main/Start.class
Normal file
Binary file not shown.
BIN
bin/module-info.class
Normal file
BIN
bin/module-info.class
Normal file
Binary file not shown.
170
pom.xml
Normal file
170
pom.xml
Normal 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>
|
||||||
68
src/db/DatabaseBackupRunner.java
Normal file
68
src/db/DatabaseBackupRunner.java
Normal 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
361
src/db/TrainingDAO.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
41
src/db/TrainingTypeDAO.java
Normal file
41
src/db/TrainingTypeDAO.java
Normal 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
115
src/db/UserDAO.java
Normal 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
15
src/main/Start.java
Normal 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
64
src/model/DataModels.java
Normal 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 (1–6)", "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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
79
src/model/PersonMapping.java
Normal file
79
src/model/PersonMapping.java
Normal 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
13
src/module-info.java
Normal 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
213
src/ui/ConfigUI.java
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
132
src/ui/EvaluationPopupUI.java
Normal file
132
src/ui/EvaluationPopupUI.java
Normal 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
343
src/ui/EvaluationUI.java
Normal 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
98
src/ui/MainUI.java
Normal 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
80
src/ui/TimeSpinner.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
332
src/ui/TrainingEditDialog.java
Normal file
332
src/ui/TrainingEditDialog.java
Normal 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 (1–6) mit optionaler Live-Evaluation ---
|
||||||
|
g.add(new Label("Bewertung (1–6)"), 0, r);
|
||||||
|
|
||||||
|
HBox evalBox = new HBox(10);
|
||||||
|
evalBox.setPadding(new Insets(0));
|
||||||
|
|
||||||
|
tfEvaluation.setPromptText("1–6");
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
32
src/ui/TrainingFormUI.java
Normal file
32
src/ui/TrainingFormUI.java
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
531
src/ui/TrainingFormUIEnterEvent.java
Normal file
531
src/ui/TrainingFormUIEnterEvent.java
Normal 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 (1–6):"), 0, row);
|
||||||
|
|
||||||
|
HBox evalBox = new HBox(10);
|
||||||
|
evalBox.setPadding(new Insets(0, 0, 0, 0));
|
||||||
|
|
||||||
|
evaluationField.setPromptText("1–6");
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
475
src/ui/TrainingFormUIPlanner.java
Normal file
475
src/ui/TrainingFormUIPlanner.java
Normal 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("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
539
src/ui/TrainingOverviewUI.java
Normal file
539
src/ui/TrainingOverviewUI.java
Normal 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
105
src/util/Config.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
BIN
target/classes/db/DatabaseBackupRunner.class
Normal file
BIN
target/classes/db/DatabaseBackupRunner.class
Normal file
Binary file not shown.
BIN
target/classes/db/TrainingDAO.class
Normal file
BIN
target/classes/db/TrainingDAO.class
Normal file
Binary file not shown.
BIN
target/classes/db/TrainingTypeDAO.class
Normal file
BIN
target/classes/db/TrainingTypeDAO.class
Normal file
Binary file not shown.
BIN
target/classes/db/UserDAO.class
Normal file
BIN
target/classes/db/UserDAO.class
Normal file
Binary file not shown.
BIN
target/classes/main/Start.class
Normal file
BIN
target/classes/main/Start.class
Normal file
Binary file not shown.
BIN
target/classes/model/DataModels.class
Normal file
BIN
target/classes/model/DataModels.class
Normal file
Binary file not shown.
BIN
target/classes/model/PersonMapping.class
Normal file
BIN
target/classes/model/PersonMapping.class
Normal file
Binary file not shown.
BIN
target/classes/module-info.class
Normal file
BIN
target/classes/module-info.class
Normal file
Binary file not shown.
BIN
target/classes/ui/ConfigUI$UserEntry.class
Normal file
BIN
target/classes/ui/ConfigUI$UserEntry.class
Normal file
Binary file not shown.
BIN
target/classes/ui/ConfigUI.class
Normal file
BIN
target/classes/ui/ConfigUI.class
Normal file
Binary file not shown.
BIN
target/classes/ui/EvaluationPopupUI.class
Normal file
BIN
target/classes/ui/EvaluationPopupUI.class
Normal file
Binary file not shown.
BIN
target/classes/ui/EvaluationUI.class
Normal file
BIN
target/classes/ui/EvaluationUI.class
Normal file
Binary file not shown.
BIN
target/classes/ui/MainUI.class
Normal file
BIN
target/classes/ui/MainUI.class
Normal file
Binary file not shown.
BIN
target/classes/ui/TimeSpinner$1.class
Normal file
BIN
target/classes/ui/TimeSpinner$1.class
Normal file
Binary file not shown.
BIN
target/classes/ui/TimeSpinner$2.class
Normal file
BIN
target/classes/ui/TimeSpinner$2.class
Normal file
Binary file not shown.
BIN
target/classes/ui/TimeSpinner.class
Normal file
BIN
target/classes/ui/TimeSpinner.class
Normal file
Binary file not shown.
BIN
target/classes/ui/TrainingEditDialog$1.class
Normal file
BIN
target/classes/ui/TrainingEditDialog$1.class
Normal file
Binary file not shown.
BIN
target/classes/ui/TrainingEditDialog.class
Normal file
BIN
target/classes/ui/TrainingEditDialog.class
Normal file
Binary file not shown.
BIN
target/classes/ui/TrainingFormUI.class
Normal file
BIN
target/classes/ui/TrainingFormUI.class
Normal file
Binary file not shown.
BIN
target/classes/ui/TrainingFormUIEnterEvent$TypeRule.class
Normal file
BIN
target/classes/ui/TrainingFormUIEnterEvent$TypeRule.class
Normal file
Binary file not shown.
BIN
target/classes/ui/TrainingFormUIEnterEvent.class
Normal file
BIN
target/classes/ui/TrainingFormUIEnterEvent.class
Normal file
Binary file not shown.
BIN
target/classes/ui/TrainingFormUIPlanner$RowControls.class
Normal file
BIN
target/classes/ui/TrainingFormUIPlanner$RowControls.class
Normal file
Binary file not shown.
BIN
target/classes/ui/TrainingFormUIPlanner.class
Normal file
BIN
target/classes/ui/TrainingFormUIPlanner.class
Normal file
Binary file not shown.
BIN
target/classes/ui/TrainingOverviewUI$1.class
Normal file
BIN
target/classes/ui/TrainingOverviewUI$1.class
Normal file
Binary file not shown.
BIN
target/classes/ui/TrainingOverviewUI.class
Normal file
BIN
target/classes/ui/TrainingOverviewUI.class
Normal file
Binary file not shown.
BIN
target/classes/util/Config.class
Normal file
BIN
target/classes/util/Config.class
Normal file
Binary file not shown.
BIN
target/lib/apiguardian-api-1.1.2.jar
Normal file
BIN
target/lib/apiguardian-api-1.1.2.jar
Normal file
Binary file not shown.
BIN
target/lib/commons-beanutils-1.9.4.jar
Normal file
BIN
target/lib/commons-beanutils-1.9.4.jar
Normal file
Binary file not shown.
BIN
target/lib/commons-collections-3.2.2.jar
Normal file
BIN
target/lib/commons-collections-3.2.2.jar
Normal file
Binary file not shown.
BIN
target/lib/commons-collections4-4.4.jar
Normal file
BIN
target/lib/commons-collections4-4.4.jar
Normal file
Binary file not shown.
BIN
target/lib/commons-io-2.16.1.jar
Normal file
BIN
target/lib/commons-io-2.16.1.jar
Normal file
Binary file not shown.
BIN
target/lib/commons-lang3-3.14.0.jar
Normal file
BIN
target/lib/commons-lang3-3.14.0.jar
Normal file
Binary file not shown.
BIN
target/lib/commons-logging-1.2.jar
Normal file
BIN
target/lib/commons-logging-1.2.jar
Normal file
Binary file not shown.
BIN
target/lib/commons-text-1.11.0.jar
Normal file
BIN
target/lib/commons-text-1.11.0.jar
Normal file
Binary file not shown.
BIN
target/lib/controlsfx-11.2.1.jar
Normal file
BIN
target/lib/controlsfx-11.2.1.jar
Normal file
Binary file not shown.
BIN
target/lib/jackson-annotations-2.17.2.jar
Normal file
BIN
target/lib/jackson-annotations-2.17.2.jar
Normal file
Binary file not shown.
BIN
target/lib/jackson-core-2.17.2.jar
Normal file
BIN
target/lib/jackson-core-2.17.2.jar
Normal file
Binary file not shown.
BIN
target/lib/jackson-databind-2.17.2.jar
Normal file
BIN
target/lib/jackson-databind-2.17.2.jar
Normal file
Binary file not shown.
BIN
target/lib/javafx-base-21.0.2-win.jar
Normal file
BIN
target/lib/javafx-base-21.0.2-win.jar
Normal file
Binary file not shown.
BIN
target/lib/javafx-base-21.0.2.jar
Normal file
BIN
target/lib/javafx-base-21.0.2.jar
Normal file
Binary file not shown.
BIN
target/lib/javafx-controls-21.0.2-win.jar
Normal file
BIN
target/lib/javafx-controls-21.0.2-win.jar
Normal file
Binary file not shown.
BIN
target/lib/javafx-controls-21.0.2.jar
Normal file
BIN
target/lib/javafx-controls-21.0.2.jar
Normal file
Binary file not shown.
BIN
target/lib/javafx-fxml-21.0.2-win.jar
Normal file
BIN
target/lib/javafx-fxml-21.0.2-win.jar
Normal file
Binary file not shown.
BIN
target/lib/javafx-fxml-21.0.2.jar
Normal file
BIN
target/lib/javafx-fxml-21.0.2.jar
Normal file
Binary file not shown.
BIN
target/lib/javafx-graphics-21.0.2-win.jar
Normal file
BIN
target/lib/javafx-graphics-21.0.2-win.jar
Normal file
Binary file not shown.
BIN
target/lib/javafx-graphics-21.0.2.jar
Normal file
BIN
target/lib/javafx-graphics-21.0.2.jar
Normal file
Binary file not shown.
BIN
target/lib/junit-jupiter-api-5.10.2.jar
Normal file
BIN
target/lib/junit-jupiter-api-5.10.2.jar
Normal file
Binary file not shown.
BIN
target/lib/junit-jupiter-engine-5.10.2.jar
Normal file
BIN
target/lib/junit-jupiter-engine-5.10.2.jar
Normal file
Binary file not shown.
BIN
target/lib/junit-platform-commons-1.10.2.jar
Normal file
BIN
target/lib/junit-platform-commons-1.10.2.jar
Normal file
Binary file not shown.
BIN
target/lib/junit-platform-engine-1.10.2.jar
Normal file
BIN
target/lib/junit-platform-engine-1.10.2.jar
Normal file
Binary file not shown.
BIN
target/lib/logback-classic-1.5.6.jar
Normal file
BIN
target/lib/logback-classic-1.5.6.jar
Normal file
Binary file not shown.
BIN
target/lib/logback-core-1.5.6.jar
Normal file
BIN
target/lib/logback-core-1.5.6.jar
Normal file
Binary file not shown.
BIN
target/lib/opencsv-5.9.jar
Normal file
BIN
target/lib/opencsv-5.9.jar
Normal file
Binary file not shown.
BIN
target/lib/opentest4j-1.3.0.jar
Normal file
BIN
target/lib/opentest4j-1.3.0.jar
Normal file
Binary file not shown.
BIN
target/lib/slf4j-api-2.0.13.jar
Normal file
BIN
target/lib/slf4j-api-2.0.13.jar
Normal file
Binary file not shown.
BIN
target/lib/sqlite-jdbc-3.46.0.0.jar
Normal file
BIN
target/lib/sqlite-jdbc-3.46.0.0.jar
Normal file
Binary file not shown.
3
target/maven-archiver/pom.properties
Normal file
3
target/maven-archiver/pom.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
artifactId=schulungsstatistiktool
|
||||||
|
groupId=schulungsstatistiktool
|
||||||
|
version=0.1.0-SNAPSHOT
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
BIN
target/schulungsstatistiktool-0.1.0-SNAPSHOT.jar
Normal file
BIN
target/schulungsstatistiktool-0.1.0-SNAPSHOT.jar
Normal file
Binary file not shown.
Reference in New Issue
Block a user