diff --git a/Contexts/MainAppContext.cs b/Contexts/MainAppContext.cs index fac806e..f9c337f 100644 --- a/Contexts/MainAppContext.cs +++ b/Contexts/MainAppContext.cs @@ -20,6 +20,9 @@ public class MainAppContext(DbContextOptions contextOptions) public DbSet TempUsers { get; set; } public DbSet Bids { get; set; } public DbSet Tasks { get; set; } + public DbSet skills { get; set; } + public DbSet ProjectLikes { get; set; } + protected override void OnModelCreating(ModelBuilder builder) { @@ -48,6 +51,9 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity().ToTable("Projects", tb => tb.HasCheckConstraint("CK_STATUS", "[Status] IN ('Available', 'Closed')")) .Property(p=>p.Status).HasDefaultValue("Available"); + builder.Entity() + .ToTable("skills", tb => tb.HasCheckConstraint("CK_NAME", "[Name] IN ('uiux', 'frontend', 'mobile', 'backend', 'fullstack')")); + builder.Entity() .HasOne(b => b.Project) .WithMany(p => p.Bids) @@ -60,9 +66,31 @@ protected override void OnModelCreating(ModelBuilder builder) .HasForeignKey(b => b.FreelancerId) .OnDelete(DeleteBehavior.NoAction); + builder.Entity() + .HasOne(s => s.freelancer) + .WithMany(f => f.Skills) + .HasForeignKey(b => b.UserId) + .OnDelete(DeleteBehavior.NoAction); builder.Entity().ToTable("Tasks"); + builder.Entity() + .HasIndex(pl => new { pl.ProjectId, pl.UserId }) + .IsUnique(); + + builder.Entity() + .HasOne(p => p.Project) + .WithMany(u => u.projectLikes) + .HasForeignKey(b => b.ProjectId) + .OnDelete(DeleteBehavior.NoAction); + + builder.Entity() + .HasOne(p => p.user) + .WithMany(u => u.projectLikes) + .HasForeignKey(b => b.UserId) + .OnDelete(DeleteBehavior.NoAction); + + base.OnModelCreating(builder); } } diff --git a/Controllers/Mobile/v1/AuthController.cs b/Controllers/Mobile/v1/AuthController.cs index 007317b..6d313ac 100644 --- a/Controllers/Mobile/v1/AuthController.cs +++ b/Controllers/Mobile/v1/AuthController.cs @@ -110,7 +110,7 @@ public async Task CompleteRegistrationAsync([FromBody] RegisterRe UserName = registerReq.Username, PhoneNumber = tempUser.PhoneNumber, PhoneNumberConfirmed = tempUser.PhoneNumberConfirmed, - Skills = registerReq.Skills ?? string.Empty, + //Skills = registerReq.Skills ?? string.Empty, }, Constants.USER_TYPE_CLIENT => new Client() { diff --git a/Controllers/Mobile/v1/DashboardController.cs b/Controllers/Mobile/v1/DashboardController.cs new file mode 100644 index 0000000..c4dec6a --- /dev/null +++ b/Controllers/Mobile/v1/DashboardController.cs @@ -0,0 +1,90 @@ +using AonFreelancing.Contexts; +using AonFreelancing.Interfaces; +using AonFreelancing.Models; +using AonFreelancing.Models.DTOs; +using AonFreelancing.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Runtime.Serialization.Formatters; + +namespace AonFreelancing.Controllers.Mobile.v1 +{ + [ApiController] + [Route("api/mobile/v1")] + public class DashboardController : BaseController + { + private readonly MainAppContext _mainAppContext; + private readonly UserManager _userManager; + private readonly IStatisticsService _statisticsService; + + public DashboardController(MainAppContext mainAppContext, IStatisticsService statisticsService, UserManager userManager) + { + _mainAppContext = mainAppContext; + _statisticsService = statisticsService; + _userManager = userManager; + } + + [HttpGet("GetMyStatistics")] + public async Task GetMyStatistics() + { + var user = await _userManager.GetUserAsync(HttpContext.User); + if (user == null) + return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), + "Unable to load user.")); + + var userId = user.Id; + + var projects = await _mainAppContext.Projects + .Where(p => p.FreelancerId == userId || p.ClientId == userId) + .ToListAsync(); + + var tasks = await _mainAppContext.Tasks + .Where(t => !t.IsDeleted && projects.Select(p => p.Id).Contains(t.ProjectId)) + .ToListAsync(); + + // Project statistics + int totalProjects = projects.Count(); + int availableProjects = projects.Count(p => p.Status == Constants.PROJECT_STATUS_AVAILABLE); + int closedProjects = projects.Count(p => p.Status == Constants.PROJECT_STATUS_CLOSED); + + // Skill issues here (Diyar) + //int totalTasks = tasks.Count; + //int toDoTasks = tasks.Count(t => t.Status.Trim().Equals(Constants.TASKS_STATUS_TO_DO, StringComparison.OrdinalIgnoreCase)); + //int inProgressTasks = tasks.Count(t => t.Status.Trim().Equals(Constants.TASKS_STATUS_IN_PROGRESS, StringComparison.OrdinalIgnoreCase)); + //int inReviewTasks = tasks.Count(t => t.Status.Trim().Equals(Constants.TASKS_STATUS_IN_REVIEW, StringComparison.OrdinalIgnoreCase)); + //int doneTasks = tasks.Count(t => t.Status.Trim().Equals(Constants.TASKS_STATUS_DONE, StringComparison.OrdinalIgnoreCase)); + + // Task statistics + int totalTasks = tasks.Count; + int toDoTasks = tasks.Count(t => t.Status == Constants.TASKS_STATUS_TO_DO); + int inProgressTasks = tasks.Count(t => t.Status == Constants.TASKS_STATUS_IN_PROGRESS); + int inReviewTasks = tasks.Count(t => t.Status == Constants.TASKS_STATUS_IN_REVIEW); + int doneTasks = tasks.Count(t => t.Status == Constants.TASKS_STATUS_DONE); + + + // Prepare response + var response = new StatisticsResponseDTO + { + Projects = new ProjectStatistics + { + Total = totalProjects, + Available = availableProjects, + Closed = closedProjects + }, + Tasks = new TaskStatistics + { + Total = totalTasks, + ToDo = _statisticsService.CalculatePercentage(totalTasks, toDoTasks), + InProgress = _statisticsService.CalculatePercentage(totalTasks, inProgressTasks), + InReview = _statisticsService.CalculatePercentage(totalTasks, inReviewTasks), + Done = _statisticsService.CalculatePercentage(totalTasks, doneTasks) + } + }; + + return Ok(CreateSuccessResponse(response)); + } + + } +} diff --git a/Controllers/Mobile/v1/ProjectsController.cs b/Controllers/Mobile/v1/ProjectsController.cs index d140b73..a5a6922 100644 --- a/Controllers/Mobile/v1/ProjectsController.cs +++ b/Controllers/Mobile/v1/ProjectsController.cs @@ -1,12 +1,14 @@ - -using AonFreelancing.Contexts; +using AonFreelancing.Contexts; using AonFreelancing.Models; using AonFreelancing.Models.DTOs; +using AonFreelancing.Services; using AonFreelancing.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.Security.Claims; namespace AonFreelancing.Controllers.Mobile.v1 { @@ -48,6 +50,52 @@ public async Task PostProjectAsync([FromBody] ProjectInputDto pro return Ok(CreateSuccessResponse("Project added.")); } + + [HttpGet("{id}")] + public async Task GetProjectDetailsAsync(long id) + { + var project = await mainAppContext.Projects + .Where(p => p.Id == id) + .Include(p => p.Bids) + .ThenInclude(b => b.Freelancer) + .FirstOrDefaultAsync(); + + if (project == null) + return NotFound(CreateErrorResponse("404", "Project not found.")); + + var orderedBids = project.Bids + .OrderByDescending(b => b.ProposedPrice) + .Select(b => new BidOutDto + { + Id = b.Id, + FreelancerId = b.FreelancerId, + Freelancer = new FreelancerShortOutDTO + { + Id = b.FreelancerId, + Name = b.Freelancer.Name + }, + ProposedPrice = b.ProposedPrice, + Notes = b.Notes, + Status = b.Status, + SubmittedAt = b.SubmittedAt, + ApprovedAt = b.ApprovedAt + }); + + + + return Ok(CreateSuccessResponse(new + { + project.Id, + project.Title, + project.Status, + project.Budget, + project.Duration, + project.Description, + Bids = orderedBids + })); + } + + [Authorize(Roles = "CLIENT")] [HttpGet("clientFeed")] public async Task GetClientFeedAsync( @@ -99,7 +147,6 @@ public async Task GetClientFeedAsync( })); } - [Authorize(Roles = "FREELANCER")] [HttpPost("{id}/bids")] public async Task SubmitBidAsync(long id, [FromBody] BidInputDto bidDto) @@ -111,6 +158,10 @@ public async Task SubmitBidAsync(long id, [FromBody] BidInputDto if (project == null) return NotFound(CreateErrorResponse("404", "Project not found.")); + if (project.Status == Constants.PROJECT_STATUS_CLOSED) + return BadRequest(CreateErrorResponse(StatusCodes.Status400BadRequest.ToString(), + "project is closed ")); + var user = await userManager.GetUserAsync(User); //if (user == null || !User.IsInRole("FREELANCER")) // return Forbid(); @@ -160,56 +211,13 @@ public async Task ApproveBidAsync(long pid, long bid) bidID.ApprovedAt = DateTime.Now; project.Status = Constants.PROJECT_STATUS_CLOSED; - + project.FreelancerId = bidID.FreelancerId; await mainAppContext.SaveChangesAsync(); return Ok(CreateSuccessResponse("Bid approved successfully.")); } - [HttpGet("{id}")] - public async Task GetProjectDetailsAsync(long id) - { - var project = await mainAppContext.Projects - .Where(p => p.Id == id) - .Include(p => p.Bids) - .ThenInclude(b => b.Freelancer) - .FirstOrDefaultAsync(); - - if (project == null) - return NotFound(CreateErrorResponse("404", "Project not found.")); - - var orderedBids = project.Bids - .OrderByDescending(b => b.ProposedPrice) - .Select(b => new BidOutDto { - Id = b.Id, - FreelancerId = b.FreelancerId, - Freelancer = new FreelancerShortOutDTO { - Id = b.FreelancerId, - Name = b.Freelancer.Name - }, - ProposedPrice = b.ProposedPrice, - Notes = b.Notes, - Status = b.Status, - SubmittedAt = b.SubmittedAt, - ApprovedAt = b.ApprovedAt - } ); - - - - return Ok(CreateSuccessResponse(new - { - project.Id, - project.Title, - project.Status, - project.Budget, - project.Duration, - project.Description, - Bids = orderedBids - })); - } - - [Authorize(Roles = "CLIENT")] [HttpPost("{id}/tasks")] public async Task CreateTaskAsync(long id, [FromBody] TaskInputDto taskDto) @@ -269,24 +277,20 @@ public async Task UploadProjectImage(long id, IFormFile file) }); } - // Define the file path to save the image (e.g., in wwwroot/images) var uploadPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images"); if (!Directory.Exists(uploadPath)) { Directory.CreateDirectory(uploadPath); } - // Generate a unique file name var fileName = Guid.NewGuid().ToString() + extension; var filePath = Path.Combine(uploadPath, fileName); - // Save the image to the server using (var stream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(stream); } - // Save the file metadata to the database var project = await mainAppContext.Projects.FindAsync(id); if (project == null) { @@ -298,11 +302,9 @@ public async Task UploadProjectImage(long id, IFormFile file) }); } - // Save the image path or filename to the project model project.ImagePath = $"/images/{fileName}"; await mainAppContext.SaveChangesAsync(); - // Return a success response with the image URL or file path return Ok(new ApiResponse { IsSuccess = true, @@ -311,6 +313,99 @@ public async Task UploadProjectImage(long id, IFormFile file) }); } + + [Authorize(Roles = "FREELANCER")] + [HttpGet("filter")] + public async Task GetProjectsFilterAsync( + [FromQuery] string? qualificationName, + [FromQuery] decimal? minBudget, + [FromQuery] decimal? maxBudget, + [FromQuery] int? timeLine) + { + var query = mainAppContext.Projects.AsQueryable(); + + if (!string.IsNullOrEmpty(qualificationName)) + { + query = query.Where(p => p.QualificationName != null && + p.QualificationName.ToLower().Contains(qualificationName.ToLower())); + } + + if (minBudget.HasValue) + { + query = query.Where(p => p.Budget >= minBudget.Value); + } + + if (maxBudget.HasValue) + { + query = query.Where(p => p.Budget <= maxBudget.Value); + } + + if (timeLine.HasValue) + { + query = query.Where(p => p.Duration <= timeLine.Value); + } + + var filteredProjects = await query.ToListAsync(); + + return Ok(CreateSuccessResponse(filteredProjects)); + + } + + // /api/mobile/v1/projects/{pid}/like + [Authorize(Roles = "CLIENT,FREELANCER")] + [HttpPost("{pid}/like")] + public async Task LikeProjectAsync(long pid, string status) + { + var user = await userManager.GetUserAsync(HttpContext.User); + + if (!ModelState.IsValid) + { + return base.CustomBadRequest(); + } + + var projectLike = await mainAppContext.ProjectLikes + .FirstOrDefaultAsync(l => l.ProjectId == pid && l.UserId == user.Id); + + if (status == Constants.PROJECT_LIKE) + { + if (projectLike == null) + { + var like = new ProjectLike + { + ProjectId = pid, + UserId = user.Id, + CreatedAt = DateTime.Now + }; + + await mainAppContext.ProjectLikes.AddAsync(like); + await mainAppContext.SaveChangesAsync(); + return Ok(CreateSuccessResponse(like)); + } + + return BadRequest(CreateErrorResponse( + StatusCodes.Status400BadRequest.ToString(), + "You already liked this project")); + } + + if (status == Constants.PROJECT_UNLIKE) + { + if (projectLike != null) + { + mainAppContext.ProjectLikes.Remove(projectLike); + await mainAppContext.SaveChangesAsync(); + return Ok(CreateSuccessResponse("unliked")); + } + + return BadRequest(CreateErrorResponse( + StatusCodes.Status400BadRequest.ToString(), + "You haven't liked this project")); + } + + return BadRequest(CreateErrorResponse( + StatusCodes.Status400BadRequest.ToString(), + "Invalid status, Use 'like' or 'unlike'.")); + } + //[HttpGet("{id}")] //public IActionResult GetProject(int id) //{ diff --git a/Controllers/Mobile/v1/TasksController.cs b/Controllers/Mobile/v1/TasksController.cs index 04529ff..2bb1d2f 100644 --- a/Controllers/Mobile/v1/TasksController.cs +++ b/Controllers/Mobile/v1/TasksController.cs @@ -14,10 +14,40 @@ namespace AonFreelancing.Controllers.Mobile.v1 { [Authorize] - [Route("api/[controller]")] + [Route("api/mobile/v1")] [ApiController] public class TasksController(MainAppContext mainAppContext, UserManager userManager) : BaseController { + [Authorize(Roles = "CLIENT")] + [HttpGet("{id}/tasks")] + public async Task GetTaskAsync(long id, [FromQuery] string? status = Constants.TASKS_STATUS_TO_DO) + { + var project = await mainAppContext.Projects.FindAsync(id); + + if (project == null || project.Status != Constants.PROJECT_STATUS_CLOSED) + { + return BadRequest(CreateErrorResponse("400", "Project not found or not closed.")); + } + + IQueryable tasksQuery = mainAppContext.Tasks.Where(t => t.ProjectId == id); + + if (!string.IsNullOrEmpty(status)) + { + tasksQuery = tasksQuery.Where(t => t.Status.ToLower() == status.ToLower()); + } + + var tasks = await tasksQuery.ToListAsync(); + + if (tasks.Count == 0) + { + return NotFound(CreateErrorResponse("404", "No tasks found.")); + } + + return Ok(CreateSuccessResponse(tasks)); + } + + + [Authorize(Roles = "CLIENT, FREELANCER")] [HttpPut("tasks/{id}/updateStatus")] public async Task UpdateTaskAsync(long id, [FromBody] TaskUpdateDTO taskUpdateDTO) @@ -54,7 +84,7 @@ public async Task UpdateTaskAsync(long id, [FromBody] TaskUpdateD [Authorize(Roles = "CLIENT")] - [HttpPut("tasks/{pid}/checkProgress")] + [HttpGet("tasks/{pid}/checkProgress")] public async Task CheckProgressStatusAsync( int pid ) { decimal countDone= await mainAppContext.Tasks.Where(s => s.Status== Constants.TASKS_STATUS_DONE&&s.ProjectId==pid && s.IsDeleted==false).CountAsync(); diff --git a/Controllers/Mobile/v1/UsersController.cs b/Controllers/Mobile/v1/UsersController.cs index 4c70fef..54a395f 100644 --- a/Controllers/Mobile/v1/UsersController.cs +++ b/Controllers/Mobile/v1/UsersController.cs @@ -12,13 +12,13 @@ namespace AonFreelancing.Controllers.Mobile.v1 [Authorize] [Route("api/mobile/v1/users")] [ApiController] - public class UsersController(MainAppContext mainAppContext, RoleManager roleManager) + public class UsersController(MainAppContext mainAppContext, UserManager userManager, RoleManager roleManager) : BaseController { [HttpGet("{id}/profile")] - public async Task GetProfileByIdAsync([FromRoute]long id) + public async Task GetProfileByIdAsync([FromRoute] long id) { - var freelancer = await mainAppContext.Users + var freelancer = await mainAppContext.Users .OfType().Where(f => f.Id == id) .Select(f => new FreelancerResponseDTO { @@ -29,7 +29,10 @@ public async Task GetProfileByIdAsync([FromRoute]long id) UserType = Constants.USER_TYPE_FREELANCER, IsPhoneNumberVerified = f.PhoneNumberConfirmed, Role = new RoleResponseDTO { Name = Constants.USER_TYPE_FREELANCER }, - Skills = f.Skills, + Skills = f.Skills.Select(p => new SkillDTO + { + Name = p.Name + }), }).FirstOrDefaultAsync(); @@ -47,25 +50,25 @@ public async Task GetProfileByIdAsync([FromRoute]long id) .Where(c => c.Id == id) .Include(c => c.Projects) .Select(c => new ClientResponseDTO - { - Id = c.Id, - Name = c.Name, - Username = c.UserName ?? string.Empty, - PhoneNumber = c.PhoneNumber ?? string.Empty, - UserType = Constants.USER_TYPE_CLIENT, - IsPhoneNumberVerified = c.PhoneNumberConfirmed, - Role = new RoleResponseDTO { Name = Constants.USER_TYPE_CLIENT }, - Projects = c.Projects.Select(p => new ProjectDetailsDTO - { - Id = p.Id, - Description = p.Description, - EndDate = p.EndDate, - StartDate = p.StartDate, - Name = p.Title, - }), - CompanyName = c.CompanyName, - - }).FirstOrDefaultAsync(); + { + Id = c.Id, + Name = c.Name, + Username = c.UserName ?? string.Empty, + PhoneNumber = c.PhoneNumber ?? string.Empty, + UserType = Constants.USER_TYPE_CLIENT, + IsPhoneNumberVerified = c.PhoneNumberConfirmed, + Role = new RoleResponseDTO { Name = Constants.USER_TYPE_CLIENT }, + Projects = c.Projects.Select(p => new ProjectDetailsDTO + { + Id = p.Id, + Description = p.Description, + EndDate = p.EndDate, + StartDate = p.StartDate, + Name = p.Title, + }), + CompanyName = c.CompanyName, + + }).FirstOrDefaultAsync(); if (client != null) @@ -74,6 +77,72 @@ public async Task GetProfileByIdAsync([FromRoute]long id) return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), "NotFound")); } + + [Authorize(Roles = "FREELANCER")] + [HttpPost("{id}/skills")] + public async Task UpdateTaskAsync(long id, [FromBody] SkillDTO skillDTO) + { + var user = await userManager.GetUserAsync(HttpContext.User); + if (user == null) + return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), + "Unable to set skills.")); + if (user.Id != id) + return BadRequest(CreateErrorResponse(StatusCodes.Status403Forbidden.ToString(), + "Not alowed")); + + + if (!ModelState.IsValid) + { + + return base.CustomBadRequest(); + } + + var freelancer = await mainAppContext.Users.OfType().FirstOrDefaultAsync(f => f.Id == id); + if (freelancer == null) + { + return BadRequest(CreateErrorResponse(StatusCodes.Status400BadRequest.ToString(), "freelancer not found.")); + } + var freelancerSkill = await mainAppContext.skills.Where(s => s.UserId == id && s.Name == skillDTO.Name).FirstOrDefaultAsync(); + if (freelancerSkill != null) + { + return BadRequest(CreateErrorResponse(StatusCodes.Status400BadRequest.ToString(), "skill aliready exist.")); + } + var skill = new Skill + { + UserId = id, + Name = skillDTO.Name, + }; + await mainAppContext.skills.AddAsync(skill); + await mainAppContext.SaveChangesAsync(); + return Ok(CreateSuccessResponse("skill Has Been added ")); + + } + + [Authorize(Roles = "FREELANCER")] + [HttpDelete("{id}/skills")] + public async Task DeleteTaskAsync(long id, [FromBody] SkillDTO skillDTO) + { + var user = await userManager.GetUserAsync(HttpContext.User); + if (user == null) + return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), + "Unable to delete skills.")); + if (user.Id != id) + return BadRequest(CreateErrorResponse(StatusCodes.Status403Forbidden.ToString(), + "Not alowed")); + + var skill = await mainAppContext.skills.Where(s => s.Name == skillDTO.Name && s.UserId == id).FirstOrDefaultAsync(); + if (skill != null) + { + mainAppContext.skills.Remove(skill); + await mainAppContext.SaveChangesAsync(); + return Ok(CreateSuccessResponse("skill Has Been deleted ")); + + } + return BadRequest(CreateErrorResponse(StatusCodes.Status400BadRequest.ToString(), + "skill not found.")); + + + } } - + } diff --git a/Controllers/Web/v1/AuthController.cs b/Controllers/Web/v1/AuthController.cs index 831f282..590f3f1 100644 --- a/Controllers/Web/v1/AuthController.cs +++ b/Controllers/Web/v1/AuthController.cs @@ -108,7 +108,7 @@ public async Task CompleteRegistrationAsync([FromBody] RegisterRe UserName = registerReq.Username, PhoneNumber = tempUser.PhoneNumber, PhoneNumberConfirmed = tempUser.PhoneNumberConfirmed, - Skills = registerReq.Skills ?? string.Empty, + //Skills = registerReq.Skills ?? string.Empty, }, Constants.USER_TYPE_CLIENT => new Client() { diff --git a/Controllers/Web/v1/UsersController.cs b/Controllers/Web/v1/UsersController.cs index 39804c6..b468708 100644 --- a/Controllers/Web/v1/UsersController.cs +++ b/Controllers/Web/v1/UsersController.cs @@ -12,13 +12,13 @@ namespace AonFreelancing.Controllers.Web.v1 [Authorize] [Route("api/web/v1/users")] [ApiController] - public class UsersController(MainAppContext mainAppContext, RoleManager roleManager) + public class UsersController(MainAppContext mainAppContext, UserManager userManager, RoleManager roleManager) : BaseController { [HttpGet("{id}/profile")] - public async Task GetProfileByIdAsync([FromRoute]long id) + public async Task GetProfileByIdAsync([FromRoute] long id) { - var freelancer = await mainAppContext.Users + var freelancer = await mainAppContext.Users .OfType().Where(f => f.Id == id) .Select(f => new FreelancerResponseDTO { @@ -29,7 +29,10 @@ public async Task GetProfileByIdAsync([FromRoute]long id) UserType = Constants.USER_TYPE_FREELANCER, IsPhoneNumberVerified = f.PhoneNumberConfirmed, Role = new RoleResponseDTO { Name = Constants.USER_TYPE_FREELANCER }, - Skills = f.Skills, + Skills = f.Skills.Select(p => new SkillDTO + { + Name = p.Name + }), }).FirstOrDefaultAsync(); @@ -47,25 +50,25 @@ public async Task GetProfileByIdAsync([FromRoute]long id) .Where(c => c.Id == id) .Include(c => c.Projects) .Select(c => new ClientResponseDTO - { - Id = c.Id, - Name = c.Name, - Username = c.UserName ?? string.Empty, - PhoneNumber = c.PhoneNumber ?? string.Empty, - UserType = Constants.USER_TYPE_CLIENT, - IsPhoneNumberVerified = c.PhoneNumberConfirmed, - Role = new RoleResponseDTO { Name = Constants.USER_TYPE_CLIENT }, - Projects = c.Projects.Select(p => new ProjectDetailsDTO - { - Id = p.Id, - Description = p.Description, - EndDate = p.EndDate, - StartDate = p.StartDate, - Name = p.Title, - }), - CompanyName = c.CompanyName, - - }).FirstOrDefaultAsync(); + { + Id = c.Id, + Name = c.Name, + Username = c.UserName ?? string.Empty, + PhoneNumber = c.PhoneNumber ?? string.Empty, + UserType = Constants.USER_TYPE_CLIENT, + IsPhoneNumberVerified = c.PhoneNumberConfirmed, + Role = new RoleResponseDTO { Name = Constants.USER_TYPE_CLIENT }, + Projects = c.Projects.Select(p => new ProjectDetailsDTO + { + Id = p.Id, + Description = p.Description, + EndDate = p.EndDate, + StartDate = p.StartDate, + Name = p.Title, + }), + CompanyName = c.CompanyName, + + }).FirstOrDefaultAsync(); if (client != null) @@ -74,6 +77,72 @@ public async Task GetProfileByIdAsync([FromRoute]long id) return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), "NotFound")); } + + [Authorize(Roles = "FREELANCER")] + [HttpPost("{id}/skills")] + public async Task UpdateTaskAsync(long id, [FromBody] SkillDTO skillDTO) + { + var user = await userManager.GetUserAsync(HttpContext.User); + if (user == null) + return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), + "Unable to set skills.")); + if (user.Id != id) + return BadRequest(CreateErrorResponse(StatusCodes.Status403Forbidden.ToString(), + "Not alowed")); + + + if (!ModelState.IsValid) + { + + return base.CustomBadRequest(); + } + + var freelancer = await mainAppContext.Users.OfType().FirstOrDefaultAsync(f => f.Id == id); + if (freelancer == null) + { + return BadRequest(CreateErrorResponse(StatusCodes.Status400BadRequest.ToString(), "freelancer not found.")); + } + var freelancerSkill = await mainAppContext.skills.Where(s => s.UserId == id && s.Name == skillDTO.Name).FirstOrDefaultAsync(); + if (freelancerSkill != null) + { + return BadRequest(CreateErrorResponse(StatusCodes.Status400BadRequest.ToString(), "skill aliready exist.")); + } + var skill = new Skill + { + UserId = id, + Name = skillDTO.Name, + }; + await mainAppContext.skills.AddAsync(skill); + await mainAppContext.SaveChangesAsync(); + return Ok(CreateSuccessResponse("skill Has Been added ")); + + } + + [Authorize(Roles = "FREELANCER")] + [HttpDelete("{id}/skills")] + public async Task DeleteTaskAsync(long id, [FromBody] SkillDTO skillDTO) + { + var user = await userManager.GetUserAsync(HttpContext.User); + if (user == null) + return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound.ToString(), + "Unable to delete skills.")); + if (user.Id != id) + return BadRequest(CreateErrorResponse(StatusCodes.Status403Forbidden.ToString(), + "Not alowed")); + + var skill = await mainAppContext.skills.Where(s => s.Name == skillDTO.Name && s.UserId == id).FirstOrDefaultAsync(); + if (skill != null) + { + mainAppContext.skills.Remove(skill); + await mainAppContext.SaveChangesAsync(); + return Ok(CreateSuccessResponse("skill Has Been deleted ")); + + } + return BadRequest(CreateErrorResponse(StatusCodes.Status400BadRequest.ToString(), + "skill not found.")); + + + } } - + } diff --git a/Interfaces/IStatisticsService.cs b/Interfaces/IStatisticsService.cs new file mode 100644 index 0000000..069aa2b --- /dev/null +++ b/Interfaces/IStatisticsService.cs @@ -0,0 +1,9 @@ +using AonFreelancing.Models.DTOs; + +namespace AonFreelancing.Interfaces +{ + public interface IStatisticsService + { + string CalculatePercentage(int total, int count); + } +} diff --git a/Migrations/20241123104906_Week08Mig.cs b/Migrations/20241123104906_Week08Mig.cs deleted file mode 100644 index 1f98dff..0000000 --- a/Migrations/20241123104906_Week08Mig.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace AonFreelancing.Migrations -{ - /// - public partial class Week08Mig : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/Migrations/20241123015445_task07_update.Designer.cs b/Migrations/20241128204641_ProjectLikesMig.Designer.cs similarity index 88% rename from Migrations/20241123015445_task07_update.Designer.cs rename to Migrations/20241128204641_ProjectLikesMig.Designer.cs index 94ee986..b66e7e6 100644 --- a/Migrations/20241123015445_task07_update.Designer.cs +++ b/Migrations/20241128204641_ProjectLikesMig.Designer.cs @@ -12,8 +12,8 @@ namespace AonFreelancing.Migrations { [DbContext(typeof(MainAppContext))] - [Migration("20241123015445_task07_update")] - partial class task07_update + [Migration("20241128204641_ProjectLikesMig")] + partial class ProjectLikesMig { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -200,6 +200,58 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("AonFreelancing.Models.ProjectLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId") + .IsUnique(); + + b.ToTable("ProjectLikes"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("skills", null, t => + { + t.HasCheckConstraint("CK_NAME", "[Name] IN ('uiux', 'frontend', 'mobile', 'backend', 'fullstack')"); + }); + }); + modelBuilder.Entity("AonFreelancing.Models.TaskEntity", b => { b.Property("Id") @@ -464,10 +516,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.HasBaseType("AonFreelancing.Models.User"); - b.Property("Skills") - .IsRequired() - .HasColumnType("nvarchar(max)"); - b.ToTable("Freelancers", (string)null); }); @@ -536,6 +584,36 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Freelancer"); }); + modelBuilder.Entity("AonFreelancing.Models.ProjectLike", b => + { + b.HasOne("AonFreelancing.Models.Project", "Project") + .WithMany("ProjectLikes") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Skill", b => + { + b.HasOne("AonFreelancing.Models.Freelancer", "freelancer") + .WithMany("Skills") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("freelancer"); + }); + modelBuilder.Entity("AonFreelancing.Models.TaskEntity", b => { b.HasOne("AonFreelancing.Models.Project", "Project") @@ -629,6 +707,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Navigation("Bids"); + b.Navigation("ProjectLikes"); + b.Navigation("Tasks"); }); @@ -642,6 +722,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("AonFreelancing.Models.Freelancer", b => { b.Navigation("Bids"); + + b.Navigation("Skills"); }); modelBuilder.Entity("AonFreelancing.Models.SystemUser", b => diff --git a/Migrations/20241123015445_task07_update.cs b/Migrations/20241128204641_ProjectLikesMig.cs similarity index 88% rename from Migrations/20241123015445_task07_update.cs rename to Migrations/20241128204641_ProjectLikesMig.cs index 832f046..9210dea 100644 --- a/Migrations/20241123015445_task07_update.cs +++ b/Migrations/20241128204641_ProjectLikesMig.cs @@ -6,7 +6,7 @@ namespace AonFreelancing.Migrations { /// - public partial class task07_update : Migration + public partial class ProjectLikesMig : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -197,8 +197,7 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Freelancers", columns: table => new { - Id = table.Column(type: "bigint", nullable: false), - Skills = table.Column(type: "nvarchar(max)", nullable: false) + Id = table.Column(type: "bigint", nullable: false) }, constraints: table => { @@ -290,6 +289,26 @@ protected override void Up(MigrationBuilder migrationBuilder) principalColumn: "Id"); }); + migrationBuilder.CreateTable( + name: "skills", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_skills", x => x.Id); + table.CheckConstraint("CK_NAME", "[Name] IN ('uiux', 'frontend', 'mobile', 'backend', 'fullstack')"); + table.ForeignKey( + name: "FK_skills_Freelancers_UserId", + column: x => x.UserId, + principalTable: "Freelancers", + principalColumn: "Id"); + }); + migrationBuilder.CreateTable( name: "Bids", columns: table => new @@ -331,6 +350,32 @@ protected override void Up(MigrationBuilder migrationBuilder) principalColumn: "Id"); }); + migrationBuilder.CreateTable( + name: "ProjectLikes", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ProjectId = table.Column(type: "bigint", nullable: false), + UserId = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProjectLikes", x => x.Id); + table.ForeignKey( + name: "FK_ProjectLikes_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ProjectLikes_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id"); + }); + migrationBuilder.CreateTable( name: "Tasks", columns: table => new @@ -423,6 +468,17 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "Bids", column: "SystemUserId"); + migrationBuilder.CreateIndex( + name: "IX_ProjectLikes_ProjectId_UserId", + table: "ProjectLikes", + columns: new[] { "ProjectId", "UserId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ProjectLikes_UserId", + table: "ProjectLikes", + column: "UserId"); + migrationBuilder.CreateIndex( name: "IX_Projects_ClientId", table: "Projects", @@ -433,6 +489,11 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "Projects", column: "FreelancerId"); + migrationBuilder.CreateIndex( + name: "IX_skills_UserId", + table: "skills", + column: "UserId"); + migrationBuilder.CreateIndex( name: "IX_Tasks_ProjectId", table: "Tasks", @@ -469,6 +530,12 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "otps"); + migrationBuilder.DropTable( + name: "ProjectLikes"); + + migrationBuilder.DropTable( + name: "skills"); + migrationBuilder.DropTable( name: "Tasks"); diff --git a/Migrations/20241123104906_Week08Mig.Designer.cs b/Migrations/20241129132926_ProjectLikesMig2.Designer.cs similarity index 88% rename from Migrations/20241123104906_Week08Mig.Designer.cs rename to Migrations/20241129132926_ProjectLikesMig2.Designer.cs index 2bd7ce4..37acab8 100644 --- a/Migrations/20241123104906_Week08Mig.Designer.cs +++ b/Migrations/20241129132926_ProjectLikesMig2.Designer.cs @@ -12,8 +12,8 @@ namespace AonFreelancing.Migrations { [DbContext(typeof(MainAppContext))] - [Migration("20241123104906_Week08Mig")] - partial class Week08Mig + [Migration("20241129132926_ProjectLikesMig2")] + partial class ProjectLikesMig2 { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -200,6 +200,58 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("AonFreelancing.Models.ProjectLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId") + .IsUnique(); + + b.ToTable("ProjectLikes"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("skills", null, t => + { + t.HasCheckConstraint("CK_NAME", "[Name] IN ('uiux', 'frontend', 'mobile', 'backend', 'fullstack')"); + }); + }); + modelBuilder.Entity("AonFreelancing.Models.TaskEntity", b => { b.Property("Id") @@ -464,10 +516,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.HasBaseType("AonFreelancing.Models.User"); - b.Property("Skills") - .IsRequired() - .HasColumnType("nvarchar(max)"); - b.ToTable("Freelancers", (string)null); }); @@ -536,6 +584,36 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Freelancer"); }); + modelBuilder.Entity("AonFreelancing.Models.ProjectLike", b => + { + b.HasOne("AonFreelancing.Models.Project", "Project") + .WithMany("projectLikes") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.User", "user") + .WithMany("projectLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("user"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Skill", b => + { + b.HasOne("AonFreelancing.Models.Freelancer", "freelancer") + .WithMany("Skills") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("freelancer"); + }); + modelBuilder.Entity("AonFreelancing.Models.TaskEntity", b => { b.HasOne("AonFreelancing.Models.Project", "Project") @@ -630,6 +708,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Bids"); b.Navigation("Tasks"); + + b.Navigation("projectLikes"); + }); + + modelBuilder.Entity("AonFreelancing.Models.User", b => + { + b.Navigation("projectLikes"); }); modelBuilder.Entity("AonFreelancing.Models.Client", b => @@ -642,6 +727,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("AonFreelancing.Models.Freelancer", b => { b.Navigation("Bids"); + + b.Navigation("Skills"); }); modelBuilder.Entity("AonFreelancing.Models.SystemUser", b => diff --git a/Migrations/20241129132926_ProjectLikesMig2.cs b/Migrations/20241129132926_ProjectLikesMig2.cs new file mode 100644 index 0000000..e11b1bc --- /dev/null +++ b/Migrations/20241129132926_ProjectLikesMig2.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AonFreelancing.Migrations +{ + /// + public partial class ProjectLikesMig2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ProjectLikes_AspNetUsers_UserId", + table: "ProjectLikes"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ProjectLikes", + type: "datetime2", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "datetime2"); + + migrationBuilder.AddForeignKey( + name: "FK_ProjectLikes_AspNetUsers_UserId", + table: "ProjectLikes", + column: "UserId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ProjectLikes_AspNetUsers_UserId", + table: "ProjectLikes"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ProjectLikes", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + oldClrType: typeof(DateTime), + oldType: "datetime2", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_ProjectLikes_AspNetUsers_UserId", + table: "ProjectLikes", + column: "UserId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Migrations/MainAppContextModelSnapshot.cs b/Migrations/MainAppContextModelSnapshot.cs index 5fa3efc..21d3af0 100644 --- a/Migrations/MainAppContextModelSnapshot.cs +++ b/Migrations/MainAppContextModelSnapshot.cs @@ -197,6 +197,58 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); + modelBuilder.Entity("AonFreelancing.Models.ProjectLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId") + .IsUnique(); + + b.ToTable("ProjectLikes"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("skills", null, t => + { + t.HasCheckConstraint("CK_NAME", "[Name] IN ('uiux', 'frontend', 'mobile', 'backend', 'fullstack')"); + }); + }); + modelBuilder.Entity("AonFreelancing.Models.TaskEntity", b => { b.Property("Id") @@ -461,10 +513,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.HasBaseType("AonFreelancing.Models.User"); - b.Property("Skills") - .IsRequired() - .HasColumnType("nvarchar(max)"); - b.ToTable("Freelancers", (string)null); }); @@ -533,6 +581,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Freelancer"); }); + modelBuilder.Entity("AonFreelancing.Models.ProjectLike", b => + { + b.HasOne("AonFreelancing.Models.Project", "Project") + .WithMany("projectLikes") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AonFreelancing.Models.User", "user") + .WithMany("projectLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("user"); + }); + + modelBuilder.Entity("AonFreelancing.Models.Skill", b => + { + b.HasOne("AonFreelancing.Models.Freelancer", "freelancer") + .WithMany("Skills") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("freelancer"); + }); + modelBuilder.Entity("AonFreelancing.Models.TaskEntity", b => { b.HasOne("AonFreelancing.Models.Project", "Project") @@ -627,6 +705,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Bids"); b.Navigation("Tasks"); + + b.Navigation("projectLikes"); + }); + + modelBuilder.Entity("AonFreelancing.Models.User", b => + { + b.Navigation("projectLikes"); }); modelBuilder.Entity("AonFreelancing.Models.Client", b => @@ -639,6 +724,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("AonFreelancing.Models.Freelancer", b => { b.Navigation("Bids"); + + b.Navigation("Skills"); }); modelBuilder.Entity("AonFreelancing.Models.SystemUser", b => diff --git a/Models/ApiResponse.cs b/Models/ApiResponse.cs index dab67e5..fdc4fb4 100644 --- a/Models/ApiResponse.cs +++ b/Models/ApiResponse.cs @@ -5,8 +5,10 @@ public class ApiResponse public bool IsSuccess { get; set; } public T Results { get; set; } public IList Errors { get; set; } + public string Message { get; set; } } + public class Error { public string Code { get; set; } diff --git a/Models/Client.cs b/Models/Client.cs index b3f5044..05117e5 100644 --- a/Models/Client.cs +++ b/Models/Client.cs @@ -16,7 +16,7 @@ public class Client : User // Has many projects, 1-m public List? Projects { get; set; } - + //public override void DisplayProfile() //{ diff --git a/Models/DTOs/ClientDTO.cs b/Models/DTOs/ClientDTO.cs index 197251c..83b19fb 100644 --- a/Models/DTOs/ClientDTO.cs +++ b/Models/DTOs/ClientDTO.cs @@ -2,7 +2,7 @@ namespace AonFreelancing.Models.DTOs { - public class ClientDTO:UserOutDTO + public class ClientDTO : UserOutDTO { public string CompanyName { get; set; } @@ -10,10 +10,10 @@ public class ClientDTO:UserOutDTO public IEnumerable Projects { get; set; } } - public class ClientInputDTO: UserDTO + public class ClientInputDTO : UserDTO { [Required] - [MinLength(4,ErrorMessage ="Invalid Company Name")] + [MinLength(4, ErrorMessage = "Invalid Company Name")] public string CompanyName { get; set; } } diff --git a/Models/DTOs/FreelancerDTO.cs b/Models/DTOs/FreelancerDTO.cs index 016b87f..738a0d3 100644 --- a/Models/DTOs/FreelancerDTO.cs +++ b/Models/DTOs/FreelancerDTO.cs @@ -2,20 +2,21 @@ namespace AonFreelancing.Models.DTOs { - public class FreelancerDTO:UserDTO + public class FreelancerDTO : UserDTO { - public string Skills { get; set; } + //public string Skills { get; set; } } public class FreelancerRequestDTO : UserDTO { - public string Skills { get; set; } + // public string Skills { get; set; } } - public class FreelancerResponseDTO : UserResponseDTO { - public string? Skills { get; set; } - + public class FreelancerResponseDTO : UserResponseDTO + { + // public string? Skills { get; set; } + public IEnumerable Skills { get; set; } } public class FreelancerShortOutDTO @@ -24,5 +25,8 @@ public class FreelancerShortOutDTO public string Name { get; set; } public string QualificationName { get; set; } + + public long LikeCount { get; set; } + } } diff --git a/Models/DTOs/ProjectInputDTO.cs b/Models/DTOs/ProjectInputDTO.cs index 1234e59..2c64581 100644 --- a/Models/DTOs/ProjectInputDTO.cs +++ b/Models/DTOs/ProjectInputDTO.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics.CodeAnalysis; namespace AonFreelancing.Models.DTOs @@ -6,10 +7,10 @@ namespace AonFreelancing.Models.DTOs public class ProjectInputDto { [Required] - [MaxLength(512, ErrorMessage ="Title is too long.")] + [MaxLength(512, ErrorMessage = "Title is too long.")] public string Title { get; set; } - [MaxLength(1024,ErrorMessage = "Description is too long.")] + [MaxLength(1024, ErrorMessage = "Description is too long.")] public string? Description { get; set; } [Required] @@ -17,11 +18,11 @@ public class ProjectInputDto public string QualificationName { get; set; } [Required] - [Range(1,int.MaxValue)] + [Range(1, 365)] public int Duration { get; set; } //Number of days [Required] - [AllowedValues("PerHour","Fixed", ErrorMessage ="Price type is invalid.")] + [AllowedValues("PerHour", "Fixed", ErrorMessage = "Price type is invalid.")] public string PriceType { get; set; } [Required] diff --git a/Models/DTOs/ProjectOutDTO.cs b/Models/DTOs/ProjectOutDTO.cs index 76e53c5..82de50d 100644 --- a/Models/DTOs/ProjectOutDTO.cs +++ b/Models/DTOs/ProjectOutDTO.cs @@ -18,5 +18,7 @@ public class ProjectOutDTO public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } public string? CreationTime { get; set; } + public long LikeCount { get; set; } + } } diff --git a/Models/DTOs/SkillDTO.cs b/Models/DTOs/SkillDTO.cs new file mode 100644 index 0000000..b7f91cb --- /dev/null +++ b/Models/DTOs/SkillDTO.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace AonFreelancing.Models.DTOs +{ + public class SkillDTO + { + + [Required] + public string Name { get; set; } + } + public class SkillDeleteDTO + { + + [Required] + public string Name { get; set; } + [Required] + public long UserId { get; set; } + } +} diff --git a/Models/DTOs/StatisticsResponseDTO.cs b/Models/DTOs/StatisticsResponseDTO.cs new file mode 100644 index 0000000..d57512f --- /dev/null +++ b/Models/DTOs/StatisticsResponseDTO.cs @@ -0,0 +1,25 @@ +namespace AonFreelancing.Models.DTOs +{ + public class StatisticsResponseDTO + { + public ProjectStatistics Projects { get; set; } + public TaskStatistics Tasks { get; set; } + } + + public class ProjectStatistics + { + public int Total { get; set; } + public int Available { get; set; } + public int Closed { get; set; } + } + + public class TaskStatistics + { + public int Total { get; set; } + public string ToDo { get; set; } + public string InReview { get; set; } + public string InProgress { get; set; } + public string Done { get; set; } + } + +} diff --git a/Models/DTOs/TaskInputDto.cs b/Models/DTOs/TaskInputDto.cs index 98281cb..227c370 100644 --- a/Models/DTOs/TaskInputDto.cs +++ b/Models/DTOs/TaskInputDto.cs @@ -12,4 +12,10 @@ public class TaskStatusDto public string NewStatus { get; set; } } + public class TaskDetailsDto + { + public string Name { get; set; } + public DateTime DeadlineAt { get; set; } + public string? Notes { get; set; } + } } diff --git a/Models/DTOs/UserDTO.cs b/Models/DTOs/UserDTO.cs index aff9fd9..6887cb4 100644 --- a/Models/DTOs/UserDTO.cs +++ b/Models/DTOs/UserDTO.cs @@ -14,7 +14,7 @@ public class UserDTO [StringLength(32)] public string PhoneNumber { get; set; } - [MinLength(4,ErrorMessage ="Too short password")] + [MinLength(4, ErrorMessage = "Too short password")] public string Password { get; set; } diff --git a/Models/Freelancer.cs b/Models/Freelancer.cs index bf2b786..8504bec 100644 --- a/Models/Freelancer.cs +++ b/Models/Freelancer.cs @@ -11,9 +11,9 @@ namespace AonFreelancing.Models public class Freelancer : User { - public string Skills { get; set; } - + // public string Skills { get; set; } + public ICollection Skills { get; set; } = new List(); //public override void DisplayProfile() //{ // Console.WriteLine($"Overrided Method in Freelancer Class"); diff --git a/Models/Project.cs b/Models/Project.cs index 4ef868a..de0637e 100644 --- a/Models/Project.cs +++ b/Models/Project.cs @@ -1,10 +1,10 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; namespace AonFreelancing.Models { - //Entity [Table("Projects")] public class Project { @@ -16,7 +16,7 @@ public class Project public long ClientId { get; set; } //FK // Belongs to a client - [ForeignKey("ClientId")] + [ForeignKey("ClientId")] public Client Client { get; set; } public DateTime CreatedAt { get; set; } @@ -33,7 +33,9 @@ public class Project public ICollection Bids { get; set; } = new List(); public string? ImagePath { get; set; } + [JsonIgnore] public ICollection Tasks { get; set; } = new List(); + public ICollection projectLikes { get; set; } = new List(); } } diff --git a/Models/ProjectLike.cs b/Models/ProjectLike.cs new file mode 100644 index 0000000..917c0b4 --- /dev/null +++ b/Models/ProjectLike.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace AonFreelancing.Models +{ + public class ProjectLike + { + [Required] + [Key] + public long Id { get; set; } + + public long ProjectId { get; set; } + public Project Project { get; set; } + + public long UserId { get; set; } + public User user { get; set; } + + public DateTime? CreatedAt { get; set; } + + } +} \ No newline at end of file diff --git a/Models/Requests/RegisterRequest.cs b/Models/Requests/RegisterInfoRequest.cs similarity index 94% rename from Models/Requests/RegisterRequest.cs rename to Models/Requests/RegisterInfoRequest.cs index 375f032..67512d5 100644 --- a/Models/Requests/RegisterRequest.cs +++ b/Models/Requests/RegisterInfoRequest.cs @@ -13,7 +13,7 @@ public record RegisterRequest( string Password, [Required, AllowedValues("FREELANCER", "CLIENT")] string UserType, - string? Skills = null, + //string? Skills = null, string? CompanyName = null ); } diff --git a/Models/Requests/RegisterPhoneRequest.cs b/Models/Requests/RegisterPhoneRequest.cs new file mode 100644 index 0000000..1d3f322 --- /dev/null +++ b/Models/Requests/RegisterPhoneRequest.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace AonFreelancing.Models.Requests +{ + public class RegisterPhoneRequest + { + + public string PhoneNumber { get; set; } + } +} diff --git a/Models/Skill.cs b/Models/Skill.cs new file mode 100644 index 0000000..b365176 --- /dev/null +++ b/Models/Skill.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace AonFreelancing.Models +{ + public class Skill + { + + [Required] + public long Id { get; set; } + + public long UserId { get; set; } + public Freelancer freelancer { get; set; } + + public string Name { get; set; } + } +} + diff --git a/Models/User.cs b/Models/User.cs index f649b6d..90506bc 100644 --- a/Models/User.cs +++ b/Models/User.cs @@ -21,5 +21,7 @@ public class User : IdentityUser public ICollection Bids { get; set; } = new List(); + public ICollection projectLikes { get; set; } = new List(); + } } diff --git a/Program.cs b/Program.cs index f51b34b..cb496f0 100644 --- a/Program.cs +++ b/Program.cs @@ -1,5 +1,5 @@ - using AonFreelancing.Contexts; +using AonFreelancing.Interfaces; using AonFreelancing.Middlewares; using AonFreelancing.Models; using AonFreelancing.Services; @@ -28,14 +28,13 @@ public static void Main(string[] args) builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddDbContext(options => options.UseSqlServer(conf.GetConnectionString("Default"))); builder.Services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); builder.Configuration.AddJsonFile("appsettings.json"); - - // JWT Authentication configuration var jwtSettings = builder.Configuration.GetSection("Jwt"); var key = Encoding.UTF8.GetBytes(jwtSettings["Key"] ?? string.Empty); @@ -59,6 +58,10 @@ public static void Main(string[] args) }; }); + builder.Services.Configure(options => + { + options.SuppressModelStateInvalidFilter = true; + }); builder.Services.AddSwaggerGen(options => { @@ -70,26 +73,20 @@ public static void Main(string[] args) Scheme = "Bearer" }); options.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - } - }, - Array.Empty() - } - }); - }); - - - builder.Services.Configure(options => - { - options.SuppressModelStateInvalidFilter = true; - }); + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); + }); var app = builder.Build(); if (app.Environment.IsDevelopment()) diff --git a/Services/StatisticsService.cs b/Services/StatisticsService.cs new file mode 100644 index 0000000..f90ac01 --- /dev/null +++ b/Services/StatisticsService.cs @@ -0,0 +1,16 @@ +using AonFreelancing.Contexts; +using AonFreelancing.Interfaces; +using AonFreelancing.Models.DTOs; +using Microsoft.EntityFrameworkCore; + +namespace AonFreelancing.Services +{ + public class StatisticsService : IStatisticsService + { + public string CalculatePercentage(int total, int count) + { + if (total == 0) return "0%"; // Handle division by zero case + return $"{Math.Round((double)count / total * 100, 2)}%"; // Calculate and format percentage + } + } +} diff --git a/Utilities/Constants.cs b/Utilities/Constants.cs index 45ab3f7..62bc886 100644 --- a/Utilities/Constants.cs +++ b/Utilities/Constants.cs @@ -19,9 +19,12 @@ public class Constants public const string BIDS_STATUS_APPROVED = "approved"; - public const string TASKS_STATUS_TO_DO = "to-do"; + public const string TASKS_STATUS_TO_DO = "To-Do"; public const string TASKS_STATUS_IN_PROGRESS = "in-progress"; public const string TASKS_STATUS_IN_REVIEW = "in-review"; public const string TASKS_STATUS_DONE = "done"; + + public const string PROJECT_LIKE = "like"; + public const string PROJECT_UNLIKE = "unlike"; } } diff --git a/appsettings.example.json b/appsettings.example.json index aace9cd..5db9cd8 100644 --- a/appsettings.example.json +++ b/appsettings.example.json @@ -19,5 +19,8 @@ "ExpireInMinutes": 30, "Key": "" }, - "Env": "SIT" + "Env": "SIT", + "ConnectionStrings": { + "Default": "" + } } diff --git a/task07_data.txt b/task07_data.txt index 89ab35c..accf40b 100644 --- a/task07_data.txt +++ b/task07_data.txt @@ -1,40 +1,36 @@ CLIENT: { - "phoneNumber": "+9647727124947" + "phoneNumber": "+9647809961817" } + { - "phone": "+9647727124947", - "otp": "123456" + "phoneNumber": "+9647809961817", + "password": "123!@#Aa" } { - "name": "yousif", - "username": "yousif1", - "phoneNumber": "+9647727124947", - "password": "123!@aA", + "name": "Abdulrahman Ahmed", + "username": "abdu", + "phoneNumber": "+9647809961817", + "password": "123!@#Aa", "userType": "CLIENT", "skills": "", - "companyName": "aon" -} - -{ - "phoneNumber": "+9647727124947", - "password": "123!@aA" + "companyName": "Aon Company" } - "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQ0xJRU5UIiwiZXhwIjoxNzMyMzI3NDQxLCJpc3MiOiJBb24iLCJhdWQiOiJBb24gRGV2In0.eH6bYdRO6jhFi11Idr4s_R2SsObxO2qaGIOZw7TVWWs" + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQ0xJRU5UIiwiZXhwIjoxNzMyNjE4MjEzLCJpc3MiOiJBb24iLCJhdWQiOiJBb24gRGV2In0.HQHplD30aRuafWTzjdfIZoP7uuFD5JLqqoQibgjyMfo" FREELANCER : { - "phoneNumber": "+9647727124948" + "phoneNumber": "+9647809961818" } { - "phone": "+9647727124948", - "otp": "123456" + "phone": "+9647809961818", + "password": "123!@#aA" } { @@ -43,27 +39,24 @@ FREELANCER : "phoneNumber": "+9647727124948", "password": "123!@aA", "userType": "FREELANCER", - "skills": "frontend", + "skills": "C#", "companyName": "" } -{ - "phoneNumber": "+9647727124948", - "password": "123!@aA" -} + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiRlJFRUxBTkNFUiIsImV4cCI6MTczMjYxODQyNCwiaXNzIjoiQW9uIiwiYXVkIjoiQW9uIERldiJ9.cOxkyQ_x_V14o4qQnRH6v5m9LcxGy9O1Ay8GX4W4Dlc" - "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiRlJFRUxBTkNFUiIsImV4cCI6MTczMjMyNzQ5NiwiaXNzIjoiQW9uIiwiYXVkIjoiQW9uIERldiJ9.QFJyWhKCKnMna_WroRFTqMG4XimLiOlEpekqFATX_Tw" PROJECT: { - "title": "aon1", - "description": "BLLLA BLLLLA", - "qualificationName": "frontend", - "duration": 247, + "title": "Test Aon Project", + "description": "Build big project for aon company", + "qualificationName": "fullstack", + "duration": 30, "priceType": "Fixed", - "budget": 800 + "budget": 300.00 } + { "title": "aon2", "description": "BLLLA BLLLLA",