Compare commits

..

No commits in common. "e707deafdf7c88bda1b62f8a395269efd8eb7b50" and "e358454b5e4045195c3a4197ed987758b5ddf69d" have entirely different histories.

40 changed files with 478 additions and 1688 deletions

View File

@ -2,14 +2,13 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>Library</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ChronoFlow.Persistence\ChronoFlow.Persistence.csproj" />
<ProjectReference Include="..\ChronoFlow.Security\ChronoFlow.Security.csproj" />
<ProjectReference Include="..\ChronoFlow.Model\ChronoFlow.Model.csproj" />
<ProjectReference Include="..\ChronoFlow.Persistence\ChronoFlow.Persistence.csproj" />
</ItemGroup>
</Project>

View File

@ -1,26 +1,16 @@
namespace ChronoFlow.Model
{
///<summary>
/// Repräsentiert einen Benutzer mit Benutzernamen, Passwort und Rolle.
/// </summary>
public class User
{
public string Username { get; set; }
public string Password { get; set; }
public string Role { get; set; }
public string Mitarbeiternummer { get; set; }
public string Abteilung { get; set; }
public int Id { get; set; }
public string OriginalUsername { get; set; } // wichtig für Updates
public bool MussPasswortAendern { get; set; }
public User()
{
Username = "";
Password = "";
Role = "";
Mitarbeiternummer = "";
Abteilung = "";
OriginalUsername = "";
}
public string Password { get; set; } //vorerst im Klartext, wird später geändert
public string Role { get; set; } //"Admin" oder "Mitarbeiter"
public string Mitarbeiternummer { get; set; } = "";
public string Abteilung { get; set; } = "";
}
}

View File

@ -1,27 +1,26 @@
using System;
namespace ChronoFlow.Model
{
public class Zeiteintrag
{
public int Id { get; set; } // <<< NEU
public string Mitarbeiter { get; set; }
public DateTime Startzeit { get; set; }
public DateTime Endzeit { get; set; }
public string Projekt { get; set; }
public string Kommentar { get; set; }
public bool Erledigt { get; set; }
public string MitarbeiterKommentar { get; set; }
public string? Projekt { get; set; }
public string? Kommentar { get; set; }
public Zeiteintrag()
public TimeSpan Dauer => Endzeit - Startzeit;
//Felder für Mitarbeiter-Rückmeldung
public bool Erledigt { get; set; }
public string? MitarbeiterKommentar { get; set; }
public override string ToString()
{
Id = 0;
Mitarbeiter = "";
Startzeit = DateTime.Now;
Endzeit = DateTime.Now;
Projekt = "";
Kommentar = "";
Erledigt = false;
MitarbeiterKommentar = "";
return $"{Mitarbeiter} - {Startzeit:HH:mm} - {Endzeit:HH:mm} | {Projekt}";
}
}
}

View File

@ -7,12 +7,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0-preview.3.25171.6" />
<ProjectReference Include="..\ChronoFlow.Model\ChronoFlow.Model.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ChronoFlow.Model\ChronoFlow.Model.csproj" />
<ProjectReference Include="..\ChronoFlow.Security\ChronoFlow.Security.csproj" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0-preview.3.25171.6" />
</ItemGroup>
</Project>

View File

@ -1,19 +0,0 @@
using System;
using ChronoFlow.Security;
namespace ChronoFlow.Persistence
{
public class SecurityReferenceTest
{
public static void TestSecurityReference()
{
string testPasswort = "Test123!";
string hashed = PasswordHasher.HashPassword(testPasswort);
Console.WriteLine($"✅ Hash erfolgreich erzeugt: {hashed}");
bool isValid = PasswordHasher.VerifyPassword(testPasswort, hashed);
Console.WriteLine($"✅ Überprüfung erfolgreich: {isValid}");
}
}
}

View File

@ -1,237 +1,93 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Data.Sqlite;
using ChronoFlow.Model;
using ChronoFlow.Security;
namespace ChronoFlow.Persistence
{
public class SqliteZeiterfassungsService
{
private readonly string _dbPath = "chrono_data.sb";
private readonly string _dbPath;
private bool _dbInitialisiert ;
public SqliteZeiterfassungsService()
{
if (!File.Exists(_dbPath))
ErstelleDatenbank();
_dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "chrono_data.sb");
InitialisiereDatenbank();
}
// IMMER prüfen, auch nach neuem Erstellen
PrüfeUndErweitereDatenbank();
private void InitialisiereDatenbank()
{
if (!File.Exists(_dbPath))
{
Console.WriteLine("📂 Datenbank existiert nicht. Erstelle neue...");
ErstelleDatenbank();
}
else
{
Console.WriteLine("✅ Datenbankdatei gefunden: " + _dbPath);
}
_dbInitialisiert = true;
ZeigeExistierendeTabellen();
}
private void ErstelleDatenbank()
{
Console.WriteLine("🛠️ ErstelleDatenbank wurde aufgerufen!");
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
var cmd1 = connection.CreateCommand();
cmd1.CommandText = @"
CREATE TABLE IF NOT EXISTS Zeiteintraege (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Mitarbeiter TEXT NOT NULL,
Startzeit TEXT NOT NULL,
Endzeit TEXT NOT NULL,
Projekt TEXT,
Kommentar TEXT,
Erledigt INTEGER,
MitarbeiterKommentar TEXT
);";
cmd1.ExecuteNonQuery();
var cmd2 = connection.CreateCommand();
cmd2.CommandText = @"
CREATE TABLE IF NOT EXISTS Benutzer (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Username TEXT NOT NULL,
Password TEXT NOT NULL,
Role TEXT NOT NULL,
Mitarbeitennummer TEXT,
Abteilung TEXT,
MussPasswortAendern INTEGER DEFAULT 1
);";
cmd2.ExecuteNonQuery();
}
public void ErstelleNeuenBenutzer(User benutzer)
{
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = @"
INSERT INTO Benutzer (Username, Password, Role, Mitarbeitennummer, Abteilung, MussPasswortAendern)
VALUES ($Username, $Password, $Role, $Mitarbeiternummer, $Abteilung, $MussPasswortAendern);
";
cmd.Parameters.AddWithValue("$Username", benutzer.Username);
cmd.Parameters.AddWithValue("$Password", benutzer.Password);
cmd.Parameters.AddWithValue("$Role", benutzer.Role);
cmd.Parameters.AddWithValue("$Mitarbeiternummer", benutzer.Mitarbeiternummer ?? "");
cmd.Parameters.AddWithValue("$Abteilung", benutzer.Abteilung ?? "");
cmd.Parameters.AddWithValue("$MussPasswortAendern", benutzer.MussPasswortAendern ? 1 : 0);
int rowsAffected = cmd.ExecuteNonQuery();
Console.WriteLine($"✅ Neuer Benutzer '{benutzer.Username}' wurde gespeichert (Rows affected: {rowsAffected}).");
}
private void PrüfeUndErweitereDatenbank()
{
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
AddColumnIfMissing(connection, "Benutzer", "Mitarbeitennummer", "TEXT");
AddColumnIfMissing(connection, "Benutzer", "Abteilung", "TEXT");
AddColumnIfMissing(connection, "Benutzer", "MussPasswortAendern", "INTEGER DEFAULT 1");
}
private void AddColumnIfMissing(SqliteConnection connection, string tableName, string columnName, string columnType)
{
var checkCmd = connection.CreateCommand();
checkCmd.CommandText = $"PRAGMA table_info({tableName});";
using var reader = checkCmd.ExecuteReader();
bool columnExists = false;
while (reader.Read())
using (var cmd = connection.CreateCommand())
{
var existingColumnName = reader.GetString(1);
if (existingColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase))
{
columnExists = true;
break;
}
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS Zeiteintraege (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Mitarbeiter TEXT NOT NULL,
Startzeit TEXT NOT NULL,
Endzeit TEXT NOT NULL,
Projekt TEXT,
Kommentar TEXT,
Erledigt INTEGER,
MitarbeiterKommentar TEXT
);
";
cmd.ExecuteNonQuery();
Console.WriteLine("🛠️ Tabelle Zeiteintraege wurde erstellt/überprüft.");
}
if (!columnExists)
using (var cmd = connection.CreateCommand())
{
var alterCmd = connection.CreateCommand();
alterCmd.CommandText = $"ALTER TABLE {tableName} ADD COLUMN {columnName} {columnType};";
alterCmd.ExecuteNonQuery();
Console.WriteLine($"✅ Spalte '{columnName}' in Tabelle '{tableName}' hinzugefügt.");
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS Benutzer (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Username TEXT NOT NULL,
Password TEXT NOT NULL,
Role TEXT NOT NULL,
Mitarbeiternummer TEXT,
Abteilung TEXT
);
";
cmd.ExecuteNonQuery();
Console.WriteLine("🛠️ Tabelle Benutzer wurde erstellt/überprüft.");
}
}
public void ErstelleStandardAdmin()
{
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
var checkCmd = connection.CreateCommand();
checkCmd.CommandText = "SELECT COUNT(*) FROM Benutzer WHERE Username = 'admin';";
var count = Convert.ToInt32(checkCmd.ExecuteScalar());
if (count == 0)
if (!_dbInitialisiert)
{
var hashedPassword = PasswordHasher.HashPassword("admin");
var insertCmd = connection.CreateCommand();
insertCmd.CommandText = @"
INSERT INTO Benutzer (Username, Password, Role, Mitarbeitennummer, Abteilung, MussPasswortAendern)
VALUES ('admin', $Password, 'Admin', '0001', 'IT', 1);
";
insertCmd.Parameters.AddWithValue("$Password", hashedPassword);
insertCmd.ExecuteNonQuery();
Console.WriteLine("✅ Standard-Admin erfolgreich eingefügt.");
}
else
{
Console.WriteLine(" Standard-Admin existiert bereits kein neuer Eintrag erstellt.");
}
}
public List<User> LadeAlleBenutzer()
{
var benutzerListe = new List<User>();
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT Id, Username, Password, Role, Mitarbeitennummer, Abteilung, MussPasswortAendern FROM Benutzer;";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
benutzerListe.Add(new User
{
Id = reader.GetInt32(0),
Username = reader.GetString(1),
Password = reader.GetString(2),
Role = reader.GetString(3),
Mitarbeiternummer = reader.IsDBNull(4) ? "" : reader.GetString(4),
Abteilung = reader.IsDBNull(5) ? "" : reader.GetString(5),
MussPasswortAendern = reader.GetInt32(6) == 1,
OriginalUsername = reader.GetString(1)
});
throw new Exception("❗ Fehler: Datenbank wurde noch nicht initialisiert!");
}
return benutzerListe;
}
public void UpdateBenutzer(User benutzer)
{
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = @"
UPDATE Benutzer
SET Username = $NewUsername,
Password = $Password,
Role = $Role,
Mitarbeitennummer = $Mitarbeiternummer,
Abteilung = $Abteilung,
MussPasswortAendern = $MussPasswortAendern
WHERE Username = $OriginalUsername;
INSERT INTO Benutzer (Username, Password, Role, Mitarbeiternummer, Abteilung)
VALUES ('admin', 'admin', 'Admin', '0001', 'IT');
";
cmd.ExecuteNonQuery();
cmd.Parameters.AddWithValue("$NewUsername", benutzer.Username);
cmd.Parameters.AddWithValue("$Password", benutzer.Password);
cmd.Parameters.AddWithValue("$Role", benutzer.Role);
cmd.Parameters.AddWithValue("$Mitarbeiternummer", benutzer.Mitarbeiternummer ?? "");
cmd.Parameters.AddWithValue("$Abteilung", benutzer.Abteilung ?? "");
cmd.Parameters.AddWithValue("$MussPasswortAendern", benutzer.MussPasswortAendern ? 1 : 0);
cmd.Parameters.AddWithValue("$OriginalUsername", benutzer.OriginalUsername);
int rowsAffected = cmd.ExecuteNonQuery();
Console.WriteLine($"✏ Benutzer aktualisiert: {benutzer.Username} (Rows affected: {rowsAffected})");
}
public void LoescheBenutzer(string username)
{
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = "DELETE FROM Benutzer WHERE Username = $Username;";
cmd.Parameters.AddWithValue("$Username", username);
int rowsAffected = cmd.ExecuteNonQuery();
Console.WriteLine($"❌ Benutzer gelöscht: {username} (Rows affected: {rowsAffected})");
}
public List<string> LadeAlleMitarbeiterNamen()
{
var namen = new List<string>();
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT Username FROM Benutzer WHERE Role = 'Mitarbeiter';";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
namen.Add(reader.GetString(0));
}
return namen;
Console.WriteLine("✅ Standard-Admin erfolgreich erstellt.");
}
public void SpeichereEintrag(Zeiteintrag eintrag)
@ -255,8 +111,6 @@ namespace ChronoFlow.Persistence
cmd.Parameters.AddWithValue("$MitarbeiterKommentar", eintrag.MitarbeiterKommentar ?? "");
cmd.ExecuteNonQuery();
Console.WriteLine($"✅ Zeiteintrag für {eintrag.Mitarbeiter} gespeichert.");
}
public List<Zeiteintrag> LadeAlleZeiteintraege()
@ -274,7 +128,6 @@ namespace ChronoFlow.Persistence
{
eintraege.Add(new Zeiteintrag
{
Id = reader.GetInt32(0),
Mitarbeiter = reader.GetString(1),
Startzeit = DateTime.Parse(reader.GetString(2)),
Endzeit = DateTime.Parse(reader.GetString(3)),
@ -288,175 +141,66 @@ namespace ChronoFlow.Persistence
return eintraege;
}
public void UpdateProjekt(Zeiteintrag projekt)
public List<User> LadeAlleBenutzer()
{
var benutzerListe = new List<User>();
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = @"
UPDATE Zeiteintraege
SET Projekt = $Projekt,
Kommentar = $Kommentar,
Startzeit = $Startzeit,
Endzeit = $Endzeit,
Mitarbeiter = $Mitarbeiter,
Erledigt = $Erledigt
WHERE Id = $Id;
";
cmd.Parameters.AddWithValue("$Projekt", projekt.Projekt);
cmd.Parameters.AddWithValue("$Kommentar", projekt.Kommentar ?? "");
cmd.Parameters.AddWithValue("$Startzeit", projekt.Startzeit.ToString("o"));
cmd.Parameters.AddWithValue("$Endzeit", projekt.Endzeit.ToString("o"));
cmd.Parameters.AddWithValue("$Mitarbeiter", projekt.Mitarbeiter);
cmd.Parameters.AddWithValue("$Erledigt", projekt.Erledigt ? 1 : 0);
cmd.Parameters.AddWithValue("$Id", projekt.Id);
int rowsAffected = cmd.ExecuteNonQuery();
Console.WriteLine($"✏ Projekt aktualisiert (Id={projekt.Id}, Rows affected: {rowsAffected})");
}
public void LoescheProjekt(int id)
{
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = "DELETE FROM Zeiteintraege WHERE Id = $Id;";
cmd.Parameters.AddWithValue("$Id", id);
int rows = cmd.ExecuteNonQuery();
Console.WriteLine($"❌ Projekt gelöscht (Id: {id}, Rows: {rows})");
}
public void UpdateProjektStatus(int id, bool erledigt)
{
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = @"
UPDATE Zeiteintraege
SET Erledigt = $Erledigt
WHERE Id = $Id;
";
cmd.Parameters.AddWithValue("$Erledigt", erledigt ? 1 : 0);
cmd.Parameters.AddWithValue("$Id", id);
int rowsAffected = cmd.ExecuteNonQuery();
Console.WriteLine($"✅ Projektstatus aktualisiert (Id={id}, Erledigt={erledigt}, Rows affected: {rowsAffected})");
}
public List<Zeiteintrag> LadeAbgeschlosseneProjekte()
{
var abgeschlossene = new List<Zeiteintrag>();
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT * FROM Zeiteintraege WHERE Erledigt = 1;";
cmd.CommandText = "SELECT Username, Password, Role, Mitarbeiternummer, Abteilung FROM Benutzer;";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
abgeschlossene.Add(new Zeiteintrag
benutzerListe.Add(new User
{
Id = reader.GetInt32(0),
Mitarbeiter = reader.GetString(1),
Startzeit = DateTime.Parse(reader.GetString(2)),
Endzeit = DateTime.Parse(reader.GetString(3)),
Projekt = reader.GetString(4),
Kommentar = reader.GetString(5),
Erledigt = Convert.ToInt32(reader["Erledigt"]) == 1,
MitarbeiterKommentar = reader.GetString(7)
Username = reader.GetString(0),
Password = reader.GetString(1),
Role = reader.GetString(2),
Mitarbeiternummer = reader.IsDBNull(3) ? "" : reader.GetString(3),
Abteilung = reader.IsDBNull(4) ? "" : reader.GetString(4)
});
}
return abgeschlossene;
return benutzerListe;
}
public List<Zeiteintrag> LadeLetzteProjekte(int anzahl = 3)
private void ZeigeExistierendeTabellen()
{
var projekte = new List<Zeiteintrag>();
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = @"
SELECT * FROM Zeiteintraege
ORDER BY Id DESC
LIMIT $Anzahl;
";
cmd.Parameters.AddWithValue("$Anzahl", anzahl);
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table';";
using var reader = cmd.ExecuteReader();
Console.WriteLine("🗂️ Tabellen in der Datenbank:");
while (reader.Read())
{
projekte.Add(new Zeiteintrag
{
Id = reader.GetInt32(0),
Mitarbeiter = reader.GetString(1),
Startzeit = DateTime.Parse(reader.GetString(2)),
Endzeit = DateTime.Parse(reader.GetString(3)),
Projekt = reader.GetString(4),
Kommentar = reader.GetString(5),
Erledigt = Convert.ToInt32(reader["Erledigt"]) == 1,
MitarbeiterKommentar = reader.GetString(7)
});
Console.WriteLine($" ➔ {reader.GetString(0)}");
}
return projekte;
}
public List<Zeiteintrag> LadeOffeneProjekte()
{
var offene = new List<Zeiteintrag>();
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT * FROM Zeiteintraege WHERE Erledigt = 0;";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
offene.Add(new Zeiteintrag
{
Id = reader.GetInt32(0),
Mitarbeiter = reader.GetString(1),
Startzeit = DateTime.Parse(reader.GetString(2)),
Endzeit = DateTime.Parse(reader.GetString(3)),
Projekt = reader.GetString(4),
Kommentar = reader.GetString(5),
Erledigt = Convert.ToInt32(reader["Erledigt"]) == 1,
MitarbeiterKommentar = reader.GetString(7)
});
}
return offene;
}
public void ResetBenutzerPasswort(string username)
/// <summary>
/// Prüft, ob ein Benutzername bereits existiert.
/// </summary>
/// <param name="username">Benutzername, der überprüft werden soll</param>
/// <returns>True, wenn Name bereits existiert, sonst False</returns>
public bool BenutzernameExistiert(string username)
{
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
var hashedDefault = PasswordHasher.HashPassword("changeme");
var cmd = connection.CreateCommand();
cmd.CommandText = @"
UPDATE Benutzer
SET Password = $Password,
MussPasswortAendern = 1
WHERE Username = $Username;
";
cmd.CommandText = "SELECT COUNT(*) FROM Benutzer WHERE Username = $username";
cmd.Parameters.AddWithValue("$username", username);
cmd.Parameters.AddWithValue("$Password", hashedDefault);
cmd.Parameters.AddWithValue("$Username", username);
var result = Convert.ToInt32(cmd.ExecuteScalar());
int rowsAffected = cmd.ExecuteNonQuery();
Console.WriteLine($"🔒 Passwort für Benutzer '{username}' zurückgesetzt (Rows affected: {rowsAffected})");
return result > 0;
}
}
}

View File

@ -1,9 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -1,67 +0,0 @@
using System;
using System.Security.Cryptography;
using System.Text;
namespace ChronoFlow.Security
{
/// <summary>
/// Diese Klasse bietet Funktionen zum sicheren Hashen und Überprüfen von Passwörtern.
/// </summary>
public static class PasswordHasher
{
/// <summary>
/// Erstellt einen sicheren Hash aus einem Passwort unter Verwendung von PBKDF2.
/// </summary>
/// <param name="password">Das Klartextpasswort</param>
/// <returns>Ein kombinierter Hash-String (Salt + Hash)</returns>
public static string HashPassword(string password)
{
using var rng = RandomNumberGenerator.Create();
byte[] salt = new byte[16];
rng.GetBytes(salt);
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100_000, HashAlgorithmName.SHA256);
byte[] hash = pbkdf2.GetBytes(32);
// Kombiniere Salt + Hash in einen String (Base64-encodiert)
byte[] hashBytes = new byte[48];
Array.Copy(salt, 0, hashBytes, 0, 16);
Array.Copy(hash, 0, hashBytes, 16, 32);
return Convert.ToBase64String(hashBytes);
}
/// <summary>
/// Überprüft, ob ein Passwort zu einem gegebenen Hash passt.
/// </summary>
/// <param name="password">Das eingegebene Klartextpasswort</param>
/// <param name="storedHash">Der gespeicherte kombinierte Hash (Base64, Salt + Hash)</param>
/// <returns>True, wenn das Passwort stimmt, sonst false</returns>
public static bool VerifyPassword(string password, string storedHash)
{
try
{
byte[] hashBytes = Convert.FromBase64String(storedHash);
byte[] salt = new byte[16];
Array.Copy(hashBytes, 0, salt, 0, 16);
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100_000, HashAlgorithmName.SHA256);
byte[] hash = pbkdf2.GetBytes(32);
for (int i = 0; i < 32; i++)
{
if (hashBytes[i + 16] != hash[i])
return false;
}
return true;
}
catch
{
// Falls der gespeicherte Hash beschädigt oder kein Base64 ist
return false;
}
}
}
}

View File

@ -1,26 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:model1="clr-namespace:ChronoFlow.Model;assembly=ChronoFlow.Model"
x:Class="ChronoFlow.View.Admin.AbgeschlosseneProjekteView">
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="20">
<TextBlock Grid.Row="0" Text="Abgeschlossene Projekte" FontSize="20" FontWeight="Bold" HorizontalAlignment="Center" Margin="0,0,0,10"/>
<TextBox Grid.Row="1" x:Name="Suchfeld" Watermark="🔍 Nach Projekt oder Mitarbeiter suchen..." KeyUp="Suchfeld_KeyUp" Margin="0,0,0,10"/>
<ScrollViewer Grid.Row="2">
<ListBox x:Name="AbgeschlosseneListe">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type model1:Zeiteintrag}">
<StackPanel Orientation="Horizontal" Spacing="10" Margin="5">
<TextBlock Text="{Binding Projekt}" Width="150"/>
<TextBlock Text="{Binding Mitarbeiter}" Width="150"/>
<TextBlock Text="{Binding Endzeit}" Width="150"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
<Button Grid.Row="3" Content="⬅ Zurück zum Dashboard" Click="ZurueckButton_Click" HorizontalAlignment="Center" Margin="0,10,0,0"/>
</Grid>
</UserControl>

View File

@ -1,49 +0,0 @@
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using ChronoFlow.Model;
using ChronoFlow.Persistence;
namespace ChronoFlow.View.Admin;
public partial class AbgeschlosseneProjekteView : UserControl
{
private readonly ViewManager _viewManager;
private readonly ObservableCollection<Zeiteintrag> _abgeschlosseneProjekte = new();
private readonly SqliteZeiterfassungsService _dbService = new();
public AbgeschlosseneProjekteView(ViewManager viewManager)
{
InitializeComponent();
_viewManager = viewManager;
LadeAbgeschlosseneProjekte();
}
private void LadeAbgeschlosseneProjekte()
{
_abgeschlosseneProjekte.Clear();
var ausDb = _dbService.LadeAbgeschlosseneProjekte();
foreach (var eintrag in ausDb)
_abgeschlosseneProjekte.Add(eintrag);
AbgeschlosseneListe.ItemsSource = _abgeschlosseneProjekte;
}
private void ZurueckButton_Click(object? sender, RoutedEventArgs e)
{
_viewManager.Show("AdminMain");
}
private void Suchfeld_KeyUp(object? sender, KeyEventArgs e)
{
var text = Suchfeld?.Text?.ToLower() ?? "";
AbgeschlosseneListe.ItemsSource = _abgeschlosseneProjekte
.Where(p => (p.Projekt?.ToLower().Contains(text) ?? false) ||
(p.Mitarbeiter?.ToLower().Contains(text) ?? false))
.ToList();
}
}

View File

@ -1,59 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:model="clr-namespace:ChronoFlow.Model;assembly=ChronoFlow.Model"
x:Class="ChronoFlow.View.Admin.AdminMainView">
<SplitView x:Name="AdminPane" DisplayMode="CompactInline" IsPaneOpen="True" CompactPaneLength="37" OpenPaneLength="200">
<!-- Linke Sidebar (SplitView-Pane) -->
<SplitView.Pane>
<StackPanel>
<!-- Burger-Button -->
<Button Content="☰" Click="TogglePane_Click"/>
<!-- Navigation Buttons -->
<Button Content="🏠 Dashboard" Click="Dashboard_Click"/>
<Button Content="📋 Alle Projekte anzeigen" Click="AlleProjekte_Click"/>
<Button Content="👥 Mitarbeiter-Liste" Click="MitarbeiterListe_Click"/>
<Button Content="✅ Abgeschlossene Projekte" Click="AbgeschlosseneProjekte_Click" />
<Button Content="⚙ Einstellungen" Click="Einstellungen_Click"/>
</StackPanel>
</SplitView.Pane>
<!-- Hauptinhalt -->
<SplitView.Content>
<StackPanel Margin="20" Spacing="15">
<!-- Überschrift -->
<TextBlock Text="Admin-Dashboard" FontSize="24" FontWeight="Bold" HorizontalAlignment="Center"/>
<!-- Aktions-Buttons (oben) -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="10">
<Button Content=" Mitarbeiter hinzufügen" Width="200" Height="50" Click="MitarbeiterHinzufuegen_Click"/>
<Button Content=" Projekt erstellen" Width="200" Height="50" Click="ProjektErstellen_Click"/>
</StackPanel>
<!-- Anzeige der letzten Projekte -->
<TextBlock Text="Zuletzt hinzugefügte Projekte" FontSize="18" FontWeight="SemiBold" Margin="0,20,0,10"/>
<ItemsControl x:Name="LetzteProjekteListe">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type model:Zeiteintrag}">
<Border BorderBrush="Gray" BorderThickness="1" Padding="10" Margin="0,5">
<StackPanel>
<TextBlock Text="{Binding Projekt}" FontWeight="Bold"/>
<TextBlock Text="{Binding Mitarbeiter}"/>
<TextBlock Text="{Binding Startzeit, StringFormat='Start: {0:G}'}"/>
<TextBlock Text="{Binding Endzeit, StringFormat='Ende: {0:G}'}"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</SplitView.Content>
</SplitView>
</UserControl>

View File

@ -1,106 +0,0 @@
using System;
using System.Collections.ObjectModel;
using Avalonia.Controls;
using Avalonia.Interactivity;
using ChronoFlow.Model;
using ChronoFlow.Persistence;
namespace ChronoFlow.View.Admin
{
public partial class AdminMainView : UserControl
{
private readonly ViewManager _viewManager;
private readonly ObservableCollection<Zeiteintrag> _letzteProjekte = new();
public AdminMainView() : this(new ViewManager(new ContentControl()))
{
Console.WriteLine("⚠ Achtung: Parameterloser Konstruktor genutzt (nur Standard-ViewManager).");
}
public AdminMainView(ViewManager viewManager)
{
InitializeComponent();
_viewManager = viewManager;
Console.WriteLine("✅ AdminMainView wird initialisiert.");
LadeLetzteProjekte();
}
private void LadeLetzteProjekte()
{
Console.WriteLine("🔄 Lade letzte Projekte...");
try
{
var dbService = new SqliteZeiterfassungsService();
var letzteAusDb = dbService.LadeLetzteProjekte(3);
_letzteProjekte.Clear();
foreach (var eintrag in letzteAusDb)
{
Console.WriteLine($"✅ Projekt geladen: {eintrag.Projekt} ({eintrag.Id})");
_letzteProjekte.Add(eintrag);
}
if (LetzteProjekteListe != null)
{
LetzteProjekteListe.ItemsSource = _letzteProjekte;
Console.WriteLine("✅ LetzteProjekteListe erfolgreich gebunden.");
}
else
{
Console.WriteLine("⚠ Warnung: LetzteProjekteListe ist null. Prüfe XAML-Bindung!");
}
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] Fehler beim Laden der letzten Projekte: {ex.Message}");
}
}
private void MitarbeiterHinzufuegen_Click(object sender, RoutedEventArgs e)
{
_viewManager.Show("MitarbeiterHinzufuegen");
}
private void ProjektErstellen_Click(object sender, RoutedEventArgs e)
{
_viewManager.Show("ProjektErstellen");
}
private void Dashboard_Click(object sender, RoutedEventArgs e)
{
_viewManager.Show("AdminMain");
}
private void AlleProjekte_Click(object sender, RoutedEventArgs e)
{
_viewManager.Show("AlleProjekte");
}
private void MitarbeiterListe_Click(object sender, RoutedEventArgs e)
{
_viewManager.Show("MitarbeiterListe");
}
private void Einstellungen_Click(object sender, RoutedEventArgs e)
{
_viewManager.Show("Einstellungen");
}
private void TogglePane_Click(object sender, RoutedEventArgs e)
{
AdminPane.IsPaneOpen = !AdminPane.IsPaneOpen;
}
public void AktualisiereLetzteProjekte()
{
LadeLetzteProjekte();
}
private void AbgeschlosseneProjekte_Click(object? sender, RoutedEventArgs e)
{
_viewManager.Show("AbgeschlosseneProjekte");
}
}
}

View File

@ -1,31 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:model1="clr-namespace:ChronoFlow.Model;assembly=ChronoFlow.Model"
x:Class="ChronoFlow.View.Admin.AlleProjekteView">
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="20">
<TextBlock Grid.Row="0" Text="Alle Projekte" FontSize="20" FontWeight="Bold" HorizontalAlignment="Center" Margin="0,0,0,10"/>
<TextBox Grid.Row="1" x:Name="Suchfeld" Watermark="🔍 Nach Projekt oder Mitarbeiter suchen..." KeyUp="Suchfeld_KeyUp" Margin="0,0,0,10"/>
<ScrollViewer Grid.Row="2">
<ListBox x:Name="ProjekteListe">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type model1:Zeiteintrag}">
<StackPanel Orientation="Horizontal" Spacing="10" Margin="5">
<TextBlock Text="{Binding Projekt}" Width="150"/>
<TextBlock Text="{Binding Mitarbeiter}" Width="150"/>
<TextBlock Text="{Binding Startzeit}" Width="150"/>
<TextBlock Text="{Binding Endzeit}" Width="150"/>
<TextBlock Text="{Binding Kommentar}" Width="200"/>
<Button Content="🖋 Bearbeiten" Tag="{Binding}" Click="Bearbeiten_Click"/>
<Button Content="🗑 Löschen" Tag="{Binding}" Click="Loeschen_Click"/>
<Button Content="✅ Abschließen" Tag="{Binding}" Click="Abschliessen_Click"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
<Button Grid.Row="3" Content="⬅ Zurück zum Dashboard" Click="ZurueckButton_Click" HorizontalAlignment="Center" Margin="0,10,0,0"/>
</Grid>
</UserControl>

View File

@ -1,89 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Interactivity;
using ChronoFlow.Model;
using ChronoFlow.Persistence;
using Avalonia.Input;
namespace ChronoFlow.View.Admin;
public partial class AlleProjekteView : UserControl
{
private readonly ViewManager _viewManager;
private readonly ObservableCollection<Zeiteintrag> _alleProjekte = new();
private readonly SqliteZeiterfassungsService _dbService = new();
public AlleProjekteView(ViewManager viewManager)
{
InitializeComponent();
_viewManager = viewManager;
LadeAlleProjekte();
}
private void LadeAlleProjekte()
{
_alleProjekte.Clear();
var ausDb = _dbService.LadeAlleZeiteintraege()
.Where(p => !p.Erledigt) // Nur nicht erledigte Projekte!
.ToList();
foreach (var eintrag in ausDb)
_alleProjekte.Add(eintrag);
ProjekteListe.ItemsSource = _alleProjekte;
}
private void Suchfeld_KeyUp(object? sender, KeyEventArgs e)
{
var text = Suchfeld?.Text?.ToLower() ?? "";
ProjekteListe.ItemsSource = _alleProjekte
.Where(p => (p.Projekt?.ToLower().Contains(text) ?? false) ||
(p.Mitarbeiter?.ToLower().Contains(text) ?? false))
.ToList();
}
private async void Bearbeiten_Click(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is Zeiteintrag projekt)
{
var dialog = new ProjektBearbeitenDialog(projekt);
var updatedProjekt = await dialog.ShowDialog<Zeiteintrag>((Window)this.VisualRoot!);
if (updatedProjekt != null)
{
_dbService.UpdateProjekt(updatedProjekt);
LadeAlleProjekte();
}
}
}
private void Loeschen_Click(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is Zeiteintrag projekt)
{
_dbService.LoescheProjekt(projekt.Id);
LadeAlleProjekte();
}
}
private void Abschliessen_Click(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is Zeiteintrag projekt)
{
projekt.Erledigt = true;
_dbService.UpdateProjekt(projekt);
Console.WriteLine($"✅ Projekt abgeschlossen: {projekt.Projekt}");
LadeAlleProjekte();
}
}
private void ZurueckButton_Click(object? sender, RoutedEventArgs e)
{
_viewManager.Show("AdminMain");
}
}

View File

@ -1,15 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ChronoFlow.View.Admin.ConfirmDialog"
Width="400" Height="200"
Title="Bestätigung">
<StackPanel Margin="20" Spacing="10">
<TextBlock x:Name="FrageText" Text="Sind Sie sicher?" FontSize="16" FontWeight="Bold" TextWrapping="Wrap" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="10">
<Button Content="✅ Ja" Width="80" Click="JaButton_Click" />
<Button Content="❌ Nein" Width="80" Click="NeinButton_Click" />
</StackPanel>
</StackPanel>
</Window>

View File

@ -1,27 +0,0 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ChronoFlow.View.Admin;
public partial class ConfirmDialog : Window
{
public bool Result { get; private set; } = false;
public ConfirmDialog(string frage)
{
InitializeComponent();
FrageText.Text = frage;
}
private void JaButton_Click(object? sender, RoutedEventArgs e)
{
Result = true;
Close(Result);
}
private void NeinButton_Click(object? sender, RoutedEventArgs e)
{
Result = false;
Close(Result);
}
}

View File

@ -1,31 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ChronoFlow.View.Admin.MitarbeiterBearbeitenDialog"
Width="450" Height="600"
Title="Mitarbeiter bearbeiten">
<StackPanel Margin="20" Spacing="10">
<TextBlock Text="Username:" />
<TextBox x:Name="UsernameBox" />
<TextBlock Text="Abteilung:" />
<TextBox x:Name="AbteilungBox" />
<TextBlock Text="Mitarbeiternummer:" />
<TextBox x:Name="MitarbeiternummerBox" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="10" Margin="0,10,0,0">
<Button Content="✅ Speichern" Width="120" Click="SpeichernButton_Click" />
<Button Content="❌ Abbrechen" Width="120" Click="AbbrechenButton_Click" />
</StackPanel>
<!-- Erfolgs-Hinweis -->
<TextBlock x:Name="FeedbackText"
Text="Änderungen erfolgreich übernommen."
Foreground="Green"
FontWeight="Bold"
HorizontalAlignment="Center"
IsVisible="False"
Margin="0,10,0,0" />
</StackPanel>
</Window>

View File

@ -1,41 +0,0 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using ChronoFlow.Model;
namespace ChronoFlow.View.Admin;
public partial class MitarbeiterBearbeitenDialog : Window
{
public User UpdatedUser { get; private set; }
public MitarbeiterBearbeitenDialog(User user)
{
InitializeComponent();
UpdatedUser = new User
{
Username = user.Username,
OriginalUsername = user.Username, // Speichern des alten Namens
Abteilung = user.Abteilung,
Mitarbeiternummer = user.Mitarbeiternummer
};
UsernameBox.Text = user.Username;
AbteilungBox.Text = user.Abteilung;
MitarbeiternummerBox.Text = user.Mitarbeiternummer;
}
private void SpeichernButton_Click(object? sender, RoutedEventArgs e)
{
UpdatedUser.Username = UsernameBox.Text ?? UpdatedUser.Username;
UpdatedUser.Abteilung = AbteilungBox.Text ?? UpdatedUser.Abteilung;
UpdatedUser.Mitarbeiternummer = MitarbeiternummerBox.Text ?? UpdatedUser.Mitarbeiternummer;
this.Close(UpdatedUser);
}
private void AbbrechenButton_Click(object? sender, RoutedEventArgs e)
{
this.Close(null);
}
}

View File

@ -1,29 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:model1="clr-namespace:ChronoFlow.Model;assembly=ChronoFlow.Model"
x:Class="ChronoFlow.View.Admin.MitarbeiterListeView">
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="20">
<TextBlock Grid.Row="0" Text="Alle Mitarbeiter" FontSize="20" FontWeight="Bold" HorizontalAlignment="Center" Margin="0,0,0,10"/>
<TextBox Grid.Row="1" x:Name="Suchfeld" Watermark="🔍 Suchen..." KeyUp="Suchfeld_KeyUp" Margin="0,0,0,10"/>
<ScrollViewer Grid.Row="2">
<ListBox x:Name="MitarbeiterListe">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type model1:User}">
<StackPanel Orientation="Horizontal" Spacing="10" Margin="5">
<TextBlock Text="{Binding Username}" Width="150"/>
<TextBlock Text="{Binding Abteilung}" Width="150"/>
<TextBlock Text="{Binding Mitarbeiternummer}" Width="150"/>
<Button Content="🖋 Bearbeiten" Tag="{Binding}" Click="Bearbeiten_Click"/>
<Button Content="🗑 Löschen" Tag="{Binding}" Click="Loeschen_Click"/>
<Button Content="🔑 Passwort zurücksetzen" Tag="{Binding}" Click="PasswortReset_Click" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
<Button Grid.Row="3" Content="⬅ Zurück zum Dashboard" Click="ZurueckButton_Click" HorizontalAlignment="Center" Margin="0,10,0,0"/>
</Grid>
</UserControl>

View File

@ -1,143 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using ChronoFlow.Model;
using ChronoFlow.Persistence;
using MessageBox.Avalonia;
using MessageBox.Avalonia.DTO;
using MessageBox.Avalonia.Enums;
namespace ChronoFlow.View.Admin;
public partial class MitarbeiterListeView : UserControl
{
private readonly ViewManager _viewManager;
private readonly ObservableCollection<User> _alleMitarbeiter = new();
private readonly SqliteZeiterfassungsService _dbService = new();
public MitarbeiterListeView(ViewManager viewManager)
{
InitializeComponent();
_viewManager = viewManager;
LadeMitarbeiter();
}
private void LadeMitarbeiter()
{
try
{
var mitarbeiter = _dbService.LadeAlleBenutzer()
.Where(m => m.Role == "Mitarbeiter")
.OrderBy(m => m.Username)
.ToList();
_alleMitarbeiter.Clear();
foreach (var user in mitarbeiter)
_alleMitarbeiter.Add(user);
MitarbeiterListe.ItemsSource = _alleMitarbeiter;
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fehler beim Laden der Mitarbeiter: {ex.Message}");
}
}
private void Suchfeld_KeyUp(object? sender, KeyEventArgs e)
{
var text = Suchfeld?.Text?.ToLower() ?? "";
MitarbeiterListe.ItemsSource = _alleMitarbeiter
.Where(m => m.Username.ToLower().Contains(text))
.ToList();
}
private async void Bearbeiten_Click(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is User benutzer)
{
var dialog = new MitarbeiterBearbeitenDialog(benutzer);
var updatedUser = await dialog.ShowDialog<User>((Window)this.VisualRoot!);
if (updatedUser != null)
{
updatedUser.Password = benutzer.Password;
updatedUser.Role = benutzer.Role;
updatedUser.OriginalUsername = benutzer.Username; // ← WICHTIG!
_dbService.UpdateBenutzer(updatedUser);
LadeMitarbeiter();
}
}
}
private async void Loeschen_Click(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is User benutzer)
{
var dialog = new ConfirmDialog($"Möchten Sie den Benutzer '{benutzer.Username}' wirklich löschen?");
var result = await dialog.ShowDialog<bool>((Window)this.VisualRoot!);
if (result)
{
_dbService.LoescheBenutzer(benutzer.Username);
LadeMitarbeiter();
}
}
}
private void ZurueckButton_Click(object? sender, RoutedEventArgs e)
{
try
{
_viewManager.Show("AdminMain");
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fehler beim Zurückspringen: {ex.Message}");
}
}
private async void ResetPasswort_Click(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is User benutzer)
{
var dialog = new ConfirmDialog($"Soll das Passwort für '{benutzer.Username}' wirklich zurückgesetzt werden?");
var result = await dialog.ShowDialog<bool>((Window)this.VisualRoot!);
if (result)
{
_dbService.ResetBenutzerPasswort(benutzer.Username);
Console.WriteLine($"✅ Passwort für {benutzer.Username} zurückgesetzt.");
}
}
}
private async void PasswortReset_Click(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is User benutzer)
{
var dialog = new ConfirmDialog($"Möchten Sie das Passwort für '{benutzer.Username}' wirklich zurücksetzen?");
var result = await dialog.ShowDialog<bool>((Window)this.VisualRoot!);
if (result)
{
benutzer.Password = "newpassword"; // 💡 hier später besser: generiertes oder festgelegtes Initialpasswort
benutzer.MussPasswortAendern = true;
_dbService.UpdateBenutzer(benutzer);
Console.WriteLine($"✅ Passwort für {benutzer.Username} wurde zurückgesetzt.");
}
}
}
}

View File

@ -1,28 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ChronoFlow.View.Admin.ProjektBearbeitenDialog"
Width="500" Height="600"
Title="Projekt bearbeiten">
<StackPanel Margin="20" Spacing="12">
<TextBlock Text="Projektname:" />
<TextBox x:Name="ProjektnameBox" />
<TextBlock Text="Kommentar:" />
<TextBox x:Name="KommentarBox" AcceptsReturn="True" Height="80" />
<TextBlock Text="Mitarbeiter auswählen:" />
<ComboBox x:Name="MitarbeiterDropdown" />
<TextBlock Text="Startdatum:" />
<DatePicker x:Name="StartzeitPicker" />
<TextBlock Text="Enddatum:" />
<DatePicker x:Name="EndzeitPicker" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="20" Margin="0,15,0,0">
<Button Content="✅ Speichern" Width="120" Click="SpeichernButton_Click" />
<Button Content="❌ Abbrechen" Width="130" Click="AbbrechenButton_Click" />
</StackPanel>
</StackPanel>
</Window>

View File

@ -1,47 +0,0 @@
using System;
using Avalonia.Controls;
using Avalonia.Interactivity;
using ChronoFlow.Model;
using ChronoFlow.Persistence;
namespace ChronoFlow.View.Admin;
public partial class ProjektBearbeitenDialog : Window
{
public Zeiteintrag UpdatedProjekt { get; private set; }
public ProjektBearbeitenDialog(Zeiteintrag projekt)
{
InitializeComponent();
var dbService = new SqliteZeiterfassungsService();
var mitarbeiter = dbService.LadeAlleMitarbeiterNamen();
MitarbeiterDropdown.ItemsSource = mitarbeiter;
MitarbeiterDropdown.SelectedItem = projekt.Mitarbeiter;
// Vorbelegen
ProjektnameBox.Text = projekt.Projekt;
KommentarBox.Text = projekt.Kommentar;
StartzeitPicker.SelectedDate = new DateTimeOffset(projekt.Startzeit);
EndzeitPicker.SelectedDate = new DateTimeOffset(projekt.Endzeit);
UpdatedProjekt = projekt;
}
private void SpeichernButton_Click(object? sender, RoutedEventArgs e)
{
UpdatedProjekt.Projekt = ProjektnameBox.Text ?? "";
UpdatedProjekt.Kommentar = KommentarBox.Text ?? "";
UpdatedProjekt.Startzeit = (StartzeitPicker.SelectedDate ?? DateTimeOffset.Now).DateTime;
UpdatedProjekt.Endzeit = (EndzeitPicker.SelectedDate ?? DateTimeOffset.Now).DateTime;
UpdatedProjekt.Mitarbeiter = MitarbeiterDropdown.SelectedItem?.ToString() ?? "";
Close(UpdatedProjekt);
}
private void AbbrechenButton_Click(object? sender, RoutedEventArgs e)
{
Close(null);
}
}

View File

@ -1,39 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ChronoFlow.View.Admin.ProjektErstellenView">
<StackPanel Margin="20" Spacing="10">
<TextBlock Text="Neues Projekt erstellen" FontSize="20" FontWeight="Bold" HorizontalAlignment="Center" />
<TextBlock Text="Projektname:" />
<TextBox x:Name="ProjektnameBox" />
<TextBlock Text="Startdatum:" />
<DatePicker x:Name="StartdatumPicker" />
<TextBlock Text="Startzeit (HH:mm):" />
<TextBox x:Name="StartzeitBox" Watermark="z.B. 09:00" />
<TextBlock Text="Enddatum:" />
<DatePicker x:Name="EnddatumPicker" />
<TextBlock Text="Endzeit (HH:mm):" />
<TextBox x:Name="EndzeitBox" Watermark="z.B. 17:00" />
<TextBlock Text="Mitarbeiter auswählen:" />
<ComboBox x:Name="MitarbeiterDropdown" />
<TextBlock Text="Kommentar:" />
<TextBox x:Name="KommentarBox" AcceptsReturn="True" Height="60" />
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center" Margin="0,10,0,0">
<Button Content="✅ Speichern" Click="SpeichernButton_Click" Width="115" />
<Button Content="⬅ Zurück zum Dashboard" Click="ZurueckButton_Click" Width="150" />
</StackPanel>
<TextBlock x:Name="FeedbackText" Foreground="Red" IsVisible="False" />
</StackPanel>
</UserControl>

View File

@ -1,102 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using ChronoFlow.Model;
using ChronoFlow.Persistence;
namespace ChronoFlow.View.Admin
{
public partial class ProjektErstellenView : UserControl
{
private readonly ViewManager _viewManager;
public ProjektErstellenView(ViewManager viewManager)
{
InitializeComponent();
_viewManager = viewManager;
var dbService = new SqliteZeiterfassungsService();
List<string> mitarbeiter = dbService.LadeAlleMitarbeiterNamen();
// ✅ Nur ItemsSource verwenden, nicht Items
MitarbeiterDropdown.ItemsSource = mitarbeiter;
}
private void SpeichernButton_Click(object? sender, RoutedEventArgs e)
{
string projektname = ProjektnameBox.Text ?? "";
DateTime startdatum = StartdatumPicker.SelectedDate?.Date ?? DateTime.Today;
DateTime enddatum = EnddatumPicker.SelectedDate?.Date ?? DateTime.Today;
string startzeitText = StartzeitBox.Text ?? "00:00";
string endzeitText = EndzeitBox.Text ?? "00:00";
string mitarbeiter = MitarbeiterDropdown.SelectedItem?.ToString() ?? "";
string kommentar = KommentarBox.Text ?? "";
if (string.IsNullOrWhiteSpace(projektname) || string.IsNullOrWhiteSpace(mitarbeiter))
{
FeedbackText.Text = "⚠ Bitte Projektname und Mitarbeiter ausfüllen!";
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
return;
}
if (!TimeSpan.TryParse(startzeitText, out var startzeit))
{
FeedbackText.Text = "⚠ Ungültige Startzeit (Format HH:mm)!";
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
return;
}
if (!TimeSpan.TryParse(endzeitText, out var endzeit))
{
FeedbackText.Text = "⚠ Ungültige Endzeit (Format HH:mm)!";
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
return;
}
DateTime startDateTime = startdatum + startzeit;
DateTime endDateTime = enddatum + endzeit;
var dbService = new SqliteZeiterfassungsService();
// 💡 Achtung: Aktuell speichern wir als Zeiteintrag (kein eigenes Projektmodell!)
dbService.SpeichereEintrag(new Zeiteintrag
{
Mitarbeiter = mitarbeiter,
Projekt = projektname,
Startzeit = startDateTime,
Endzeit = endDateTime,
Kommentar = kommentar,
Erledigt = false
});
FeedbackText.Text = "✅ Projekt erfolgreich gespeichert.";
FeedbackText.Foreground = Brushes.Green;
FeedbackText.IsVisible = true;
// Felder zurücksetzen
ProjektnameBox.Text = "";
KommentarBox.Text = "";
StartdatumPicker.SelectedDate = DateTime.Today;
EnddatumPicker.SelectedDate = DateTime.Today;
StartzeitBox.Text = "09:00";
EndzeitBox.Text = "17:00";
MitarbeiterDropdown.SelectedItem = null;
// 🔄 Dashboard aktualisieren, wenn zurück
if (_viewManager.TryGetView<AdminMainView>("AdminMain", out var adminView))
{
adminView.AktualisiereLetzteProjekte();
}
}
private void ZurueckButton_Click(object? sender, RoutedEventArgs e)
{
_viewManager.Show("AdminMain");
}
}
}

View File

@ -1,9 +1,10 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:themes="clr-namespace:Avalonia.Themes.Fluent;assembly=Avalonia.Themes.Fluent"
x:Class="ChronoFlow.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<themes:FluentTheme />
<FluentTheme />
</Application.Styles>
</Application>

View File

@ -1,31 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>WinExe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.6" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.6" />
<PackageReference Include="MessageBox.Avalonia" Version="0.10.4" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.6" />
<PackageReference Include="Avalonia" Version="11.2.7"/>
<PackageReference Include="Avalonia.Desktop" Version="11.2.7"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.7"/>
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.7"/>
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.7">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ChronoFlow.Controller\ChronoFlow.Controller.csproj" />
<ProjectReference Include="..\ChronoFlow.Persistence\ChronoFlow.Persistence.csproj" />
<ProjectReference Include="..\ChronoFlow.Security\ChronoFlow.Security.csproj" />
<ProjectReference Include="..\ChronoFlow.Model\ChronoFlow.Model.csproj" />
<ProjectReference Include="..\ChronoFlow.Controller\ChronoFlow.Controller.csproj" />
<ProjectReference Include="..\ChronoFlow.Model\ChronoFlow.Model.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="MitarbeiterHinzufuegenView.axaml.cs">
<DependentUpon>MitarbeiterHinzufuegenView.axaml</DependentUpon>
<SubType>Code</SubType>
<Folder Include="Assets\" />
</ItemGroup>
<ItemGroup>
<Compile Update="ZeiterfassungView.axaml.cs">
<DependentUpon>ZeiterfassungView.axaml</DependentUpon>
</Compile>
</ItemGroup>
</Project>

View File

@ -1,20 +1,18 @@
<!-- Datei: View/LoginWindow.axaml -->
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ChronoFlow.View.LoginWindow"
Width="400" Height="300"
Title="ChronoFlow Login">
Title="ChronoFlow Login" Width="400" Height="250" WindowStartupLocation="CenterScreen">
<StackPanel Margin="20" Spacing="10">
<TextBlock Text="ChronoFlow Login" FontSize="24" FontWeight="Bold" HorizontalAlignment="Center" />
<TextBlock Text="ChronoFlow Login" FontWeight="Bold" FontSize="18" HorizontalAlignment="Center"/>
<TextBlock Text="Benutzername" />
<TextBox x:Name="UsernameBox" />
<TextBox x:Name="UsernameBox" Watermark="Benutzername"/>
<TextBlock Text="PasswordBox" FontSize="18" TextAlignment="Center" Margin="0,0,0,10"/>
<TextBox x:Name = "PasswordBox" PasswordChar="*" Text="Enabled"/>
<TextBlock Text="Passwort" />
<TextBox x:Name="PasswordBox" PasswordChar="●" />
<TextBlock x:Name="ErrorText" Foreground="Red" IsVisible="False"/>
<Button Content="Anmelden" Click="LoginButton_Click" HorizontalAlignment="Center" Width="120" Margin="0,10,0,0" />
<TextBlock x:Name="ErrorText" Foreground="Red" IsVisible="False" TextWrapping="Wrap" TextAlignment="Center" />
<Button Content="Anmelden" Click="LoginButton_Click" HorizontalAlignment="Center"/>
</StackPanel>
</Window>

View File

@ -1,11 +1,8 @@
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Interactivity;
using ChronoFlow.Controller;
using ChronoFlow.Persistence;
using ChronoFlow.Security;
using ChronoFlow.View.Security;
namespace ChronoFlow.View
{
@ -14,28 +11,28 @@ namespace ChronoFlow.View
/// </summary>
public partial class LoginWindow : Window
{
private readonly LoginController _loginController;
private LoginController _loginController;
public LoginWindow()
{
InitializeComponent();
_loginController = new LoginController();
InitializeComponent(); // Verbindet XAML mit diesem Code
_loginController = new LoginController(); // Unsere "Logik-Klasse"
var service = new SqliteZeiterfassungsService();
service.ErstelleStandardAdmin();
}
/// <summary>
/// Wird ausgeführt, wenn der Benutzer auf "Anmelden" klickt.
/// </summary>
private async void LoginButton_Click(object? sender, RoutedEventArgs e)
private void LoginButton_Click(object? sender, RoutedEventArgs e)
{
var username = UsernameBox.Text?.Trim();
var password = PasswordBox.Text?.Trim();
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
{
ErrorText.Text = "Bitte Benutzername und Passwort eingeben.";
ErrorText.Text = "Bitte Benutzername und Passwort eingeben";
ErrorText.IsVisible = true;
return;
}
@ -43,80 +40,19 @@ namespace ChronoFlow.View
var service = new SqliteZeiterfassungsService();
var benutzerListe = service.LadeAlleBenutzer();
var matchingUsers = benutzerListe.Where(u => u.Username == username).ToList();
var user = benutzerListe.FirstOrDefault(u => u.Username == username && u.Password == password);
if (matchingUsers.Count == 0)
if (user != null)
{
ErrorText.Text = "Benutzername nicht gefunden.";
ErrorText.IsVisible = true;
return;
}
if (matchingUsers.Count > 1)
{
Console.WriteLine("[WARNUNG] Mehrere Benutzer mit gleichem Namen gefunden! Bitte Datenbank prüfen.");
ErrorText.Text = "Interner Fehler: Mehrere Benutzer mit gleichem Namen.";
ErrorText.IsVisible = true;
return;
}
var user = matchingUsers.First();
Console.WriteLine($"[DEBUG] Benutzer gefunden: {user.Username}");
Console.WriteLine($"[DEBUG] Gespeicherter Hash in DB: {user.Password}");
Console.WriteLine($"[DEBUG] Eingegebenes Passwort: {password}");
bool isMatch = PasswordHasher.VerifyPassword(password, user.Password);
Console.WriteLine($"[DEBUG] Passwortprüfung erfolgreich: {isMatch}");
if (!isMatch)
{
ErrorText.Text = "Falsches Passwort. Bitte erneut versuchen.";
ErrorText.IsVisible = true;
return;
}
// Wenn Passwortänderung erforderlich, Dialog anzeigen
if (user.MussPasswortAendern)
{
var dialog = new PasswortAendernDialog(user);
var neuesPasswort = await dialog.ShowDialog<string>(this);
if (!string.IsNullOrEmpty(neuesPasswort))
{
string neuerHash = PasswordHasher.HashPassword(neuesPasswort);
Console.WriteLine($"[DEBUG] Neues Passwort (klar): {neuesPasswort}");
Console.WriteLine($"[DEBUG] Neuer gespeicherter Hash: {neuerHash}");
user.Password = neuerHash;
user.MussPasswortAendern = false;
service.UpdateBenutzer(user);
Console.WriteLine("✅ Passwort wurde erfolgreich geändert.");
}
else
{
ErrorText.Text = "Sie müssen ein neues Passwort setzen!";
ErrorText.IsVisible = true;
return;
}
}
// 🚀 Login erfolgreich → MainWindow starten
try
{
Console.WriteLine("🚀 Öffne MainWindow...");
// Wenn erfolgreich: öffne das MainWindow
var main = new MainWindow(user);
main.Show();
Console.WriteLine("✅ MainWindow wurde geöffnet.");
this.Close();
Console.WriteLine("✅ LoginWindow wurde geschlossen.");
this.Close(); // Schließe das Login-Fenster
}
catch (Exception ex)
else
{
Console.WriteLine($"[ERROR] MainWindow konnte nicht geöffnet werden: {ex.Message}");
ErrorText.Text = "Interner Fehler beim Starten des Hauptfensters.";
// Wenn fehlgeschlagen: Fehlermeldung anzeigen
ErrorText.Text = "Login fehlgeschlagen. Bitte prüfen Sie Ihre Eingaben.";
ErrorText.IsVisible = true;
}
}

View File

@ -2,12 +2,25 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="600"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ChronoFlow.View.MainWindow"
Title="ChronoFlow">
Title="ChronoFlow.View">
<Grid>
<ContentControl x:Name="ContentArea" />
</Grid>
<SplitView x:Name="PaneView" DisplayMode="CompactInline" IsPaneOpen="True" CompactPaneLength="38" OpenPaneLength="200">
<SplitView.Pane>
<StackPanel>
<Button Content="☰" Click="PaneOpenClose_Click"/>
<Button Content="⏱ Zeiterfassung" Click="Zeiterfassung_Click"/>
<Button Content="📄 Auswertung"/>
<Button Content="👤 Mitarbeiter hinzufügen" Click="MitarbeiterHinzufuegen_Click"/>
<Button Content="⚙ Einstellungen"/>
</StackPanel>
</SplitView.Pane>
<SplitView.Content>
<ContentControl x:Name="ContentArea" />
</SplitView.Content>
</SplitView>
</Window>

View File

@ -1,63 +1,54 @@
using System;
using Avalonia.Controls;
using Avalonia.Interactivity;
using ChronoFlow.Model;
using ChronoFlow.View.Admin;
namespace ChronoFlow.View
namespace ChronoFlow.View;
public partial class MainWindow : Window
{
public partial class MainWindow : Window
private readonly ViewManager _viewManager;
private readonly User _loggedInUser;
public MainWindow(User user)
{
private readonly ViewManager _viewManager;
private readonly User _loggedInUser;
InitializeComponent();
public MainWindow(User user)
_loggedInUser = user;
// ✅ Workaround: Lokale Kopie für Lambda-Nutzung im Register
var currentUser = _loggedInUser;
_viewManager = new ViewManager(ContentArea);
// ✅ Register-Aufruf mit stabiler local variable
_viewManager.Register("Zeiterfassung", () => new ZeiterfassungView(currentUser));
_viewManager.Register("MitarbeiterHinzufuegen", () => new MitarbeiterHinzufuegenView());
// Begrüßungsanzeige
ContentArea.Content = new TextBlock
{
InitializeComponent();
Text = $"Willkommen bei ChronoFlow, {currentUser.Username}!",
FontSize = 24,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
};
_loggedInUser = user;
Console.WriteLine($"[DEBUG] MainWindow gestartet für Benutzer: {_loggedInUser.Username} ({_loggedInUser.Role})");
// Fenstertitel dynamisch setzen
this.Title = $"ChronoFlow - Willkommen {currentUser.Username} ({currentUser.Role})";
}
_viewManager = new ViewManager(ContentArea);
private void PaneOpenClose_Click(object sender, RoutedEventArgs e)
{
PaneView.IsPaneOpen = !PaneView.IsPaneOpen;
}
_viewManager.Register("ProjektErstellen", () => new ProjektErstellenView(_viewManager));
_viewManager.Register("MitarbeiterHinzufuegen", () => new MitarbeiterHinzufuegenView());
_viewManager.Register("AdminMain", () => new AdminMainView(_viewManager));
_viewManager.Register("AlleProjekte", () => new AlleProjekteView(_viewManager));
_viewManager.Register("MitarbeiterListe", () => new MitarbeiterListeView(_viewManager));
_viewManager.Register("AbgeschlosseneProjekte", () => new AbgeschlosseneProjekteView(_viewManager));
// ⏳ später: _viewManager.Register("MitarbeiterMain", () => new MitarbeiterMainView(_viewManager));
private void Zeiterfassung_Click(object? sender, RoutedEventArgs e)
{
_viewManager.Show("Zeiterfassung");
}
if (_loggedInUser.Role == "Admin")
{
_viewManager.Show("AdminMain");
}
else if (_loggedInUser.Role == "Mitarbeiter")
{
_viewManager.Show("Zeiterfassung"); // ⏳ später: MitarbeiterMain
}
this.Title = $"ChronoFlow - Willkommen {_loggedInUser.Username} ({_loggedInUser.Role})";
}
private void Zeiterfassung_Click(object? sender, RoutedEventArgs e)
{
_viewManager.Show("Zeiterfassung");
}
private void MitarbeiterHinzufuegen_Click(object? sender, RoutedEventArgs e)
{
_viewManager.Show("MitarbeiterHinzufuegen");
}
private void AdminDashboard_Click(object? sender, RoutedEventArgs e)
{
_viewManager.Show("AdminMain");
}
public void ShowAdminDashboard()
{
_viewManager.Show("AdminMain");
}
private void MitarbeiterHinzufuegen_Click(object? sender, RoutedEventArgs e)
{
_viewManager.Show("MitarbeiterHinzufuegen");
}
}

View File

@ -17,7 +17,6 @@
<TextBox x:Name="AbteilungBox" Watermark="Abteilung"/>
<Button Content="💾 Speichern" Click="SpeichernButton_Click" HorizontalAlignment="Center"/>
<Button Content="⬅ Zurück zum Dashboard" Click="ZurueckZumDashboard_Click" HorizontalAlignment="Center" Margin="0,10,0,0"/>
<TextBlock x:Name="FeedbackText" Foreground="Green" IsVisible="False" TextAlignment="Center"/>
</StackPanel>
</UserControl>

View File

@ -3,8 +3,8 @@ using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using ChronoFlow.Model;
using Microsoft.Data.Sqlite;
using ChronoFlow.Persistence;
using ChronoFlow.Security;
namespace ChronoFlow.View
{
@ -16,74 +16,63 @@ namespace ChronoFlow.View
}
private void SpeichernButton_Click(object? sender, RoutedEventArgs e)
{
try
{
var service = new SqliteZeiterfassungsService();
string username = UsernameBox.Text?.Trim() ?? "";
string password = PasswordBox.Text?.Trim() ?? "";
string rolle = (RoleBox.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "";
string mitarbeiternummer = MitarbeiternummerBox.Text?.Trim() ?? "";
string abteilung = AbteilungBox.Text?.Trim() ?? "";
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(rolle))
{
try
{
var service = new SqliteZeiterfassungsService();
string username = UsernameBox.Text?.Trim() ?? "";
string password = PasswordBox.Text?.Trim() ?? "";
string rolle = (RoleBox.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "";
string mitarbeiternummer = MitarbeiternummerBox.Text?.Trim() ?? "";
string abteilung = AbteilungBox.Text?.Trim() ?? "";
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(rolle))
{
FeedbackText.Text = "⚠ Bitte alle Pflichtfelder ausfüllen!";
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
return;
}
var neuerBenutzer = new User
{
Username = username,
Password = PasswordHasher.HashPassword(password),
Role = rolle,
Mitarbeiternummer = mitarbeiternummer,
Abteilung = abteilung,
MussPasswortAendern = true
};
service.ErstelleNeuenBenutzer(neuerBenutzer);
FeedbackText.Text = "✅ Mitarbeiter erfolgreich gespeichert.";
FeedbackText.Foreground = Brushes.Green;
FeedbackText.IsVisible = true;
ClearFields();
}
catch (Exception ex)
{
FeedbackText.Text = $"❌ Fehler: {ex.Message}";
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
Console.WriteLine("❌ Ausnahme beim Speichern:");
Console.WriteLine(ex.ToString());
}
FeedbackText.Text = "⚠ Bitte alle Pflichtfelder ausfüllen!";
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
return;
}
private void ClearFields()
// ❗ Hier neue Prüfung, ob Benutzername existiert:
if (service.BenutzernameExistiert(username))
{
UsernameBox.Text = "";
PasswordBox.Text = "";
MitarbeiternummerBox.Text = "";
AbteilungBox.Text = "";
RoleBox.SelectedIndex = -1;
FeedbackText.Text = "⚠ Benutzername existiert bereits!";
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
return;
}
private void ZurueckZumDashboard_Click(object? sender, RoutedEventArgs e)
{
var mainWindow = this.VisualRoot as MainWindow;
if (mainWindow != null)
{
mainWindow.ShowAdminDashboard();
}
else
{
Console.WriteLine("⚠️ MainWindow nicht gefunden!");
}
}
using var connection = new SqliteConnection("Data Source=chrono_data.sb");
connection.Open();
var cmd = connection.CreateCommand();
cmd.CommandText = @"
INSERT INTO Benutzer (Username, Password, Role, Mitarbeiternummer, Abteilung)
VALUES ($Username, $Password, $Role, $Mitarbeiternummer, $Abteilung);";
cmd.Parameters.AddWithValue("$Username", username);
cmd.Parameters.AddWithValue("$Password", password);
cmd.Parameters.AddWithValue("$Role", rolle);
cmd.Parameters.AddWithValue("$Mitarbeiternummer", mitarbeiternummer);
cmd.Parameters.AddWithValue("$Abteilung", abteilung);
cmd.ExecuteNonQuery();
FeedbackText.Text = "✅ Mitarbeiter erfolgreich gespeichert.";
FeedbackText.Foreground = Brushes.Green;
FeedbackText.IsVisible = true;
}
catch (Exception ex)
{
FeedbackText.Text = $"❌ Fehler: {ex.Message}";
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
Console.WriteLine("❌ Ausnahme beim Speichern:");
Console.WriteLine(ex.ToString());
}
}
}
}

View File

@ -1,23 +1,21 @@
using Avalonia;
using System;
using ChronoFlow.Persistence;
namespace ChronoFlow.View;
class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args)
{
// Aufruf des Test-Checkers INSIDE der Main-Methode!
SecurityReferenceTest.TestSecurityReference();
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
}
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}

View File

@ -1,23 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ChronoFlow.View.Security.PasswortAendernDialog"
Width="400" Height="250"
Title="Passwort ändern">
<StackPanel Margin="20" Spacing="10">
<TextBlock x:Name="UsernameTextBlock" Text="Benutzer: " FontSize="16" FontWeight="Bold" />
<TextBlock Text="Neues Passwort:" />
<TextBox x:Name="NeuesPasswortBox" />
<TextBlock Text="Passwort bestätigen:" />
<TextBox x:Name="BestaetigenBox" />
<TextBlock x:Name="FehlerText" Foreground="Red" IsVisible="False" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="10" Margin="0,10,0,0">
<Button Content="💾 Speichern" Click="SpeichernButton_Click" Width="100" />
<Button Content="❌ Abbrechen" Click="AbbrechenButton_Click" Width="100" />
</StackPanel>
</StackPanel>
</Window>

View File

@ -1,46 +0,0 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using ChronoFlow.Model;
namespace ChronoFlow.View.Security;
public partial class PasswortAendernDialog : Window
{
public string NeuesPasswort { get; private set; } = "";
private readonly User _user;
// Konstruktor mit Benutzerobjekt
public PasswortAendernDialog(User user)
{
InitializeComponent();
_user = user;
}
private void SpeichernButton_Click(object? sender, RoutedEventArgs e)
{
var passwort = NeuesPasswortBox.Text?.Trim();
var bestaetigen = BestaetigenBox.Text?.Trim();
if (string.IsNullOrWhiteSpace(passwort) || string.IsNullOrWhiteSpace(bestaetigen))
{
FehlerText.Text = "Bitte alle Felder ausfüllen.";
FehlerText.IsVisible = true;
return;
}
if (passwort != bestaetigen)
{
FehlerText.Text = "Passwörter stimmen nicht überein.";
FehlerText.IsVisible = true;
return;
}
NeuesPasswort = passwort;
Close(NeuesPasswort);
}
private void AbbrechenButton_Click(object? sender, RoutedEventArgs e)
{
Close(null);
}
}

View File

@ -1,62 +1,49 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Microsoft.Data.Sqlite;
namespace ChronoFlow.View
{
///<summary>
/// Verwaltet alle Views der Anwendung und wechselt sie bei Bedarf.
/// </summary>
public class ViewManager
{
private readonly ContentControl _contentControl;
private readonly ContentControl _targetControl;
private readonly Dictionary<string, Func<UserControl>> _registieredViews = new();
// Dictionary speichert die registrierten Views mit ihren Erstellungs-Methoden (Factories)
private readonly Dictionary<string, Func<UserControl>> _views = new();
public ViewManager(ContentControl contentControl)
public ViewManager(ContentControl targetControl)
{
_contentControl = contentControl;
_targetControl = targetControl;
}
/// <summary>
/// Registriert eine View mit einem eindeutigen Namen.
///<summary>
/// Registriert eine View mit einem Namen
/// </summary>
public void Register(string name, Func<UserControl> factory)
public void Show(string name, Func<UserControl> viewFactory)
{
if (!_views.ContainsKey(name))
{
_views[name] = factory;
}
_registieredViews[name] = viewFactory;
}
/// <summary>
/// Zeigt eine registrierte View an.
///<summary>
/// Zeigt die View mit dem gegebenen Namen an.
/// </summary>
public void Show(string name)
{
if (_views.TryGetValue(name, out var factory))
{
var view = factory();
_contentControl.Content = view;
}
if(_registieredViews.TryGetValue(name, out var factory))
_targetControl.Content = factory();
else
{
throw new InvalidOperationException($"View {name} is not registered");
}
}
/// <summary>
/// Holt eine bereits registrierte View als konkreten Typ (z. B. AdminMainView).
/// </summary>
public bool TryGetView<T>(string name, out T? view) where T : class
public void Register(string name, Func<UserControl> viewFactory)
{
if (_views.TryGetValue(name, out var factory))
{
var instance = factory();
view = instance as T;
return view != null;
}
view = null;
return false;
_registieredViews[name] = viewFactory;
}
}
}

View File

@ -0,0 +1,44 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ChronoFlow.View.ZeiterfassungView"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:ChronoFlow.View"
mc:Ignorable="d">
<StackPanel x:Name="EingabePanel" Spacing="10">
<TextBlock Text="Zeiterfassung" FontWeight="Bold" FontSize="20" HorizontalAlignment="Center"/>
<ComboBox x:Name="MitarbeiterBoxDropdown"
Width="250"
PlaceholderText="Mitarbeitername auswählen"
Margin="0,5"/>
<DatePicker x:Name="DatumPicker"/>
<TextBox x:Name="StartzeitBox" Watermark="Startzeit (z.B. 08:00)"/>
<TextBox x:Name="EndzeitBox" Watermark="Endzeit (z.B. 16:30)"/>
<TextBox x:Name="ProjektBox" Watermark="Projektname"/>
<TextBox x:Name="KommentarBox" Watermark="Kommentar (Optional)"/>
<Button Content="Eintrag speichern" Click="SpeichernButton_Click" HorizontalAlignment="Center"/>
<TextBlock x:Name="FeedbackText" Foreground="Green" IsVisible="False"/>
<TextBlock Text="Gespeicherte Einträge:" FontWeight="Bold" Margin="0,20,0,5"></TextBlock>
<ListBox x:Name="Eintragsliste" Margin="0,20,0,0">
<ListBox.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Gray" BorderThickness="1" CornerRadius="4" Padding="8" Margin="4">
<StackPanel>
<!-- Anzeige -->
<TextBlock Text="{Binding}" FontWeight="Bold"></TextBlock>
<!-- Nur für Mitarbeiter sichtbar: Status-Buttons + Kommentar -->
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,5,0,0">
<Button Content="✅" Click="MarkiereErledigt_Click"/>
<Button Content="❌" Click="MarkiereNichtErledigt_Click"/>
</StackPanel>
<TextBox x:Name="KommentarEingabe" Watermark="Kommentar" LostFocus="Kommentar_LostFocus"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,134 @@
using System;
using System.Collections.ObjectModel;
using System.Linq; // Wichtig für .Where und .Select
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using ChronoFlow.Controller;
using ChronoFlow.Model;
using ChronoFlow.Persistence; // Wichtig für SqliteZeiterfassungsService
namespace ChronoFlow.View
{
public partial class ZeiterfassungView : UserControl
{
private readonly ZeiterfassungsController _controller;
private readonly ObservableCollection<Zeiteintrag> _anzeigeEinträge;
private readonly User _user;
public ZeiterfassungView(User user)
{
InitializeComponent();
_user = user;
_controller = new ZeiterfassungsController();
_anzeigeEinträge = new ObservableCollection<Zeiteintrag>();
// ✅ Benutzer aus Datenbank laden und Dropdown füllen
var benutzer = new SqliteZeiterfassungsService().LadeAlleBenutzer();
var nurMitarbeiter = benutzer
.Where(b => b.Role == "Mitarbeiter")
.Select(b => b.Username)
.ToList();
MitarbeiterBoxDropdown.ItemsSource = nurMitarbeiter;
// Einträge aus SQLite laden
var geladeneEintraege = _controller.LadeAlleEintraege();
foreach (var eintrag in geladeneEintraege)
{
_anzeigeEinträge.Add(eintrag);
}
Eintragsliste.ItemsSource = _anzeigeEinträge;
// Eingabeformular nur für Admin sichtbar
if (_user.Role != "Admin" && EingabePanel != null)
{
EingabePanel.IsVisible = false;
}
}
private void SpeichernButton_Click(object? sender, RoutedEventArgs e)
{
try
{
string mitarbeiter = MitarbeiterBoxDropdown.SelectedItem?.ToString() ?? "";
DateTime datum = DatumPicker.SelectedDate?.Date ?? DateTime.Today;
string startText = StartzeitBox.Text ?? "";
string endText = EndzeitBox.Text ?? "";
if (!TimeSpan.TryParse(startText, out TimeSpan startZeit))
{
FeedbackText.Text = "Ungültige Startzeit!";
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
return;
}
if (!TimeSpan.TryParse(endText, out TimeSpan endZeit))
{
FeedbackText.Text = "Ungültige Endzeit!";
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
return;
}
var eintrag = new Zeiteintrag
{
Mitarbeiter = mitarbeiter,
Startzeit = datum.Date + startZeit,
Endzeit = datum.Date + endZeit,
Projekt = ProjektBox.Text,
Kommentar = KommentarBox.Text,
Erledigt = false
};
_controller.SpeichereEintrag(eintrag);
_anzeigeEinträge.Add(eintrag);
FeedbackText.Text = "Eintrag gespeichert.";
FeedbackText.Foreground = Brushes.Green;
FeedbackText.IsVisible = true;
}
catch (Exception ex)
{
FeedbackText.Text = $"Fehler: {ex.Message}";
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
}
}
private void MarkiereErledigt_Click(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.DataContext is Zeiteintrag eintrag)
{
eintrag.Erledigt = true;
RefreshListe();
}
}
private void MarkiereNichtErledigt_Click(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.DataContext is Zeiteintrag eintrag)
{
eintrag.Erledigt = false;
RefreshListe();
}
}
private void Kommentar_LostFocus(object? sender, RoutedEventArgs e)
{
if (sender is TextBox tb && tb.DataContext is Zeiteintrag eintrag)
{
eintrag.MitarbeiterKommentar = tb.Text;
}
}
private void RefreshListe()
{
Eintragsliste.ItemsSource = null;
Eintragsliste.ItemsSource = _anzeigeEinträge;
}
}
}

View File

@ -8,8 +8,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChronoFlow.Persistence", "C
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChronoFlow.Controller", "ChronoFlow.Controller\ChronoFlow.Controller.csproj", "{BCCF491C-6A5D-45E4-B490-C553A550F559}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChronoFlow.Security", "ChronoFlow.Security\ChronoFlow.Security.csproj", "{51F4750C-938D-451F-8E32-B7FB7436D125}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -32,9 +30,5 @@ Global
{BCCF491C-6A5D-45E4-B490-C553A550F559}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BCCF491C-6A5D-45E4-B490-C553A550F559}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BCCF491C-6A5D-45E4-B490-C553A550F559}.Release|Any CPU.Build.0 = Release|Any CPU
{51F4750C-938D-451F-8E32-B7FB7436D125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{51F4750C-938D-451F-8E32-B7FB7436D125}.Debug|Any CPU.Build.0 = Debug|Any CPU
{51F4750C-938D-451F-8E32-B7FB7436D125}.Release|Any CPU.ActiveCfg = Release|Any CPU
{51F4750C-938D-451F-8E32-B7FB7436D125}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -1,5 +0,0 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASqliteCommand_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F7af32a60b614a4736554243e7b8aba5c9a167efe6e7254e6648651482183_003FSqliteCommand_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASqliteException_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F154220569126135ad5d7314bf2bc694d3cf7c95840d481d44f0336f4f1f8e9c_003FSqliteException_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=8201A15C_002D62F0_002D4397_002DA6E3_002DE8B34C171052_002Fd_003AAdmin_002Ff_003AAdminMainView_002Eaxaml_002Fz_003A2_002D0/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=8201A15C_002D62F0_002D4397_002DA6E3_002DE8B34C171052_002Fd_003AAdmin_002Ff_003AProjektErstellenView_002Eaxaml_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>