commit fa7c0195882064ec64265b1971e02e94b862a296 Author: Christian Berger Date: Mon Nov 24 09:15:38 2025 +0100 init diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..a0e398d --- /dev/null +++ b/.classpath @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..c775e38 --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + schulungsstatistiktool + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.jdt.core.javanature + + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..43c8d71 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -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 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/bin/main/Start.class b/bin/main/Start.class new file mode 100644 index 0000000..e97d96f Binary files /dev/null and b/bin/main/Start.class differ diff --git a/bin/module-info.class b/bin/module-info.class new file mode 100644 index 0000000..82b1ad3 Binary files /dev/null and b/bin/module-info.class differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9da8883 --- /dev/null +++ b/pom.xml @@ -0,0 +1,170 @@ + + 4.0.0 + + schulungsstatistiktool + schulungsstatistiktool + 0.1.0-SNAPSHOT + + + 17 + 17 + 21.0.2 + 2.0.13 + 1.5.6 + 2.17.2 + 3.14.0 + 2.16.1 + + + + src + + + resources + + **/*.fxml + **/*.properties + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 17 + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + true + lib/ + main.Start + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.6.0 + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/lib + + + + + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + schulungsstatistiktool.ui.MainApp + + + + + + + + + org.openjfx + javafx-controls + ${javafx.version} + + + org.openjfx + javafx-fxml + ${javafx.version} + + + + + org.controlsfx + controlsfx + 11.2.1 + + + + + org.xerial + sqlite-jdbc + 3.46.0.0 + + + + + com.opencsv + opencsv + 5.9 + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + + org.apache.commons + commons-lang3 + ${commons.lang.version} + + + commons-io + commons-io + ${commons.io.version} + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + + + org.junit.jupiter + junit-jupiter-api + 5.10.2 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.10.2 + test + + + diff --git a/src/db/DatabaseBackupRunner.java b/src/db/DatabaseBackupRunner.java new file mode 100644 index 0000000..7524749 --- /dev/null +++ b/src/db/DatabaseBackupRunner.java @@ -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(); + } + } +} diff --git a/src/db/TrainingDAO.java b/src/db/TrainingDAO.java new file mode 100644 index 0000000..3a193c1 --- /dev/null +++ b/src/db/TrainingDAO.java @@ -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> getAllTrainings() { + List> 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 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> getTrainingsWithFilters( + String titel, + String typ, + String inhaltstyp, + String modus, + String teilnehmendeTyp, + String durchfuehrendePerson, + boolean nurAusgefallene + ) { + List> result = new ArrayList<>(); + + StringBuilder sql = new StringBuilder("SELECT * FROM trainings WHERE 1=1"); + List 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 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 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 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 getAllDurchfuehrendePersonen() { + final String sql = """ + SELECT DISTINCT durchfuehrende_person + FROM trainings + WHERE durchfuehrende_person IS NOT NULL AND durchfuehrende_person <> '' + ORDER BY durchfuehrende_person + """; + List 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); + } + } + +} diff --git a/src/db/TrainingTypeDAO.java b/src/db/TrainingTypeDAO.java new file mode 100644 index 0000000..ea23982 --- /dev/null +++ b/src/db/TrainingTypeDAO.java @@ -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 getAllTrainingTypes() { + List 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; + } +} diff --git a/src/db/UserDAO.java b/src/db/UserDAO.java new file mode 100644 index 0000000..5af25bf --- /dev/null +++ b/src/db/UserDAO.java @@ -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 getAllUsers() { + List 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; + } + } +} \ No newline at end of file diff --git a/src/main/Start.java b/src/main/Start.java new file mode 100644 index 0000000..d9b5e5a --- /dev/null +++ b/src/main/Start.java @@ -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(); } +} diff --git a/src/model/DataModels.java b/src/model/DataModels.java new file mode 100644 index 0000000..576d781 --- /dev/null +++ b/src/model/DataModels.java @@ -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 getCsvToSqliteMapping() { + Map 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 getVeranstaltungstypen() { + return Arrays.asList("schulung", "fuehrung", "coffeelecture", "selbstlerneinheit"); + } + + /** Modus der Veranstaltung (Präsenz, Online, Selbstlerneinheit) */ + public static List getModi() { + return Arrays.asList("online", "praesenz"); + } + + /** Typ der Teilnehmenden */ + public static List getTeilnehmendeTypen() { + return Arrays.asList("studierende", "mitarbeitende", "externe", "sonstige", "gemischt"); + } + + /** Gründe für ausgefallene Veranstaltungen */ + public static List getAusgefallenGruende() { + return Arrays.asList( + "", + "keine anmeldung", + "weniger als 3 anmeldungen", + "nicht erschienen", + "sonstiger grund" + ); + } + +} diff --git a/src/model/PersonMapping.java b/src/model/PersonMapping.java new file mode 100644 index 0000000..9d67443 --- /dev/null +++ b/src/model/PersonMapping.java @@ -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 KUERZEL_TO_NAME = new LinkedHashMap<>(); + private static final List 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 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 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 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; + } +} \ No newline at end of file diff --git a/src/module-info.java b/src/module-info.java new file mode 100644 index 0000000..355ad75 --- /dev/null +++ b/src/module-info.java @@ -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; +} \ No newline at end of file diff --git a/src/ui/ConfigUI.java b/src/ui/ConfigUI.java new file mode 100644 index 0000000..e1c8194 --- /dev/null +++ b/src/ui/ConfigUI.java @@ -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 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 kuerzelCol = new TableColumn<>("Kürzel"); + kuerzelCol.setCellValueFactory(data -> data.getValue().kuerzelProperty()); + + TableColumn 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 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; } + } +} \ No newline at end of file diff --git a/src/ui/EvaluationPopupUI.java b/src/ui/EvaluationPopupUI.java new file mode 100644 index 0000000..6d72140 --- /dev/null +++ b/src/ui/EvaluationPopupUI.java @@ -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 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; + } +} diff --git a/src/ui/EvaluationUI.java b/src/ui/EvaluationUI.java new file mode 100644 index 0000000..08ee98a --- /dev/null +++ b/src/ui/EvaluationUI.java @@ -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> all = dao.getAllTrainings(); + + // --- Gruppieren nach Person (damit Admin Tabs bekommt) --- + Map>> 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> 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>> entry : byPerson.entrySet()) { + String kuerzel = entry.getKey(); + List> 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> 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> evalByType = new TreeMap<>(); + Set typesWithNoEval = new TreeSet<>(); + + for (Map 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 allTypes = new TreeSet<>(); + allTypes.addAll(evalByType.keySet()); + allTypes.addAll(typesWithNoEval); + + for (String type : allTypes) { + List 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> rowsForType = rows.stream() + .filter(r -> type.equals(Objects.toString(r.get("inhaltstyp"), ""))) + .collect(Collectors.toList()); + + LineChart 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 createTinyChart(List> rows) { + // Nur valide Bewertungen behalten (1..6) – Liste bleibt 1:1 zu vals ausgerichtet + // --- Sort chronologically: oldest → newest --- + List> 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 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 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 seriesRaw = new XYChart.Series<>(); + for (int i = 0; i < vals.size(); i++) { + double y = vals.get(i); + XYChart.Data 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 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 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 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 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; + } + + + + + + +} diff --git a/src/ui/MainUI.java b/src/ui/MainUI.java new file mode 100644 index 0000000..a281728 --- /dev/null +++ b/src/ui/MainUI.java @@ -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 + } +} diff --git a/src/ui/TimeSpinner.java b/src/ui/TimeSpinner.java new file mode 100644 index 0000000..a31ef30 --- /dev/null +++ b/src/ui/TimeSpinner.java @@ -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 { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + + public TimeSpinner() { + this(LocalTime.of(10, 0)); + } + + public TimeSpinner(LocalTime defaultTime) { + setEditable(true); + + SpinnerValueFactory 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)); + } +} diff --git a/src/ui/TrainingEditDialog.java b/src/ui/TrainingEditDialog.java new file mode 100644 index 0000000..ee5de60 --- /dev/null +++ b/src/ui/TrainingEditDialog.java @@ -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 { + + 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 cbModus = new ComboBox<>(); + private final TextField tfTeilnAnz = new TextField(); + private final ComboBox cbTeilnTyp = new ComboBox<>(); + private final ComboBox cbPerson = new ComboBox<>(); + private final ComboBox tfAusgefallen = new ComboBox<>(); + private final TextField tfTitel = new TextField(); + private final ComboBox cbInhalt = new ComboBox<>(); + private final ComboBox cbVeranst = new ComboBox<>(); + private final TextArea taKommentar = new TextArea(); + private final TextField tfEvaluation = new TextField(); + + public TrainingEditDialog(Map row, + List modi, + List teilnTypen, + List veranstaltungstypen, + List inhaltstypen, + List 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; } + } + + +} diff --git a/src/ui/TrainingFormUI.java b/src/ui/TrainingFormUI.java new file mode 100644 index 0000000..602f064 --- /dev/null +++ b/src/ui/TrainingFormUI.java @@ -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 + } +} diff --git a/src/ui/TrainingFormUIEnterEvent.java b/src/ui/TrainingFormUIEnterEvent.java new file mode 100644 index 0000000..8c195ff --- /dev/null +++ b/src/ui/TrainingFormUIEnterEvent.java @@ -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 veranstaltungstypBox = new ComboBox<>(); + private final ComboBox 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 modusBox = new ComboBox<>(); + private final TextField teilnehmerAnzahlField = new TextField(); + private final ComboBox teilnehmerTypBox = new ComboBox<>(); + //private final TextField durchfuehrendePersonField = new TextField(); + private final ComboBox durchfuehrendePersonBox = new ComboBox<>(); + private final ComboBox 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 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 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 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 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(); + } +} diff --git a/src/ui/TrainingFormUIPlanner.java b/src/ui/TrainingFormUIPlanner.java new file mode 100644 index 0000000..ddab837 --- /dev/null +++ b/src/ui/TrainingFormUIPlanner.java @@ -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 veranstaltungstypBox = new ComboBox<>(); + private final ComboBox inhaltstypBox = new ComboBox<>(); + private final TextField titelField = new TextField(); + private final TextField dauerField = new TextField(); + private final ComboBox modusBox = new ComboBox<>(); + private final ComboBox 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 rows = new ArrayList<>(MAX_ROWS); + + // Datenquellen (Personenliste aus DB) + private List 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 types = dao.getAllTrainingTypes(); + inhaltstypBox.getItems().setAll(types); + } + + private void reloadPersonen() { + try { + // 1. Hole alle Personen der letzten 12 Monate aus der DB + List 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 personBox = new ComboBox<>(); + + RowControls(int nr, List 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 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 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(""); + } + } +} diff --git a/src/ui/TrainingOverviewUI.java b/src/ui/TrainingOverviewUI.java new file mode 100644 index 0000000..c3876ad --- /dev/null +++ b/src/ui/TrainingOverviewUI.java @@ -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 typFilter = new ComboBox<>(); + private final ComboBox durchfuehrendeFilter = new ComboBox<>(); + private final ComboBox inhaltstypFilter = new ComboBox<>(); + private final ComboBox modusFilter = new ComboBox<>(); + private final ComboBox 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 personenRaw = new TrainingDAO().getAllDurchfuehrendePersonen(); + List 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 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> 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>> 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 years = new ArrayList<>(byYear.keySet()); + years.sort(Comparator.comparingInt((String y) -> Integer.parseInt(y)).reversed()); + + for (String year : years) { + List> rows = byYear.get(year); + + // --- Nur aktive (nicht ausgefallene) Zeilen berücksichtigen --- + List> 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> 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> createTable(List> data) { + TableView> table = new TableView<>(); + table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_SUBSEQUENT_COLUMNS); + + Map headerToKey = new LinkedHashMap<>(model.DataModels.getCsvToSqliteMapping()); + + for (Map.Entry e : headerToKey.entrySet()) { + String header = e.getKey(); + String col = e.getValue(); + TableColumn, String> column = new TableColumn<>(header); + column.setCellValueFactory(cd -> { + Map 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, ?> c : table.getColumns()) { + double w = (double) c.getUserData() / totalWeight * available; + c.setPrefWidth(w); + } + }); + + ObservableList> observableData = FXCollections.observableArrayList(data); + table.setItems(observableData); + + // --- Zeilenaufbau --- + table.setRowFactory(tv -> { + TableRow> r = new TableRow<>(); + + // Doppelklick -> Edit-Dialog mit Rechteprüfung + r.setOnMouseClicked(ev -> { + if (ev.getClickCount() == 2 && !r.isEmpty()) { + Map row = r.getItem(); + if (!canEdit(row)) { + new Alert(Alert.AlertType.INFORMATION, + "Keine Berechtigung zum Bearbeiten.").showAndWait(); + return; + } + + List modi = model.DataModels.getModi(); + List teilnTypen = model.DataModels.getTeilnehmendeTypen(); + List veranstaltungstypen = model.DataModels.getVeranstaltungstypen(); + List inhaltstypen = new db.TrainingTypeDAO(util.Config.getDbPath()).getAllTrainingTypes(); + List personen = new db.TrainingDAO().getAllDurchfuehrendePersonen(); + + ui.TrainingEditDialog dlg = new ui.TrainingEditDialog( + row, modi, teilnTypen, veranstaltungstypen, inhaltstypen, personen + ); + Optional ok = dlg.showAndWait(); + if (ok.isPresent() && ok.get()) { + int idx = table.getItems().indexOf(row); + if (idx >= 0) { + Map 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> r, Map 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 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 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> 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 headerToKey = model.DataModels.getCsvToSqliteMapping(); + + + // Überschriften + List headers = table.getColumns().stream() + .map(TableColumn::getText) + .toList(); + sb.append(String.join("\t", headers)).append("\n"); + + // Zeilen exportieren + for (Map row : table.getItems()) { + List 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(); + } + +} diff --git a/src/util/Config.java b/src/util/Config.java new file mode 100644 index 0000000..9146f9f --- /dev/null +++ b/src/util/Config.java @@ -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()); + } + } + + +} diff --git a/target/classes/db/DatabaseBackupRunner.class b/target/classes/db/DatabaseBackupRunner.class new file mode 100644 index 0000000..1190b7b Binary files /dev/null and b/target/classes/db/DatabaseBackupRunner.class differ diff --git a/target/classes/db/TrainingDAO.class b/target/classes/db/TrainingDAO.class new file mode 100644 index 0000000..1bd54c3 Binary files /dev/null and b/target/classes/db/TrainingDAO.class differ diff --git a/target/classes/db/TrainingTypeDAO.class b/target/classes/db/TrainingTypeDAO.class new file mode 100644 index 0000000..08b666d Binary files /dev/null and b/target/classes/db/TrainingTypeDAO.class differ diff --git a/target/classes/db/UserDAO.class b/target/classes/db/UserDAO.class new file mode 100644 index 0000000..2ccd33a Binary files /dev/null and b/target/classes/db/UserDAO.class differ diff --git a/target/classes/main/Start.class b/target/classes/main/Start.class new file mode 100644 index 0000000..27038b5 Binary files /dev/null and b/target/classes/main/Start.class differ diff --git a/target/classes/model/DataModels.class b/target/classes/model/DataModels.class new file mode 100644 index 0000000..b147634 Binary files /dev/null and b/target/classes/model/DataModels.class differ diff --git a/target/classes/model/PersonMapping.class b/target/classes/model/PersonMapping.class new file mode 100644 index 0000000..ab9ede9 Binary files /dev/null and b/target/classes/model/PersonMapping.class differ diff --git a/target/classes/module-info.class b/target/classes/module-info.class new file mode 100644 index 0000000..68ab48d Binary files /dev/null and b/target/classes/module-info.class differ diff --git a/target/classes/ui/ConfigUI$UserEntry.class b/target/classes/ui/ConfigUI$UserEntry.class new file mode 100644 index 0000000..31a395b Binary files /dev/null and b/target/classes/ui/ConfigUI$UserEntry.class differ diff --git a/target/classes/ui/ConfigUI.class b/target/classes/ui/ConfigUI.class new file mode 100644 index 0000000..7583da2 Binary files /dev/null and b/target/classes/ui/ConfigUI.class differ diff --git a/target/classes/ui/EvaluationPopupUI.class b/target/classes/ui/EvaluationPopupUI.class new file mode 100644 index 0000000..3e2c905 Binary files /dev/null and b/target/classes/ui/EvaluationPopupUI.class differ diff --git a/target/classes/ui/EvaluationUI.class b/target/classes/ui/EvaluationUI.class new file mode 100644 index 0000000..58601a4 Binary files /dev/null and b/target/classes/ui/EvaluationUI.class differ diff --git a/target/classes/ui/MainUI.class b/target/classes/ui/MainUI.class new file mode 100644 index 0000000..b44b5b1 Binary files /dev/null and b/target/classes/ui/MainUI.class differ diff --git a/target/classes/ui/TimeSpinner$1.class b/target/classes/ui/TimeSpinner$1.class new file mode 100644 index 0000000..83cd01a Binary files /dev/null and b/target/classes/ui/TimeSpinner$1.class differ diff --git a/target/classes/ui/TimeSpinner$2.class b/target/classes/ui/TimeSpinner$2.class new file mode 100644 index 0000000..c9db8c1 Binary files /dev/null and b/target/classes/ui/TimeSpinner$2.class differ diff --git a/target/classes/ui/TimeSpinner.class b/target/classes/ui/TimeSpinner.class new file mode 100644 index 0000000..f221e75 Binary files /dev/null and b/target/classes/ui/TimeSpinner.class differ diff --git a/target/classes/ui/TrainingEditDialog$1.class b/target/classes/ui/TrainingEditDialog$1.class new file mode 100644 index 0000000..3c34b2f Binary files /dev/null and b/target/classes/ui/TrainingEditDialog$1.class differ diff --git a/target/classes/ui/TrainingEditDialog.class b/target/classes/ui/TrainingEditDialog.class new file mode 100644 index 0000000..86ec922 Binary files /dev/null and b/target/classes/ui/TrainingEditDialog.class differ diff --git a/target/classes/ui/TrainingFormUI.class b/target/classes/ui/TrainingFormUI.class new file mode 100644 index 0000000..ba7be15 Binary files /dev/null and b/target/classes/ui/TrainingFormUI.class differ diff --git a/target/classes/ui/TrainingFormUIEnterEvent$TypeRule.class b/target/classes/ui/TrainingFormUIEnterEvent$TypeRule.class new file mode 100644 index 0000000..43f8c91 Binary files /dev/null and b/target/classes/ui/TrainingFormUIEnterEvent$TypeRule.class differ diff --git a/target/classes/ui/TrainingFormUIEnterEvent.class b/target/classes/ui/TrainingFormUIEnterEvent.class new file mode 100644 index 0000000..65db680 Binary files /dev/null and b/target/classes/ui/TrainingFormUIEnterEvent.class differ diff --git a/target/classes/ui/TrainingFormUIPlanner$RowControls.class b/target/classes/ui/TrainingFormUIPlanner$RowControls.class new file mode 100644 index 0000000..7d18867 Binary files /dev/null and b/target/classes/ui/TrainingFormUIPlanner$RowControls.class differ diff --git a/target/classes/ui/TrainingFormUIPlanner.class b/target/classes/ui/TrainingFormUIPlanner.class new file mode 100644 index 0000000..d5a27c9 Binary files /dev/null and b/target/classes/ui/TrainingFormUIPlanner.class differ diff --git a/target/classes/ui/TrainingOverviewUI$1.class b/target/classes/ui/TrainingOverviewUI$1.class new file mode 100644 index 0000000..1fa58d7 Binary files /dev/null and b/target/classes/ui/TrainingOverviewUI$1.class differ diff --git a/target/classes/ui/TrainingOverviewUI.class b/target/classes/ui/TrainingOverviewUI.class new file mode 100644 index 0000000..db3b042 Binary files /dev/null and b/target/classes/ui/TrainingOverviewUI.class differ diff --git a/target/classes/util/Config.class b/target/classes/util/Config.class new file mode 100644 index 0000000..ea96fd8 Binary files /dev/null and b/target/classes/util/Config.class differ diff --git a/target/lib/apiguardian-api-1.1.2.jar b/target/lib/apiguardian-api-1.1.2.jar new file mode 100644 index 0000000..2b678e1 Binary files /dev/null and b/target/lib/apiguardian-api-1.1.2.jar differ diff --git a/target/lib/commons-beanutils-1.9.4.jar b/target/lib/commons-beanutils-1.9.4.jar new file mode 100644 index 0000000..b73543c Binary files /dev/null and b/target/lib/commons-beanutils-1.9.4.jar differ diff --git a/target/lib/commons-collections-3.2.2.jar b/target/lib/commons-collections-3.2.2.jar new file mode 100644 index 0000000..fa5df82 Binary files /dev/null and b/target/lib/commons-collections-3.2.2.jar differ diff --git a/target/lib/commons-collections4-4.4.jar b/target/lib/commons-collections4-4.4.jar new file mode 100644 index 0000000..da06c3e Binary files /dev/null and b/target/lib/commons-collections4-4.4.jar differ diff --git a/target/lib/commons-io-2.16.1.jar b/target/lib/commons-io-2.16.1.jar new file mode 100644 index 0000000..eb3c2b0 Binary files /dev/null and b/target/lib/commons-io-2.16.1.jar differ diff --git a/target/lib/commons-lang3-3.14.0.jar b/target/lib/commons-lang3-3.14.0.jar new file mode 100644 index 0000000..da9302f Binary files /dev/null and b/target/lib/commons-lang3-3.14.0.jar differ diff --git a/target/lib/commons-logging-1.2.jar b/target/lib/commons-logging-1.2.jar new file mode 100644 index 0000000..93a3b9f Binary files /dev/null and b/target/lib/commons-logging-1.2.jar differ diff --git a/target/lib/commons-text-1.11.0.jar b/target/lib/commons-text-1.11.0.jar new file mode 100644 index 0000000..7815497 Binary files /dev/null and b/target/lib/commons-text-1.11.0.jar differ diff --git a/target/lib/controlsfx-11.2.1.jar b/target/lib/controlsfx-11.2.1.jar new file mode 100644 index 0000000..6559062 Binary files /dev/null and b/target/lib/controlsfx-11.2.1.jar differ diff --git a/target/lib/jackson-annotations-2.17.2.jar b/target/lib/jackson-annotations-2.17.2.jar new file mode 100644 index 0000000..c13bcb9 Binary files /dev/null and b/target/lib/jackson-annotations-2.17.2.jar differ diff --git a/target/lib/jackson-core-2.17.2.jar b/target/lib/jackson-core-2.17.2.jar new file mode 100644 index 0000000..34be902 Binary files /dev/null and b/target/lib/jackson-core-2.17.2.jar differ diff --git a/target/lib/jackson-databind-2.17.2.jar b/target/lib/jackson-databind-2.17.2.jar new file mode 100644 index 0000000..3750b8c Binary files /dev/null and b/target/lib/jackson-databind-2.17.2.jar differ diff --git a/target/lib/javafx-base-21.0.2-win.jar b/target/lib/javafx-base-21.0.2-win.jar new file mode 100644 index 0000000..abaa591 Binary files /dev/null and b/target/lib/javafx-base-21.0.2-win.jar differ diff --git a/target/lib/javafx-base-21.0.2.jar b/target/lib/javafx-base-21.0.2.jar new file mode 100644 index 0000000..2b217d5 Binary files /dev/null and b/target/lib/javafx-base-21.0.2.jar differ diff --git a/target/lib/javafx-controls-21.0.2-win.jar b/target/lib/javafx-controls-21.0.2-win.jar new file mode 100644 index 0000000..9adcdfc Binary files /dev/null and b/target/lib/javafx-controls-21.0.2-win.jar differ diff --git a/target/lib/javafx-controls-21.0.2.jar b/target/lib/javafx-controls-21.0.2.jar new file mode 100644 index 0000000..822d8b4 Binary files /dev/null and b/target/lib/javafx-controls-21.0.2.jar differ diff --git a/target/lib/javafx-fxml-21.0.2-win.jar b/target/lib/javafx-fxml-21.0.2-win.jar new file mode 100644 index 0000000..6f42a05 Binary files /dev/null and b/target/lib/javafx-fxml-21.0.2-win.jar differ diff --git a/target/lib/javafx-fxml-21.0.2.jar b/target/lib/javafx-fxml-21.0.2.jar new file mode 100644 index 0000000..d95ab77 Binary files /dev/null and b/target/lib/javafx-fxml-21.0.2.jar differ diff --git a/target/lib/javafx-graphics-21.0.2-win.jar b/target/lib/javafx-graphics-21.0.2-win.jar new file mode 100644 index 0000000..cb0dc57 Binary files /dev/null and b/target/lib/javafx-graphics-21.0.2-win.jar differ diff --git a/target/lib/javafx-graphics-21.0.2.jar b/target/lib/javafx-graphics-21.0.2.jar new file mode 100644 index 0000000..884499b Binary files /dev/null and b/target/lib/javafx-graphics-21.0.2.jar differ diff --git a/target/lib/junit-jupiter-api-5.10.2.jar b/target/lib/junit-jupiter-api-5.10.2.jar new file mode 100644 index 0000000..b779e1b Binary files /dev/null and b/target/lib/junit-jupiter-api-5.10.2.jar differ diff --git a/target/lib/junit-jupiter-engine-5.10.2.jar b/target/lib/junit-jupiter-engine-5.10.2.jar new file mode 100644 index 0000000..112f5ab Binary files /dev/null and b/target/lib/junit-jupiter-engine-5.10.2.jar differ diff --git a/target/lib/junit-platform-commons-1.10.2.jar b/target/lib/junit-platform-commons-1.10.2.jar new file mode 100644 index 0000000..517db20 Binary files /dev/null and b/target/lib/junit-platform-commons-1.10.2.jar differ diff --git a/target/lib/junit-platform-engine-1.10.2.jar b/target/lib/junit-platform-engine-1.10.2.jar new file mode 100644 index 0000000..1f96270 Binary files /dev/null and b/target/lib/junit-platform-engine-1.10.2.jar differ diff --git a/target/lib/logback-classic-1.5.6.jar b/target/lib/logback-classic-1.5.6.jar new file mode 100644 index 0000000..7ac4c1b Binary files /dev/null and b/target/lib/logback-classic-1.5.6.jar differ diff --git a/target/lib/logback-core-1.5.6.jar b/target/lib/logback-core-1.5.6.jar new file mode 100644 index 0000000..9c75e36 Binary files /dev/null and b/target/lib/logback-core-1.5.6.jar differ diff --git a/target/lib/opencsv-5.9.jar b/target/lib/opencsv-5.9.jar new file mode 100644 index 0000000..de5fe04 Binary files /dev/null and b/target/lib/opencsv-5.9.jar differ diff --git a/target/lib/opentest4j-1.3.0.jar b/target/lib/opentest4j-1.3.0.jar new file mode 100644 index 0000000..7ec7bc5 Binary files /dev/null and b/target/lib/opentest4j-1.3.0.jar differ diff --git a/target/lib/slf4j-api-2.0.13.jar b/target/lib/slf4j-api-2.0.13.jar new file mode 100644 index 0000000..a800cc2 Binary files /dev/null and b/target/lib/slf4j-api-2.0.13.jar differ diff --git a/target/lib/sqlite-jdbc-3.46.0.0.jar b/target/lib/sqlite-jdbc-3.46.0.0.jar new file mode 100644 index 0000000..feeddf4 Binary files /dev/null and b/target/lib/sqlite-jdbc-3.46.0.0.jar differ diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties new file mode 100644 index 0000000..2aa85df --- /dev/null +++ b/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=schulungsstatistiktool +groupId=schulungsstatistiktool +version=0.1.0-SNAPSHOT diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..3111a30 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -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 diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..45dc997 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -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 diff --git a/target/schulungsstatistiktool-0.1.0-SNAPSHOT.jar b/target/schulungsstatistiktool-0.1.0-SNAPSHOT.jar new file mode 100644 index 0000000..8757819 Binary files /dev/null and b/target/schulungsstatistiktool-0.1.0-SNAPSHOT.jar differ