Feat: Adds project task creation
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,33 @@
|
||||
@rendermode InteractiveServer
|
||||
@inject ProjectService ProjectService
|
||||
|
||||
<NavLink href="@($"/projects/tasks/creation/{@ProjectId}")" class="btn btn-primary">+</NavLink>
|
||||
|
||||
@if (Model is null)
|
||||
{
|
||||
<p class="alert alert-warning">Project not found</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>@ProjectId</p>
|
||||
<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 {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
public string? Description { get; set; }
|
||||
public DateTime DueDate { 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 DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace WorkManagementTool.Services
|
||||
public async Task<List<ProjectTask>> GetAllTasksAsync(int projectId)
|
||||
{
|
||||
return await _context.ProjectTasks
|
||||
.Include(t => t.NextTasks)
|
||||
.Include(t => t.PreviousTasks)
|
||||
.Include(t => t.Project)
|
||||
.Where(w => w.Project.Id == projectId)
|
||||
.ToListAsync();
|
||||
@@ -25,20 +25,82 @@ namespace WorkManagementTool.Services
|
||||
public async Task<ProjectTask?> GetTaskByIdAsync(int taskId)
|
||||
{
|
||||
return await _context.ProjectTasks
|
||||
.Include(t => t.NextTasks)
|
||||
.Include(t => t.PreviousTasks)
|
||||
.Include(t => t.Project)
|
||||
.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);
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user