Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -364,4 +364,5 @@ FodyWeavers.xsd

appsettings.json
appsettings.Development.json
appsettings.example.json
/.idea
4 changes: 4 additions & 0 deletions AonFreelancing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@
<PackageReference Include="Twilio" Version="7.6.0" />
</ItemGroup>

<ItemGroup>
<Folder Include="uploads\" />
</ItemGroup>

</Project>
24 changes: 14 additions & 10 deletions Contexts/MainAppContext.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using AonFreelancing.Models;

using AonFreelancing.Models;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System.Reflection.Emit;
using static System.Net.WebRequestMethods;

namespace AonFreelancing.Contexts
{
public class MainAppContext(DbContextOptions<MainAppContext> contextOptions)
public class MainAppContext(DbContextOptions<MainAppContext> contextOptions)
: IdentityDbContext<User, ApplicationRole, long>(contextOptions)
{
// For TPT design, no need to define each one
Expand All @@ -20,20 +21,23 @@ public class MainAppContext(DbContextOptions<MainAppContext> contextOptions)
public DbSet<TempUser> TempUsers { get; set; }
public DbSet<Bid> Bids { get; set; }
public DbSet<TaskEntity> Tasks { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("aon");
}
protected override void OnModelCreating(ModelBuilder builder)
{

// For TPT design
builder.Entity<User>().ToTable("AspNetUsers")
.HasIndex(u=>u.PhoneNumber).IsUnique();
.HasIndex(u => u.PhoneNumber).IsUnique();
builder.Entity<TempUser>().ToTable("TempUser")
.HasIndex(u=>u.PhoneNumber).IsUnique();
.HasIndex(u => u.PhoneNumber).IsUnique();

builder.Entity<Freelancer>().ToTable("Freelancers");
builder.Entity<Client>().ToTable("Clients");
builder.Entity<SystemUser>().ToTable("SystemUsers");
builder.Entity<OTP>().ToTable("otps", o => o.HasCheckConstraint("CK_CODE","LEN([Code]) = 6"));
builder.Entity<OTP>().ToTable("otps", o => o.HasCheckConstraint("CK_CODE", "LEN([Code]) = 6"));

//set up relationships
builder.Entity<TempUser>().HasOne<OTP>()
Expand All @@ -42,11 +46,11 @@ protected override void OnModelCreating(ModelBuilder builder)
.HasPrincipalKey<TempUser>(nameof(TempUser.PhoneNumber));

builder.Entity<Project>().ToTable("Projects", tb => tb.HasCheckConstraint("CK_PRICE_TYPE", "[PriceType] IN ('Fixed', 'PerHour')"));

builder.Entity<Project>()
.ToTable("Projects", tb => tb.HasCheckConstraint("CK_QUALIFICATION_NAME", "[QualificationName] IN ('uiux', 'frontend', 'mobile', 'backend', 'fullstack')"));
builder.Entity<Project>().ToTable("Projects", tb => tb.HasCheckConstraint("CK_STATUS", "[Status] IN ('Available', 'Closed')"))
.Property(p=>p.Status).HasDefaultValue("Available");
.Property(p => p.Status).HasDefaultValue("Available");

builder.Entity<Bid>()
.HasOne(b => b.Project)
Expand Down
44 changes: 43 additions & 1 deletion Controllers/Mobile/v1/AuthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using AonFreelancing.Models.DTOs;
using AonFreelancing.Models.Requests;
using AonFreelancing.Models.Responses;
using AonFreelancing.Services;
using AonFreelancing.Models.Services;
using AonFreelancing.Utilities;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -37,6 +37,48 @@ JwtService jwtService
_jwtService = jwtService;
}


[HttpPost("RegisterByPhone")]

public async Task <IActionResult> RegisterPhoneNumberAsync([FromBody] RegistByPhoneNumberDTO reg)
{
var user= await _mainAppContext.TempUsers.Where(ph=>ph.PhoneNumber==reg.PhoneNumber).FirstOrDefaultAsync();


if (user != null && !user.PhoneNumberConfirmed==false)
{

return Unauthorized(CreateErrorResponse(StatusCodes.Status401Unauthorized.ToString(), "Verify Your Account First"));
}

user = new TempUser() { PhoneNumber=reg.PhoneNumber };

await _mainAppContext.TempUsers.AddAsync(user);

string otpCode = _otpManager.GenerateOtp();
await _mainAppContext.SaveChangesAsync();

OTP otp = new OTP()
{
Code = otpCode,
PhoneNumber = reg.PhoneNumber,
CreatedDate = DateTime.Now,
ExpiresAt = DateTime.Now.AddMinutes(1),
};


//send the otp to the specified phone number
await _otpManager.SendOTPAsync(otp.Code, reg.PhoneNumber);
await _mainAppContext.OTPs.AddAsync(otp);
await _mainAppContext.SaveChangesAsync();



return Ok();


}

[HttpPost("sendVerificationCode")]
public async Task<IActionResult> SendVerificationCodeAsync([FromBody] PhoneNumberReq phoneNumberReq)
{
Expand Down
3 changes: 2 additions & 1 deletion Controllers/Mobile/v1/FreelancersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

namespace AonFreelancing.Controllers.Mobile.v1
{
[Authorize]
[Authorize ]

[Route("api/mobile/v1/freelancers")]
[ApiController]
public class FreelancersController : BaseController
Expand Down
172 changes: 39 additions & 133 deletions Controllers/Mobile/v1/ProjectsController.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@


using AonFreelancing.Contexts;
using AonFreelancing.Models;
using AonFreelancing.Models.DTOs;
using AonFreelancing.Models.Services;
using AonFreelancing.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
Expand All @@ -13,15 +15,16 @@ namespace AonFreelancing.Controllers.Mobile.v1
[Authorize]
[Route("api/mobile/v1/projects")]
[ApiController]
public class ProjectsController(MainAppContext mainAppContext, UserManager<User> userManager) : BaseController
public class ProjectsController(MainAppContext mainAppContext, UserManager<User> userManager,TaskService taskService) : BaseController
{

[Authorize(Roles = "CLIENT")]
[HttpPost]
public async Task<IActionResult> PostProjectAsync([FromBody] ProjectInputDto projectInputDto)
{
if(!ModelState.IsValid)
if (!ModelState.IsValid)
{

return base.CustomBadRequest();
}
var user = await userManager.GetUserAsync(HttpContext.User);
Expand All @@ -40,6 +43,7 @@ public async Task<IActionResult> PostProjectAsync([FromBody] ProjectInputDto pro
Budget = projectInputDto.Budget,
PriceType = projectInputDto.PriceType,
CreatedAt = DateTime.Now,

};

await mainAppContext.Projects.AddAsync(project);
Expand All @@ -62,12 +66,12 @@ public async Task<IActionResult> GetClientFeedAsync(

var count = await query.CountAsync();

if(!string.IsNullOrEmpty(trimmedQuery))
if (!string.IsNullOrEmpty(trimmedQuery))
{
query = query
.Where(p=>p.Title.ToLower().Contains(trimmedQuery));
.Where(p => p.Title.ToLower().Contains(trimmedQuery));
}
if(qualificationNames != null && qualificationNames.Count >0)
if (qualificationNames != null && qualificationNames.Count > 0)
{
query = query
.Where(p => qualificationNames.Contains(p.QualificationName));
Expand All @@ -78,7 +82,7 @@ public async Task<IActionResult> GetClientFeedAsync(
.Take(pageSize)
.Select(p => new ProjectOutDTO
{
Id= p.Id,
Id = p.Id,
Title = p.Title,
Description = p.Description,
Status = p.Status,
Expand All @@ -92,10 +96,11 @@ public async Task<IActionResult> GetClientFeedAsync(
CreationTime = StringOperations.GetTimeAgo(p.CreatedAt)
})
.ToListAsync();

return Ok(CreateSuccessResponse(new {
Total=count,
Items=projects

return Ok(CreateSuccessResponse(new
{
Total = count,
Items = projects
}));
}

Expand Down Expand Up @@ -133,7 +138,7 @@ public async Task<IActionResult> SubmitBidAsync(int id, [FromBody] BidInputDto b
FreelancerId = user.Id,
ProposedPrice = bidDto.ProposedPrice,
Notes = bidDto.Notes,
Status = "pending",
Status = "pending",
SubmittedAt = DateTime.Now
};

Expand Down Expand Up @@ -181,30 +186,32 @@ public async Task<IActionResult> GetProjectDetailsAsync(int id)

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
} );



.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,
project.Description,
Bids = orderedBids
}));
}
Expand Down Expand Up @@ -233,109 +240,8 @@ public async Task<IActionResult> CreateTaskAsync(int id, [FromBody] TaskInputDto
}


[Authorize(Roles = "CLIENT, FREELANCER")]
[HttpPut("tasks/{id}")]
public async Task<IActionResult> UpdateTaskStatusAsync(int id, [FromBody] TaskStatusDto taskStatusDto)
{
var task = await mainAppContext.Tasks.FindAsync(id);
if (task == null)
{
var errorResponse = new ApiResponse<string>
{
IsSuccess = false,
Results = null,
Errors = new List<Error>
{
new Error { Code = "404", Message = "Task not found." }
}
};
return NotFound(errorResponse);
}

var validStatuses = new List<string> { "to-do", "in-progress", "in-review", "done" };

if (!validStatuses.Contains(taskStatusDto.NewStatus.ToLower()))
{
var errorResponse = new ApiResponse<string>
{
IsSuccess = false,
Results = null,
Errors = new List<Error>
{
new Error { Code = "400", Message = "Invalid status provided." }
}
};
return BadRequest(errorResponse);
}

if (task.Status.ToLower() == "to do" && taskStatusDto.NewStatus.ToLower() != "in progress")
{
var errorResponse = new ApiResponse<string>
{
IsSuccess = false,
Results = null,
Errors = new List<Error>
{
new Error { Code = "400", Message = "Invalid status transition from 'To Do'." }
}
};
return BadRequest(errorResponse);
}
if (task.Status.ToLower() == "in progress" &&
taskStatusDto.NewStatus.ToLower() != "in review" && taskStatusDto.NewStatus.ToLower() != "done")
{
var errorResponse = new ApiResponse<string>
{
IsSuccess = false,
Results = null,
Errors = new List<Error>
{
new Error { Code = "400", Message = "Invalid status transition from 'In Progress'." }
}
};
return BadRequest(errorResponse);
}
if (task.Status.ToLower() == "in review" && taskStatusDto.NewStatus.ToLower() != "done")
{
var errorResponse = new ApiResponse<string>
{
IsSuccess = false,
Results = null,
Errors = new List<Error>
{
new Error { Code = "400", Message = "Invalid status transition from 'In Review'." }
}
};
return BadRequest(errorResponse);
}
if (task.Status.ToLower() == "done")
{
var errorResponse = new ApiResponse<string>
{
IsSuccess = false,
Results = null,
Errors = new List<Error>
{
new Error { Code = "400", Message = "No further status transitions allowed from 'Done'." }
}
};
return BadRequest(errorResponse);
}

task.Status = taskStatusDto.NewStatus;

task.CompletedAt = taskStatusDto.NewStatus.ToLower() == "done" ? DateTime.UtcNow : (DateTime?)null;

await mainAppContext.SaveChangesAsync();

var successResponse = new ApiResponse<string>
{
IsSuccess = true,
Results = "Task status updated.",
Errors = null
};
return Ok(successResponse);
}




[Authorize(Roles = "CLIENT")]
Expand Down Expand Up @@ -427,4 +333,4 @@ public async Task<IActionResult> UploadProjectImage(int id, IFormFile file)

//}
}
}
}
Loading