Final Changes

This commit is contained in:
Viperion 2025-06-29 17:50:02 +02:00
parent d18df4600a
commit 146aba1157
14 changed files with 444 additions and 214 deletions

View File

@ -1,18 +1,70 @@
using System;
namespace ChronoFlow.Model namespace ChronoFlow.Model
{ {
/// <summary>
/// Repräsentiert einen Benutzer im System (Mitarbeiter oder Admin).
/// Wird für Authentifizierung, Rechteverwaltung und Zuordnung verwendet.
/// </summary>
public class User public class User
{ {
/// <summary>
/// Der Benutzername, der zur Anmeldung verwendet wird.
/// </summary>
public string Username { get; set; } public string Username { get; set; }
/// <summary>
/// Das gehashte Passwort des Benutzers.
/// </summary>
public string Password { get; set; } public string Password { get; set; }
/// <summary>
/// Rolle des Benutzers, z.B. "Admin" oder "Mitarbeiter".
/// </summary>
public string Role { get; set; } public string Role { get; set; }
/// <summary>
/// Interne Mitarbeiternummer, optional.
/// </summary>
public string Mitarbeiternummer { get; set; } public string Mitarbeiternummer { get; set; }
/// <summary>
/// Abteilung des Mitarbeiters, z.B. „Produktion“, „IT“, etc.
/// </summary>
public string Abteilung { get; set; } public string Abteilung { get; set; }
/// <summary>
/// Primärschlüssel in der Datenbank.
/// </summary>
public int Id { get; set; } public int Id { get; set; }
public string OriginalUsername { get; set; } // wichtig für Updates
/// <summary>
/// Der ursprüngliche Benutzername wird z.B. für Updates verwendet,
/// um Namensänderungen korrekt zu verarbeiten.
/// </summary>
public string OriginalUsername { get; set; }
/// <summary>
/// Gibt an, ob der Benutzer beim nächsten Login sein Passwort ändern muss.
/// Wird z.B. bei neu erstellten Konten gesetzt.
/// </summary>
public bool MussPasswortAendern { get; set; } public bool MussPasswortAendern { get; set; }
/// <summary>
/// Letzter erfolgreicher Login des Benutzers.
/// Dient z.B. zur Änderungsverfolgung von Projekten seit dem letzten Login.
/// </summary>
public DateTime LetzterLogin { get; set; } = DateTime.MinValue;
/// <summary>
/// Vorheriger Login-Zeitpunkt wird beim nächsten Login zu LetzterLogin verschoben.
/// Ermöglicht die Erkennung von Änderungen zwischen zwei Sessions.
/// </summary>
public DateTime VorletzterLogin { get; set; }
/// <summary>
/// Konstruktor initialisiert leere Zeichenketten zur Vermeidung von Nullwerten.
/// </summary>
public User() public User()
{ {
Username = ""; Username = "";
@ -22,7 +74,5 @@ namespace ChronoFlow.Model
Abteilung = ""; Abteilung = "";
OriginalUsername = ""; OriginalUsername = "";
} }
public DateTime LetzterLogin { get; set; } = DateTime.MinValue;
public DateTime VorletzterLogin { get; set; }
} }
} }

View File

@ -2,25 +2,53 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:model1="clr-namespace:ChronoFlow.Model;assembly=ChronoFlow.Model" xmlns:model1="clr-namespace:ChronoFlow.Model;assembly=ChronoFlow.Model"
x:Class="ChronoFlow.View.Admin.AbgeschlosseneProjekteView"> x:Class="ChronoFlow.View.Admin.AbgeschlosseneProjekteView">
<!-- Haupt-Layout mit vier Zeilen: Titel, Suchfeld, Liste, Zurück-Button -->
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="20"> <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"/> <!-- 🔷 Überschrift -->
<TextBlock Grid.Row="0"
Text="Abgeschlossene Projekte"
FontSize="20"
FontWeight="Bold"
HorizontalAlignment="Center"
Margin="0,0,0,10"/>
<!-- 🔍 Suchfeld zum Filtern der Projekte (nach Name oder Mitarbeiter) -->
<TextBox Grid.Row="1"
x:Name="Suchfeld"
Watermark="🔍 Nach Projekt oder Mitarbeiter suchen..."
KeyUp="Suchfeld_KeyUp"
Margin="0,0,0,10"/>
<!-- 📋 Scrollbare Liste abgeschlossener Projekte -->
<ScrollViewer Grid.Row="2"> <ScrollViewer Grid.Row="2">
<ListBox x:Name="AbgeschlosseneListe"> <ListBox x:Name="AbgeschlosseneListe">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<!-- Darstellung jedes abgeschlossenen Zeiteintrags -->
<DataTemplate DataType="{x:Type model1:Zeiteintrag}"> <DataTemplate DataType="{x:Type model1:Zeiteintrag}">
<StackPanel Orientation="Horizontal" Spacing="10" Margin="5"> <StackPanel Orientation="Horizontal" Spacing="10" Margin="5">
<!-- 📌 Projektname -->
<TextBlock Text="{Binding Projekt}" Width="150"/> <TextBlock Text="{Binding Projekt}" Width="150"/>
<!-- 👤 Mitarbeitender -->
<TextBlock Text="{Binding Mitarbeiter}" Width="150"/> <TextBlock Text="{Binding Mitarbeiter}" Width="150"/>
<!-- ⏰ Endzeit -->
<TextBlock Text="{Binding Endzeit}" Width="150"/> <TextBlock Text="{Binding Endzeit}" Width="150"/>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
</ScrollViewer> </ScrollViewer>
<Button Grid.Row="3" Content="⬅ Zurück zum Dashboard" Click="ZurueckButton_Click" HorizontalAlignment="Center" Margin="0,10,0,0"/> <!-- 🔙 Zurück-Button zum Admin-Dashboard -->
<Button Grid.Row="3"
Content="⬅ Zurück zum Dashboard"
Click="ZurueckButton_Click"
HorizontalAlignment="Center"
Margin="0,10,0,0"/>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -99,7 +99,7 @@ namespace ChronoFlow.View.Admin
} }
/// <summary> /// <summary>
/// Wird z.B. vom ProjektErstellenView aufgerufen, um die Liste neu zu laden. /// Wird z.B. vom ProjektErstellenView aufgerufen, um die Liste neu zu laden.
/// </summary> /// </summary>
public void AktualisiereLetzteProjekte() public void AktualisiereLetzteProjekte()
{ {

View File

@ -4,12 +4,30 @@
Width="400" Height="200" Width="400" Height="200"
Title="Bestätigung"> Title="Bestätigung">
<!-- 📦 Haupt-Layout: vertikale Anordnung mit Abstand -->
<StackPanel Margin="20" Spacing="10"> <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"> <!-- ❓ Frage-Text (wird im Code gesetzt, z.B. "Möchten Sie das wirklich löschen?") -->
<Button Content="✅ Ja" Width="80" Click="JaButton_Click" /> <TextBlock x:Name="FrageText"
<Button Content="❌ Nein" Width="80" Click="NeinButton_Click" /> Text="Sind Sie sicher?"
FontSize="16"
FontWeight="Bold"
TextWrapping="Wrap" />
<!-- 🔘 Buttons für Ja / Nein -->
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="10">
<!-- ✅ Bestätigen -->
<Button Content="✅ Ja"
Width="80"
Click="JaButton_Click" />
<!-- ❌ Abbrechen -->
<Button Content="❌ Nein"
Width="80"
Click="NeinButton_Click" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</Window> </Window>

View File

@ -2,28 +2,63 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:model1="clr-namespace:ChronoFlow.Model;assembly=ChronoFlow.Model" xmlns:model1="clr-namespace:ChronoFlow.Model;assembly=ChronoFlow.Model"
x:Class="ChronoFlow.View.Admin.MitarbeiterListeView"> x:Class="ChronoFlow.View.Admin.MitarbeiterListeView">
<!-- 🧱 Hauptlayout mit 4 Zeilen: Titel, Suchfeld, Liste, Zurück-Button -->
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="20"> <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"/> <!-- 🧑‍🤝‍🧑 Titel der Ansicht -->
<TextBlock Grid.Row="0"
Text="Alle Mitarbeiter"
FontSize="20"
FontWeight="Bold"
HorizontalAlignment="Center"
Margin="0,0,0,10"/>
<!-- 🔍 Suchfeld zur Live-Suche nach Mitarbeitern (KeyUp-Event im CodeBehind) -->
<TextBox Grid.Row="1"
x:Name="Suchfeld"
Watermark="🔍 Suchen..."
KeyUp="Suchfeld_KeyUp"
Margin="0,0,0,10"/>
<!-- 📜 Scrollbarer Bereich für Mitarbeitereinträge -->
<ScrollViewer Grid.Row="2"> <ScrollViewer Grid.Row="2">
<ListBox x:Name="MitarbeiterListe"> <ListBox x:Name="MitarbeiterListe">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<!-- 🎨 Vorlage für jeden einzelnen Mitarbeiter (aus Model.User) -->
<DataTemplate DataType="{x:Type model1:User}"> <DataTemplate DataType="{x:Type model1:User}">
<StackPanel Orientation="Horizontal" Spacing="10" Margin="5"> <StackPanel Orientation="Horizontal"
Spacing="10"
Margin="5">
<!-- 🧾 Benutzerinformationen -->
<TextBlock Text="{Binding Username}" Width="150"/> <TextBlock Text="{Binding Username}" Width="150"/>
<TextBlock Text="{Binding Abteilung}" Width="150"/> <TextBlock Text="{Binding Abteilung}" Width="150"/>
<TextBlock Text="{Binding Mitarbeiternummer}" 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"/> <!-- ✏ Aktionen für jeden Mitarbeiter -->
<Button Content="🔑 Passwort zurücksetzen" Tag="{Binding}" Click="PasswortReset_Click" /> <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> </StackPanel>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
</ScrollViewer> </ScrollViewer>
<Button Grid.Row="3" Content="⬅ Zurück zum Dashboard" Click="ZurueckButton_Click" HorizontalAlignment="Center" Margin="0,10,0,0"/> <!-- 🔙 Zurück zum Admin-Dashboard -->
<Button Grid.Row="3"
Content="⬅ Zurück zum Dashboard"
Click="ZurueckButton_Click"
HorizontalAlignment="Center"
Margin="0,10,0,0"/>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -2,70 +2,64 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ChronoFlow.View.Admin.ProjektErstellenView"> x:Class="ChronoFlow.View.Admin.ProjektErstellenView">
<!-- Scrollfähiger Bereich für kleinere Fenster --> <!-- Gesamter Inhalt ist scrollbar -->
<ScrollViewer> <ScrollViewer Padding="10">
<StackPanel Margin="25" Spacing="15"> <StackPanel Margin="25" Spacing="15">
<!-- Titelüberschrift --> <!-- Titel -->
<TextBlock Text="Neues Projekt erstellen" <TextBlock Text="Neues Projekt erstellen"
FontSize="20" FontSize="20"
FontWeight="Bold" FontWeight="Bold"
HorizontalAlignment="Center" /> HorizontalAlignment="Center" />
<!-- Eingabefeld: Projektname --> <!-- Projektname -->
<TextBlock Text="Projektname:" /> <TextBlock Text="Projektname:" />
<TextBox x:Name="ProjektnameBox" /> <TextBox x:Name="ProjektnameBox" />
<!-- Startdatum auswählen --> <!-- Startzeit -->
<TextBlock Text="Startdatum:" /> <TextBlock Text="Startdatum:" />
<DatePicker x:Name="StartdatumPicker" /> <DatePicker x:Name="StartdatumPicker" />
<!-- Startzeit im Format HH:mm -->
<TextBlock Text="Startzeit (HH:mm):" /> <TextBlock Text="Startzeit (HH:mm):" />
<TextBox x:Name="StartzeitBox" Watermark="z.B. 09:00" /> <TextBox x:Name="StartzeitBox" Watermark="z.B. 09:00" />
<!-- Enddatum auswählen --> <!-- Endzeit -->
<TextBlock Text="Enddatum:" /> <TextBlock Text="Enddatum:" />
<DatePicker x:Name="EnddatumPicker" /> <DatePicker x:Name="EnddatumPicker" />
<!-- Endzeit im Format HH:mm -->
<TextBlock Text="Endzeit (HH:mm):" /> <TextBlock Text="Endzeit (HH:mm):" />
<TextBox x:Name="EndzeitBox" Watermark="z.B. 17:00" /> <TextBox x:Name="EndzeitBox" Watermark="z.B. 17:00" />
<!-- Projektleiter über Dropdown zuweisen --> <!-- Projektleiter -->
<TextBlock Text="Projektleiter auswählen:" /> <TextBlock Text="Projektleiter auswählen:" />
<ComboBox x:Name="ProjektleiterDropdown" /> <ComboBox x:Name="ProjektleiterDropdown" />
<StackPanel> <!-- Mitarbeiterauswahl -->
<TextBlock Text="Mitarbeiter auswählen:" Margin="0,10,0,5" /> <TextBlock Text="Mitarbeitende auswählen:" Margin="0,10,0,5" />
<Expander Header=" Mitarbeitende auswählen" Background="#222" Foreground="White">
<ListBox x:Name="MitarbeiterListBox" <StackPanel Orientation="Horizontal" Spacing="10" Margin="0,0,0,5">
SelectionMode="Multiple" <Button Content="✅ Alle auswählen" Click="AlleMitarbeiterAuswaehlen_Click" />
Height="250" <Button Content="❌ Keine auswählen" Click="KeineMitarbeiterAuswaehlen_Click" />
MinWidth="300"
BorderBrush="Gray"
BorderThickness="1"
Background="#333"
Foreground="White"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Padding="4" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Expander>
</StackPanel> </StackPanel>
<!-- Kommentarbereich --> <!-- Scrollbarer Kartenbereich mit Checkboxen -->
<ScrollViewer Height="240"
VerticalScrollBarVisibility="Auto"
Background="#2a2a2a"
Margin="0,0,0,10"
Padding="10,10,10,-17"
CornerRadius="8">
<ListBox x:Name="MitarbeiterCheckList"
BorderThickness="0"
SelectionMode="Multiple" />
</ScrollViewer>
<!-- Kommentar -->
<TextBlock Text="Kommentar:" /> <TextBlock Text="Kommentar:" />
<TextBox x:Name="KommentarBox" AcceptsReturn="True" Height="80" /> <TextBox x:Name="KommentarBox" AcceptsReturn="True" Height="80" />
<!-- Hinweise zur Deadline-Farbe --> <!-- Farblegende für Deadlines -->
<Border Background="#111111" <Border Background="#111111" CornerRadius="5" Padding="10" Margin="0,10,0,0">
CornerRadius="5"
Padding="10"
Margin="0,10,0,0">
<StackPanel Spacing="5"> <StackPanel Spacing="5">
<StackPanel Orientation="Horizontal" Spacing="5"> <StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock Text="🛈" FontWeight="Bold" /> <TextBlock Text="🛈" FontWeight="Bold" />
@ -77,28 +71,14 @@
</StackPanel> </StackPanel>
</Border> </Border>
<!-- Aktions-Buttons --> <!-- Aktionsbuttons -->
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center" Margin="0,10,0,0">
Spacing="10" <Button Content="✅ Speichern" Click="SpeichernButton_Click" Width="115" Height="36" />
HorizontalAlignment="Center" <Button Content="🧪 3 Demo-Projekte (rot/gelb/grün)" Click="DemoProjekteButton_Click" Width="250" Height="36" />
Margin="0,10,0,0"> <Button Content="⬅ Zurück zum Dashboard" Click="ZurueckButton_Click" Width="180" Height="36" />
<Button Content="✅ Speichern"
Click="SpeichernButton_Click"
Width="115"
Height="36" />
<Button Content="🧪 3 Demo-Projekte (rot/gelb/grün)"
Click="DemoProjekteButton_Click"
Width="250"
Height="36" />
<Button Content="⬅ Zurück zum Dashboard"
Click="ZurueckButton_Click"
Width="180"
Height="36" />
</StackPanel> </StackPanel>
<!-- Optionaler Fehler-/Hinweistext --> <!-- Feedbackbereich -->
<TextBlock x:Name="FeedbackText" <TextBlock x:Name="FeedbackText"
Foreground="Red" Foreground="Red"
IsVisible="False" IsVisible="False"

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
@ -16,6 +17,7 @@ namespace ChronoFlow.View.Admin;
public partial class ProjektErstellenView : UserControl public partial class ProjektErstellenView : UserControl
{ {
private readonly ViewManager _viewManager; private readonly ViewManager _viewManager;
private readonly Dictionary<string, CheckBox> _mitarbeiterCheckBoxMap = new();
public ProjektErstellenView() : this(new ViewManager(new ContentControl())) public ProjektErstellenView() : this(new ViewManager(new ContentControl()))
{ {
@ -30,114 +32,153 @@ public partial class ProjektErstellenView : UserControl
try try
{ {
var dbService = new SqliteZeiterfassungsService(); var dbService = new SqliteZeiterfassungsService();
List<string> mitarbeiter = dbService.LadeAlleMitarbeiterNamen(); var mitarbeiter = dbService.LadeAlleMitarbeiterNamen();
// 🔍 Konsolenausgabe zur Kontrolle
if (mitarbeiter is { Count: > 0 }) if (mitarbeiter is { Count: > 0 })
{ {
Console.WriteLine("✅ Mitarbeitende erfolgreich geladen:"); Console.WriteLine("✅ Mitarbeitende erfolgreich geladen:");
foreach (var name in mitarbeiter)
Console.WriteLine("- " + name);
ProjektleiterDropdown.ItemsSource = mitarbeiter;
// Liste manuell aufbauen und Checkboxen erzeugen
_mitarbeiterCheckBoxMap.Clear();
var checkBoxen = new List<CheckBox>();
foreach (var name in mitarbeiter) foreach (var name in mitarbeiter)
{ {
Console.WriteLine($"- {name}"); var cb = new CheckBox
{
Content = name,
Foreground = Brushes.White,
FontSize = 14,
Margin = new Thickness(4)
};
_mitarbeiterCheckBoxMap[name] = cb;
checkBoxen.Add(cb);
} }
// ItemsSource der ListBox mit den Checkboxen befüllen
MitarbeiterCheckList.ItemsSource = checkBoxen;
} }
else else
{ {
Console.WriteLine("⚠️ Achtung: Mitarbeitendenliste ist leer."); Console.WriteLine("⚠️ Achtung: Mitarbeitendenliste ist leer.");
} }
// Items in GUI setzen
MitarbeiterListBox!.ItemsSource = mitarbeiter;
ProjektleiterDropdown!.ItemsSource = mitarbeiter;
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"❌ Fehler beim Laden der Mitarbeiterauswahl: {ex.Message}"); Console.WriteLine($"❌ Fehler beim Laden der Mitarbeiterauswahl: {ex.Message}");
FeedbackText!.Text = "⚠ Fehler beim Laden der Mitarbeiter."; FeedbackText.Text = "⚠ Fehler beim Laden der Mitarbeiter.";
FeedbackText.Foreground = Brushes.Red; FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true; FeedbackText.IsVisible = true;
} }
} }
private List<string> ErmittleAusgewaehlteMitarbeiter()
{
return _mitarbeiterCheckBoxMap
.Where(kvp => kvp.Value.IsChecked == true)
.Select(kvp => kvp.Key)
.ToList();
}
private void AlleMitarbeiterAuswaehlen_Click(object? sender, RoutedEventArgs e)
{
foreach (var cb in _mitarbeiterCheckBoxMap.Values)
{
cb.IsChecked = true;
}
}
private void KeineMitarbeiterAuswaehlen_Click(object? sender, RoutedEventArgs e)
{
foreach (var cb in _mitarbeiterCheckBoxMap.Values)
{
cb.IsChecked = false;
}
}
private void SpeichernButton_Click(object? sender, RoutedEventArgs e) private void SpeichernButton_Click(object? sender, RoutedEventArgs e)
{
FeedbackText.IsVisible = false;
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 kommentar = KommentarBox.Text ?? "";
string projektleiter = ProjektleiterDropdown.SelectedItem?.ToString() ?? "";
var ausgewaehlteMitarbeiter = MitarbeiterListBox?.SelectedItems?.Cast<string>().ToList() ?? new List<string>();
if (string.IsNullOrWhiteSpace(projektname) || ausgewaehlteMitarbeiter.Count == 0 || string.IsNullOrWhiteSpace(projektleiter))
{ {
FeedbackText.Text = "⚠ Bitte alle Pflichtfelder ausfüllen (Projektname, mindestens ein Mitarbeiter, Projektleiter)!"; FeedbackText.IsVisible = false;
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
return;
}
if (!TimeSpan.TryParse(startzeitText, out var startzeit) || !TimeSpan.TryParse(endzeitText, out var endzeit)) string projektname = ProjektnameBox.Text ?? "";
{ DateTime startdatum = StartdatumPicker.SelectedDate?.Date ?? DateTime.Today;
FeedbackText.Text = "⚠ Ungültige Zeitangaben (Format HH:mm)!"; DateTime enddatum = EnddatumPicker.SelectedDate?.Date ?? DateTime.Today;
FeedbackText.Foreground = Brushes.Red; string startzeitText = StartzeitBox.Text ?? "00:00";
FeedbackText.IsVisible = true; string endzeitText = EndzeitBox.Text ?? "00:00";
return; string kommentar = KommentarBox.Text ?? "";
} string projektleiter = ProjektleiterDropdown.SelectedItem?.ToString() ?? "";
DateTime startDateTime = startdatum + startzeit; var ausgewaehlteMitarbeiter = ErmittleAusgewaehlteMitarbeiter();
DateTime endDateTime = enddatum + endzeit;
try if (string.IsNullOrWhiteSpace(projektname) || ausgewaehlteMitarbeiter.Count == 0 || string.IsNullOrWhiteSpace(projektleiter))
{
var dbService = new SqliteZeiterfassungsService();
foreach (var einzelnerMitarbeiter in ausgewaehlteMitarbeiter)
{ {
dbService.SpeichereEintrag(new Zeiteintrag FeedbackText.Text = "⚠ Bitte alle Pflichtfelder ausfüllen (Projektname, mindestens ein Mitarbeiter, Projektleiter)!";
{ FeedbackText.Foreground = Brushes.Red;
Mitarbeiter = einzelnerMitarbeiter, FeedbackText.IsVisible = true;
Projekt = projektname, return;
Startzeit = startDateTime,
Endzeit = endDateTime,
Kommentar = kommentar,
Projektleiter = projektleiter,
Erledigt = false
});
} }
FeedbackText.Text = "✅ Projekt erfolgreich gespeichert."; if (!TimeSpan.TryParse(startzeitText, out var startzeit) || !TimeSpan.TryParse(endzeitText, out var endzeit))
FeedbackText.Foreground = Brushes.Green; {
FeedbackText.IsVisible = true; FeedbackText.Text = "⚠ Ungültige Zeitangaben (Format HH:mm)!";
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
return;
}
ProjektnameBox.Text = ""; DateTime startDateTime = startdatum + startzeit;
KommentarBox.Text = ""; DateTime endDateTime = enddatum + endzeit;
StartdatumPicker.SelectedDate = DateTime.Today;
EnddatumPicker.SelectedDate = DateTime.Today;
StartzeitBox.Text = "09:00";
EndzeitBox.Text = "17:00";
MitarbeiterListBox.SelectedItems?.Clear();
MitarbeiterListBox.SelectedIndex = -1;
ProjektleiterDropdown.SelectedItem = null;
if (_viewManager.TryGetView<AdminMainView>("AdminMain", out var adminView) && adminView != null) try
adminView.AktualisiereLetzteProjekte(); {
var dbService = new SqliteZeiterfassungsService();
foreach (var name in ausgewaehlteMitarbeiter)
{
dbService.SpeichereEintrag(new Zeiteintrag
{
Mitarbeiter = name,
Projekt = projektname,
Startzeit = startDateTime,
Endzeit = endDateTime,
Kommentar = kommentar,
Projektleiter = projektleiter,
Erledigt = false
});
}
FeedbackText.Text = "✅ Projekt erfolgreich gespeichert.";
FeedbackText.Foreground = Brushes.Green;
FeedbackText.IsVisible = true;
ProjektnameBox.Text = "";
KommentarBox.Text = "";
StartdatumPicker.SelectedDate = DateTime.Today;
EnddatumPicker.SelectedDate = DateTime.Today;
StartzeitBox.Text = "09:00";
EndzeitBox.Text = "17:00";
ProjektleiterDropdown.SelectedItem = null;
foreach (var cb in _mitarbeiterCheckBoxMap.Values)
cb.IsChecked = false;
if (_viewManager.TryGetView<AdminMainView>("AdminMain", out var adminView) && adminView != null)
adminView.AktualisiereLetzteProjekte();
}
catch (Exception ex)
{
FeedbackText.Text = $"❌ Fehler beim Speichern: {ex.Message}";
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
Console.WriteLine("❌ Ausnahme beim Speichern:");
Console.WriteLine(ex);
}
} }
catch (Exception ex)
{
FeedbackText.Text = $"❌ Fehler beim Speichern: {ex.Message}";
FeedbackText.Foreground = Brushes.Red;
FeedbackText.IsVisible = true;
Console.WriteLine("❌ Ausnahme beim Speichern:");
Console.WriteLine(ex.ToString());
}
}
private void DemoProjekteButton_Click(object? sender, RoutedEventArgs e) private void DemoProjekteButton_Click(object? sender, RoutedEventArgs e)
{ {
@ -149,7 +190,9 @@ public partial class ProjektErstellenView : UserControl
return; return;
} }
if (MitarbeiterListBox.SelectedItems?.Count == 0) var ausgewaehlteMitarbeiter = ErmittleAusgewaehlteMitarbeiter();
if (ausgewaehlteMitarbeiter.Count == 0)
{ {
FeedbackText.Text = "❗ Bitte wähle mindestens einen Mitarbeiter für die Demo-Projekte."; FeedbackText.Text = "❗ Bitte wähle mindestens einen Mitarbeiter für die Demo-Projekte.";
FeedbackText.Foreground = Brushes.Red; FeedbackText.Foreground = Brushes.Red;
@ -160,8 +203,7 @@ public partial class ProjektErstellenView : UserControl
var service = new SqliteZeiterfassungsService(); var service = new SqliteZeiterfassungsService();
var heute = DateTime.Today; var heute = DateTime.Today;
var mitarbeiter = MitarbeiterListBox?.SelectedItems?.Cast<string>().ToList() ?? new List<string>(); foreach (var name in ausgewaehlteMitarbeiter)
foreach (var name in mitarbeiter)
{ {
var projekte = new List<Zeiteintrag> var projekte = new List<Zeiteintrag>
{ {

View File

@ -5,19 +5,44 @@ using Avalonia.Media;
namespace ChronoFlow.View.Converter namespace ChronoFlow.View.Converter
{ {
/// <summary>
/// Konvertiert einen booleschen Wert in eine Farbe (Brush).
/// True => Blau, False => Grau.
/// Wird z.B. für farbliche Statusanzeigen in der Oberfläche verwendet.
/// </summary>
public class BoolToBrushConverter : IValueConverter public class BoolToBrushConverter : IValueConverter
{ {
/// <summary>
/// Die Farbe, die bei "true" verwendet wird.
/// Kann in XAML überschrieben werden.
/// </summary>
public IBrush TrueBrush { get; set; } = Brushes.Blue; public IBrush TrueBrush { get; set; } = Brushes.Blue;
/// <summary>
/// Die Farbe, die bei "false" verwendet wird.
/// Kann in XAML überschrieben werden.
/// </summary>
public IBrush FalseBrush { get; set; } = Brushes.Gray; public IBrush FalseBrush { get; set; } = Brushes.Gray;
/// <summary>
/// Führt die Umwandlung des bool-Wertes in eine Brush durch.
/// </summary>
/// <param name="value">Der Wert, der umgewandelt werden soll (sollte bool sein).</param>
/// <param name="targetType">Zieltyp der Bindung (erwartet IBrush).</param>
/// <param name="parameter">Optionaler Parameter (nicht verwendet).</param>
/// <param name="culture">Kultureinstellungen (nicht verwendet).</param>
/// <returns>TrueBrush oder FalseBrush abhängig vom bool-Wert.</returns>
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {
if (value is bool boolValue && boolValue) if (value is bool boolValue && boolValue)
return Brushes.Blue; return TrueBrush;
return Brushes.Gray; return FalseBrush;
} }
/// <summary>
/// Umkehrkonvertierung wird hier nicht benötigt.
/// </summary>
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {
throw new NotImplementedException(); // wird i.d.R. nicht benötigt throw new NotImplementedException(); // wird i.d.R. nicht benötigt

View File

@ -11,14 +11,15 @@ using Microsoft.Data.Sqlite;
namespace ChronoFlow.View; namespace ChronoFlow.View;
/// <summary> /// <summary>
/// Das Fenster für den Benutzer-Login. /// Das Login-Fenster für Benutzer der Anwendung.
/// Es prüft die Anmeldedaten, erzwingt ggf. eine Passwortänderung und startet das Hauptfenster.
/// </summary> /// </summary>
public partial class LoginWindow : Window public partial class LoginWindow : Window
{ {
private readonly LoginController _loginController; private readonly LoginController _loginController;
/// <summary> /// <summary>
/// Konstruktor Initialisiert die Oberfläche und legt bei Bedarf einen Standard-Admin an. /// Konstruktor Initialisiert UI und legt bei Bedarf den Standard-Admin an.
/// </summary> /// </summary>
public LoginWindow() public LoginWindow()
{ {
@ -28,7 +29,7 @@ public partial class LoginWindow : Window
try try
{ {
var service = new SqliteZeiterfassungsService(); var service = new SqliteZeiterfassungsService();
service.ErstelleStandardAdmin(); service.ErstelleStandardAdmin(); // Falls noch kein Admin vorhanden, wird ein Default-Admin erstellt.
} }
catch (SqliteException ex) when (ex.SqliteErrorCode == 5) catch (SqliteException ex) when (ex.SqliteErrorCode == 5)
{ {
@ -43,13 +44,14 @@ public partial class LoginWindow : Window
} }
/// <summary> /// <summary>
/// Wird aufgerufen, wenn der Benutzer auf "Anmelden" klickt. /// Wird aufgerufen, wenn der Benutzer auf „Anmelden“ klickt.
/// </summary> /// </summary>
private async 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();
// Validierung: Felder dürfen nicht leer sein
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.";
@ -64,13 +66,14 @@ public partial class LoginWindow : Window
} }
catch (SqliteException ex) when (ex.SqliteErrorCode == 5) catch (SqliteException ex) when (ex.SqliteErrorCode == 5)
{ {
ErrorText.Text = "⚠️ Die Datenbank ist gesperrt. Bitte schließen Sie andere Programme (z.B. DB Browser) und versuchen Sie es erneut."; ErrorText.Text = "⚠️ Die Datenbank ist gesperrt. Bitte schließen Sie andere Programme und versuchen Sie es erneut.";
ErrorText.IsVisible = true; ErrorText.IsVisible = true;
return; return;
} }
var benutzerListe = service.LadeAlleBenutzer(); var benutzerListe = service.LadeAlleBenutzer();
// Benutzer mit eingegebenem Namen (case-insensitive) finden
var matchingUsers = benutzerListe var matchingUsers = benutzerListe
.Where(u => u.Username.Equals(username, StringComparison.OrdinalIgnoreCase)) .Where(u => u.Username.Equals(username, StringComparison.OrdinalIgnoreCase))
.ToList(); .ToList();
@ -91,6 +94,7 @@ public partial class LoginWindow : Window
var user = matchingUsers.First(); var user = matchingUsers.First();
// Passwort-Überprüfung mit Hashing
if (!PasswordHasher.VerifyPassword(password, user.Password)) if (!PasswordHasher.VerifyPassword(password, user.Password))
{ {
ErrorText.Text = "Falsches Passwort. Bitte erneut versuchen."; ErrorText.Text = "Falsches Passwort. Bitte erneut versuchen.";
@ -98,7 +102,7 @@ public partial class LoginWindow : Window
return; return;
} }
// Passwort muss geändert werden // Benutzer muss Passwort ändern? → Dialog anzeigen
if (user.MussPasswortAendern) if (user.MussPasswortAendern)
{ {
var dialog = new PasswortAendernDialog(user); var dialog = new PasswortAendernDialog(user);
@ -121,13 +125,13 @@ public partial class LoginWindow : Window
try try
{ {
// Login-Zeiten aktualisieren // Login-Zeit aktualisieren (für spätere Änderungsbenachrichtigungen)
var vorher = user.LetzterLogin; var vorher = user.LetzterLogin;
user.LetzterLogin = DateTime.Now; user.LetzterLogin = DateTime.Now;
user.VorletzterLogin = vorher; user.VorletzterLogin = vorher;
service.UpdateLoginZeiten(user); service.UpdateLoginZeiten(user);
// Hauptfenster öffnen // Hauptfenster öffnen und Login-Fenster schließen
var main = new MainWindow(user); var main = new MainWindow(user);
main.Show(); main.Show();
main.Activate(); main.Activate();

View File

@ -91,7 +91,7 @@ public partial class MainWindow : Window
} }
/// <summary> /// <summary>
/// Extern aufrufbar für "Zurück zum Dashboard"-Funktion (z.B. in anderen Views). /// Extern aufrufbar für "Zurück zum Dashboard"-Funktion (z.B. in anderen Views).
/// </summary> /// </summary>
public void ShowAdminDashboard() public void ShowAdminDashboard()
{ {

View File

@ -11,71 +11,86 @@ using ChronoFlow.Controller;
namespace ChronoFlow.View.Mitarbeiter; namespace ChronoFlow.View.Mitarbeiter;
/// <summary> /// <summary>
/// ViewModel für die Mitarbeiter-Aufgabenansicht. /// ViewModel für die Aufgabenansicht eines Mitarbeiters.
/// Enthält alle Aufgaben für den eingeloggten Benutzer und erlaubt das Speichern von Änderungen. /// Lädt relevante Aufgaben (nicht erledigt) und ermöglicht das Speichern von Kommentaren und Bearbeitungsstatus.
/// </summary> /// </summary>
public partial class EmployeeTasksViewModel : ObservableObject public partial class EmployeeTasksViewModel : ObservableObject
{ {
private readonly User _benutzer; private readonly User _benutzer; // Der aktuell eingeloggte Benutzer
private readonly ZeiterfassungsController controller = new(); // Controller für Datenzugriff
private readonly ZeiterfassungsController controller = new(); private readonly string aktuellerBenutzername; // Benutzername (String, z.B. für Vergleich)
private readonly string aktuellerBenutzername;
// Liste aller offenen, relevanten Aufgaben für diesen Benutzer
[ObservableProperty] [ObservableProperty]
private ObservableCollection<Zeiteintrag> eintraege = new(); private ObservableCollection<Zeiteintrag> eintraege = new();
// Status-Text für Hinweise oder Rückmeldungen an den Benutzer
[ObservableProperty] [ObservableProperty]
private string? statusText; private string? statusText;
// Gibt an, ob überhaupt Aufgaben vorhanden sind (z.B. für "Keine Aufgaben"-Hinweis in der View)
public bool HatKeineEintraege => Eintraege.Count == 0; public bool HatKeineEintraege => Eintraege.Count == 0;
// Wenn sich die Einträge ändern, wird auch das Property `HatKeineEintraege` neu berechnet
partial void OnEintraegeChanged(ObservableCollection<Zeiteintrag>? oldValue, ObservableCollection<Zeiteintrag> newValue) partial void OnEintraegeChanged(ObservableCollection<Zeiteintrag>? oldValue, ObservableCollection<Zeiteintrag> newValue)
{ {
OnPropertyChanged(nameof(HatKeineEintraege)); OnPropertyChanged(nameof(HatKeineEintraege));
} }
/// <summary>
/// Konstruktor übernimmt eingeloggten Benutzer, speichert Name für spätere Vergleiche
/// und lädt initial die relevanten Einträge.
/// </summary>
public EmployeeTasksViewModel(User benutzer) public EmployeeTasksViewModel(User benutzer)
{ {
_benutzer = benutzer; _benutzer = benutzer;
aktuellerBenutzername = benutzer.Username; aktuellerBenutzername = benutzer.Username;
controller = new ZeiterfassungsController(); // falls du das oben nicht hast controller = new ZeiterfassungsController();
_ = LadeEintraegeAsync(); _ = LadeEintraegeAsync(); // asynchrone Initialladung
} }
/// <summary>
/// Lädt alle offenen Aufgaben für diesen Benutzer (oder wenn er Projektleiter ist).
/// Sortiert die Aufgaben nach Enddatum.
/// Markiert Aufgaben als „geändert“, wenn sie seit dem letzten Login bearbeitet wurden.
/// </summary>
[RelayCommand] [RelayCommand]
public async Task LadeEintraegeAsync() public async Task LadeEintraegeAsync()
{ {
// 🔄 Einträge synchron laden, aber async verpacken // Alle Aufgaben laden (aus Datenbank)
var alleEintraege = await Task.Run(() => controller.LadeAlleEintraege()); var alleEintraege = await Task.Run(() => controller.LadeAlleEintraege());
// Relevante Aufgaben filtern: noch nicht erledigt und betreffen den Benutzer
var relevanteEintraege = alleEintraege var relevanteEintraege = alleEintraege
.Where(e => !e.Erledigt && .Where(e => !e.Erledigt &&
(e.Mitarbeiter == aktuellerBenutzername || e.Projektleiter == aktuellerBenutzername)) (e.Mitarbeiter == aktuellerBenutzername || e.Projektleiter == aktuellerBenutzername))
.OrderBy(e => e.Endzeit) .OrderBy(e => e.Endzeit)
.ToList(); .ToList();
// Prüfung, ob Aufgabe nach letztem Login geändert wurde
foreach (var eintrag in relevanteEintraege) foreach (var eintrag in relevanteEintraege)
{ {
eintrag.WurdeSeitLoginBearbeitet = eintrag.LetzteBearbeitung > _benutzer.VorletzterLogin; eintrag.WurdeSeitLoginBearbeitet = eintrag.LetzteBearbeitung > _benutzer.VorletzterLogin;
} }
// Hinweis anzeigen, wenn Aufgaben seit letztem Login verändert wurden
if (relevanteEintraege.Any(e => e.WurdeSeitLoginBearbeitet)) if (relevanteEintraege.Any(e => e.WurdeSeitLoginBearbeitet))
StatusText = "📢 Es wurden Aufgaben seit Ihrem letzten Login geändert."; StatusText = "📢 Es wurden Aufgaben seit Ihrem letzten Login geändert.";
// Die View wird über `Eintraege` gebunden, daher muss hier neu zugewiesen werden
Eintraege = new ObservableCollection<Zeiteintrag>(relevanteEintraege); Eintraege = new ObservableCollection<Zeiteintrag>(relevanteEintraege);
} }
/// <summary>
/// Speichert alle Änderungen (Kommentar + Erledigt-Status) der aktuellen Aufgaben.
/// Danach wird die Liste neu geladen.
/// </summary>
[RelayCommand] [RelayCommand]
public async Task SpeichereEintraegeAsync() public async Task SpeichereEintraegeAsync()
{ {
// Achtung: Hier verwendest du noch das Repository, obwohl du oben mit dem Controller arbeitest. // Achtung: Diese Methode ruft direkt den Service auf.
// Du kannst entweder: // Dies könnte in Zukunft auch über den Controller geschehen.
// A) Das Repository auch im ViewModel übergeben
// B) Die Methode UpdateStatusUndKommentarAsync auch in den Controller packen
foreach (var eintrag in Eintraege) foreach (var eintrag in Eintraege)
{ {
await Task.Run(() => await Task.Run(() =>
@ -85,6 +100,7 @@ public partial class EmployeeTasksViewModel : ObservableObject
}); });
} }
// Rückmeldung anzeigen und Liste aktualisieren
StatusText = "✅ Änderungen gespeichert."; StatusText = "✅ Änderungen gespeichert.";
await LadeEintraegeAsync(); await LadeEintraegeAsync();
} }

View File

@ -17,7 +17,7 @@ public partial class MitarbeiterMainView : UserControl
private readonly ObservableCollection<string> _notifications = new(); private readonly ObservableCollection<string> _notifications = new();
/// <summary> /// <summary>
/// Öffentlicher, parameterloser Konstruktor erforderlich für Avalonia Runtime Loader (z.B. Designer). /// Öffentlicher, parameterloser Konstruktor erforderlich für Avalonia Runtime Loader (z.B. Designer).
/// Startet mit Dummy-Werten. /// Startet mit Dummy-Werten.
/// </summary> /// </summary>
public MitarbeiterMainView() public MitarbeiterMainView()

View File

@ -109,33 +109,44 @@ namespace ChronoFlow.View
/// </summary> /// </summary>
private void DemoBenutzerErstellen_Click(object? sender, RoutedEventArgs e) private void DemoBenutzerErstellen_Click(object? sender, RoutedEventArgs e)
{ {
var namen = new[] { "Max Mustermann", "Lena Schmidt", "Tobias Becker", "Julia Klein", "Nico Weber" }; string[] vornamen = { "Max", "Lena", "Tobias", "Julia", "Nico", "Emma", "Finn", "Sophie", "Noah", "Laura" };
var zufallsname = namen[new Random().Next(namen.Length)]; string[] nachnamen = { "Mustermann", "Schmidt", "Becker", "Klein", "Weber", "Meier", "Schulz", "Huber", "Lang", "Richter" };
var service = new SqliteZeiterfassungsService(); var service = new SqliteZeiterfassungsService();
var existierendeUsernames = service.LadeAlleBenutzer().Select(u => u.Username).ToHashSet(StringComparer.OrdinalIgnoreCase);
// Prüfen, ob Name bereits vorhanden ist var rnd = new Random();
if (service.LadeAlleBenutzer().Any(u => u.Username.Equals(zufallsname, StringComparison.OrdinalIgnoreCase))) int versuche = 0;
while (versuche < 100)
{ {
FeedbackText.Text = "⚠ Demo-Mitarbeiter existiert bereits."; string vorname = vornamen[rnd.Next(vornamen.Length)];
FeedbackText.Foreground = Brushes.OrangeRed; string nachname = nachnamen[rnd.Next(nachnamen.Length)];
FeedbackText.IsVisible = true; string name = $"{vorname} {nachname}";
return;
if (!existierendeUsernames.Contains(name))
{
var demoUser = new User
{
Username = name,
Password = PasswordHasher.HashPassword("newpassword"),
Role = "Mitarbeiter",
MussPasswortAendern = true
};
service.ErstelleNeuenBenutzer(demoUser);
FeedbackText.Text = $"✅ Demo-Mitarbeiter '{name}' erstellt (Passwort: newpassword)";
FeedbackText.Foreground = Brushes.Green;
FeedbackText.IsVisible = true;
return;
}
versuche++;
} }
// Demo-User erstellen FeedbackText.Text = "❌ Fehler: Konnte keinen eindeutigen Demo-Mitarbeiter generieren.";
var demoUser = new User FeedbackText.Foreground = Brushes.Red;
{
Username = zufallsname,
Password = PasswordHasher.HashPassword("newpassword"),
Role = "Mitarbeiter",
MussPasswortAendern = true
};
service.ErstelleNeuenBenutzer(demoUser);
FeedbackText.Text = $"✅ Demo-Mitarbeiter '{zufallsname}' erstellt (Passwort: newpassword)";
FeedbackText.Foreground = Brushes.Green;
FeedbackText.IsVisible = true; FeedbackText.IsVisible = true;
} }
} }

View File

@ -4,20 +4,41 @@
Width="400" Height="250" Width="400" Height="250"
Title="Passwort ändern"> Title="Passwort ändern">
<!-- Haupt-Layout: vertikaler StackPanel für die Anordnung aller Elemente -->
<StackPanel Margin="20" Spacing="10"> <StackPanel Margin="20" Spacing="10">
<TextBlock x:Name="UsernameTextBlock" Text="Benutzer: " FontSize="16" FontWeight="Bold" />
<!-- Überschrift: Benutzername (wird zur Laufzeit ergänzt) -->
<TextBlock x:Name="UsernameTextBlock"
Text="Benutzer: "
FontSize="16"
FontWeight="Bold" />
<!-- Eingabe neues Passwort -->
<TextBlock Text="Neues Passwort:" /> <TextBlock Text="Neues Passwort:" />
<TextBox x:Name="NeuesPasswortBox" PasswordChar="●" /> <TextBox x:Name="NeuesPasswortBox"
PasswordChar="●" /> <!-- Passwort wird maskiert -->
<!-- Eingabe Bestätigung des Passworts -->
<TextBlock Text="Passwort bestätigen:" /> <TextBlock Text="Passwort bestätigen:" />
<TextBox x:Name="BestaetigenBox" PasswordChar="●" /> <TextBox x:Name="BestaetigenBox"
PasswordChar="●" /> <!-- Wiederholung zur Sicherheit -->
<TextBlock x:Name="FehlerText" Foreground="Red" IsVisible="False" /> <!-- Fehlermeldung (standardmäßig ausgeblendet) -->
<TextBlock x:Name="FehlerText"
Foreground="Red"
IsVisible="False" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="10" Margin="0,10,0,0"> <!-- Aktionsbuttons (zentriert nebeneinander) -->
<Button Content="💾 Speichern" Click="SpeichernButton_Click" Width="100" /> <StackPanel Orientation="Horizontal"
<Button Content="❌ Abbrechen" Click="AbbrechenButton_Click" Width="100" /> 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>
</StackPanel> </StackPanel>
</Window> </Window>