diff --git a/StudiApp/StudiApp.Test.Unit/ConverterTests/AverageMarkConverterTest.cs b/StudiApp/StudiApp.Test.Unit/ConverterTests/AverageMarkConverterTest.cs index abbe3422044afa0c63c8edd8fc47cf59a9d1bdac..420fd8bf9e3d5e95bb711f3f25a2c7d61c76bf9c 100644 --- a/StudiApp/StudiApp.Test.Unit/ConverterTests/AverageMarkConverterTest.cs +++ b/StudiApp/StudiApp.Test.Unit/ConverterTests/AverageMarkConverterTest.cs @@ -11,6 +11,8 @@ namespace StudiApp.Test.Unit.ConverterTests [TestCase(1.0, 6.0, 0.7, 0.3, 2.5)] [TestCase(5.0, 6.0, 0.35, 0.65, 5.65)] [TestCase(5.0, 6.0, 0.01, 0.99, 5.99)] + [TestCase(4.0, 6.0, 1, 1, 5.0)] + [TestCase(4.5, 5, 0.2, 0.4, 4.83)] public void AverageMarkTest(double mark1, double mark2, double percentage1, double percentage2, double expected) { var m1 = new Mark() diff --git a/StudiApp/StudiApp.Test.Unit/StudiApp.Test.Unit.csproj b/StudiApp/StudiApp.Test.Unit/StudiApp.Test.Unit.csproj index 7d11f0debfc4bc89a1c1c791233648754999259b..056e01bbcedd613ca1577581ba006d28d863ce26 100644 --- a/StudiApp/StudiApp.Test.Unit/StudiApp.Test.Unit.csproj +++ b/StudiApp/StudiApp.Test.Unit/StudiApp.Test.Unit.csproj @@ -10,10 +10,12 @@ <ItemGroup> <PackageReference Include="JunitXml.TestLogger" Version="3.0.124" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> + <PackageReference Include="Moq" Version="4.18.4" /> <PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit3TestAdapter" Version="4.3.0" /> <PackageReference Include="NUnit.Analyzers" Version="3.5.0" /> <PackageReference Include="coverlet.collector" Version="3.1.2" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\StudiApp\StudiApp.csproj" /> diff --git a/StudiApp/StudiApp.Test.Unit/ViewModelsTests/MarkCreateViewModelTest.cs b/StudiApp/StudiApp.Test.Unit/ViewModelsTests/MarkCreateViewModelTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..f00b98332d1a8b003c64a1d44cb82d945452a4c6 --- /dev/null +++ b/StudiApp/StudiApp.Test.Unit/ViewModelsTests/MarkCreateViewModelTest.cs @@ -0,0 +1,15 @@ +using System.Runtime.CompilerServices; +using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Serialization; +using NUnit.Framework.Internal; +using StudiApp.Models; +using StudiApp.ViewModels; + +namespace StudiApp.Test.Unit.ViewModelsTests; + + + +[TestFixture] +public class MarkCreateViewModelTest +{ + +} \ No newline at end of file diff --git a/StudiApp/StudiApp.Test.Unit/ViewModelsTests/MarkEditViewModelTest.cs b/StudiApp/StudiApp.Test.Unit/ViewModelsTests/MarkEditViewModelTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..ce27f174d76b5b453e51fb5fb2444824a426228b --- /dev/null +++ b/StudiApp/StudiApp.Test.Unit/ViewModelsTests/MarkEditViewModelTest.cs @@ -0,0 +1,10 @@ +using StudiApp.Models; +using StudiApp.ViewModels; + +namespace StudiApp.Test.Unit.ViewModelsTests; + +[TestFixture] +public class MarkEditViewModelTest +{ + +} \ No newline at end of file diff --git a/StudiApp/StudiApp.Test.Unit/ViewModelsTests/MarksOverviewViewModelTest.cs b/StudiApp/StudiApp.Test.Unit/ViewModelsTests/MarksOverviewViewModelTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..c3ba7bc0b50ccfc18c2263cbe14c901b75f7eb08 --- /dev/null +++ b/StudiApp/StudiApp.Test.Unit/ViewModelsTests/MarksOverviewViewModelTest.cs @@ -0,0 +1,82 @@ +using StudiApp.Models; +using StudiApp.Services.Repository; +using StudiApp.ViewModels; +using StudiApp.Interfaces; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; + +namespace StudiApp.Test.Unit.ViewModelsTests; + +[TestFixture] +public class MarksOverviewViewModelTest +{ + private MarksOverviewViewModel vm; + private Semester semester1; + private Semester semester2; + private List<Semester> semesters; + private List<Course> semester1Courses; + private List<Course> semester2Courses; + private List<Mark> semester1Marks; + private List<Mark> semester2Marks; + + [SetUp] + public void Setup() + { + semester1 = new Semester() + { + DateRange = new DateRange() { Start = new DateTime(2032, 1, 1), End = new DateTime(2023, 6, 1) }, + Name = "Semester 1", + Id = 1 + }; + semester2 = new Semester() + { + DateRange = new DateRange() { Start = new DateTime(2032, 6, 2), End = new DateTime(2023, 12, 1) }, + Name = "Semester 2", + Id = 2 + }; + semesters = new List<Semester>() { semester1, semester2 }; + + + var mark1 = new Mark { Id = 1, Value = 4, Description = "Exam 1", Percentage = 0.5 }; + var mark2 = new Mark { Id = 2, Value = 4, Description = "Exam 2", Percentage = 0.5 }; + semester1Marks = new List<Mark>() { mark1 }; + semester2Marks = new List<Mark>() { mark2 }; + var allMarks = new List<Mark>() { mark1, mark2 }; + + var course1 = new Course() { Id = 1, Name = "Course 1", Ects = 4, IsPassed = true, SemesterId = 1, Marks = semester1Marks }; + var course2 = new Course() { Id = 2, Name = "Course 2", Ects = 2, IsPassed = false, SemesterId = 2, Marks = semester2Marks }; + semester1Courses = new List<Course>() { course1 }; + semester2Courses = new List<Course>() { course2 }; + var allCourses = new List<Course>() { course1, course2 }; + + + var semesterRepository = new Mock<IRepository<Semester, int>>(); + semesterRepository.Setup(r => r.GetAll()) + .Returns(Task.FromResult(semesters)); + + var courseRepository = new Mock<IRepository<Course, int>>(); + courseRepository.Setup(r => r.GetAll()) + .Returns(Task.FromResult(allCourses)); + + var markRepository = new Mock<IRepository<Mark, int>>(); + markRepository.Setup(r => r.GetAll()) + .Returns(Task.FromResult(allMarks)); + + vm = new MarksOverviewViewModel( + new NullLogger<MarksOverviewViewModel>(), + semesterRepository.Object, + courseRepository.Object, + markRepository.Object); + } + + + [Test] + public void OnSelectedSemesterChangedTest() + { + vm.SelectedSemester = semester2; + + var actualId = vm.Marks[0].Id; + var expectedId= semester2Marks[0].Id; + Assert.That(actualId, Is.EqualTo(expectedId)); + } +} diff --git a/StudiApp/StudiApp/AppShell.xaml.cs b/StudiApp/StudiApp/AppShell.xaml.cs index c7f0a9c87cf69fe3fb0f95bb78a919154028eb5d..b74844ae3b5c9d443ea1a0a5d33d90c9af9d0384 100644 --- a/StudiApp/StudiApp/AppShell.xaml.cs +++ b/StudiApp/StudiApp/AppShell.xaml.cs @@ -1,4 +1,5 @@ -using StudiApp.Views; +using System.Runtime.CompilerServices; +using StudiApp.Views; namespace StudiApp; @@ -16,5 +17,6 @@ public partial class AppShell : Shell Routing.RegisterRoute(nameof(CoursesEditView), typeof(CoursesEditView)); Routing.RegisterRoute(nameof(TodoEditView), typeof(TodoEditView)); Routing.RegisterRoute(nameof(MarksEditView), typeof(MarksEditView)); + Routing.RegisterRoute(nameof(MarksCreateView), typeof(MarksCreateView)); } } diff --git a/StudiApp/StudiApp/MauiProgram.cs b/StudiApp/StudiApp/MauiProgram.cs index 2e37fcc87489bfa0d113cfcfea15b7daf10516e7..16dc5c2b64dd339b5dcfe339aeb31dde39303a37 100644 --- a/StudiApp/StudiApp/MauiProgram.cs +++ b/StudiApp/StudiApp/MauiProgram.cs @@ -8,6 +8,7 @@ using StudiApp.Services.Repository; using StudiApp.ViewModels; using StudiApp.Views; using Syncfusion.Maui.Core.Hosting; +using CommunityToolkit.Maui; namespace StudiApp; @@ -68,11 +69,11 @@ public static class MauiProgram mauiAppBuilder.Services.AddSingleton<TimetableViewModel>(); mauiAppBuilder.Services.AddSingleton<TodoOverviewViewModel>(); mauiAppBuilder.Services.AddSingleton<SettingsViewModel>(); - mauiAppBuilder.Services.AddSingleton<IRepository<Mark, int>, SqliteRepository<Mark>>(); mauiAppBuilder.Services.AddTransient<CoursesEditViewModel>(); mauiAppBuilder.Services.AddTransient<TodoEditViewModel>(); mauiAppBuilder.Services.AddTransient<MarksEditViewModel>(); + mauiAppBuilder.Services.AddTransient<MarksCreateViewModel>(); return mauiAppBuilder; } @@ -88,6 +89,7 @@ public static class MauiProgram mauiAppBuilder.Services.AddTransient<CoursesEditView>(); mauiAppBuilder.Services.AddTransient<TodoEditView>(); mauiAppBuilder.Services.AddTransient<MarksEditView>(); + mauiAppBuilder.Services.AddTransient<MarksCreateView>(); return mauiAppBuilder; } diff --git a/StudiApp/StudiApp/Services/Converters/AverageMarkConverter.cs b/StudiApp/StudiApp/Services/Converters/AverageMarkConverter.cs index f84f27e6e5f579f2d5c18cca8fd61cc14ae530c4..901a3019db03db5e9dcddfe7eee878cbd0c73976 100644 --- a/StudiApp/StudiApp/Services/Converters/AverageMarkConverter.cs +++ b/StudiApp/StudiApp/Services/Converters/AverageMarkConverter.cs @@ -16,7 +16,9 @@ namespace StudiApp.Services.Converters throw new ArgumentException("A Percentage in the marks list is over 100% (1.0) or under 1% (0.01)"); } - var average = marks.Sum(mark => mark.Value * mark.Percentage); + var marksSum = marks.Sum(mark => mark.Value * mark.Percentage); + var percentageSum = marks.Sum(mark => mark.Percentage); + var average = marksSum / percentageSum; return Math.Round(average, 2); } } diff --git a/StudiApp/StudiApp/StudiApp.csproj b/StudiApp/StudiApp/StudiApp.csproj index ea15876a25ef0fb3addd5a6d3a799cd4de4d3152..0b7f3e9201eb8df33ce33251e82e00e9657be8a1 100644 --- a/StudiApp/StudiApp/StudiApp.csproj +++ b/StudiApp/StudiApp/StudiApp.csproj @@ -49,6 +49,7 @@ <PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0" /> <PackageReference Include="MetroLog.Maui" Version="2.1.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" /> + <PackageReference Include="Syncfusion.Maui.Inputs" Version="21.1.39" /> <PackageReference Include="Syncfusion.Maui.Scheduler" Version="21.1.37" /> <PackageReference Include="sqlite-net-pcl" Version="1.8.116" /> <PackageReference Include="SQLiteNetExtensions" Version="2.1.0" /> @@ -69,6 +70,9 @@ <MauiXaml Update="Views\DashboardView.xaml"> <SubType>Designer</SubType> </MauiXaml> + <MauiXaml Update="Views\MarksCreateView.xaml"> + <Generator>MSBuild:Compile</Generator> + </MauiXaml> <MauiXaml Update="Views\SettingsView.xaml"> <Generator>MSBuild:Compile</Generator> </MauiXaml> @@ -84,6 +88,9 @@ <DependentUpon>DashboardView.xaml</DependentUpon> <SubType>Code</SubType> </Compile> + <Compile Update="Views\MarksCreateView.xaml.cs"> + <DependentUpon>MarksCreateView.xaml</DependentUpon> + </Compile> <Compile Update="Views\MarksEditView.xaml.cs"> <DependentUpon>MarksEditView.xaml</DependentUpon> </Compile> diff --git a/StudiApp/StudiApp/ViewModels/MarksCreateViewModel.cs b/StudiApp/StudiApp/ViewModels/MarksCreateViewModel.cs new file mode 100644 index 0000000000000000000000000000000000000000..10b754c11bf0958bd4802eebdeca4e15a5beeb28 --- /dev/null +++ b/StudiApp/StudiApp/ViewModels/MarksCreateViewModel.cs @@ -0,0 +1,167 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.Logging; +using StudiApp.Interfaces; +using StudiApp.Models; +using StudiApp.Services.Converters; +using System.Collections.ObjectModel; +using System.Runtime.CompilerServices; + +namespace StudiApp.ViewModels +{ + [QueryProperty(nameof(Mark), "Mark")] + public partial class MarksCreateViewModel : ObservableObject, IQueryAttributable + { + private readonly ILogger<MarksCreateViewModel> _logger; + private readonly IRepository<Mark, int> _markRepository; + private readonly IRepository<Course, int> _courseRepository; + + [ObservableProperty] private Mark _mark; + [ObservableProperty] private string _percentage; + [ObservableProperty] private ObservableCollection<Course> _courses; + + [ObservableProperty] private bool _courseHasError; + [ObservableProperty] private bool _descriptionHasError; + [ObservableProperty] private bool _markHasError; + [ObservableProperty] private bool _percentageHasError; + + private string _errorMessage; + + public MarksCreateViewModel( + ILogger<MarksCreateViewModel> logger, + IRepository<Mark, int> markMaRepository, + IRepository<Course, int> coursesRepository) + { + _logger = logger; + _markRepository = markMaRepository; + _courseRepository = coursesRepository; + _mark = new Mark(); + _courses = new ObservableCollection<Course>(); + + _logger.LogInformation($"Initialized {nameof(MarksCreateViewModel)}"); + + LoadCourses(); + } + + [RelayCommandAttribute] + public async void Save() + { + Mark.Percentage = ConvertPercentageToDouble(Percentage); + + var isInputValid = ValidateInput(); + + if (isInputValid) + { + await _markRepository.Create(Mark); + + _logger.LogInformation("New Mark created"); + + await Shell.Current.GoToAsync(".."); + } + else + { + // TODO: Implement dialog service + } + + + } + + public void ApplyQueryAttributes(IDictionary<string, object> query) + { + } + + private async void LoadCourses() + { + + List<Course> courses = await _courseRepository.GetAll(); + + foreach (var course in courses) + { + Courses.Add(course); + } + + } + + private double ConvertPercentageToDouble(string percentage) + { + return PercentageConverter.ConvertToDouble(percentage); + } + + private bool ValidateInput() + { + _errorMessage = string.Empty; + + return ValidateCourse() && + ValidateDescription() && + ValidateMarkValue() && + ValidatePercentage(); + } + + private bool ValidateCourse() + { + if (Mark.Course != null) + { + CourseHasError = false; + return true; + } + + CourseHasError = true; + _errorMessage += "A course must be chosen\n"; + return false; + } + + private bool ValidateDescription() + { + if (Mark.Description is { Length: <= 250 and > 1 }) + { + DescriptionHasError = false; + return true; + } + + DescriptionHasError = true; + _errorMessage += "Description between 1 - 250 chars\n"; + return false; + + } + + private bool ValidateMarkValue() + { + if (Mark.Value is >= 1.0 and <= 6.0) + { + MarkHasError = false; + return true; + } + + MarkHasError = true; + _errorMessage += "Mark value out of range, must be between 1.0 and 6.0\n"; + return false; + } + + private bool ValidatePercentage() + { + if (Mark.Percentage is > 0 and <= 1 && ValidatePercentageOverAllMarks()) + { + PercentageHasError = false; + return true; + } + + if (!ValidatePercentageOverAllMarks()) + { + MarkHasError = true; + _errorMessage += "Percentage is above 100% over all marks of this course"; + return false; + } + + MarkHasError = true; + _errorMessage += "Percentage out of range, must be in range 0.1 up to 1.0"; + return false; + } + + private bool ValidatePercentageOverAllMarks() + { + var overallCourseMarkPercentage = Mark.Course.Marks.Sum(mark => mark.Percentage); + + return (overallCourseMarkPercentage + Mark.Percentage) <= 1.0; + } + } +} diff --git a/StudiApp/StudiApp/ViewModels/MarksEditViewModel.cs b/StudiApp/StudiApp/ViewModels/MarksEditViewModel.cs index 497cc721ade679551c773f5abdadca45f279b377..d94f9c87910394ee9eb9a59ffb1b0ba284b8e090 100644 --- a/StudiApp/StudiApp/ViewModels/MarksEditViewModel.cs +++ b/StudiApp/StudiApp/ViewModels/MarksEditViewModel.cs @@ -1,8 +1,169 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.Logging; +using StudiApp.Interfaces; +using StudiApp.Models; +using StudiApp.Services.Converters; -namespace StudiApp.ViewModels +namespace StudiApp.ViewModels; + +[QueryProperty(nameof(Mark), "Mark")] +public partial class MarksEditViewModel : ObservableObject, IQueryAttributable { - public partial class MarksEditViewModel : ObservableObject + private readonly ILogger<MarksEditViewModel> _logger; + private readonly IRepository<Mark, int> _markRepository; + private readonly IRepository<Course, int> _courseRepository; + + [ObservableProperty] private Mark _mark; + [ObservableProperty] private string _percentage; + [ObservableProperty] private ObservableCollection<Course> _courses; + + [ObservableProperty] private bool _courseHasError; + [ObservableProperty] private bool _descriptionHasError; + [ObservableProperty] private bool _markHasError; + [ObservableProperty] private bool _percentageHasError; + + private string _errorMessage; + + public MarksEditViewModel( + ILogger<MarksEditViewModel> logger, + IRepository<Mark, int> markRepository, + IRepository<Course, int> courseRepository) + { + _logger = logger; + _markRepository = markRepository; + _courseRepository = courseRepository; + _courses = new ObservableCollection<Course>(); + + LoadCourses(); + + _logger.LogInformation($"Initialized {nameof(MarksEditViewModel)}"); + } + + public void ApplyQueryAttributes(IDictionary<string, object> query) + { + Mark = (query[nameof(Mark)] as Mark)!; + Percentage = PercentageConverter.ConvertToString(Mark.Percentage); + _logger.LogInformation($"Edit Page for Mark {Mark.Id} loaded"); + } + + [RelayCommandAttribute] + public async void Save() + { + Mark.Percentage = ConvertPercentageToDouble(Percentage); + + var isInputValid = ValidateInput(); + + if (isInputValid) + { + _logger.LogInformation("Mark name: " + Mark.Description); + + await _markRepository.Update(Mark); + + _logger.LogInformation("Altered Mark Saved"); + + await Shell.Current.GoToAsync(".."); + } + else + { + // TODO: Implement dialog service + } + } + + private async void LoadCourses() + { + + List<Course> courses = await _courseRepository.GetAll(); + + foreach (var course in courses) + { + Courses.Add(course); + } + + } + + private double ConvertPercentageToDouble(string percentage) + { + return PercentageConverter.ConvertToDouble(percentage); + } + + private bool ValidateInput() + { + _errorMessage = string.Empty; + + return ValidateCourse() && + ValidateDescription() && + ValidateMarkValue() && + ValidatePercentage(); + } + + private bool ValidateCourse() + { + if (Mark.Course != null) + { + CourseHasError = false; + return true; + } + + CourseHasError = true; + _errorMessage += "A course must be chosen\n"; + return false; + } + + private bool ValidateDescription() + { + if (Mark.Description is { Length: <= 250 and > 1 }) + { + DescriptionHasError = false; + return true; + } + + DescriptionHasError = true; + _errorMessage += "Description between 1 - 250 chars\n"; + return false; + + } + + private bool ValidateMarkValue() { + if (Mark.Value is >= 1.0 and <= 6.0) + { + MarkHasError = false; + return true; + } + + MarkHasError = true; + _errorMessage += "Mark value out of range, must be between 1.0 and 6.0\n"; + return false; + } + + private bool ValidatePercentage() + { + if (Mark.Percentage is > 0 and <= 1 && ValidatePercentageOverAllMarks()) + { + PercentageHasError = false; + return true; + } + + if (!ValidatePercentageOverAllMarks()) + { + MarkHasError = true; + _errorMessage += "Percentage is above 100% over all marks of this course"; + return false; + } + + MarkHasError = true; + _errorMessage += "Percentage out of range, must be in range 0.1 up to 1.0"; + return false; } + + private bool ValidatePercentageOverAllMarks() + { + var overallCourseMarkPercentage = Mark.Course.Marks.Sum(mark => mark.Percentage); + + return (overallCourseMarkPercentage) <= 1.0; + } + } + diff --git a/StudiApp/StudiApp/ViewModels/MarksOverviewViewModel.cs b/StudiApp/StudiApp/ViewModels/MarksOverviewViewModel.cs index 5ff7dde722b7f31a27ddd3296fa15870809e8cb7..2bb04a2855ae2c0e6f87e7bcfbe15182eaa738ae 100644 --- a/StudiApp/StudiApp/ViewModels/MarksOverviewViewModel.cs +++ b/StudiApp/StudiApp/ViewModels/MarksOverviewViewModel.cs @@ -1,8 +1,150 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.Logging; +using StudiApp.Interfaces; +using StudiApp.Models; +using StudiApp.Services.Converters; +using StudiApp.Views; -namespace StudiApp.ViewModels +namespace StudiApp.ViewModels; +public partial class MarksOverviewViewModel : ObservableObject { - public class MarksOverviewViewModel : ObservableObject + private readonly ILogger<MarksOverviewViewModel> _logger; + + private readonly IRepository<Semester, int> _semesterRepository; + private readonly IRepository<Course, int> _courseRepository; + private readonly IRepository<Mark, int> _markRepository; + + [ObservableProperty] + private double _totalAverageMark; + + [ObservableProperty] + private int _totalEcts; + + [ObservableProperty] + private double _semesterAverageMark; + + [ObservableProperty] + private int _semesterEcts; + + [ObservableProperty] private ObservableCollection<Mark> _marks; + + [ObservableProperty] + private ObservableCollection<Semester> _semesters; + + [ObservableProperty] + private Semester _selectedSemester; + + public MarksOverviewViewModel(ILogger<MarksOverviewViewModel> logger, + IRepository<Semester, int> semesterRepository, + IRepository<Course, int> courseRepository, + IRepository<Mark, int> markRepository) + { + _semesterRepository = semesterRepository; + _courseRepository = courseRepository; + _markRepository = markRepository; + _logger = logger; + + InitializeSemesterData(); + LoadTotalOverview(); + } + + [RelayCommand] + public void OnAppearing() + { + LoadSemesterOverview(); + } + + private async void InitializeSemesterData() + { + //CreateMockData(); // TODO: remove this when not needed anymore + var result = await _semesterRepository.GetAll(); + Semesters = new ObservableCollection<Semester>(result); + SelectedSemester = Semesters[0]; // TODO: get actual current + } + + private async void LoadSemesterOverview() + { + // TODO: if SelectedSemester is null, show text 'Select a semester' + var result = await _courseRepository.GetAll(); + var semesterCourses = result.Where(c => c.SemesterId == SelectedSemester.Id).ToList(); + var semesterMarks = GetMarksFromCourses(semesterCourses); + + Marks = new ObservableCollection<Mark>(semesterMarks); + SemesterEcts = GetObtainedEcts(semesterCourses); + SemesterAverageMark = AverageMarkConverter.MarksToAverage(semesterMarks); + } + + private void LoadTotalOverview() { + _courseRepository.GetAll().ContinueWith(t => TotalEcts = GetObtainedEcts(t.Result)); + _markRepository.GetAll().ContinueWith(t => TotalAverageMark = AverageMarkConverter.MarksToAverage(t.Result)); + } + + private List<Mark> GetMarksFromCourses(List<Course> courses) + { + return courses.SelectMany(c => c.Marks).ToList(); + } + + partial void OnSelectedSemesterChanged(Semester value) + { + LoadSemesterOverview(); + } + + private int GetObtainedEcts(IEnumerable<Course> courses) + { + var passedCourses = courses.Where(course => course.IsPassed); + return passedCourses.Sum(course => course.Ects); + } + + [RelayCommand] + public async Task EditMark(Mark mark) + { + await Shell.Current.GoToAsync(nameof(MarksEditView), + new Dictionary<string, object> { {"Mark", mark} }); + _logger.LogInformation($"Navigating to {nameof(MarksEditView)} (edit mark)"); + } + + [RelayCommand] + public async Task NewMark() + { + await Shell.Current.GoToAsync(nameof(MarksCreateView)); + _logger.LogInformation($"Navigating to {nameof(MarksCreateView)} (create new mark)"); + } + + + // TODO: remove this function when not needed anymore + private async void CreateMockData() + { + var semester1 = new Semester() + { + DateRange = new DateRange() { Start = new DateTime(2032, 1, 1), End = new DateTime(2023, 6, 1) }, + Name = "Semester 1" + }; + var semester2 = new Semester() + { + DateRange = new DateRange() { Start = new DateTime(2032, 6, 2), End = new DateTime(2023, 12, 1) }, + Name = "Semester 2" + }; + + semester1 = await _semesterRepository.Create(semester1); + semester2 = await _semesterRepository.Create(semester2); + + var course1 = new Course() { Name = "Course 1", Ects = 4, IsPassed = true, Semester = semester1, SemesterId = semester1.Id }; + var course2 = new Course() { Name = "Course 2", Ects = 2, IsPassed = true, Semester = semester1, SemesterId = semester1.Id }; + var course3 = new Course() { Name = "Course 3", Ects = 4, IsPassed = true, Semester = semester2, SemesterId = semester2.Id }; + + course1 = await _courseRepository.Create(course1); + course2 = await _courseRepository.Create(course2); + course3 = await _courseRepository.Create(course3); + + await _markRepository.Create(new Mark { Value = 4, Description = "Exam 1", Percentage = 0.5, Course = course1, CourseId = course1.Id }); + await _markRepository.Create(new Mark { Value = 4, Description = "Exam 2", Percentage = 0.5, Course = course1, CourseId = course1.Id }); + await _markRepository.Create(new Mark { Value = 4.5, Description = "Exam 1", Percentage = 0.5, Course = course2, CourseId = course2.Id }); + await _markRepository.Create(new Mark { Value = 5.5, Description = "Exam 2", Percentage = 0.5, Course = course2, CourseId = course2.Id }); + await _markRepository.Create(new Mark { Value = 1, Description = "Testat 1", Percentage = 0.5, Course = course3, CourseId = course3.Id }); + await _markRepository.Create(new Mark { Value = 4.5, Description = "Testat 2", Percentage = 0.3, Course = course3, CourseId = course3.Id }); + await _markRepository.Create(new Mark { Value = 4, Description = "Final Exam", Percentage = 0.2, Course = course3, CourseId = course3.Id }); } } diff --git a/StudiApp/StudiApp/Views/MarksCreateView.xaml b/StudiApp/StudiApp/Views/MarksCreateView.xaml new file mode 100644 index 0000000000000000000000000000000000000000..469401880c0bc846ccfaa11c6cfd8411ff1b8811 --- /dev/null +++ b/StudiApp/StudiApp/Views/MarksCreateView.xaml @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="utf-8"?> + +<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" + xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" + x:Class="StudiApp.Views.MarksCreateView" + x:DataType="viewmodel:MarksCreateViewModel" + xmlns:viewmodel="clr-namespace:StudiApp.ViewModels" + xmlns:inputLayout="clr-namespace:Syncfusion.Maui.Core;assembly=Syncfusion.Maui.Core" + xmlns:control="clr-namespace:Syncfusion.Maui.Inputs;assembly=Syncfusion.Maui.Inputs" + Title="Create Mark"> + + <ContentPage.Content> + <Grid Padding="20" RowSpacing="10"> + + <Grid.RowDefinitions> + <RowDefinition Height="auto"/> + <RowDefinition Height="auto"/> + <RowDefinition Height="auto"/> + <RowDefinition Height="auto"/> + </Grid.RowDefinitions> + + <Grid.ColumnDefinitions> + <ColumnDefinition /> + <ColumnDefinition /> + </Grid.ColumnDefinitions> + + <inputLayout:SfTextInputLayout Grid.Column="0" Grid.Row="0" + Grid.ColumnSpan="2" + WidthRequest="350" + HeightRequest="75" + Margin="0, -10, 0, -10" + ContainerBackground="White" + IsHintAlwaysFloated="true" + ContainerType="Outlined" + Hint="Course" + HasError="{Binding CourseHasError}"> + <control:SfComboBox ItemsSource="{Binding Courses}" + DisplayMemberPath="Name" + TextMemberPath="Name" + SelectedItem="{Binding Mark.Course}"> + </control:SfComboBox> + </inputLayout:SfTextInputLayout> + + <inputLayout:SfTextInputLayout Grid.Row="1" Grid.Column="0" + Grid.ColumnSpan="2" + WidthRequest="350" + HeightRequest="75" + Margin="0,-10,0,-10" + ContainerBackground="White" + IsHintAlwaysFloated="true" + ContainerType="Outlined" + Hint="Description" + HasError="{Binding DescriptionHasError}"> + <Entry Text="{Binding Mark.Description}"/> + </inputLayout:SfTextInputLayout> + + <HorizontalStackLayout Grid.Row="2" Grid.Column="0" + Grid.ColumnSpan="2" + WidthRequest="350"> + + <inputLayout:SfTextInputLayout HeightRequest="75" + WidthRequest="175" + Margin="0,-10,0,-10" + ContainerBackground="White" + IsHintAlwaysFloated="true" + ContainerType="Outlined" + Hint="Mark"> + <control:SfMaskedEntry MaskType="RegEx" + Mask="[1-6]\.[0-9]{0,2}" + Value="{Binding Mark.Value, Mode=TwoWay}"/> + + </inputLayout:SfTextInputLayout> + + <inputLayout:SfTextInputLayout HeightRequest="75" + WidthRequest="175" + Margin="0,-10,0,-10" + ContainerBackground="White" + IsHintAlwaysFloated="true" + ContainerType="Outlined" + Hint="Percentage"> + <control:SfMaskedEntry MaskType="RegEx" + Mask="[0-9]{1,3}\%" + Value="{Binding Percentage, Mode=TwoWay}"/> + </inputLayout:SfTextInputLayout> + + </HorizontalStackLayout> + + <Button Grid.Row="3" + Grid.Column="1" + HeightRequest="40" + WidthRequest="125" + HorizontalOptions="End" + CornerRadius="20" + Text="Save" + Command="{Binding SaveCommand}"/> + + </Grid> + </ContentPage.Content> +</ContentPage> \ No newline at end of file diff --git a/StudiApp/StudiApp/Views/MarksCreateView.xaml.cs b/StudiApp/StudiApp/Views/MarksCreateView.xaml.cs new file mode 100644 index 0000000000000000000000000000000000000000..4d570acaa60c48a84dd168a66a498e2e525697b6 --- /dev/null +++ b/StudiApp/StudiApp/Views/MarksCreateView.xaml.cs @@ -0,0 +1,12 @@ +using StudiApp.ViewModels; + +namespace StudiApp.Views; + +public partial class MarksCreateView : ContentPage +{ + public MarksCreateView(MarksCreateViewModel vm) + { + BindingContext = vm; + InitializeComponent(); + } +} \ No newline at end of file diff --git a/StudiApp/StudiApp/Views/MarksEditView.xaml b/StudiApp/StudiApp/Views/MarksEditView.xaml index b95c09c8937e210e884c672af619f2c561450756..73106af3d253159c5bc96bd3de9da679833cbeb7 100644 --- a/StudiApp/StudiApp/Views/MarksEditView.xaml +++ b/StudiApp/StudiApp/Views/MarksEditView.xaml @@ -3,9 +3,88 @@ <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="StudiApp.Views.MarksEditView" + x:DataType="viewmodel:MarksEditViewModel" xmlns:viewmodel="clr-namespace:StudiApp.ViewModels" - x:DataType="viewmodel:MarksEditViewModel"> + xmlns:inputLayout="clr-namespace:Syncfusion.Maui.Core;assembly=Syncfusion.Maui.Core" + xmlns:control="clr-namespace:Syncfusion.Maui.Inputs;assembly=Syncfusion.Maui.Inputs" + Title="Edit Mark"> <ContentPage.Content> - + <Grid Padding="20" RowSpacing="10"> + + <Grid.RowDefinitions> + <RowDefinition Height="auto"/> + <RowDefinition Height="auto"/> + <RowDefinition Height="auto"/> + <RowDefinition Height="auto"/> + </Grid.RowDefinitions> + + <Grid.ColumnDefinitions> + <ColumnDefinition /> + <ColumnDefinition /> + </Grid.ColumnDefinitions> + + <inputLayout:SfTextInputLayout Grid.Column="0" Grid.Row="0" + Grid.ColumnSpan="2" + WidthRequest="350" + HeightRequest="75" + Margin="0, -10, 0, -10" + ContainerBackground="White" + IsHintAlwaysFloated="true" + ContainerType="Outlined" + Hint="Course"> + <control:SfComboBox ItemsSource="{Binding Courses}" + DisplayMemberPath="Name" + SelectedItem="{Binding Mark.Course, Mode=TwoWay}"> + </control:SfComboBox> + </inputLayout:SfTextInputLayout> + + <inputLayout:SfTextInputLayout Grid.Row="1" Grid.Column="0" + Grid.ColumnSpan="2" + WidthRequest="350" + HeightRequest="75" + Margin="0,-10,0,-10" + ContainerBackground="White" + IsHintAlwaysFloated="true" + ContainerType="Outlined" + Hint="Description"> + <Entry Text="{Binding Mark.Description}"/> + </inputLayout:SfTextInputLayout> + + <HorizontalStackLayout Grid.Row="2" Grid.Column="0" + Grid.ColumnSpan="2" + WidthRequest="350"> + + <inputLayout:SfTextInputLayout HeightRequest="75" + WidthRequest="175" + Margin="0,-10,0,-10" + ContainerBackground="White" + IsHintAlwaysFloated="true" + ContainerType="Outlined" + Hint="Mark"> + <Entry Text="{Binding Mark.Value}"/> + </inputLayout:SfTextInputLayout> + + <inputLayout:SfTextInputLayout HeightRequest="75" + WidthRequest="175" + Margin="0,-10,0,-10" + ContainerBackground="White" + IsHintAlwaysFloated="true" + ContainerType="Outlined" + Hint="Percentage"> + <Entry Text="{Binding Percentage}"/> + </inputLayout:SfTextInputLayout> + + </HorizontalStackLayout> + + <Button Grid.Row="3" + Grid.Column="1" + HeightRequest="40" + WidthRequest="125" + HorizontalOptions="End" + CornerRadius="20" + Text="Save" + Command="{Binding SaveCommand}"/> + + </Grid> </ContentPage.Content> </ContentPage> \ No newline at end of file diff --git a/StudiApp/StudiApp/Views/MarksOverviewView.xaml b/StudiApp/StudiApp/Views/MarksOverviewView.xaml index 333cd93db73e0b2c7066b0e256b1625dc7d9bfe4..f4a4dee58642335f2a7f2814f3b06e01dd0a4a15 100644 --- a/StudiApp/StudiApp/Views/MarksOverviewView.xaml +++ b/StudiApp/StudiApp/Views/MarksOverviewView.xaml @@ -4,8 +4,161 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="StudiApp.Views.MarksOverviewView" xmlns:viewmodel="clr-namespace:StudiApp.ViewModels" + xmlns:model="clr-namespace:StudiApp.Models" + xmlns:mct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" x:DataType="viewmodel:MarksOverviewViewModel"> - <ContentPage.Content> - - </ContentPage.Content> -</ContentPage> \ No newline at end of file + <ContentPage.Behaviors> + <mct:EventToCommandBehavior EventName="Appearing" Command="{Binding AppearingCommand}"/> + </ContentPage.Behaviors> + <ScrollView> + <VerticalStackLayout Spacing="10" Margin="10"> + <Grid> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="*" /> + <ColumnDefinition Width="Auto" /> + </Grid.ColumnDefinitions> + <Label Text="Total Overview" + FontSize="20" + VerticalOptions="Center"/> + <Picker x:Name="SemesterPicker" + Title="Select semester" + Grid.Column="1" + ItemsSource="{ Binding Semesters }" + ItemDisplayBinding="{ Binding Name }" + SelectedItem="{ Binding SelectedSemester }"> + </Picker> + </Grid> + + <Frame Padding="10"> + <HorizontalStackLayout> + <Grid> + <Ellipse Fill="Black" + WidthRequest="40" + HeightRequest="40" + HorizontalOptions="Start" /> + <Label Text="{ Binding TotalEcts }" + TextColor="White" + FontSize="14" + HorizontalOptions="Center" + VerticalOptions="CenterAndExpand" /> + </Grid> + <Label Text="Total ECTS" + VerticalOptions="Center" + Margin="10,0"/> + </HorizontalStackLayout> + </Frame> + <Frame Padding="10"> + <HorizontalStackLayout> + <Grid> + <Ellipse Fill="Black" + WidthRequest="40" + HeightRequest="40" + HorizontalOptions="Start"/> + <Label Text="{ Binding TotalAverageMark }" + TextColor="White" + FontSize="14" + HorizontalOptions="Center" + VerticalOptions="CenterAndExpand" /> + </Grid> + <Label Text="Average Mark" + VerticalOptions="Center" + Margin="10,0"/> + </HorizontalStackLayout> + </Frame> + + <Label Text="Semester Overview" + FontSize="20" + VerticalOptions="Center"/> + + <Frame Padding="10"> + <HorizontalStackLayout> + <Grid> + <Ellipse Fill="Black" + WidthRequest="40" + HeightRequest="40" + HorizontalOptions="Start" /> + <Label Text="{ Binding SemesterEcts }" + TextColor="White" + FontSize="14" + HorizontalOptions="Center" + VerticalOptions="CenterAndExpand" /> + </Grid> + <Label Text="Obtained ECTS" + VerticalOptions="Center" + Margin="10,0"/> + </HorizontalStackLayout> + </Frame> + <Frame Padding="10"> + <HorizontalStackLayout> + <Grid> + <Ellipse Fill="Black" + WidthRequest="40" + HeightRequest="40" + HorizontalOptions="Start"/> + <Label Text="{ Binding SemesterAverageMark }" + TextColor="White" + FontSize="14" + HorizontalOptions="Center" + VerticalOptions="CenterAndExpand" /> + </Grid> + <Label Text="Average Mark" + VerticalOptions="Center" + Margin="10,0"/> + </HorizontalStackLayout> + </Frame> + + + + <Label Text="Marks List" + FontSize="20" + VerticalOptions="Center"/> + + <CollectionView ItemsSource="{Binding Marks}" SelectionMode="None"> + <CollectionView.ItemsLayout> + <LinearItemsLayout Orientation="Vertical" ItemSpacing="10" /> + </CollectionView.ItemsLayout> + <CollectionView.ItemTemplate> + <DataTemplate x:DataType="{x:Type model:Mark}"> + <Frame Padding="10"> + <Frame.GestureRecognizers> + <TapGestureRecognizer Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:MarksOverviewViewModel}}, Path=EditMarkCommand}" + CommandParameter="{Binding}"/> + </Frame.GestureRecognizers> + + <HorizontalStackLayout> + <Grid> + <Ellipse Fill="Black" + WidthRequest="40" + HeightRequest="40" + HorizontalOptions="Start" /> + <Label Text="{ Binding Value }" + TextColor="White" + FontSize="14" + HorizontalOptions="Center" + VerticalOptions="CenterAndExpand" /> + </Grid> + + <VerticalStackLayout Margin="10,0"> + <Label> + <Label.Text> + <MultiBinding StringFormat="{}{0} - {1}"> + <Binding Path="Course.Name"></Binding> + <Binding Path="Description"></Binding> + </MultiBinding> + </Label.Text> + </Label> + <Label Text="{ Binding Course.Semester.Name }"></Label> + </VerticalStackLayout> + </HorizontalStackLayout> + </Frame> + </DataTemplate> + </CollectionView.ItemTemplate> + </CollectionView> + + <Button Text="New Mark" + HorizontalOptions="End" + Command="{Binding NewMarkCommand}"/> + + </VerticalStackLayout> + </ScrollView> +</ContentPage>