Neue Klasse ParticipationRatingScale (Core) ist die einzige Quelle für Rohwerte je Aspekt-Typ. Scale3/Binary liegen bewusst direkt auf derselben -2..+2-Achse wie Scale5 (nur mit weniger Zwischenschritten), damit die bestehende Gewichtung/Mittelwertbildung zur Mitarbeitsnote unverändert kompatibel bleibt. Points ist grundverschieden (echter Zählwert 0..MaxPoints, neues Feld auf ParticipationAspect) und wird nur zur Aggregation linear auf dieselbe Achse normiert. Ohne diese Normierung hätte ein Punkte-Aspekt die Mitarbeitsnote verfälscht: drei Stellen summierten bisher den Rohwert direkt (ParticipationGradeDialog- ViewModel.Recompute, ParticipationWizardViewModels.WeightedRating und .ComputeSuggestion) - alle drei sind jetzt auf die Normierung umgestellt. Raster: Punkte-Aspekte zeigen ein NumericUpDown statt fester Stufen-Buttons. Schnelleingabe-Dialog: Zifferntasten/+/- sind jetzt typabhängig, Legende zeigt live die für den aktuellen Aspekt gültigen Tasten. Aspekt-Verwaltung um "Max. Punkte"-Feld ergänzt (nur bei Typ "Punkte" sichtbar). Bewusst nicht angefasst: die Trendlinien-Visualisierung im Mitarbeits-Assistenten bleibt fest auf die drei Standardaspekte zugeschnitten - eigener, größerer Umbau. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
359 lines
14 KiB
C#
359 lines
14 KiB
C#
using Avalonia.Controls;
|
||
using Avalonia.Controls.Templates;
|
||
using Avalonia.Data;
|
||
using LehrerApp.Core.Interfaces;
|
||
using LehrerApp.Core.Models;
|
||
using LehrerApp.Core.Services;
|
||
using LehrerApp.Desktop.ViewModels.Groups;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
|
||
namespace LehrerApp.Desktop.Views.Groups;
|
||
|
||
public partial class ParticipationTabView : UserControl
|
||
{
|
||
private ParticipationTabViewModel? _vm;
|
||
|
||
public ParticipationTabView() => InitializeComponent();
|
||
|
||
protected override void OnDataContextChanged(EventArgs e)
|
||
{
|
||
base.OnDataContextChanged(e);
|
||
|
||
if (DataContext is ParticipationTabViewModel vm)
|
||
{
|
||
_vm = vm;
|
||
vm.OnAddSession = ShowAddSessionDialog;
|
||
vm.OnQuickInput = ShowQuickInputDialog;
|
||
vm.OnStatusQuickInput = ShowStatusQuickInputDialog;
|
||
vm.OnComputeGrade = ShowComputeGradeDialog;
|
||
vm.OnOpenWizard = ShowWizardDialog;
|
||
vm.OnManageAspects = ShowManageAspectsDialog;
|
||
vm.Aspects.CollectionChanged += (_, _) => BuildColumns();
|
||
vm.PropertyChanged += (_, pe) =>
|
||
{
|
||
if (pe.PropertyName == nameof(ParticipationTabViewModel.RebuildColumnsSignal))
|
||
BuildColumns();
|
||
};
|
||
BuildColumns();
|
||
}
|
||
}
|
||
|
||
private void BuildColumns()
|
||
{
|
||
var grid = this.FindControl<DataGrid>("RatingGrid");
|
||
if (grid is null || _vm is null) return;
|
||
|
||
grid.Columns.Clear();
|
||
|
||
grid.Columns.Add(new DataGridTextColumn
|
||
{
|
||
Header = "Schüler",
|
||
Binding = new Binding("Name"),
|
||
Width = new DataGridLength(160, DataGridLengthUnitType.Pixel),
|
||
});
|
||
|
||
// Aspekt-Spalten
|
||
foreach (var (aspect, i) in _vm.Aspects.Select((a, i) => (a, i)))
|
||
{
|
||
var idx = i;
|
||
grid.Columns.Add(new DataGridTemplateColumn
|
||
{
|
||
Header = $"{aspect.Label} [{AspectShortcut(i)}]",
|
||
Width = new DataGridLength(1, DataGridLengthUnitType.Star),
|
||
CellTemplate = BuildCellTemplate(idx, forCompetency: false, _vm.IsReadOnly),
|
||
});
|
||
}
|
||
|
||
grid.Columns.Add(new DataGridTemplateColumn
|
||
{
|
||
Header = "HA",
|
||
Width = new DataGridLength(50, DataGridLengthUnitType.Pixel),
|
||
CellTemplate = BuildHomeworkCellTemplate(_vm.IsReadOnly),
|
||
});
|
||
grid.Columns.Add(new DataGridTemplateColumn
|
||
{
|
||
Header = "Anwesenheit",
|
||
Width = new DataGridLength(80, DataGridLengthUnitType.Pixel),
|
||
CellTemplate = BuildAttendanceCellTemplate(_vm.IsReadOnly),
|
||
});
|
||
|
||
// Kompetenz-Spalten (opt-in)
|
||
if (_vm.StudentCompetencyRatingsVisible && _vm.ActiveCompetencyCodes.Count > 0)
|
||
{
|
||
foreach (var (code, i) in _vm.ActiveCompetencyCodes.Select((c, i) => (c, i)))
|
||
{
|
||
var idx = i;
|
||
grid.Columns.Add(new DataGridTemplateColumn
|
||
{
|
||
Header = code,
|
||
Width = new DataGridLength(80, DataGridLengthUnitType.Pixel),
|
||
CellTemplate = BuildCellTemplate(idx, forCompetency: true, _vm.IsReadOnly),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
private static IDataTemplate BuildCellTemplate(int cellIndex, bool forCompetency, bool isReadOnly)
|
||
{
|
||
return new FuncDataTemplate<ParticipationStudentRow>((row, _) =>
|
||
{
|
||
if (row is null) return new TextBlock();
|
||
|
||
var cell = forCompetency
|
||
? row.CompetencyCells.ElementAtOrDefault(cellIndex)
|
||
: row.Cells.ElementAtOrDefault(cellIndex);
|
||
if (cell is null) return new TextBlock();
|
||
|
||
// Punkte-Aspekte (3.1.3) sind eine freie Zahl 0..MaxPoints statt einer festen
|
||
// Stufenauswahl — dafür ein NumericUpDown statt der Stufen-Buttons unten.
|
||
return cell.Type == AspectValueType.Points
|
||
? BuildPointsCell(cell, isReadOnly)
|
||
: BuildStepButtonsCell(cell, isReadOnly);
|
||
});
|
||
}
|
||
|
||
private static Control BuildStepButtonsCell(RatingCell cell, bool isReadOnly)
|
||
{
|
||
var panel = new StackPanel
|
||
{
|
||
Orientation = Avalonia.Layout.Orientation.Horizontal,
|
||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||
Spacing = 2,
|
||
Margin = new Avalonia.Thickness(0, 2),
|
||
};
|
||
|
||
foreach (var (val, label) in ParticipationRatingScale.Steps(cell.Type))
|
||
{
|
||
var btn = new Button
|
||
{
|
||
Content = label,
|
||
Padding = new Avalonia.Thickness(5, 1),
|
||
FontSize = 11,
|
||
Opacity = cell.Value == val ? 1.0 : 0.3,
|
||
IsEnabled = !isReadOnly,
|
||
};
|
||
var capturedVal = val;
|
||
btn.Click += (_, _) => cell.SetValue(capturedVal);
|
||
cell.PropertyChanged += (_, pe) =>
|
||
{
|
||
if (pe.PropertyName == nameof(RatingCell.Value))
|
||
btn.Opacity = cell.Value == capturedVal ? 1.0 : 0.3;
|
||
};
|
||
panel.Children.Add(btn);
|
||
}
|
||
|
||
return panel;
|
||
}
|
||
|
||
private static Control BuildPointsCell(RatingCell cell, bool isReadOnly)
|
||
{
|
||
var updown = new NumericUpDown
|
||
{
|
||
Minimum = 0,
|
||
Maximum = cell.MaxPoints,
|
||
Increment = 1,
|
||
FormatString = "0",
|
||
Value = cell.Value,
|
||
IsEnabled = !isReadOnly,
|
||
Width = 72,
|
||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||
};
|
||
updown.ValueChanged += (_, e) =>
|
||
{
|
||
var newVal = e.NewValue.HasValue ? (int?)e.NewValue.Value : null;
|
||
if (newVal != cell.Value) cell.SetValue(newVal);
|
||
};
|
||
cell.PropertyChanged += (_, pe) =>
|
||
{
|
||
if (pe.PropertyName != nameof(RatingCell.Value)) return;
|
||
var current = cell.Value.HasValue ? (decimal?)cell.Value.Value : null;
|
||
if (updown.Value != current) updown.Value = current;
|
||
};
|
||
return updown;
|
||
}
|
||
|
||
private static IDataTemplate BuildHomeworkCellTemplate(bool isReadOnly)
|
||
{
|
||
return new FuncDataTemplate<ParticipationStudentRow>((row, _) =>
|
||
{
|
||
if (row is null) return new TextBlock();
|
||
|
||
var btn = new Button
|
||
{
|
||
FontSize = 12,
|
||
Width = 30,
|
||
Height = 26,
|
||
Padding = new Avalonia.Thickness(3, 1),
|
||
Command = row.ToggleHomeworkCommand,
|
||
IsEnabled = !isReadOnly,
|
||
};
|
||
void RefreshHomework()
|
||
{
|
||
btn.Content = row.HomeworkSymbol;
|
||
ToolTip.SetTip(btn, row.HomeworkTooltip + " – klicken für nächsten Status");
|
||
if (row.Homework is null)
|
||
{
|
||
btn.ClearValue(Button.BackgroundProperty);
|
||
btn.ClearValue(Button.ForegroundProperty);
|
||
btn.Opacity = 0.38;
|
||
}
|
||
else
|
||
{
|
||
btn.Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse(HomeworkDisplay.Color(row.Homework)));
|
||
btn.Foreground = Avalonia.Media.Brushes.White;
|
||
btn.Opacity = 1;
|
||
}
|
||
}
|
||
RefreshHomework();
|
||
row.PropertyChanged += (_, pe) =>
|
||
{
|
||
if (pe.PropertyName == nameof(ParticipationStudentRow.HomeworkSymbol))
|
||
RefreshHomework();
|
||
};
|
||
return new StackPanel
|
||
{
|
||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||
Children = { btn },
|
||
};
|
||
});
|
||
}
|
||
|
||
private static IDataTemplate BuildAttendanceCellTemplate(bool isReadOnly)
|
||
{
|
||
return new FuncDataTemplate<ParticipationStudentRow>((row, _) =>
|
||
{
|
||
if (row is null) return new TextBlock();
|
||
|
||
var btn = new Button
|
||
{
|
||
FontSize = 12,
|
||
Width = 30,
|
||
Height = 26,
|
||
Padding = new Avalonia.Thickness(3, 1),
|
||
Command = row.CycleAttendanceCommand,
|
||
IsEnabled = !isReadOnly,
|
||
};
|
||
void RefreshAttendance()
|
||
{
|
||
btn.Content = string.IsNullOrEmpty(row.AttendanceLabel) ? "·" : row.AttendanceLabel;
|
||
ToolTip.SetTip(btn, row.AttendanceTooltip + " – klicken für nächsten Status");
|
||
if (row.Attendance is null)
|
||
{
|
||
btn.ClearValue(Button.BackgroundProperty);
|
||
btn.ClearValue(Button.ForegroundProperty);
|
||
btn.Opacity = 0.38;
|
||
}
|
||
else
|
||
{
|
||
btn.Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse(AttendanceDisplay.Color(row.Attendance)));
|
||
btn.Foreground = Avalonia.Media.Brushes.White;
|
||
btn.Opacity = 1;
|
||
}
|
||
}
|
||
RefreshAttendance();
|
||
row.PropertyChanged += (_, pe) =>
|
||
{
|
||
if (pe.PropertyName == nameof(ParticipationStudentRow.AttendanceLabel))
|
||
RefreshAttendance();
|
||
};
|
||
return new StackPanel
|
||
{
|
||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||
Children = { btn },
|
||
};
|
||
});
|
||
}
|
||
|
||
private static string AspectShortcut(int i) => i switch
|
||
{
|
||
0 => "Q", 1 => "W", 2 => "E", 3 => "R", 4 => "T", _ => ""
|
||
};
|
||
|
||
private async Task<LehrerApp.Core.Models.ParticipationSession?> ShowAddSessionDialog()
|
||
{
|
||
var vm = new AddSessionDialogViewModel();
|
||
var dialog = new AddSessionDialog { DataContext = vm };
|
||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||
if (owner is null) return null;
|
||
var ok = await dialog.ShowDialog<bool>(owner);
|
||
return ok ? vm.Result : null;
|
||
}
|
||
|
||
private async Task ShowQuickInputDialog(ParticipationTabViewModel tabVm)
|
||
{
|
||
if (tabVm.StudentRows.Count == 0) return;
|
||
var quickVm = new QuickInputViewModel(
|
||
tabVm.StudentRows.ToList(),
|
||
tabVm.Aspects.ToList());
|
||
var dialog = new ParticipationQuickInputDialog { DataContext = quickVm };
|
||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||
if (owner is not null)
|
||
await dialog.ShowDialog(owner);
|
||
}
|
||
|
||
private async Task ShowStatusQuickInputDialog(ParticipationTabViewModel tabVm)
|
||
{
|
||
if (tabVm.StudentRows.Count == 0 || tabVm.SelectedSession is null) return;
|
||
var quickVm = new AttendanceHomeworkQuickInputViewModel(
|
||
tabVm.StudentRows, tabVm.SelectedSessionDisplay);
|
||
var dialog = new AttendanceHomeworkQuickInputDialog { DataContext = quickVm };
|
||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||
if (owner is not null)
|
||
await dialog.ShowDialog(owner);
|
||
}
|
||
|
||
private async Task ShowComputeGradeDialog(ParticipationTabViewModel tabVm)
|
||
{
|
||
var dialogVm = new ParticipationGradeDialogViewModel(
|
||
App.Services.GetRequiredService<IParticipationSessionRepository>(),
|
||
App.Services.GetRequiredService<IParticipationRepository>(),
|
||
App.Services.GetRequiredService<IParticipationAspectRepository>(),
|
||
App.Services.GetRequiredService<IStudentRepository>(),
|
||
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||
App.Services.GetRequiredService<IGradeRepository>(),
|
||
App.Services.GetRequiredService<GradingService>(),
|
||
tabVm.GroupId, tabVm.SchoolYear, tabVm.GradingSystem);
|
||
|
||
var dialog = new ParticipationGradeDialog { DataContext = dialogVm };
|
||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||
if (owner is not null)
|
||
await dialog.ShowDialog(owner);
|
||
}
|
||
|
||
private async Task ShowWizardDialog(ParticipationTabViewModel tabVm)
|
||
{
|
||
var dialogVm = new ParticipationWizardDialogViewModel(
|
||
App.Services.GetRequiredService<IParticipationSessionRepository>(),
|
||
App.Services.GetRequiredService<IParticipationRepository>(),
|
||
App.Services.GetRequiredService<IParticipationAspectRepository>(),
|
||
App.Services.GetRequiredService<IParticipationSectionRepository>(),
|
||
App.Services.GetRequiredService<IStudentRepository>(),
|
||
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||
App.Services.GetRequiredService<IExamRepository>(),
|
||
App.Services.GetRequiredService<IExamResultRepository>(),
|
||
App.Services.GetRequiredService<IGradeRepository>(),
|
||
App.Services.GetRequiredService<GradingService>(),
|
||
tabVm.GroupId, tabVm.SchoolYear, tabVm.GradingSystem, tabVm.GroupLabel);
|
||
|
||
var dialog = new ParticipationWizardDialog { DataContext = dialogVm };
|
||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||
if (owner is not null)
|
||
await dialog.ShowDialog(owner);
|
||
}
|
||
|
||
private async Task ShowManageAspectsDialog(ParticipationTabViewModel tabVm)
|
||
{
|
||
var dialogVm = new ParticipationAspectsDialogViewModel(
|
||
App.Services.GetRequiredService<IParticipationAspectRepository>(), tabVm.GroupId);
|
||
|
||
var dialog = new ParticipationAspectsDialog { DataContext = dialogVm };
|
||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||
if (owner is not null)
|
||
await dialog.ShowDialog(owner);
|
||
}
|
||
}
|