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
|
@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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,21 +25,83 @@ 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)
|
||||||
{
|
{
|
||||||
|
// 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);
|
_context.ProjectTasks.Update(task);
|
||||||
await _context.SaveChangesAsync();
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteTaskAsync(int taskId)
|
public async Task DeleteTaskAsync(int taskId)
|
||||||
|
|||||||
Reference in New Issue
Block a user