diff --git a/DotnetFoundation/DotnetFoundation.Api/Controllers/TaskController.cs b/DotnetFoundation/DotnetFoundation.Api/Controllers/TaskController.cs new file mode 100644 index 0000000..021c8d3 --- /dev/null +++ b/DotnetFoundation/DotnetFoundation.Api/Controllers/TaskController.cs @@ -0,0 +1,123 @@ +using DotnetFoundation.Application.Interfaces.Services; +using DotnetFoundation.Application.Models.Common; +using DotnetFoundation.Application.Models.DTOs.TaskDetailsDTO; +using DotnetFoundation.Application.Models.Enums; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DotnetFoundation.Api.Controllers; + +[ApiController] +[Route("api/tasks")] +public class TaskController : ControllerBase +{ + private readonly ITaskDetailsService _taskDetailsService; + public TaskController(ITaskDetailsService TaskDetailsService) + { + _taskDetailsService = TaskDetailsService; + } + + /// + /// Get all tasks. + /// + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>>> GetAllTasksAsync() + { + BaseResponse> response = new(ResponseStatus.Fail); + + response.Data = await _taskDetailsService.GetAllTasksAsync().ConfigureAwait(false); + response.Status = ResponseStatus.Success; + + return Ok(response); + } + + /// + /// Get all active tasks. + /// + [HttpGet("active")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>>> GetActiveTasksAsync() + { + BaseResponse> response = new(ResponseStatus.Fail); + + response.Data = await _taskDetailsService.GetActiveTasksAsync().ConfigureAwait(false); + response.Status = ResponseStatus.Success; + + return Ok(response); + } + + /// + /// Get task details by Id. + /// + /// Id of task record + [HttpGet("{taskId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetTaskByIdAsync(int taskId) + { + BaseResponse response = new(ResponseStatus.Fail); + + response.Data = await _taskDetailsService.GetTaskByIdAsync(taskId).ConfigureAwait(false); + response.Status = ResponseStatus.Success; + + return Ok(response); + } + + /// + /// Add new task. + /// + /// Role request details + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> InsertTaskAsync(TaskDetailsRequest detailRequest) + { + BaseResponse response = new(ResponseStatus.Fail); + + response.Data = await _taskDetailsService.InsertTaskAsync(detailRequest).ConfigureAwait(false); + response.Status = ResponseStatus.Success; + + return Ok(response); + } + + /// + /// Update details of a task when the Id is passed. + /// + /// Id of task record + /// Modified details for task record + [HttpPut("{taskId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> UpdateTaskAsync(int taskId, TaskDetailsRequest modifiedDetails) + { + BaseResponse response = new(ResponseStatus.Fail); + + response.Data = await _taskDetailsService.UpdateTaskAsync(taskId, modifiedDetails).ConfigureAwait(false); + response.Status = ResponseStatus.Success; + + return Ok(response); + } + + /// + /// Change status of task to inactive. + /// + /// Id of task record + [HttpDelete("{taskId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> InactiveTaskAsync(int taskId) + { + BaseResponse response = new(ResponseStatus.Fail); + + response.Data = await _taskDetailsService.InactiveTaskAsync(taskId).ConfigureAwait(false); + response.Status = ResponseStatus.Success; + + return Ok(response); + } +} diff --git a/DotnetFoundation/DotnetFoundation.Api/Helpers/AutoMappingProfile.cs b/DotnetFoundation/DotnetFoundation.Api/Helpers/AutoMappingProfile.cs index b9484cc..d2117d9 100644 --- a/DotnetFoundation/DotnetFoundation.Api/Helpers/AutoMappingProfile.cs +++ b/DotnetFoundation/DotnetFoundation.Api/Helpers/AutoMappingProfile.cs @@ -1,5 +1,6 @@ using AutoMapper; using DotnetFoundation.Application.Models.DTOs.UserDTO; +using DotnetFoundation.Application.Models.DTOs.TaskDetailsDTO; using DotnetFoundation.Domain.Entities; namespace DotnetFoundation.Api.Helpers; @@ -9,5 +10,6 @@ public class AutoMappingProfile : Profile public AutoMappingProfile() { CreateMap(); + CreateMap(); } } diff --git a/DotnetFoundation/DotnetFoundation.Application/Interfaces/Persistence/ITaskDetailsRepository.cs b/DotnetFoundation/DotnetFoundation.Application/Interfaces/Persistence/ITaskDetailsRepository.cs new file mode 100644 index 0000000..58fdc0c --- /dev/null +++ b/DotnetFoundation/DotnetFoundation.Application/Interfaces/Persistence/ITaskDetailsRepository.cs @@ -0,0 +1,17 @@ +using DotnetFoundation.Application.Models.DTOs.TaskDetailsDTO; +using DotnetFoundation.Domain.Entities; + +namespace DotnetFoundation.Application.Interfaces.Persistence; + +/// +/// Represents the repository interface for handling task related operations. +/// +public interface ITaskDetailsRepository +{ + public Task> GetAllTasksAsync(); + public Task> GetActiveTasksAsync(); + public Task GetTaskByIdAsync(int id); + public Task InsertTaskAsync(TaskDetails taskDetails); + public Task UpdateTaskAsync(TaskDetailsRequest modifiedDetails, TaskDetails existingDetails); + public Task InactiveTaskAsync(TaskDetails existingDetails); +} diff --git a/DotnetFoundation/DotnetFoundation.Application/Interfaces/Services/ITaskDetailsService.cs b/DotnetFoundation/DotnetFoundation.Application/Interfaces/Services/ITaskDetailsService.cs new file mode 100644 index 0000000..b6fb6ff --- /dev/null +++ b/DotnetFoundation/DotnetFoundation.Application/Interfaces/Services/ITaskDetailsService.cs @@ -0,0 +1,16 @@ +using DotnetFoundation.Application.Models.DTOs.TaskDetailsDTO; + +namespace DotnetFoundation.Application.Interfaces.Services; + +/// +/// Provides functionality for task related operations. +/// +public interface ITaskDetailsService +{ + public Task> GetAllTasksAsync(); + public Task> GetActiveTasksAsync(); + public Task GetTaskByIdAsync(int id); + public Task InsertTaskAsync(TaskDetailsRequest request); + public Task UpdateTaskAsync(int id, TaskDetailsRequest modifiedDetails); + public Task InactiveTaskAsync(int id); +} \ No newline at end of file diff --git a/DotnetFoundation/DotnetFoundation.Application/Interfaces/Services/IUsersService.cs b/DotnetFoundation/DotnetFoundation.Application/Interfaces/Services/IUsersService.cs index 10d9a37..1edb0a7 100644 --- a/DotnetFoundation/DotnetFoundation.Application/Interfaces/Services/IUsersService.cs +++ b/DotnetFoundation/DotnetFoundation.Application/Interfaces/Services/IUsersService.cs @@ -1,5 +1,6 @@ using DotnetFoundation.Application.Models.DTOs.UserDTO; using DotnetFoundation.Domain.Enums; + namespace DotnetFoundation.Application.Interfaces.Services; public interface IUserService diff --git a/DotnetFoundation/DotnetFoundation.Application/Models/DTOs/TaskDetailsDTO/TaskDetailsRequest.cs b/DotnetFoundation/DotnetFoundation.Application/Models/DTOs/TaskDetailsDTO/TaskDetailsRequest.cs new file mode 100644 index 0000000..ac8a60f --- /dev/null +++ b/DotnetFoundation/DotnetFoundation.Application/Models/DTOs/TaskDetailsDTO/TaskDetailsRequest.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; + +namespace DotnetFoundation.Application.Models.DTOs.TaskDetailsDTO; + +public record TaskDetailsRequest +{ + /// + /// Gets the Description of the task. + /// + [Required(ErrorMessage = "Description is required")] + public string Description { get; init; } = string.Empty; + /// + /// Gets the Budgeted Hours of the task. + /// + [Required(ErrorMessage = "BudgetedHours is required")] + [Range(0, int.MaxValue, ErrorMessage = "BudgetedHours should be in range of 0 to int.Maxvalue")] + public int BudgetedHours { get; init; } + /// + /// Gets the user Id of the user the task is assigned to. + /// + [Required(ErrorMessage = "AssignedTo is required")] + public int AssignedTo { get; init; } + /// + /// Gets the Category of the task. + /// + public string? Category { get; init; } +} diff --git a/DotnetFoundation/DotnetFoundation.Application/Models/DTOs/TaskDetailsDTO/TaskDetailsResponse.cs b/DotnetFoundation/DotnetFoundation.Application/Models/DTOs/TaskDetailsDTO/TaskDetailsResponse.cs new file mode 100644 index 0000000..79623db --- /dev/null +++ b/DotnetFoundation/DotnetFoundation.Application/Models/DTOs/TaskDetailsDTO/TaskDetailsResponse.cs @@ -0,0 +1,20 @@ +using DotnetFoundation.Domain.Enums; + +namespace DotnetFoundation.Application.Models.DTOs.TaskDetailsDTO; + +/// +/// Represents the response for authentication-related actions. +/// +public record TaskDetailsResponse +{ + public int Id { get; init; } + public required string Description { get; init; } + public int BudgetedHours { get; init; } + public int AssignedTo { get; init; } + public string? Category { get; init; } + public Status Status { get; init; } + public DateTime CreatedOn { get; init; } + public int CreatedBy { get; init; } + public DateTime ModifiedOn { get; init; } + public int ModifiedBy { get; init; } +} \ No newline at end of file diff --git a/DotnetFoundation/DotnetFoundation.Domain/Entities/TaskDetails.cs b/DotnetFoundation/DotnetFoundation.Domain/Entities/TaskDetails.cs new file mode 100644 index 0000000..9828efb --- /dev/null +++ b/DotnetFoundation/DotnetFoundation.Domain/Entities/TaskDetails.cs @@ -0,0 +1,11 @@ +using DotnetFoundation.Domain.Common; + +namespace DotnetFoundation.Domain.Entities; + +public class TaskDetails : BaseEntity +{ + public string Description { get; set; } = null!; + public int BudgetedHours { get; set; } + public int AssignedTo { get; set; } + public string? Category { get; set; } +} \ No newline at end of file diff --git a/DotnetFoundation/DotnetFoundation.Infrastructure/DependencyInjection.cs b/DotnetFoundation/DotnetFoundation.Infrastructure/DependencyInjection.cs index 835188b..172cf98 100644 --- a/DotnetFoundation/DotnetFoundation.Infrastructure/DependencyInjection.cs +++ b/DotnetFoundation/DotnetFoundation.Infrastructure/DependencyInjection.cs @@ -1,5 +1,6 @@ using DotnetFoundation.Application.Interfaces.Integrations; using DotnetFoundation.Application.Interfaces.Persistence; +using DotnetFoundation.Application.Interfaces.Services; using DotnetFoundation.Infrastructure.Identity; using DotnetFoundation.Infrastructure.Integrations; using DotnetFoundation.Infrastructure.Persistence; @@ -73,7 +74,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi // Configure service scope for repositories services.AddScoped(); services.AddScoped(); - + services.AddScoped(); services.AddHttpClient(); return services; diff --git a/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/20240228094549_CreateTasks.Designer.cs b/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/20240228094549_CreateTasks.Designer.cs new file mode 100644 index 0000000..742fe58 --- /dev/null +++ b/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/20240228094549_CreateTasks.Designer.cs @@ -0,0 +1,363 @@ +// +using System; +using DotnetFoundation.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DotnetFoundation.Infrastructure.Migrations +{ + [DbContext(typeof(SqlDatabaseContext))] + [Migration("20240228094549_CreateTasks")] + partial class CreateTasks + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("DotnetFoundation.Domain.Entities.TaskDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AssignedTo") + .HasColumnType("int"); + + b.Property("BudgetedHours") + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("tasks", (string)null); + }); + + modelBuilder.Entity("DotnetFoundation.Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IdentityApplicationUserId") + .HasColumnType("varchar(255)"); + + b.Property("LastName") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IdentityApplicationUserId") + .IsUnique(); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("DotnetFoundation.Infrastructure.Identity.IdentityApplicationUser", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("DotnetFoundation.Infrastructure.Identity.ApplicationUser", b => + { + b.HasOne("DotnetFoundation.Infrastructure.Identity.IdentityApplicationUser", "IdentityApplicationUser") + .WithOne("ApplicationUser") + .HasForeignKey("DotnetFoundation.Infrastructure.Identity.ApplicationUser", "IdentityApplicationUserId"); + + b.Navigation("IdentityApplicationUser"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("DotnetFoundation.Infrastructure.Identity.IdentityApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("DotnetFoundation.Infrastructure.Identity.IdentityApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DotnetFoundation.Infrastructure.Identity.IdentityApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("DotnetFoundation.Infrastructure.Identity.IdentityApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DotnetFoundation.Infrastructure.Identity.IdentityApplicationUser", b => + { + b.Navigation("ApplicationUser") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/20240228094549_CreateTasks.cs b/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/20240228094549_CreateTasks.cs new file mode 100644 index 0000000..bd38a18 --- /dev/null +++ b/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/20240228094549_CreateTasks.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DotnetFoundation.Infrastructure.Migrations +{ + /// + public partial class CreateTasks : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "tasks", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Description = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + BudgetedHours = table.Column(type: "int", nullable: false), + AssignedTo = table.Column(type: "int", nullable: false), + Category = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedOn = table.Column(type: "datetime(6)", nullable: false), + CreatedBy = table.Column(type: "int", nullable: false), + ModifiedOn = table.Column(type: "datetime(6)", nullable: false), + ModifiedBy = table.Column(type: "int", nullable: false), + Status = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_tasks", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "tasks"); + } + } +} diff --git a/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/20240229050021_IndexingForTasks.Designer.cs b/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/20240229050021_IndexingForTasks.Designer.cs new file mode 100644 index 0000000..8c11058 --- /dev/null +++ b/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/20240229050021_IndexingForTasks.Designer.cs @@ -0,0 +1,377 @@ +// +using System; +using DotnetFoundation.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DotnetFoundation.Infrastructure.Migrations +{ + [DbContext(typeof(SqlDatabaseContext))] + [Migration("20240229050021_IndexingForTasks")] + partial class IndexingForTasks + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("DotnetFoundation.Domain.Entities.TaskDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AssignedTo") + .HasColumnType("int"); + + b.Property("BudgetedHours") + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AssignedTo"); + + b.HasIndex("Status"); + + b.ToTable("tasks", (string)null); + }); + + modelBuilder.Entity("DotnetFoundation.Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Country") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IdentityApplicationUserId") + .HasColumnType("varchar(255)"); + + b.Property("LastName") + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IdentityApplicationUserId") + .IsUnique(); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("DotnetFoundation.Infrastructure.Identity.IdentityApplicationUser", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("DotnetFoundation.Infrastructure.Identity.ApplicationUser", b => + { + b.HasOne("DotnetFoundation.Infrastructure.Identity.IdentityApplicationUser", "IdentityApplicationUser") + .WithOne("ApplicationUser") + .HasForeignKey("DotnetFoundation.Infrastructure.Identity.ApplicationUser", "IdentityApplicationUserId"); + + b.Navigation("IdentityApplicationUser"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("DotnetFoundation.Infrastructure.Identity.IdentityApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("DotnetFoundation.Infrastructure.Identity.IdentityApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DotnetFoundation.Infrastructure.Identity.IdentityApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("DotnetFoundation.Infrastructure.Identity.IdentityApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DotnetFoundation.Infrastructure.Identity.IdentityApplicationUser", b => + { + b.Navigation("ApplicationUser") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/20240229050021_IndexingForTasks.cs b/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/20240229050021_IndexingForTasks.cs new file mode 100644 index 0000000..abaa80b --- /dev/null +++ b/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/20240229050021_IndexingForTasks.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DotnetFoundation.Infrastructure.Migrations +{ + /// + public partial class IndexingForTasks : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_tasks_AssignedTo", + table: "tasks", + column: "AssignedTo"); + + migrationBuilder.CreateIndex( + name: "IX_tasks_Status", + table: "tasks", + column: "Status"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_tasks_AssignedTo", + table: "tasks"); + + migrationBuilder.DropIndex( + name: "IX_tasks_Status", + table: "tasks"); + } + } +} diff --git a/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/SqlDatabaseContextModelSnapshot.cs b/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/SqlDatabaseContextModelSnapshot.cs index f785d82..e422436 100644 --- a/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/SqlDatabaseContextModelSnapshot.cs +++ b/DotnetFoundation/DotnetFoundation.Infrastructure/Migrations/SqlDatabaseContextModelSnapshot.cs @@ -19,6 +19,49 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasAnnotation("ProductVersion", "8.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 64); + modelBuilder.Entity("DotnetFoundation.Domain.Entities.TaskDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AssignedTo") + .HasColumnType("int"); + + b.Property("BudgetedHours") + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("longtext"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("CreatedOn") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("ModifiedOn") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AssignedTo"); + + b.HasIndex("Status"); + + b.ToTable("tasks", (string)null); + }); + modelBuilder.Entity("DotnetFoundation.Infrastructure.Identity.ApplicationUser", b => { b.Property("Id") @@ -35,6 +78,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetime(6)"); b.Property("Email") + .IsRequired() .HasColumnType("longtext"); b.Property("FirstName") diff --git a/DotnetFoundation/DotnetFoundation.Infrastructure/Persistence/TaskRepository.cs b/DotnetFoundation/DotnetFoundation.Infrastructure/Persistence/TaskRepository.cs new file mode 100644 index 0000000..f5e336c --- /dev/null +++ b/DotnetFoundation/DotnetFoundation.Infrastructure/Persistence/TaskRepository.cs @@ -0,0 +1,67 @@ +using DotnetFoundation.Application.Models.DTOs.TaskDetailsDTO; +using DotnetFoundation.Application.Interfaces.Persistence; +using DotnetFoundation.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using DotnetFoundation.Domain.Enums; + +namespace DotnetFoundation.Infrastructure.Persistence; + +public class TaskDetailsRepository : ITaskDetailsRepository +{ + private readonly SqlDatabaseContext _dbContext; + public TaskDetailsRepository(SqlDatabaseContext sqlDatabaseContext) + { + _dbContext = sqlDatabaseContext; + } + + public async Task> GetAllTasksAsync() + { + List taskObj = await _dbContext.TaskDetails.ToListAsync().ConfigureAwait(false); + return taskObj; + } + + public async Task> GetActiveTasksAsync() + { + List taskObj = (await _dbContext.TaskDetails + .Where(task => task.Status == Status.ACTIVE) + .ToListAsync().ConfigureAwait(false)); + return taskObj; + } + + public async Task GetTaskByIdAsync(int id) + { + TaskDetails? taskObj = await _dbContext.TaskDetails.FindAsync(id).ConfigureAwait(false); + return taskObj; + } + + public async Task InsertTaskAsync(TaskDetails taskDetails) + { + _dbContext.TaskDetails.Add(taskDetails); + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + + return taskDetails.Id; + } + + public async Task UpdateTaskAsync(TaskDetailsRequest modifiedDetails, TaskDetails existingDetails) + { + // Modify data + existingDetails.Description = modifiedDetails.Description; + existingDetails.Category = modifiedDetails.Category; + existingDetails.BudgetedHours = modifiedDetails.BudgetedHours; + existingDetails.AssignedTo = modifiedDetails.AssignedTo; + existingDetails.ModifiedOn = DateTime.UtcNow; + + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + return existingDetails; + } + + public async Task InactiveTaskAsync(TaskDetails existingDetails) + { + // Modify task to INACTIVE + existingDetails.Status = Status.INACTIVE; + existingDetails.ModifiedOn = DateTime.UtcNow; + + await _dbContext.SaveChangesAsync().ConfigureAwait(false); + return existingDetails; + } +} \ No newline at end of file diff --git a/DotnetFoundation/DotnetFoundation.Infrastructure/SqlDatabaseContext.cs b/DotnetFoundation/DotnetFoundation.Infrastructure/SqlDatabaseContext.cs index 20045bd..a5d5cf8 100644 --- a/DotnetFoundation/DotnetFoundation.Infrastructure/SqlDatabaseContext.cs +++ b/DotnetFoundation/DotnetFoundation.Infrastructure/SqlDatabaseContext.cs @@ -1,6 +1,6 @@ namespace DotnetFoundation.Infrastructure; - +using DotnetFoundation.Domain.Entities; using DotnetFoundation.Infrastructure.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; @@ -12,6 +12,7 @@ public SqlDatabaseContext(DbContextOptions options) : base(o } public DbSet ApplicationUsers { get; set; } + public DbSet TaskDetails { get; set; } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); @@ -24,8 +25,18 @@ protected override void OnModelCreating(ModelBuilder builder) .HasOne(au => au.IdentityApplicationUser) .WithOne(iu => iu.ApplicationUser) .HasForeignKey(au => au.IdentityApplicationUserId); - } + // for tasks + builder.Entity() + .HasKey(t => t.Id); + + builder.Entity() + .HasIndex(t => t.Status); + builder.Entity() + .HasIndex(t => t.AssignedTo); + + builder.Entity().ToTable("tasks"); + } } diff --git a/DotnetFoundation/DotnetFoundation.Services/DependencyInjection.cs b/DotnetFoundation/DotnetFoundation.Services/DependencyInjection.cs index 63bd29a..477eec7 100644 --- a/DotnetFoundation/DotnetFoundation.Services/DependencyInjection.cs +++ b/DotnetFoundation/DotnetFoundation.Services/DependencyInjection.cs @@ -1,5 +1,6 @@ using DotnetFoundation.Application.Interfaces.Services; using DotnetFoundation.Services.Services.Authentication; +using DotnetFoundation.Services.Services.TaskDetailsService; using DotnetFoundation.Services.Services.UserService; using Microsoft.Extensions.DependencyInjection; @@ -11,6 +12,7 @@ public static IServiceCollection AddServices(this IServiceCollection services) // Configure service scope for services / BLLs services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/DotnetFoundation/DotnetFoundation.Services/Services/TaskDetailsService/TaskDetailsService.cs b/DotnetFoundation/DotnetFoundation.Services/Services/TaskDetailsService/TaskDetailsService.cs new file mode 100644 index 0000000..675de83 --- /dev/null +++ b/DotnetFoundation/DotnetFoundation.Services/Services/TaskDetailsService/TaskDetailsService.cs @@ -0,0 +1,95 @@ +using AutoMapper; +using DotnetFoundation.Application.Interfaces.Persistence; +using DotnetFoundation.Application.Interfaces.Services; +using DotnetFoundation.Application.Models.DTOs.TaskDetailsDTO; +using DotnetFoundation.Domain.Entities; +using DotnetFoundation.Domain.Enums; + +namespace DotnetFoundation.Services.Services.TaskDetailsService; + +public class TaskDetailsService : ITaskDetailsService +{ + private readonly ITaskDetailsRepository _taskDetailsRepository; + private readonly IUserRepository _userRepository; + private readonly IMapper _mapper; + + public TaskDetailsService(ITaskDetailsRepository taskDetailsRepository, IMapper mapper, IUserRepository userRepository) + { + _taskDetailsRepository = taskDetailsRepository; + _userRepository = userRepository; + _mapper = mapper; + } + + + public async Task> GetAllTasksAsync() + { + List response = await _taskDetailsRepository.GetAllTasksAsync().ConfigureAwait(false); + return _mapper.Map>(response); + } + + public async Task> GetActiveTasksAsync() + { + List response = await _taskDetailsRepository.GetActiveTasksAsync().ConfigureAwait(false); + return _mapper.Map>(response); + } + + public async Task GetTaskByIdAsync(int id) + { + TaskDetails response = await _taskDetailsRepository.GetTaskByIdAsync(id).ConfigureAwait(false) + ?? throw new Exception($"Task with Id={id} does not exist"); + return _mapper.Map(response); + } + + public async Task InsertTaskAsync(TaskDetailsRequest detailsRequest) + { + User? user = await _userRepository.GetUserByIdAsync(detailsRequest.AssignedTo).ConfigureAwait(false) + ?? throw new Exception($"AssignedTo with userId = \"{detailsRequest.AssignedTo}\" does not exist. Cannot add task."); + + // Create new TaskDetails object and add relevant details + TaskDetails taskDetails = new TaskDetails + { + Description = detailsRequest.Description, + BudgetedHours = detailsRequest.BudgetedHours, + AssignedTo = detailsRequest.AssignedTo, + Category = detailsRequest.Category, + Status = Status.ACTIVE, + CreatedBy = detailsRequest.AssignedTo, + ModifiedBy = detailsRequest.AssignedTo, + ModifiedOn = DateTime.UtcNow, + }; + + int? taskId = await _taskDetailsRepository.InsertTaskAsync(taskDetails).ConfigureAwait(false) + ?? throw new Exception($"Error inserting TaskDetails for \"{detailsRequest.Description}\""); + + taskDetails.Id = (int)taskId; + + return _mapper.Map(taskDetails); + } + + public async Task UpdateTaskAsync(int id, TaskDetailsRequest modifiedDetails) + { + TaskDetails? existingDetails = await _taskDetailsRepository.GetTaskByIdAsync(id).ConfigureAwait(false) + ?? throw new Exception($"Task with Id={id} does not exist"); + + User? user = await _userRepository.GetUserByIdAsync(modifiedDetails.AssignedTo).ConfigureAwait(false) + ?? throw new Exception($"AssignedTo with userId = \"{modifiedDetails.AssignedTo}\" does not exist. Cannot add task."); + + TaskDetails? modifiedTask = await _taskDetailsRepository.UpdateTaskAsync(modifiedDetails, existingDetails).ConfigureAwait(false) + ?? throw new Exception($"An error occurred while updating Task with id = \"{id}\""); + + return _mapper.Map(modifiedTask); + } + + public async Task InactiveTaskAsync(int id) + { + TaskDetails? existingDetails = await _taskDetailsRepository.GetTaskByIdAsync(id).ConfigureAwait(false); + if (existingDetails == null) + { + throw new Exception($"Task with Id = \"{id}\" does not exist"); + } + + TaskDetails? response = await _taskDetailsRepository.InactiveTaskAsync(existingDetails).ConfigureAwait(false) + ?? throw new Exception($"Error while deactivating Task id = \"{id}\""); + return _mapper.Map(response); + } +} \ No newline at end of file