Feat: Add school subjet and homework management management (still WIP)

This commit is contained in:
Namu
2025-11-30 19:02:29 +01:00
parent 31c15c78bd
commit f333cac61a
15 changed files with 741 additions and 15 deletions

View File

@@ -0,0 +1,77 @@
@page "/homeworks/creation/{IdSchoolSubject:int}"
@using System.Threading.Tasks
@using WorkManagementTool.Services
@using WorkManagementTool.Data.Entities
@inject HomeworkService HomeworkService
@inject NavigationManager NavigationManager
@inject SchoolSubjectService SchoolSubjectService
<h3>HomeworkCreation</h3>
@if (SchoolSubject is null)
{
<p>School subject not found</p>
}
else
{
<EditForm Model="@Model" OnValidSubmit="Submit" FormName="homeworks-creation">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group">
<label for="title" class="form-label">Title</label>
<InputText id="title" class="form-control" @bind-Value="Model!.Title" />
</div>
<div class="form-group">
<label for="description" class="form-label">Description</label>
<InputTextArea id="description" class="form-control" @bind-Value="Model!.Description" />
</div>
<div class="form-group">
<label for="dueDate" class="form-label">Due date</label>
<InputDate id="dueDate" class="form-control" @bind-Value="Model!.DueDate" />
</div>
<div class="form-group form-check">
<InputCheckbox id="isCompleted" class="form-check-input" @bind-Value="Model!.IsCompleted" />
<label for="isCompleted" class="form-check-label">Completed</label>
</div>
<div class="form-group">
<label for="deliveryMethod" class="form-label">Delivery method</label>
<InputText id="deliveryMethod" class="form-control" @bind-Value="Model!.DeliveryMethod" />
</div>
<button type="submit" class="btn btn-primary">+</button>
</EditForm>
}
@code {
[SupplyParameterFromForm]
private Homework? Model { get; set; }
[Parameter]
public int IdSchoolSubject { get; set; }
private SchoolSubject? SchoolSubject { get; set; }
protected override async Task OnInitializedAsync()
{
Model ??= new Homework
{
Title = null!,
Deleted = false,
DueDate = DateTime.Now.AddDays(1),
IsCompleted = false,
};
SchoolSubject = await SchoolSubjectService.GetSchoolSubjectByIdAsync(IdSchoolSubject);
}
private async Task Submit()
{
if (Model is not null && SchoolSubject is not null)
{
Model!.SchoolSubject = SchoolSubject!;
await HomeworkService.AddHomeworkAsync(Model!);
NavigationManager.NavigateTo($"/homeworks/{IdSchoolSubject}");
}
}
}

View File

@@ -0,0 +1,39 @@
<h3>HomeworkDisplay</h3>
@using WorkManagementTool.Services
@using WorkManagementTool.Data.Entities;
@inject HomeworkService HomeworkService
@if (homework is null)
{
<p>Homework not found</p>
}
else
{
<dl>
<dh>Title</dh>
<dd>@homework.Title</dd>
<dh>Description</dh>
<dd>@homework.Description</dd>
<dh>Due date</dh>
<dd>@homework.DueDate</dd>
<dh>Completed</dh>
<dd>@homework.IsCompleted</dd>
</dl>
}
@code {
[Parameter]
public int Id { get; set; }
private Homework? homework;
protected override async Task OnInitializedAsync()
{
homework = await HomeworkService.GetHomeworkByIdAsync(Id);
}
}

View File

@@ -0,0 +1,63 @@
@page "/homeworks/{IdSchoolSubject:int}"
@rendermode InteractiveServer
@using WorkManagementTool.Services
@using WorkManagementTool.Data.Entities
@inject HomeworkService HomeworkService
<h3>HomeworkListing</h3>
<NavLink class="btn btn-primary" href="@($"/homeworks/creation/{IdSchoolSubject}")">+</NavLink>
<div class="form-check mb-3">
<input type="checkbox"
class="form-check-input"
id="showCompleted"
@bind="showCompleted"
@bind:after="LoadHomeworksAsync">
<label class="form-check-label" for="showCompleted">Show Completed</label>
</div>
@if (homeworkList == null)
{
<p>Loading...</p>
}
else if (homeworkList.Count == 0)
{
<p>No homework found</p>
}
else
{
<ul>
@foreach (var homework in homeworkList)
{
<li>
<strong>@homework.Title</strong> - Due: @homework.DueDate.ToShortDateString() - Completed: @(homework.IsCompleted ? "Yes" : "No")
</li>
}
</ul>
}
@code {
private List<Homework>? homeworkList;
[Parameter]
public int IdSchoolSubject { get; set; }
private bool showCompleted { get; set; } = false;
protected override async Task OnInitializedAsync()
{
await LoadHomeworksAsync();
}
private async Task LoadHomeworksAsync()
{
homeworkList = new();
var uncompletedHomework = await HomeworkService.GetUncompletedHomeworkAsync(IdSchoolSubject);
homeworkList.AddRange(uncompletedHomework);
if (showCompleted)
{
var completedHomework = await HomeworkService.GetCompletedHomeworksAsync(IdSchoolSubject);
homeworkList.AddRange(completedHomework);
}
}
}

View File

@@ -26,7 +26,7 @@
<div class="nav-item px-3">
<NavLink class="nav-link" href="school-subjects">
<span class="bi bi-lock-nav-menu" aria-hidden="true"></span> School Subjects
School Subjects
</NavLink>
</div>

View File

@@ -7,8 +7,10 @@
<h3>SchoolSubjectCreation</h3>
<form method="post" @onsubmit="Submit" @formname="school-subject-creation">
<AntiforgeryToken />
<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" />
@@ -18,13 +20,23 @@
<InputTextArea class="form-control" @bind-value="Model!.Description"></InputTextArea>
</div>
<button type="submit" class="btn btn-primary">Create School Subject</button>
</form>
</EditForm>
@code {
[SupplyParameterFromForm]
private SchoolSubject? Model { get; set; }
protected override void OnInitialized() => Model ??= new();
protected override void OnInitialized()
{
if (Model is null)
{
Model = new SchoolSubject
{
Name = null!,
Description = null,
};
}
}
private async Task Submit()
{

View File

@@ -23,10 +23,13 @@ else
<ul>
@foreach (var schoolSubject in schoolSubjects)
{
<SchoolSubjectDisplay Id="@schoolSubject.Id" />
<div class="btn-group">
<SchoolSubjectDeletion Id="@schoolSubject.Id" OnDeleted="HandleDeletion" />
</div>
<li>
<SchoolSubjectDisplay Id="@schoolSubject.Id" />
<div class="btn-group">
<SchoolSubjectDeletion Id="@schoolSubject.Id" OnDeleted="HandleDeletion" />
<NavLink href="@($"/homeworks/{schoolSubject.Id}")" class="btn btn-primary">Manage homeworks</NavLink>
</div>
</li>
}
</ul>
}

View File

@@ -1,14 +1,25 @@
namespace WorkManagementTool.Data.Entities
using System.ComponentModel.DataAnnotations;
namespace WorkManagementTool.Data.Entities
{
public class Homework
{
public int Id { get; set; }
public string Title { get; set; } = null!;
[Required]
[MaxLength(200, ErrorMessage = "Title must be less than 200 caracters")]
[MinLength(3, ErrorMessage = "Title must be at least 3 caracters")]
public required string Title { get; set; } = null!;
[MaxLength(1000, ErrorMessage = "Description must be less than 1000 caracters")]
[MinLength(3, ErrorMessage = "Description must be at least 3 caracters")]
public string? Description { get; set; }
public DateTime DueDate { get; set; }
public bool IsCompleted { get; set; }
[Required]
[DataType(DataType.Date)]
public required DateTime DueDate { get; set; }
public required bool IsCompleted { get; set; }
public SchoolSubject SchoolSubject { get; set; } = null!;
public string? DeliveryMethod { get; set; }
public ApplicationUser CreatedBy { get; set; } = null!;
public bool Deleted { get; set; } = false;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
}

View File

@@ -1,9 +1,14 @@
namespace WorkManagementTool.Data.Entities
using System.ComponentModel.DataAnnotations;
namespace WorkManagementTool.Data.Entities
{
public class SchoolSubject
{
public int Id { get; set; }
public string Name { get; set; } = null!;
[Required]
[MaxLength(100, ErrorMessage = "Name must be less than 100 caracters")]
public required 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;
public bool Deleted { get; set; } = false;

View File

@@ -0,0 +1,441 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using WorkManagementTool.Data;
#nullable disable
namespace WorkManagementTool.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20251130000007_AddRequiredFieldsAndMissingCreatedAtForHomework")]
partial class AddRequiredFieldsAndMissingCreatedAtForHomework
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserPasskey<string>", b =>
{
b.Property<byte[]>("CredentialId")
.HasMaxLength(1024)
.HasColumnType("BLOB");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("CredentialId");
b.HasIndex("UserId");
b.ToTable("AspNetUserPasskeys", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("WorkManagementTool.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("WorkManagementTool.Data.Entities.Homework", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("CreatedById")
.HasColumnType("TEXT");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("DeliveryMethod")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("TEXT");
b.Property<DateTime>("DueDate")
.HasColumnType("TEXT");
b.Property<bool>("IsCompleted")
.HasColumnType("INTEGER");
b.Property<int>("SchoolSubjectId")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.HasIndex("SchoolSubjectId");
b.ToTable("Homeworks");
});
modelBuilder.Entity("WorkManagementTool.Data.Entities.SchoolSubject", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("CreatedById")
.HasColumnType("TEXT");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.ToTable("SchoolSubjects");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("WorkManagementTool.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("WorkManagementTool.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserPasskey<string>", b =>
{
b.HasOne("WorkManagementTool.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("Microsoft.AspNetCore.Identity.IdentityPasskeyData", "Data", b1 =>
{
b1.Property<byte[]>("IdentityUserPasskeyCredentialId");
b1.Property<byte[]>("AttestationObject")
.IsRequired();
b1.Property<byte[]>("ClientDataJson")
.IsRequired();
b1.Property<DateTimeOffset>("CreatedAt");
b1.Property<bool>("IsBackedUp");
b1.Property<bool>("IsBackupEligible");
b1.Property<bool>("IsUserVerified");
b1.Property<string>("Name");
b1.Property<byte[]>("PublicKey")
.IsRequired();
b1.Property<uint>("SignCount");
b1.PrimitiveCollection<string>("Transports");
b1.HasKey("IdentityUserPasskeyCredentialId");
b1.ToTable("AspNetUserPasskeys");
b1.ToJson("Data");
b1.WithOwner()
.HasForeignKey("IdentityUserPasskeyCredentialId");
});
b.Navigation("Data")
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("WorkManagementTool.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("WorkManagementTool.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("WorkManagementTool.Data.Entities.Homework", b =>
{
b.HasOne("WorkManagementTool.Data.ApplicationUser", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
b.HasOne("WorkManagementTool.Data.Entities.SchoolSubject", "SchoolSubject")
.WithMany()
.HasForeignKey("SchoolSubjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CreatedBy");
b.Navigation("SchoolSubject");
});
modelBuilder.Entity("WorkManagementTool.Data.Entities.SchoolSubject", b =>
{
b.HasOne("WorkManagementTool.Data.ApplicationUser", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
b.Navigation("CreatedBy");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace WorkManagementTool.Migrations
{
/// <inheritdoc />
public partial class AddRequiredFieldsAndMissingCreatedAtForHomework : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "CreatedAt",
table: "Homeworks",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<bool>(
name: "Deleted",
table: "Homeworks",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CreatedAt",
table: "Homeworks");
migrationBuilder.DropColumn(
name: "Deleted",
table: "Homeworks");
}
}
}

View File

@@ -237,13 +237,20 @@ namespace WorkManagementTool.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("CreatedById")
.HasColumnType("TEXT");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("DeliveryMethod")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("TEXT");
b.Property<DateTime>("DueDate")
@@ -257,6 +264,7 @@ namespace WorkManagementTool.Migrations
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.HasKey("Id");
@@ -284,10 +292,12 @@ namespace WorkManagementTool.Migrations
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");

View File

@@ -20,6 +20,30 @@ namespace WorkManagementTool.Services
.ToListAsync();
}
public async Task<List<Homework>> GetAllHomeworksAsync()
{
return await _context.Homeworks.ToListAsync();
}
public async Task<List<Homework>> GetCompletedHomeworksAsync(int idSchoolSubject)
{
return await _context.Homeworks
.Where(w => w.IsCompleted && w.SchoolSubject.Id == idSchoolSubject)
.ToListAsync();
}
public async Task<List<Homework>> GetUncompletedHomeworkAsync(int idSchoolSubject)
{
return await _context.Homeworks
.Where(w => !w.IsCompleted && w.SchoolSubject.Id == idSchoolSubject)
.ToListAsync();
}
public async Task<Homework?> GetHomeworkByIdAsync(int id)
{
return await _context.Homeworks.FindAsync(id);
}
public async Task AddHomeworkAsync(Homework homework)
{
_context.Homeworks.Add(homework);