Feat: Adds project task creation
Some checks failed
build / build (push) Failing after 27s
SonarQube Scan / SonarQube Trigger (push) Successful in 33s

This commit is contained in:
Namu
2026-01-26 16:13:31 +01:00
parent 9c5d24354a
commit 0cd4cd37fc
4 changed files with 274 additions and 7 deletions

View File

@@ -0,0 +1,186 @@
@page "/projects/tasks/creation/{ProjectId:int}"
@using Microsoft.AspNetCore.Authorization
@using Services;
@using Data.Entities;
@using Microsoft.AspNetCore.Components.Forms
@attribute [Authorize]
@rendermode InteractiveServer
@inject ProjectService ProjectService
@inject ProjectTaskService ProjectTaskService
@inject NavigationManager NavigationManager
@inject ILogger<ProjectTaskCreation> Logger
<h3>Project Task Creation</h3>
@if (Project is null)
{
<p class="alert alert-warning">Project not found</p>
}
else
{
<h5>@Project.Name task creation</h5>
<EditForm EditContext="@editContext" OnValidSubmit="Submit" class="mb-3" FormName="project-task-creation">
<ValidationSummary />
<div class="form-group mb-2">
<label for="title" class="form-label">Title</label>
<InputText id="title" class="form-control" @bind-Value="Model!.Title" />
<ValidationMessage For="@(() => Model!.Title)" />
</div>
<div class="form-group mb-2">
<label for="description" class="form-label">Description</label>
<InputTextArea id="description" class="form-control" @bind-Value="Model!.Description" />
<ValidationMessage For="@(() => Model!.Description)" />
</div>
<div class="form-group mb-2">
<label for="due-date" class="form-label">Due date</label>
<InputDate id="due-date" class="form-control" @bind-Value="Model!.DueDate" />
<ValidationMessage For="@(() => Model!.DueDate)" />
</div>
@if (Project.Tasks != null && Project.Tasks.Any())
{
<div class="form-group mb-3">
<label class="form-label">Previous tasks (0..N)</label>
<div class="border rounded p-2" style="max-height:220px; overflow:auto;">
@foreach (var t in Project.Tasks)
{
<div class="form-check">
<input class="form-check-input"
type="checkbox"
id="prev-@t.Id"
checked="@selectedPreviousTaskIds.Contains(t.Id)"
@onchange="e => TogglePreviousTask((ChangeEventArgs)e, t.Id)" />
<label class="form-check-label" for="prev-@t.Id">
@t.Title (@t.DueDate.ToShortDateString())
</label>
</div>
}
</div>
@if (previousTasksError is not null)
{
<div class="text-danger mt-1">@previousTasksError</div>
}
</div>
}
<button type="submit" class="btn btn-primary">+</button>
</EditForm>
}
@code {
[Parameter]
public int ProjectId { get; set; }
public Project? Project { get; set; }
public ProjectTask? Model { get; set; }
private EditContext? editContext;
private ValidationMessageStore? messageStore;
// contient les ids sélectionnés via les checkboxes
private HashSet<int> selectedPreviousTaskIds = new();
private string? previousTasksError;
protected async override Task OnInitializedAsync()
{
Project = await ProjectService.GetProjectByIdAsync(ProjectId);
Model = new()
{
DueDate = DateTime.Now.AddDays(1)
};
editContext = new EditContext(Model!);
messageStore = new ValidationMessageStore(editContext);
editContext.OnFieldChanged += (_, args) =>
{
if (args.FieldIdentifier.FieldName == nameof(ProjectTask.DueDate))
{
ValidatePreviousDates();
}
};
}
private void TogglePreviousTask(ChangeEventArgs e, int id)
{
var isChecked = e?.Value is bool b && b;
if (!isChecked && e?.Value is string s)
isChecked = s == "true" || s == "on" || s == "checked";
if (isChecked)
selectedPreviousTaskIds.Add(id);
else
selectedPreviousTaskIds.Remove(id);
ValidatePreviousDates();
}
private void ValidatePreviousDates()
{
if (messageStore is null || editContext is null || Model is null)
return;
messageStore.Clear();
if (selectedPreviousTaskIds.Count > 0)
{
var invalids = new List<string>();
foreach (var id in selectedPreviousTaskIds)
{
var prev = Project?.Tasks?.FirstOrDefault(t => t.Id == id);
if (prev != null)
{
if (prev.DueDate >= Model.DueDate)
{
invalids.Add($"« {prev.Title} » due {prev.DueDate.ToShortDateString()} must be before the new task due date.");
}
}
}
if (invalids.Any())
{
messageStore.Add(new FieldIdentifier(Model, nameof(Model.DueDate)), invalids);
previousTasksError = "Certaines tâches précédentes ont une date d'échéance non cohérente.";
}
else
{
previousTasksError = null;
}
}
else
{
previousTasksError = null;
}
editContext.NotifyValidationStateChanged();
}
private async Task Submit()
{
if (Model is null || Project is null || editContext is null)
return;
ValidatePreviousDates();
var valid = editContext.Validate();
if (!valid)
{
Logger.LogWarning("Project task form invalid.");
return;
}
// On passe uniquement les ids au service (solution simple et sans conflit de tracking)
Model.Project = Project;
await ProjectTaskService.AddTaskAsync(Model, selectedPreviousTaskIds);
NavigationManager.NavigateTo($"/projects/tasks/{ProjectId}");
}
}

View File

@@ -8,14 +8,33 @@
@rendermode InteractiveServer @rendermode InteractiveServer
@inject ProjectService ProjectService @inject ProjectService ProjectService
<NavLink href="@($"/projects/tasks/creation/{@ProjectId}")" class="btn btn-primary">+</NavLink>
@if (Model is null) @if (Model is null)
{ {
<p class="alert alert-warning">Project not found</p> <p class="alert alert-warning">Project not found</p>
} }
else else
{ {
<p>@ProjectId</p>
<h3>@Model.Name</h3> <h3>@Model.Name</h3>
@if (Model.Tasks is null || Model.Tasks.Count == 0)
{
<p class="alert alert-warning">No task found in the project</p>
}
else
{
<ul>
@foreach (ProjectTask task in Model.Tasks)
{
<li>
<h5 data-bs-toggle="tooltip" data-bs-title="@task.Description">@task.Title</h5>
<p>Created at: @task.CreatedAt, Due date : @task.DueDate</p>
<input type="checkbox" readonly Value="@task.IsCompleted"/>
</li>
}
</ul>
}
} }
@code { @code {

View File

@@ -7,7 +7,7 @@
public string? Description { get; set; } public string? Description { get; set; }
public DateTime DueDate { get; set; } public DateTime DueDate { get; set; }
public bool IsCompleted { get; set; } public bool IsCompleted { get; set; }
public List<ProjectTask>? NextTasks { get; set; } public List<ProjectTask>? PreviousTasks { get; set; }
public Project Project { get; set; } = null!; public Project Project { get; set; } = null!;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
} }

View File

@@ -16,7 +16,7 @@ namespace WorkManagementTool.Services
public async Task<List<ProjectTask>> GetAllTasksAsync(int projectId) public async Task<List<ProjectTask>> GetAllTasksAsync(int projectId)
{ {
return await _context.ProjectTasks return await _context.ProjectTasks
.Include(t => t.NextTasks) .Include(t => t.PreviousTasks)
.Include(t => t.Project) .Include(t => t.Project)
.Where(w => w.Project.Id == projectId) .Where(w => w.Project.Id == projectId)
.ToListAsync(); .ToListAsync();
@@ -25,20 +25,82 @@ namespace WorkManagementTool.Services
public async Task<ProjectTask?> GetTaskByIdAsync(int taskId) public async Task<ProjectTask?> GetTaskByIdAsync(int taskId)
{ {
return await _context.ProjectTasks return await _context.ProjectTasks
.Include(t => t.NextTasks) .Include(t => t.PreviousTasks)
.Include(t => t.Project) .Include(t => t.Project)
.FirstOrDefaultAsync(t => t.Id == taskId); .FirstOrDefaultAsync(t => t.Id == taskId);
} }
public async Task AddTaskAsync(ProjectTask task) // Ajout : accepter une liste d'ids pour PreviousTasks afin d'éviter les conflits de tracking
public async Task AddTaskAsync(ProjectTask task, IEnumerable<int>? previousIds = null)
{ {
if (previousIds != null && previousIds.Any())
{
var trackedPrev = await _context.ProjectTasks
.Where(t => previousIds.Contains(t.Id))
.ToListAsync();
task.PreviousTasks = trackedPrev;
}
else
{
task.PreviousTasks = new List<ProjectTask>();
}
_context.ProjectTasks.Add(task); _context.ProjectTasks.Add(task);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
} }
public async Task UpdateTaskAsync(ProjectTask task) // Mise à jour : charger l'entité existante et appliquer les modifications + dépendances
public async Task UpdateTaskAsync(ProjectTask task, IEnumerable<int>? previousIds = null)
{ {
_context.ProjectTasks.Update(task); // Récupérer l'entité suivie par le contexte
var existing = await _context.ProjectTasks
.Include(t => t.PreviousTasks)
.Include(t => t.Project)
.FirstOrDefaultAsync(t => t.Id == task.Id);
if (existing is null)
{
// si l'entité n'existe pas encore dans la BDD, fallback : attacher et mettre à jour
if (previousIds != null && previousIds.Any())
{
var trackedPrev = await _context.ProjectTasks
.Where(t => previousIds.Contains(t.Id))
.ToListAsync();
task.PreviousTasks = trackedPrev;
}
_context.ProjectTasks.Update(task);
await _context.SaveChangesAsync();
return;
}
// Mettre à jour les propriétés scalaires
existing.Title = task.Title;
existing.Description = task.Description;
existing.DueDate = task.DueDate;
existing.IsCompleted = task.IsCompleted;
existing.CreatedAt = task.CreatedAt;
// Mettre à jour la collection PreviousTasks selon les ids fournis
if (previousIds != null)
{
if (previousIds.Any())
{
var trackedPrev = await _context.ProjectTasks
.Where(t => previousIds.Contains(t.Id))
.ToListAsync();
// Remplacer la collection existante par les entités tracées
existing.PreviousTasks = trackedPrev;
}
else
{
existing.PreviousTasks = new List<ProjectTask>();
}
}
_context.ProjectTasks.Update(existing);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
} }