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