Fix: Correct the submission of new school subjects
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 36s

This commit is contained in:
Namu
2025-12-20 21:46:44 +01:00
parent e1341bd0a1
commit 85fbc5eb19
6 changed files with 343 additions and 22 deletions

View File

@@ -0,0 +1,315 @@
@page "/calendar"
@using Microsoft.AspNetCore.Authorization
@using WorkManagementTool.Services
@using WorkManagementTool.Data.Entities
@attribute [Authorize]
@inject SchoolSubjectService SchoolSubjectService
@inject HomeworkService HomeworkService
<h3>Calendar</h3>
<div class="calendar-controls mb-3">
<button class="btn btn-sm btn-outline-primary" @onclick="() => ChangeMonth(-1)">«</button>
<span class="mx-2 fw-bold">@currentMonth.ToString("MMMM yyyy")</span>
<button class="btn btn-sm btn-outline-primary" @onclick="() => ChangeMonth(1)">»</button>
<div class="form-check ms-4 d-inline-block">
<input class="form-check-input" type="checkbox" id="showCompleted" @bind="showCompleted" @bind:after="LoadDataAsync" />
<label class="form-check-label" for="showCompleted">Afficher les rendus complétés</label>
</div>
</div>
@if (Subjects == null || Subjects.Count == 0)
{
<p>Chargement des matières...</p>
}
else
{
<div class="calendar-legend mb-3">
@foreach (var s in Subjects)
{
<div class="legend-item">
<span class="legend-color" style="background:@GetSubjectColor(s)"></span>
<span class="legend-name">@s.Name</span>
</div>
}
</div>
<div class="calendar-grid">
@foreach (var d in WeekDayHeaders)
{
<div class="calendar-weekday">@d</div>
}
@foreach (var day in DaysForCurrentMonth)
{
var isOtherMonth = day.Month != currentMonth.Month;
var hwForDay = GetHomeworksForDate(day);
<div class="calendar-day @(isOtherMonth ? "other-month" : "")">
<div class="date-number">@day.Day</div>
@if (hwForDay != null && hwForDay.Count > 0)
{
<div class="homeworks-list">
@foreach (var hw in hwForDay)
{
var color = GetSubjectColor(hw.SchoolSubject);
<div class="hw-pill" title="@($"{hw.Title} - {hw.SchoolSubject?.Name}")" style="background:@color">
@Shorten(hw.Title, 28)
</div>
}
</div>
}
</div>
}
</div>
}
<style>
.calendar-controls {
display: flex;
align-items: center;
}
.calendar-legend {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.legend-color {
width: 18px;
height: 18px;
border-radius: 4px;
display: inline-block;
border: 1px solid rgba(0,0,0,0.12);
}
.legend-name {
font-size: 0.9rem;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7,1fr);
gap: 6px;
}
.calendar-weekday {
font-weight: 700;
text-align: center;
padding: 6px 0;
background: #f5f5f5;
border-radius: 4px;
}
.calendar-day {
min-height: 80px;
padding: 6px;
border: 1px solid #e9e9e9;
border-radius: 6px;
background: white;
position: relative;
}
.calendar-day.other-month {
background: #fbfbfb;
color: #9a9a9a;
opacity: 0.8;
}
.date-number {
font-size: 0.9rem;
font-weight: 600;
}
.homeworks-list {
margin-top: 6px;
display: flex;
flex-direction: column;
gap: 4px;
}
.hw-pill {
color: #fff;
padding: 3px 6px;
border-radius: 6px;
font-size: 0.8rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
@code {
[Parameter]
public List<SchoolSubject> Subjects { get; set; } = new();
private DateTime currentMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
private bool showCompleted { get; set; } = false;
// mapping subjectId -> color
private readonly Dictionary<int, string> subjectColors = new();
// mapping date -> list of homeworks
private readonly Dictionary<DateTime, List<Homework>> homeworksByDate = new();
private static readonly string[] WeekDayHeaders = new[] { "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim" };
private IEnumerable<DateTime> DaysForCurrentMonth
{
get
{
var first = StartOfCalendar(currentMonth);
var last = EndOfCalendar(currentMonth);
for (var d = first; d <= last; d = d.AddDays(1))
yield return d;
}
}
protected override async Task OnInitializedAsync()
{
await LoadDataAsync();
}
private async Task LoadDataAsync()
{
// Load subjects if not provided by parent
if (Subjects == null || Subjects.Count == 0)
{
Subjects = await SchoolSubjectService.GetAllSchoolSubjectsAsync();
}
// Assign colors deterministically
foreach (var s in Subjects)
{
if (!subjectColors.ContainsKey(s.Id))
subjectColors[s.Id] = GenerateColorFromId(s.Id);
}
// Clear and load homeworks grouped by date
homeworksByDate.Clear();
foreach (var s in Subjects)
{
// services used elsewhere: GetUncompletedHomeworkAsync and GetCompletedHomeworksAsync
var list = new List<Homework>();
try
{
var uncompleted = await HomeworkService.GetUncompletedHomeworkAsync(s.Id);
if (uncompleted != null)
list.AddRange(uncompleted);
if (showCompleted)
{
var completed = await HomeworkService.GetCompletedHomeworksAsync(s.Id);
if (completed != null)
list.AddRange(completed);
}
}
catch
{
// ignore per-subject errors to keep calendar robust
}
foreach (var hw in list)
{
var date = hw.DueDate.Date;
if (!homeworksByDate.ContainsKey(date))
homeworksByDate[date] = new List<Homework>();
homeworksByDate[date].Add(hw);
}
}
StateHasChanged();
}
private List<Homework>? GetHomeworksForDate(DateTime date)
{
return homeworksByDate.TryGetValue(date.Date, out var list) ? list : null;
}
private string GetSubjectColor(SchoolSubject? subject)
{
if (subject is null) return "#6c757d"; // fallback
if (subjectColors.TryGetValue(subject.Id, out var c)) return c;
var gen = GenerateColorFromId(subject.Id);
subjectColors[subject.Id] = gen;
return gen;
}
private static string GenerateColorFromId(int id)
{
// deterministic pastel-ish color from id using HSL -> RGB
var hue = (id * 47) % 360; // disperse hues
var s = 65; // saturation %
var l = 50; // lightness %
var (r, g, b) = HslToRgb(hue, s, l);
return $"rgb({r},{g},{b})";
}
private static (int r, int g, int b) HslToRgb(int h, int s, int l)
{
double hh = h / 360.0;
double ss = s / 100.0;
double ll = l / 100.0;
double q = ll < 0.5 ? ll * (1 + ss) : ll + ss - ll * ss;
double p = 2 * ll - q;
double[] t = new double[3] { hh + 1.0 / 3.0, hh, hh - 1.0 / 3.0 };
for (int i = 0; i < 3; i++)
{
if (t[i] < 0) t[i] += 1;
if (t[i] > 1) t[i] -= 1;
}
double[] c = new double[3];
for (int i = 0; i < 3; i++)
{
if (t[i] < 1.0 / 6.0) c[i] = p + ((q - p) * 6 * t[i]);
else if (t[i] < 1.0 / 2.0) c[i] = q;
else if (t[i] < 2.0 / 3.0) c[i] = p + ((q - p) * (2.0 / 3.0 - t[i]) * 6);
else c[i] = p;
}
return ((int)(c[0] * 255), (int)(c[1] * 255), (int)(c[2] * 255));
}
private static DateTime StartOfCalendar(DateTime month)
{
// Start on Monday
var firstOfMonth = new DateTime(month.Year, month.Month, 1);
var diff = ((int)firstOfMonth.DayOfWeek + 6) % 7; // convert Sun=0.. to Mon=0..
return firstOfMonth.AddDays(-diff);
}
private static DateTime EndOfCalendar(DateTime month)
{
var last = new DateTime(month.Year, month.Month, DateTime.DaysInMonth(month.Year, month.Month));
var diff = (7 - (((int)last.DayOfWeek + 6) % 7) - 1);
return last.AddDays(diff);
}
private void ChangeMonth(int offset)
{
currentMonth = currentMonth.AddMonths(offset);
StateHasChanged();
}
private static string Shorten(string? text, int max)
{
if (string.IsNullOrEmpty(text)) return string.Empty;
if (text.Length <= max) return text;
return text.Substring(0, max - 1) + "…";
}
}

View File

@@ -1,48 +1,53 @@
@page "/school-subjects/creation"
@rendermode InteractiveServer
@attribute [Authorize]
@using Microsoft.AspNetCore.Authorization
@using WorkManagementTool.Services
@using WorkManagementTool.Data.Entities;
@inject SchoolSubjectService SchoolSubjectService
@inject NavigationManager NavigationManager
@inject ILogger<SchoolSubjectCreation> Logger
<h3>SchoolSubjectCreation</h3>
<EditForm Model="@Model" OnValidSubmit="Submit" FormName="school-subject-creation">
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="alert alert-danger">@errorMessage</div>
}
<EditForm Model="@Model"
OnValidSubmit="Submit"
FormName="school-subject-creation">
<ValidationSummary />
<DataAnnotationsValidator />
<div class="form-group">
<label for="name" class="form-label">Name</label>
<InputText class="form-control" @bind-value="Model!.Name" />
<InputText id="name" class="form-control" @bind-Value="Model!.Name" />
<ValidationMessage For="@(() => Model!.Name)" />
</div>
<div class="form-group">
<label for="description" class="form-label">Description</label>
<InputTextArea class="form-control" @bind-value="Model!.Description"></InputTextArea>
<InputTextArea id="description" class="form-control" @bind-Value="Model!.Description"></InputTextArea>
<ValidationMessage For="@(() => Model!.Description)" />
</div>
<button type="submit" class="btn btn-primary">Create School Subject</button>
</EditForm>
@code {
[SupplyParameterFromForm]
private SchoolSubject? Model { get; set; }
private SchoolSubject Model { get; set; } = new();
protected override void OnInitialized()
{
if (Model is null)
{
Model = new SchoolSubject
{
Name = null!,
Description = null,
};
}
}
private string? errorMessage;
private async Task Submit()
{
await SchoolSubjectService.AddSchoolSubjectAsync(Model!);
if (Model is not null)
{
await SchoolSubjectService.AddSchoolSubjectAsync(Model);
Logger.LogInformation("SchoolSubject created: {Name}", Model.Name);
NavigationManager.NavigateTo("/school-subjects");
}
}
}

View File

@@ -5,9 +5,10 @@ namespace WorkManagementTool.Data.Entities
public class SchoolSubject
{
public int Id { get; set; }
[Required]
[MaxLength(100, ErrorMessage = "Name must be less than 100 caracters")]
public required string Name { get; set; } = null!;
//[Required]
//[MaxLength(100, ErrorMessage = "Name must be less than 100 caracters")]
//[MinLength(1, ErrorMessage = "Name must containes more than 1 caracters")]
public string Name { get; set; } = null!;
[MaxLength(500, ErrorMessage = "Description must be less than 500 caracters")]
public string? Description { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;