Fix: Correct the submission of new school subjects
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 36s
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 36s
This commit is contained in:
315
WorkManagementTool/Components/Calendar/Calendar.razor
Normal file
315
WorkManagementTool/Components/Calendar/Calendar.razor
Normal 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) + "…";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,48 +1,53 @@
|
|||||||
@page "/school-subjects/creation"
|
@page "/school-subjects/creation"
|
||||||
|
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
@using WorkManagementTool.Services
|
@using WorkManagementTool.Services
|
||||||
@using WorkManagementTool.Data.Entities;
|
@using WorkManagementTool.Data.Entities;
|
||||||
@inject SchoolSubjectService SchoolSubjectService
|
@inject SchoolSubjectService SchoolSubjectService
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
|
@inject ILogger<SchoolSubjectCreation> Logger
|
||||||
|
|
||||||
<h3>SchoolSubjectCreation</h3>
|
<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 />
|
<ValidationSummary />
|
||||||
<DataAnnotationsValidator />
|
<DataAnnotationsValidator />
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name" class="form-label">Name</label>
|
<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>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="description" class="form-label">Description</label>
|
<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>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Create School Subject</button>
|
<button type="submit" class="btn btn-primary">Create School Subject</button>
|
||||||
</EditForm>
|
</EditForm>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[SupplyParameterFromForm]
|
private SchoolSubject Model { get; set; } = new();
|
||||||
private SchoolSubject? Model { get; set; }
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
private string? errorMessage;
|
||||||
{
|
|
||||||
if (Model is null)
|
|
||||||
{
|
|
||||||
Model = new SchoolSubject
|
|
||||||
{
|
|
||||||
Name = null!,
|
|
||||||
Description = null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Submit()
|
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");
|
NavigationManager.NavigateTo("/school-subjects");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,10 @@ namespace WorkManagementTool.Data.Entities
|
|||||||
public class SchoolSubject
|
public class SchoolSubject
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
[Required]
|
//[Required]
|
||||||
[MaxLength(100, ErrorMessage = "Name must be less than 100 caracters")]
|
//[MaxLength(100, ErrorMessage = "Name must be less than 100 caracters")]
|
||||||
public required string Name { get; set; } = null!;
|
//[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")]
|
[MaxLength(500, ErrorMessage = "Description must be less than 500 caracters")]
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user