Compare commits

..

3 Commits

Author SHA1 Message Date
e707deafdf ... 2025-05-05 14:43:58 +02:00
53a3c1b4da Merge remote-tracking branch 'origin/master'
# Conflicts:
#	ChronoFlow.Persistence/SqliteZeiterfassungsService.cs
#	ChronoFlow.View/MitarbeiterHinzufuegenView.axaml.cs
2025-05-05 14:43:41 +02:00
0ffe6da3c6 admin view + secure Login (hash + Salt) 2025-05-05 14:43:11 +02:00
40 changed files with 1691 additions and 481 deletions

View File

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

View File

@ -1,16 +1,26 @@
namespace ChronoFlow.Model namespace ChronoFlow.Model
{ {
///<summary>
/// Repräsentiert einen Benutzer mit Benutzernamen, Passwort und Rolle.
/// </summary>
public class User public class User
{ {
public string Username { get; set; } public string Username { get; set; }
public string Password { get; set; } //vorerst im Klartext, wird später geändert public string Password { get; set; }
public string Role { get; set; } //"Admin" oder "Mitarbeiter" public string Role { get; set; }
public string Mitarbeiternummer { get; set; } = ""; public string Mitarbeiternummer { get; set; }
public string Abteilung { 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 = "";
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,67 @@
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

@ -0,0 +1,26 @@
<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

@ -0,0 +1,49 @@
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

@ -0,0 +1,59 @@
<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

@ -0,0 +1,106 @@
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

@ -0,0 +1,31 @@
<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

@ -0,0 +1,89 @@
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

@ -0,0 +1,15 @@
<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

@ -0,0 +1,27 @@
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

@ -0,0 +1,31 @@
<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

@ -0,0 +1,41 @@
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

@ -0,0 +1,29 @@
<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

@ -0,0 +1,143 @@
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

@ -0,0 +1,28 @@
<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

@ -0,0 +1,47 @@
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

@ -0,0 +1,39 @@
<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

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

View File

@ -1,37 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<OutputType>WinExe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.7"/> <PackageReference Include="Avalonia" Version="11.0.6" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.7"/> <PackageReference Include="Avalonia.Desktop" Version="11.0.6" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.7"/> <PackageReference Include="MessageBox.Avalonia" Version="0.10.4" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.7"/> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.6" />
<!--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>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ChronoFlow.Controller\ChronoFlow.Controller.csproj" /> <ProjectReference Include="..\ChronoFlow.Controller\ChronoFlow.Controller.csproj" />
<ProjectReference Include="..\ChronoFlow.Model\ChronoFlow.Model.csproj" /> <ProjectReference Include="..\ChronoFlow.Persistence\ChronoFlow.Persistence.csproj" />
<ProjectReference Include="..\ChronoFlow.Security\ChronoFlow.Security.csproj" />
<ProjectReference Include="..\ChronoFlow.Model\ChronoFlow.Model.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Assets\" /> <Compile Update="MitarbeiterHinzufuegenView.axaml.cs">
</ItemGroup> <DependentUpon>MitarbeiterHinzufuegenView.axaml</DependentUpon>
<SubType>Code</SubType>
<ItemGroup>
<Compile Update="ZeiterfassungView.axaml.cs">
<DependentUpon>ZeiterfassungView.axaml</DependentUpon>
</Compile> </Compile>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

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

View File

@ -1,8 +1,11 @@
using System;
using System.Linq; using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using ChronoFlow.Controller; using ChronoFlow.Controller;
using ChronoFlow.Persistence; using ChronoFlow.Persistence;
using ChronoFlow.Security;
using ChronoFlow.View.Security;
namespace ChronoFlow.View namespace ChronoFlow.View
{ {
@ -11,28 +14,28 @@ namespace ChronoFlow.View
/// </summary> /// </summary>
public partial class LoginWindow : Window public partial class LoginWindow : Window
{ {
private LoginController _loginController; private readonly LoginController _loginController;
public LoginWindow() public LoginWindow()
{ {
InitializeComponent(); // Verbindet XAML mit diesem Code InitializeComponent();
_loginController = new LoginController(); // Unsere "Logik-Klasse" _loginController = new LoginController();
var service = new SqliteZeiterfassungsService(); var service = new SqliteZeiterfassungsService();
service.ErstelleStandardAdmin(); service.ErstelleStandardAdmin();
} }
/// <summary> /// <summary>
/// Wird ausgeführt, wenn der Benutzer auf "Anmelden" klickt. /// Wird ausgeführt, wenn der Benutzer auf "Anmelden" klickt.
/// </summary> /// </summary>
private void LoginButton_Click(object? sender, RoutedEventArgs e) private async void LoginButton_Click(object? sender, RoutedEventArgs e)
{ {
var username = UsernameBox.Text?.Trim(); var username = UsernameBox.Text?.Trim();
var password = PasswordBox.Text?.Trim(); var password = PasswordBox.Text?.Trim();
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
{ {
ErrorText.Text = "Bitte Benutzername und Passwort eingeben"; ErrorText.Text = "Bitte Benutzername und Passwort eingeben.";
ErrorText.IsVisible = true; ErrorText.IsVisible = true;
return; return;
} }
@ -40,19 +43,80 @@ namespace ChronoFlow.View
var service = new SqliteZeiterfassungsService(); var service = new SqliteZeiterfassungsService();
var benutzerListe = service.LadeAlleBenutzer(); var benutzerListe = service.LadeAlleBenutzer();
var user = benutzerListe.FirstOrDefault(u => u.Username == username && u.Password == password); var matchingUsers = benutzerListe.Where(u => u.Username == username).ToList();
if (user != null) if (matchingUsers.Count == 0)
{ {
// Wenn erfolgreich: öffne das MainWindow 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...");
var main = new MainWindow(user); var main = new MainWindow(user);
main.Show(); main.Show();
this.Close(); // Schließe das Login-Fenster Console.WriteLine("✅ MainWindow wurde geöffnet.");
this.Close();
Console.WriteLine("✅ LoginWindow wurde geschlossen.");
} }
else catch (Exception ex)
{ {
// Wenn fehlgeschlagen: Fehlermeldung anzeigen Console.WriteLine($"[ERROR] MainWindow konnte nicht geöffnet werden: {ex.Message}");
ErrorText.Text = "Login fehlgeschlagen. Bitte prüfen Sie Ihre Eingaben."; ErrorText.Text = "Interner Fehler beim Starten des Hauptfensters.";
ErrorText.IsVisible = true; ErrorText.IsVisible = true;
} }
} }

View File

@ -2,25 +2,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="600"
x:Class="ChronoFlow.View.MainWindow" x:Class="ChronoFlow.View.MainWindow"
Title="ChronoFlow.View"> Title="ChronoFlow">
<SplitView x:Name="PaneView" DisplayMode="CompactInline" IsPaneOpen="True" CompactPaneLength="38" OpenPaneLength="200"> <Grid>
<ContentControl x:Name="ContentArea" />
</Grid>
<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> </Window>

View File

@ -1,54 +1,63 @@
using System;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using ChronoFlow.Model; using ChronoFlow.Model;
using ChronoFlow.View.Admin;
namespace ChronoFlow.View; namespace ChronoFlow.View
public partial class MainWindow : Window
{ {
private readonly ViewManager _viewManager; public partial class MainWindow : Window
private readonly User _loggedInUser;
public MainWindow(User user)
{ {
InitializeComponent(); private readonly ViewManager _viewManager;
private readonly User _loggedInUser;
_loggedInUser = user; public MainWindow(User 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
{ {
Text = $"Willkommen bei ChronoFlow, {currentUser.Username}!", InitializeComponent();
FontSize = 24,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
};
// Fenstertitel dynamisch setzen _loggedInUser = user;
this.Title = $"ChronoFlow - Willkommen {currentUser.Username} ({currentUser.Role})"; Console.WriteLine($"[DEBUG] MainWindow gestartet für Benutzer: {_loggedInUser.Username} ({_loggedInUser.Role})");
}
private void PaneOpenClose_Click(object sender, RoutedEventArgs e) _viewManager = new ViewManager(ContentArea);
{
PaneView.IsPaneOpen = !PaneView.IsPaneOpen;
}
private void Zeiterfassung_Click(object? sender, RoutedEventArgs e) _viewManager.Register("ProjektErstellen", () => new ProjektErstellenView(_viewManager));
{ _viewManager.Register("MitarbeiterHinzufuegen", () => new MitarbeiterHinzufuegenView());
_viewManager.Show("Zeiterfassung"); _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 MitarbeiterHinzufuegen_Click(object? sender, RoutedEventArgs e) if (_loggedInUser.Role == "Admin")
{ {
_viewManager.Show("MitarbeiterHinzufuegen"); _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");
}
} }
} }

View File

@ -17,6 +17,7 @@
<TextBox x:Name="AbteilungBox" Watermark="Abteilung"/> <TextBox x:Name="AbteilungBox" Watermark="Abteilung"/>
<Button Content="💾 Speichern" Click="SpeichernButton_Click" HorizontalAlignment="Center"/> <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"/> <TextBlock x:Name="FeedbackText" Foreground="Green" IsVisible="False" TextAlignment="Center"/>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

View File

@ -3,8 +3,8 @@ using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using ChronoFlow.Model; using ChronoFlow.Model;
using Microsoft.Data.Sqlite;
using ChronoFlow.Persistence; using ChronoFlow.Persistence;
using ChronoFlow.Security;
namespace ChronoFlow.View namespace ChronoFlow.View
{ {
@ -16,63 +16,74 @@ namespace ChronoFlow.View
} }
private void SpeichernButton_Click(object? sender, RoutedEventArgs e) 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))
{ {
FeedbackText.Text = "⚠ Bitte alle Pflichtfelder ausfüllen!"; try
FeedbackText.Foreground = Brushes.Red; {
FeedbackText.IsVisible = true; var service = new SqliteZeiterfassungsService();
return;
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());
}
} }
// ❗ Hier neue Prüfung, ob Benutzername existiert: private void ClearFields()
if (service.BenutzernameExistiert(username))
{ {
FeedbackText.Text = "⚠ Benutzername existiert bereits!"; UsernameBox.Text = "";
FeedbackText.Foreground = Brushes.Red; PasswordBox.Text = "";
FeedbackText.IsVisible = true; MitarbeiternummerBox.Text = "";
return; AbteilungBox.Text = "";
RoleBox.SelectedIndex = -1;
} }
using var connection = new SqliteConnection("Data Source=chrono_data.sb"); private void ZurueckZumDashboard_Click(object? sender, RoutedEventArgs e)
connection.Open(); {
var mainWindow = this.VisualRoot as MainWindow;
var cmd = connection.CreateCommand(); if (mainWindow != null)
cmd.CommandText = @" {
INSERT INTO Benutzer (Username, Password, Role, Mitarbeiternummer, Abteilung) mainWindow.ShowAdminDashboard();
VALUES ($Username, $Password, $Role, $Mitarbeiternummer, $Abteilung);"; }
else
cmd.Parameters.AddWithValue("$Username", username); {
cmd.Parameters.AddWithValue("$Password", password); Console.WriteLine("⚠️ MainWindow nicht gefunden!");
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,21 +1,23 @@
using Avalonia; using Avalonia;
using System; using System;
using ChronoFlow.Persistence;
namespace ChronoFlow.View; namespace ChronoFlow.View;
class Program 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] [STAThread]
public static void Main(string[] args) => BuildAvaloniaApp() public static void Main(string[] args)
.StartWithClassicDesktopLifetime(args); {
// Aufruf des Test-Checkers INSIDE der Main-Methode!
SecurityReferenceTest.TestSecurityReference();
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp() public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>() => AppBuilder.Configure<App>()
.UsePlatformDetect() .UsePlatformDetect()
.WithInterFont()
.LogToTrace(); .LogToTrace();
} }

View File

@ -0,0 +1,23 @@
<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

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

View File

@ -1,44 +0,0 @@
<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

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

View File

@ -0,0 +1,5 @@
<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>