pull last changes

This commit is contained in:
Ruidy Nemausat 2020-02-25 11:20:19 +01:00
commit e3f8d5df6c
79 changed files with 1369 additions and 1226 deletions

2
.gitignore vendored
View file

@ -7,4 +7,4 @@ app.db*
.DS_Store .DS_Store
app.db app.db
client/node_modules client/node_modules
Scripts/ client/src/pages/TestPage.tsx

View file

@ -1,44 +1,42 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using TicketManager.Data; using TicketManager.Data;
using TicketManager.Models; using TicketManager.Models;
using Microsoft.AspNetCore.Authorization;
namespace TicketManager.Controllers namespace TicketManager.Controllers
{ {
[Route("api/v1/[controller]")] // [Authorize]
[Route("api/v1/users")]
[ApiController] [ApiController]
public class UsersController : ControllerBase public class UsersController : ControllerBase
{ {
private readonly AppDbContext _context; private readonly IAppUserRepository _users;
public UsersController(AppDbContext context) public UsersController(IAppUserRepository users)
{ {
_context = context; _users = users;
} }
// GET: api/Users // GET: api/Users
[HttpGet] [HttpGet]
public async Task<ActionResult<IEnumerable<AppUser>>> GetUsers() public async Task<IEnumerable<AppUser>> GetUsers()
{ {
return await getAllAppUsersAsync(); return await _users.List();
} }
// GET: api/Users/5 // GET: api/Users/5
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<ActionResult<AppUser>> GetUser(Guid id) public async Task<ActionResult<AppUser>> GetUser(Guid id)
{ {
var user = await getAppUserByIdAsync(id); var user = await _users.GetUser(id);
if (user == null) if (user == null)
{ {
return NotFound(); return NotFound();
} }
return user; return user;
} }
@ -52,16 +50,13 @@ namespace TicketManager.Controllers
{ {
return BadRequest(); return BadRequest();
} }
_context.Entry(user).State = EntityState.Modified;
try try
{ {
await _context.SaveChangesAsync(); await _users.Update(user);
} }
catch (DbUpdateConcurrencyException) catch (DbUpdateConcurrencyException)
{ {
if (!UserExists(id)) if (!_users.Exists(id))
{ {
return NotFound(); return NotFound();
} }
@ -70,7 +65,6 @@ namespace TicketManager.Controllers
throw; throw;
} }
} }
return NoContent(); return NoContent();
} }
@ -80,32 +74,27 @@ namespace TicketManager.Controllers
[HttpPost] [HttpPost]
public async Task<ActionResult<AppUser>> PostUser(AppUser user) public async Task<ActionResult<AppUser>> PostUser(AppUser user)
{ {
_context.AppUsers.Add(user); await _users.Add(user);
await _context.SaveChangesAsync();
return CreatedAtAction("GetUser", new { id = user.Id }, user); return CreatedAtAction("GetUser", new { id = user.Id }, user);
} }
// DELETE: api/Users/5 // DELETE: api/Users/5
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<ActionResult<AppUser>> DeleteUser(int id) public async Task<ActionResult<AppUser>> DeleteUser(Guid id)
{ {
var user = await _context.AppUsers.FindAsync(id); var user = await _users.GetUser(id);
if (user == null) if (user == null)
{ {
return NotFound(); return NotFound();
} }
await _users.Delete(user);
_context.AppUsers.Remove(user);
await _context.SaveChangesAsync();
return user; return user;
} }
[HttpGet("{id}/projects")] [HttpGet("{id}/projects")]
public async Task<ActionResult<IEnumerable<Project>>> GetAppUserProjects(Guid id) public async Task<ActionResult<IEnumerable<Project>>> GetAppUserProjects(Guid id)
{ {
AppUser user = await getAppUserByIdAsync(id); AppUser user = await _users.GetUser(id);
if (user == null) if (user == null)
{ {
return BadRequest(); return BadRequest();
@ -116,36 +105,12 @@ namespace TicketManager.Controllers
[HttpGet("{id}/tickets/")] [HttpGet("{id}/tickets/")]
public async Task<ActionResult<IEnumerable<Ticket>>> GetAppUserTickets(Guid id) public async Task<ActionResult<IEnumerable<Ticket>>> GetAppUserTickets(Guid id)
{ {
AppUser user = await getAppUserByIdAsync(id); AppUser user = await _users.GetUser(id);
if (user == null) if (user == null)
{ {
return BadRequest(); return BadRequest();
} }
return user.GetTickets(); return user.GetTickets();
} }
private bool UserExists(Guid id)
{
return _context.AppUsers.Any(e => e.Id == id);
}
private IQueryable<AppUser> appUserQuery()
{
return _context.AppUsers
.Include(p => p.Assignments)
.ThenInclude(a => a.Project)
.ThenInclude(p => p.Tickets)
.Include(p => p.Edits);
}
private async Task<ActionResult<IEnumerable<AppUser>>> getAllAppUsersAsync()
{
return await appUserQuery().ToListAsync();
}
private async Task<AppUser> getAppUserByIdAsync(Guid id)
{
return await appUserQuery().FirstOrDefaultAsync(a => a.Id == id);
}
} }
} }

View file

@ -1,8 +1,7 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using TicketManager.Data; using TicketManager.Data;
@ -10,6 +9,7 @@ using TicketManager.Models;
namespace TicketManager.Controllers namespace TicketManager.Controllers
{ {
[Authorize]
[Route("api/[controller]")] [Route("api/[controller]")]
[ApiController] [ApiController]
public class AssignmentsController : ControllerBase public class AssignmentsController : ControllerBase

View file

@ -1,8 +1,7 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using TicketManager.Data; using TicketManager.Data;
@ -10,6 +9,7 @@ using TicketManager.Models;
namespace TicketManager.Controllers namespace TicketManager.Controllers
{ {
[Authorize]
[Route("api/v1/[controller]")] [Route("api/v1/[controller]")]
[ApiController] [ApiController]
public class FilesController : ControllerBase public class FilesController : ControllerBase

View file

@ -1,110 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TicketManager.Data;
using TicketManager.Models;
namespace TicketManager.Controllers
{
[Route("api/v1/[controller]")]
[ApiController]
public class HistoriesController : ControllerBase
{
private readonly AppDbContext _context;
public HistoriesController(AppDbContext context)
{
_context = context;
}
// GET: api/Histories
[HttpGet]
public async Task<ActionResult<IEnumerable<History>>> GetEdits()
{
return await _context.Edits.ToListAsync();
}
// GET: api/Histories/5
[HttpGet("{id}")]
public async Task<ActionResult<History>> GetHistory(int id)
{
var history = await _context.Edits.FindAsync(id);
if (history == null)
{
return NotFound();
}
return history;
}
// PUT: api/Histories/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://aka.ms/RazorPagesCRUD.
[HttpPut("{id}")]
public async Task<IActionResult> PutHistory(int id, History history)
{
if (id != history.Id)
{
return BadRequest();
}
_context.Entry(history).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!HistoryExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/Histories
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://aka.ms/RazorPagesCRUD.
[HttpPost]
public async Task<ActionResult<History>> PostHistory(History history)
{
_context.Edits.Add(history);
await _context.SaveChangesAsync();
return CreatedAtAction("GetHistory", new { id = history.Id }, history);
}
// DELETE: api/Histories/5
[HttpDelete("{id}")]
public async Task<ActionResult<History>> DeleteHistory(int id)
{
var history = await _context.Edits.FindAsync(id);
if (history == null)
{
return NotFound();
}
_context.Edits.Remove(history);
await _context.SaveChangesAsync();
return history;
}
private bool HistoryExists(int id)
{
return _context.Edits.Any(e => e.Id == id);
}
}
}

View file

@ -1,8 +1,7 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using TicketManager.Data; using TicketManager.Data;
@ -10,6 +9,7 @@ using TicketManager.Models;
namespace TicketManager.Controllers namespace TicketManager.Controllers
{ {
[Authorize]
[Route("api/v1/[controller]")] [Route("api/v1/[controller]")]
[ApiController] [ApiController]
public class NotesController : ControllerBase public class NotesController : ControllerBase

View file

@ -1,53 +1,54 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net.Mime;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using TicketManager.Data; using TicketManager.Data;
using TicketManager.Models; using TicketManager.Models;
using TicketManager.DTO;
namespace TicketManager.Controllers namespace TicketManager.Controllers
{ {
// [Authorize(Roles = "Admin")]
// [Authorize]
[Produces("application/json")] [Produces("application/json")]
[Route("api/v1/[controller]")] [Route("api/v1/[controller]")]
[ApiController] [ApiController]
public class ProjectsController : ControllerBase public class ProjectsController : ControllerBase
{ {
private readonly IProjectRepository _projectRepo; private IProjectRepository _projects;
public ProjectsController(IProjectRepository context)
public ProjectsController(IProjectRepository projectRepo)
{ {
_projectRepo = projectRepo; _projects = context;
} }
/// <summary> /// <summary>
/// Returns all existing projects. /// Returns all projects stored in the database.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Sample request: /// Sample request:
/// ///
/// GET: api/Projects /// GET: api/v1/Projects
/// ///
/// </remarks> /// </remarks>
/// <response code="200">Returns all existing projects</response> /// <response code="200">Returns a list of projects</response>
[HttpGet] [HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IEnumerable<Project>> GetProjects() public async Task<IEnumerable<Project>> GetProjects()
{ {
return await _projectRepo.List();
return await _projects.List();
} }
/// <summary> /// <summary>
/// Returns a specific project. /// Locate a specific project stored in the database by its Id
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Sample request: /// Sample request:
/// ///
/// GET: api/Projects/2 /// GET: api/v1/Projects/2
/// ///
/// </remarks> /// </remarks>
/// <param name="id">Identifier of the ressource</param> /// <param name="id">Identifier of the ressource</param>
@ -56,177 +57,167 @@ namespace TicketManager.Controllers
[HttpGet("{id}")] [HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Project>> GetProject(int id) public async Task<ActionResult<ProjectDTO>> GetProject(int id)
{ {
Project project = await _projectRepo.Get(id); Project project = await _projects.Get(id);
if (project == null) { return NotFound(); } if (project == null)
return project; {
return NotFound();
}
return new ProjectDTO(project);
} }
// /// <summary> /// <summary>
// /// Updates a specific project. /// Updates the specific project with Id.
// /// </summary> /// </summary>
// /// <remarks> /// <remarks>
// /// Sample request: /// Sample request:
// /// ///
// /// PUT: api/Projects/3 /// PUT: api/v1/Projects/3
// /// { /// {
// /// "id": "357727fd-5262-4522-b8a3-38271d43de84", /// "id": "357727fd-5262-4522-b8a3-38271d43de84",
// /// "firstName": "Thomas", /// "firstName": "Thomas",
// /// "lastName": "Price", /// "lastName": "Price",
// /// "presentation": "New Team?!", /// "presentation": "New Team?!",
// /// "email": "tp@mail.com", /// "email": "tp@mail.com",
// /// "phone": "0198237645" /// "phone": "0198237645"
// /// } /// }
// /// ///
// /// </remarks> /// </remarks>
// /// <response code="200">Returns the modified project</response> /// <response code="204">Request was succesful but no content is changed</response>
// /// <response code="204">Request was succesful but no content is changed</response> /// <response code="404">If the required project is null</response>
// /// <response code="404">If the required project is null</response> [HttpPut("{id}")]
// [HttpPut("{id}")] [ProducesResponseType(StatusCodes.Status204NoContent)]
// [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)]
// [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<IActionResult> PutProject(int id, Project project)
// [ProducesResponseType(StatusCodes.Status404NotFound)] {
// public async Task<IActionResult> PutProject(int id, Project project) if (id != project.Id) { return BadRequest(); }
// { try
// if (id != project.Id) { return BadRequest(); } {
await _projects.Update(project);
}
catch (DbUpdateConcurrencyException)
{
if (!_projects.Exists(id)) { return NotFound(); }
else { throw; }
}
return NoContent();
}
// try /// <summary>
// { /// Creates a project.
// await _projectRepo.Update(project); /// </summary>
// } /// <remarks>
// catch (DbUpdateConcurrencyException) /// Sample request:
// { ///
// if (!_projectRepo.Exists(id)) /// POST: api/v1/Projects/
// { /// {
// return NotFound(); /// "firstName": "Thomas",
// } /// "lastName": "Price",
// else /// "presentation": "New Team?!",
// { /// "email": "tp@mail.com",
// throw; /// "phone": "0198237645"
// } /// }
// } ///
/// </remarks>
/// <response code="201">Returns the created project</response>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Project>> PostProject(Project project)
{
if (!ModelState.IsValid) { return BadRequest(); }
await _projects.Add(project);
return CreatedAtAction("GetProject", new { id = project.Id }, project);
}
// return NoContent(); /// <summary>
// } /// Deletes the project identified by its Id
/// </summary>
/// <remarks>
/// Sample request:
///
/// DELETE: api/v1/Projects/5
///
/// </remarks>
/// <response code="200">Returns the deleted project</response>
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProject(int id)
{
var project = await _projects.Get(id);
if (project == null)
{
return NotFound();
}
await _projects.Delete(project);
return Ok();
}
// /// <summary> /// <summary>
// /// Creates a project. /// Gets a project members.
// /// </summary> /// </summary>
// /// <remarks> /// <remarks>
// /// Sample request: /// Sample request:
// /// ///
// /// POST: api/Projects/ /// GET: api/v1/Projects/5/Members
// /// { ///
// /// "firstName": "Thomas", /// </remarks>
// /// "lastName": "Price", /// <response code="200">Returns the project members as a list of users.</response>
// /// "presentation": "New Team?!", [ProducesResponseType(StatusCodes.Status200OK)]
// /// "email": "tp@mail.com", [ProducesResponseType(StatusCodes.Status404NotFound)]
// /// "phone": "0198237645" [HttpGet("{id}/members")]
// /// } public async Task<ActionResult<List<AppUser>>> GetProjectMembers(int id)
// /// {
// /// </remarks> var project = await _projects.Get(id);
// /// <response code="201">Returns the created project</response> if (project == null)
// [HttpPost] { return NotFound(); }
// [ProducesResponseType(StatusCodes.Status201Created)] return project.GetMembers();
// [ProducesResponseType(StatusCodes.Status404NotFound)] }
// public async Task<ActionResult<Project>> PostProject(Project project)
// {
// if (!ModelState.IsValid) { return BadRequest(); }
// await _projectRepo.AddAsync(project);
// return CreatedAtAction("GetProject", new { id = project.Id }, project); /// <summary>
// } /// Updates a project members.
/// </summary>
/// <remarks>
/// Sample request:
///
// /// <summary> /// PUT: api/v1/Projects/5/Members
// /// Deletes a project. /// {
// /// </summary> /// "id": "357727fd-5262-4522-b8a3-38271d43de84",
// /// <remarks> /// "firstName": "Thomas",
// /// Sample request: /// "lastName": "Price",
// /// /// "presentation": "New Team?!",
// /// DELETE: api/Projects/5 /// "email": "tp@mail.com",
// /// /// "phone": "0198237645"
// /// </remarks> /// }
// /// <response code="200">Returns the deleted project</response> /// </remarks>
// [ProducesResponseType(StatusCodes.Status200OK)] /// <response code="204">No content</response>
// [ProducesResponseType(StatusCodes.Status404NotFound)] /// <response code="404">Not Found</response>
// [HttpDelete("{id}")] [ProducesResponseType(StatusCodes.Status204NoContent)]
// public async Task<ActionResult<Project>> DeleteProject(int id) [ProducesResponseType(StatusCodes.Status404NotFound)]
// { [HttpPut("{id}/members")]
// var project = await _projectRepo.GetByIdAsync(id); public async Task<ActionResult<Project>> SetProjectMembers(int id, List<AppUser> projectMembers)
// if (project == null) {
// { Project project = await _projects.Get(id);
// return NotFound(); if (project == null)
// } {
// await _projectRepo.DeleteAsync(id); return NotFound();
// return project; }
// } project.SetMembers(projectMembers);
try
// /// <summary> {
// /// Gets a project members. await _projects.Update(project);
// /// </summary> }
// /// <remarks> catch (DbUpdateException /* ex */)
// /// Sample request: {
// /// //Log the error (uncomment ex variable name and write a log.)
// /// GET: api/Projects/5/Members ModelState.AddModelError("", "Unable to save changes. " +
// /// "Try again, and if the problem persists, " +
// /// </remarks> "see your system administrator.");
// /// <response code="200">Returns the project members</response> }
// [ProducesResponseType(StatusCodes.Status200OK)] return NoContent();
// [ProducesResponseType(StatusCodes.Status404NotFound)] }
// [HttpGet("{id}/members")]
// public async Task<ActionResult<List<AppUser>>> GetProjectMembers(int id)
// {
// Project project = await _projectRepo.GetByIdAsync(id);
// if (project == null)
// { return NotFound(); }
// return project.GetMembers();
// }
// /// <summary>
// /// Updates a project members.
// /// </summary>
// /// <remarks>
// /// Sample request:
// ///
// /// PUT: api/Projects/5/Members
// /// {
// /// "id": "357727fd-5262-4522-b8a3-38271d43de84",
// /// "firstName": "Thomas",
// /// "lastName": "Price",
// /// "presentation": "New Team?!",
// /// "email": "tp@mail.com",
// /// "phone": "0198237645"
// /// }
// /// </remarks>
// /// <response code="204">No content</response>
// [ProducesResponseType(StatusCodes.Status204NoContent)]
// [ProducesResponseType(StatusCodes.Status404NotFound)]
// [HttpPut("{id}/members")]
// public async Task<ActionResult<Project>> SetProjectMembers(int id, List<AppUser> projectMembers)
// {
// Project project = await _projectRepo.GetByIdAsync(id);
// if (project == null)
// {
// return NotFound();
// }
// project.SetMembers(projectMembers);
// try
// {
// await _projectRepo.UpdateAsync(project);
// }
// catch (DbUpdateException /* ex */)
// {
// //Log the error (uncomment ex variable name and write a log.)
// ModelState.AddModelError("", "Unable to save changes. " +
// "Try again, and if the problem persists, " +
// "see your system administrator.");
// }
// return NoContent();
// }
// // /// <summary> // // /// <summary>
// // /// Assign a user to a project. // // /// Assign a user to a project.
@ -234,7 +225,7 @@ namespace TicketManager.Controllers
// // /// <remarks> // // /// <remarks>
// // /// Sample request: // // /// Sample request:
// // /// // // ///
// // /// POST: api/Projects/addmembers // // /// POST: api/v1/Projects/addmembers
// // /// [{ // // /// [{
// // /// "id": "357727fd-5262-4522-b8a3-38271d43de84", // // /// "id": "357727fd-5262-4522-b8a3-38271d43de84",
// // /// "firstName": "Thomas", // // /// "firstName": "Thomas",
@ -277,7 +268,7 @@ namespace TicketManager.Controllers
// // /// <remarks> // // /// <remarks>
// // /// Sample request: // // /// Sample request:
// // /// // // ///
// // /// PUT: api/Projects/removemembers // // /// PUT: api/v1/Projects/removemembers
// // /// [{ // // /// [{
// // /// "id": "357727fd-5262-4522-b8a3-38271d43de84", // // /// "id": "357727fd-5262-4522-b8a3-38271d43de84",
// // /// "firstName": "Thomas", // // /// "firstName": "Thomas",
@ -309,9 +300,5 @@ namespace TicketManager.Controllers
// // } // // }
// // return NoContent(); // // return NoContent();
// // } // // }
} }
} }

View file

@ -1,317 +0,0 @@
// using System;
// using System.Collections.Generic;
// using System.Linq;
// using System.Net.Mime;
// using System.Threading.Tasks;
// using Microsoft.AspNetCore.Http;
// using Microsoft.AspNetCore.Mvc;
// using Microsoft.EntityFrameworkCore;
// using TicketManager.Data;
// using TicketManager.Models;
// namespace TicketManager.Controllers
// {
// [Produces("application/json")]
// [Route("api/v1/[controller]")]
// [ApiController]
// public class ProjectsController : ControllerBase
// {
// private readonly IProjectRepository _projectRepo;
// public ProjectsController(IProjectRepository projectRepo)
// {
// _projectRepo = projectRepo;
// }
// /// <summary>
// /// Returns all existing projects.
// /// </summary>
// /// <remarks>
// /// Sample request:
// ///
// /// GET: api/Projects
// ///
// /// </remarks>
// /// <response code="200">Returns all existing projects</response>
// [HttpGet]
// [ProducesResponseType(StatusCodes.Status200OK)]
// public async Task<IEnumerable<Project>> GetProjects()
// {
// return await _projectRepo.ListAsync();
// // GetAllProjectsAsync();
// }
// /// <summary>
// /// Returns a specific project.
// /// </summary>
// /// <remarks>
// /// Sample request:
// ///
// /// GET: api/Projects/2
// ///
// /// </remarks>
// /// <response code="200">Returns a specific project</response>
// /// <response code="404">If the required project is null</response>
// [HttpGet("{id}")]
// [ProducesResponseType(StatusCodes.Status200OK)]
// [ProducesResponseType(StatusCodes.Status404NotFound)]
// public async Task<ActionResult<Project>> GetProject(int id)
// {
// Project project = await _projectRepo.GetByIdAsync(id);
// if (project == null) { return NotFound(); }
// return project;
// }
// /// <summary>
// /// Updates a specific project.
// /// </summary>
// /// <remarks>
// /// Sample request:
// ///
// /// PUT: api/Projects/3
// /// {
// /// "id": "357727fd-5262-4522-b8a3-38271d43de84",
// /// "firstName": "Thomas",
// /// "lastName": "Price",
// /// "presentation": "New Team?!",
// /// "email": "tp@mail.com",
// /// "phone": "0198237645"
// /// }
// ///
// /// </remarks>
// /// <response code="200">Returns the modified project</response>
// /// <response code="204">Request was succesful but no content is changed</response>
// /// <response code="404">If the required project is null</response>
// [HttpPut("{id}")]
// [ProducesResponseType(StatusCodes.Status200OK)]
// [ProducesResponseType(StatusCodes.Status204NoContent)]
// [ProducesResponseType(StatusCodes.Status404NotFound)]
// public async Task<IActionResult> PutProject(int id, Project project)
// {
// if (id != project.Id) { return BadRequest(); }
// try
// {
// await _projectRepo.UpdateAsync(project);
// }
// catch (DbUpdateConcurrencyException)
// {
// if (!_projectRepo.Exists(id))
// {
// return NotFound();
// }
// else
// {
// throw;
// }
// }
// return NoContent();
// }
// /// <summary>
// /// Creates a project.
// /// </summary>
// /// <remarks>
// /// Sample request:
// ///
// /// POST: api/Projects/
// /// {
// /// "firstName": "Thomas",
// /// "lastName": "Price",
// /// "presentation": "New Team?!",
// /// "email": "tp@mail.com",
// /// "phone": "0198237645"
// /// }
// ///
// /// </remarks>
// /// <response code="201">Returns the created project</response>
// [HttpPost]
// [ProducesResponseType(StatusCodes.Status201Created)]
// [ProducesResponseType(StatusCodes.Status404NotFound)]
// public async Task<ActionResult<Project>> PostProject(Project project)
// {
// if (!ModelState.IsValid) { return BadRequest(); }
// await _projectRepo.AddAsync(project);
// return CreatedAtAction("GetProject", new { id = project.Id }, project);
// }
// /// <summary>
// /// Deletes a project.
// /// </summary>
// /// <remarks>
// /// Sample request:
// ///
// /// DELETE: api/Projects/5
// ///
// /// </remarks>
// /// <response code="200">Returns the deleted project</response>
// [ProducesResponseType(StatusCodes.Status200OK)]
// [ProducesResponseType(StatusCodes.Status404NotFound)]
// [HttpDelete("{id}")]
// public async Task<ActionResult<Project>> DeleteProject(int id)
// {
// var project = await _projectRepo.GetByIdAsync(id);
// if (project == null)
// {
// return NotFound();
// }
// await _projectRepo.DeleteAsync(id);
// return project;
// }
// /// <summary>
// /// Gets a project members.
// /// </summary>
// /// <remarks>
// /// Sample request:
// ///
// /// GET: api/Projects/5/Members
// ///
// /// </remarks>
// /// <response code="200">Returns the project members</response>
// [ProducesResponseType(StatusCodes.Status200OK)]
// [ProducesResponseType(StatusCodes.Status404NotFound)]
// [HttpGet("{id}/members")]
// public async Task<ActionResult<List<AppUser>>> GetProjectMembers(int id)
// {
// Project project = await _projectRepo.GetByIdAsync(id);
// if (project == null)
// { return NotFound(); }
// return project.GetMembers();
// }
// /// <summary>
// /// Updates a project members.
// /// </summary>
// /// <remarks>
// /// Sample request:
// ///
// /// PUT: api/Projects/5/Members
// /// {
// /// "id": "357727fd-5262-4522-b8a3-38271d43de84",
// /// "firstName": "Thomas",
// /// "lastName": "Price",
// /// "presentation": "New Team?!",
// /// "email": "tp@mail.com",
// /// "phone": "0198237645"
// /// }
// /// </remarks>
// /// <response code="204">No content</response>
// [ProducesResponseType(StatusCodes.Status204NoContent)]
// [ProducesResponseType(StatusCodes.Status404NotFound)]
// [HttpPut("{id}/members")]
// public async Task<ActionResult<Project>> SetProjectMembers(int id, List<AppUser> projectMembers)
// {
// Project project = await _projectRepo.GetByIdAsync(id);
// if (project == null)
// {
// return NotFound();
// }
// project.SetMembers(projectMembers);
// try
// {
// await _projectRepo.UpdateAsync(project);
// }
// catch (DbUpdateException /* ex */)
// {
// //Log the error (uncomment ex variable name and write a log.)
// ModelState.AddModelError("", "Unable to save changes. " +
// "Try again, and if the problem persists, " +
// "see your system administrator.");
// }
// return NoContent();
// }
// // /// <summary>
// // /// Assign a user to a project.
// // /// </summary>
// // /// <remarks>
// // /// Sample request:
// // ///
// // /// POST: api/Projects/addmembers
// // /// [{
// // /// "id": "357727fd-5262-4522-b8a3-38271d43de84",
// // /// "firstName": "Thomas",
// // /// "lastName": "Price",
// // /// "presentation": "New Team?!",
// // /// "email": "tp@mail.com",
// // /// "phone": "0198237645"
// // /// }]
// // ///
// // /// </remarks>
// // /// <response code="204">Returns the created project</response>
// // [ProducesResponseType(StatusCodes.Status204NoContent)]
// // [ProducesResponseType(StatusCodes.Status404NotFound)]
// // [HttpPut("{id}/addMembers")]
// // public async Task<ActionResult<Project>> AddMembersToProject(int id, List<AppUser> usersToAdd)
// // {
// // if (usersToAdd == null)
// // {
// // return BadRequest();
// // }
// // Project project = await GetProjectByIdAsync(id);
// // project.AddMembers(usersToAdd);
// // try
// // {
// // await _context.SaveChangesAsync();
// // }
// // catch (DbUpdateException /* ex */)
// // {
// // //Log the error (uncomment ex variable name and write a log.)
// // ModelState.AddModelError("", "Unable to save changes. " +
// // "Try again, and if the problem persists, " +
// // "see your system administrator.");
// // }
// // return NoContent();
// // }
// // /// <summary>
// // /// Remove a user to a project.
// // /// </summary>
// // /// <remarks>
// // /// Sample request:
// // ///
// // /// PUT: api/Projects/removemembers
// // /// [{
// // /// "id": "357727fd-5262-4522-b8a3-38271d43de84",
// // /// "firstName": "Thomas",
// // /// "lastName": "Price",
// // /// "presentation": "New Team?!",
// // /// "email": "tp@mail.com",
// // /// "phone": "0198237645"
// // /// }]
// // ///
// // /// </remarks>
// // /// <response code="204">Returns the created project</response>
// // [ProducesResponseType(StatusCodes.Status204NoContent)]
// // [ProducesResponseType(StatusCodes.Status404NotFound)]
// // [HttpPut("{id}/removeMembers")]
// // public async Task<ActionResult<Project>> RemoveMembersFromProject(int id, List<AppUser> usersToRemove)
// // {
// // Project project = await GetProjectByIdAsync(id);
// // project.RemoveMembers(usersToRemove);
// // try
// // {
// // await _context.SaveChangesAsync();
// // }
// // catch (DbUpdateException /* ex */)
// // {
// // //Log the error (uncomment ex variable name and write a log.)
// // ModelState.AddModelError("", "Unable to save changes. " +
// // "Try again, and if the problem persists, " +
// // "see your system administrator.");
// // }
// // return NoContent();
// // }
// }
// }

View file

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using TicketManager.Data; using TicketManager.Data;
@ -10,35 +8,34 @@ using TicketManager.Models;
namespace TicketManager.Controllers namespace TicketManager.Controllers
{ {
[Authorize]
[Route("api/v1/[controller]")] [Route("api/v1/[controller]")]
[ApiController] [ApiController]
public class TicketsController : ControllerBase public class TicketsController : ControllerBase
{ {
private readonly AppDbContext _context; private readonly ITicketRepository _tickets;
public TicketsController(AppDbContext context) public TicketsController(ITicketRepository tickets)
{ {
_context = context; _tickets = tickets;
} }
// GET: api/Tickets // GET: api/Tickets
[HttpGet] [HttpGet]
public async Task<ActionResult<IEnumerable<Ticket>>> GetTickets() public async Task<IEnumerable<Ticket>> GetTickets()
{ {
return await getAllTicketsAsync(); return await _tickets.List();
} }
// GET: api/Tickets/5 // GET: api/Tickets/5
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<ActionResult<Ticket>> GetTicket(int id) public async Task<ActionResult<Ticket>> GetTicket(int id)
{ {
var ticket = await getTicketByIdAsync(id); var ticket = await _tickets.Get(id);
if (ticket == null) if (ticket == null)
{ {
return NotFound(); return NotFound();
} }
return ticket; return ticket;
} }
@ -52,16 +49,13 @@ namespace TicketManager.Controllers
{ {
return BadRequest(); return BadRequest();
} }
_context.Entry(ticket).State = EntityState.Modified;
try try
{ {
await _context.SaveChangesAsync(); await _tickets.Update(ticket);
} }
catch (DbUpdateConcurrencyException) catch (DbUpdateConcurrencyException)
{ {
if (!TicketExists(id)) if (!_tickets.Exists(id))
{ {
return NotFound(); return NotFound();
} }
@ -70,7 +64,6 @@ namespace TicketManager.Controllers
throw; throw;
} }
} }
return NoContent(); return NoContent();
} }
@ -80,9 +73,7 @@ namespace TicketManager.Controllers
[HttpPost] [HttpPost]
public async Task<ActionResult<Ticket>> PostTicket(Ticket ticket) public async Task<ActionResult<Ticket>> PostTicket(Ticket ticket)
{ {
_context.Tickets.Add(ticket); await _tickets.Add(ticket);
await _context.SaveChangesAsync();
return CreatedAtAction("GetTicket", new { id = ticket.Id }, ticket); return CreatedAtAction("GetTicket", new { id = ticket.Id }, ticket);
} }
@ -90,59 +81,28 @@ namespace TicketManager.Controllers
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<ActionResult<Ticket>> DeleteTicket(int id) public async Task<ActionResult<Ticket>> DeleteTicket(int id)
{ {
var ticket = await _context.Tickets.FindAsync(id); var ticket = await _tickets.Get(id);
if (ticket == null) if (ticket == null)
{ {
return NotFound(); return NotFound();
} }
await _tickets.Delete(ticket);
_context.Tickets.Remove(ticket);
await _context.SaveChangesAsync();
return ticket; return ticket;
} }
[HttpGet("{id}/assignees")] [HttpGet("{id}/assignees")]
public async Task<ActionResult<List<AppUser>>> GetTicketAssignees(int id) public async Task<ActionResult<List<AppUser>>> GetTicketAssignees(int id)
{ {
Ticket ticket = await getTicketByIdAsync(id); Ticket ticket = await _tickets.Get(id);
return ticket.GetAssignees(); return ticket.GetAssignees();
} }
[HttpPut("{id}/closed")] [HttpPut("{id}/closed")]
public async Task<ActionResult> CloseTicket(int id) public async Task<IActionResult> CloseTicket(int id)
{ {
Ticket ticket = await getTicketByIdAsync(id); Ticket ticket = await _tickets.Get(id);
ticket.Close(); ticket.Close();
return NoContent(); return await PutTicket(ticket.Id, ticket);
}
private bool TicketExists(int id)
{
return _context.Tickets.Any(e => e.Id == id);
}
private IQueryable<Ticket> ticketQuery() // problem with link
{
return _context.Tickets
.Include(p => p.Project)
.ThenInclude(a => a.Assignments)
.ThenInclude(p => p.User)
// .Include(p => p.Edits)
// .Include(p => p.Notes)
// .Include(p => p.Files)
// .Include(p => p.Creator)
;
}
private async Task<ActionResult<IEnumerable<Ticket>>> getAllTicketsAsync()
{
return await ticketQuery().ToListAsync();
}
private async Task<Ticket> getTicketByIdAsync(int id)
{
return await ticketQuery().FirstOrDefaultAsync(a => a.Id == id);
} }
} }
} }

View file

@ -12,7 +12,7 @@ namespace TicketManager.Data
public DbSet<AppUser> AppUsers { get; set; } public DbSet<AppUser> AppUsers { get; set; }
public DbSet<Ticket> Tickets { get; set; } public DbSet<Ticket> Tickets { get; set; }
public DbSet<Assignment> Assignments { get; set; } public DbSet<Assignment> Assignments { get; set; }
public DbSet<History> Edits { get; set; } public DbSet<Activity> Activities { get; set; }
public DbSet<Note> Notes { get; set; } public DbSet<Note> Notes { get; set; }
public DbSet<File> Files { get; set; } public DbSet<File> Files { get; set; }

37
Data/AppUserRepository.cs Normal file
View file

@ -0,0 +1,37 @@
using System.Threading.Tasks;
using TicketManager.Models;
using System.Linq;
using System;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
namespace TicketManager.Data
{
public class AppUserRepository : GenericRepository<AppUser>, IAppUserRepository
{
private readonly IQueryable<AppUser> _query;
public AppUserRepository(AppDbContext context) : base(context)
{
_query = _dbSet
.Include(p => p.Assignments)
.ThenInclude(a => a.Project)
.ThenInclude(p => p.Tickets)
.Include(p => p.Activities);
}
public async Task<AppUser> GetUser(Guid id)
{
return await _query.FirstOrDefaultAsync(p => p.Id == id);
}
public override async Task<IEnumerable<AppUser>> List()
{
return await _query.ToListAsync();
}
public bool Exists(Guid id)
{
return _dbSet.Any(e => e.Id == id);
}
}
}

View file

@ -17,16 +17,18 @@ namespace TicketManager.Data
_dbSet = _context.Set<T>(); _dbSet = _context.Set<T>();
} }
public void Add(T entity) public async Task<int> Add(T entity)
{ {
_dbSet.Add(entity); _dbSet.Add(entity);
return await _context.SaveChangesAsync();
} }
public void Delete(T entity) public async Task<int> Delete(T entity)
{ {
if (_context.Entry(entity).State == EntityState.Detached) if (_context.Entry(entity).State == EntityState.Detached)
{ _dbSet.Attach(entity); } { _dbSet.Attach(entity); }
_dbSet.Remove(entity); _dbSet.Remove(entity);
return await _context.SaveChangesAsync();
} }
public async Task<IEnumerable<T>> Find(int id, Expression<Func<T, bool>> expr) public async Task<IEnumerable<T>> Find(int id, Expression<Func<T, bool>> expr)
@ -43,10 +45,11 @@ namespace TicketManager.Data
return await _dbSet.AsNoTracking().ToListAsync(); return await _dbSet.AsNoTracking().ToListAsync();
} }
public void Update(T entity) public async Task<int> Update(T entity)
{ {
_dbSet.Attach(entity); _dbSet.Attach(entity);
_context.Entry(entity).State = EntityState.Modified; _context.Entry(entity).State = EntityState.Modified;
return await _context.SaveChangesAsync();
} }
} }
} }

View file

@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
using TicketManager.Models;
namespace TicketManager.Data
{
public interface IAppUserRepository : IGenericRepository<AppUser>
{
Task<AppUser> GetUser(Guid id);
bool Exists(Guid id);
}
}

View file

@ -11,10 +11,10 @@ namespace TicketManager.Data
Task<T> Get(int id); Task<T> Get(int id);
Task<IEnumerable<T>> Find(int id, Expression<Func<T, bool>> expr); Task<IEnumerable<T>> Find(int id, Expression<Func<T, bool>> expr);
void Add(T entity); Task<int> Add(T entity);
void Update(T entity); Task<int> Update(T entity);
void Delete(T entity); Task<int> Delete(T entity);
} }
} }

View file

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using TicketManager.Models;
namespace TicketManager.Data
{
public interface ITicketRepository : IGenericRepository<Ticket>
{
bool Exists(int id);
}
}

View file

@ -6,6 +6,8 @@ namespace TicketManager.Data
public interface IUnitOfWork : IDisposable public interface IUnitOfWork : IDisposable
{ {
IProjectRepository Projects { get; } IProjectRepository Projects { get; }
IAppUserRepository AppUsers { get; }
ITicketRepository Tickets { get; }
Task<int> Complete(); Task<int> Complete();
} }
} }

View file

@ -1,57 +0,0 @@
// using System.Threading.Tasks;
// using TicketManager.Models;
// using System.Linq;
// using Microsoft.EntityFrameworkCore;
// using System.Collections.Generic;
// using Microsoft.AspNetCore.Mvc;
// namespace TicketManager.Data
// {
// public class ProjectRepository : IProjectRepository
// {
// private readonly AppDbContext _context;
// private readonly IQueryable<Project> _query;
// public ProjectRepository(AppDbContext context)
// {
// _context = context;
// _query = _context.Projects
// .Include(p => p.Assignments)
// .ThenInclude(a => a.User)
// .Include(p => p.Tickets)
// .Include(p => p.Manager)
// .Include(p => p.Files);
// }
// public Task AddAsync(Project project)
// {
// _context.Projects.Add(project);
// return _context.SaveChangesAsync();
// }
// public async Task<int> DeleteAsync(int id)
// {
// Project project = await GetByIdAsync(id);
// _context.Projects.Remove(project);
// return await _context.SaveChangesAsync();
// }
// public async Task<Project> GetByIdAsync(int id)
// {
// return await _query.FirstOrDefaultAsync(p => p.Id == id);
// }
// public async Task<IEnumerable<Project>> ListAsync()
// {
// return await _query.ToListAsync();
// }
// public Task UpdateAsync(Project project)
// {
// _context.Entry(project).State = EntityState.Modified;
// return _context.SaveChangesAsync();
// }
// public bool Exists(int id)
// { return _context.Projects.Any(e => e.Id == id); }
// }
// }

View file

@ -15,8 +15,7 @@ namespace TicketManager.Data
.Include(p => p.Assignments).ThenInclude(a => a.User) .Include(p => p.Assignments).ThenInclude(a => a.User)
.Include(p => p.Tickets) .Include(p => p.Tickets)
.Include(p => p.Manager) .Include(p => p.Manager)
.Include(p => p.Files) .Include(p => p.Files);
.AsNoTracking();
} }
public override async Task<Project> Get(int id) public override async Task<Project> Get(int id)
@ -30,7 +29,9 @@ namespace TicketManager.Data
} }
public bool Exists(int id) public bool Exists(int id)
{ return _dbSet.Any(e => e.Id == id); } {
return _dbSet.Any(e => e.Id == id);
}
public async Task<IEnumerable<AppUser>> GetMembers(int id) public async Task<IEnumerable<AppUser>> GetMembers(int id)
{ {

40
Data/TicketRepository.cs Normal file
View file

@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using TicketManager.Models;
using Microsoft.EntityFrameworkCore;
namespace TicketManager.Data
{
public class TicketRepository : GenericRepository<Ticket>, ITicketRepository
{
private IQueryable<Ticket> _query;
public TicketRepository(AppDbContext context) : base(context)
{
_query = _dbSet
.Include(p => p.Project)
.ThenInclude(a => a.Assignments)
.ThenInclude(p => p.User)
// .Include(p => p.Edits)
// .Include(p => p.Notes)
// .Include(p => p.Files)
// .Include(p => p.Creator)
;
}
public override async Task<Ticket> Get(int id)
{
return await _query.FirstOrDefaultAsync(p => p.Id == id);
}
public override async Task<IEnumerable<Ticket>> List()
{
return await _query.ToListAsync();
}
public bool Exists(int id)
{
return _dbSet.Any(e => e.Id == id);
}
}
}

View file

@ -10,15 +10,20 @@ namespace TicketManager.Data
{ {
_context = context; _context = context;
Projects = new ProjectRepository(_context); Projects = new ProjectRepository(_context);
Tickets = new TicketRepository(_context);
AppUsers = new AppUserRepository(_context);
} }
public IProjectRepository Projects { get; private set; } public IProjectRepository Projects { get; private set; }
public IAppUserRepository AppUsers { get; private set; }
public ITicketRepository Tickets { get; private set; }
public async Task<int> Complete() public async Task<int> Complete()
{ {
return await _context.SaveChangesAsync(); return await _context.SaveChangesAsync();
} }
public void Dispose() public void Dispose()
{ {
_context.DisposeAsync(); _context.DisposeAsync();

View file

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using TicketManager.Models;
namespace TicketManager.DTO
{
public class ProjectDTO
{
public ProjectDTO(Project project)
{
Id = project.Id;
Title = project.Title;
Description = project.Description;
CreatedAt = project.CreatedAt;
Progression = project.Progression;
Status = project.Status.ToString();
Manager = project.Manager;
Users = project.GetMembers();
Tickets = project.Tickets;
Activities = project.Activities;
Files = project.Files;
}
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public DateTime CreatedAt { get; private set; } = DateTime.Now;
public DateTime PlannedEnding { get; set; }
public decimal Progression { get; set; }
public string Status { get; set; }
public AppUser Manager { get; set; }
public List<AppUser> Users { get; set; } = new List<AppUser>();
public List<Ticket> Tickets { get; set; } = new List<Ticket>();
public List<Activity> Activities { get; set; } = new List<Activity>();
public List<File> Files { get; set; } = new List<File>();
}
}

View file

@ -1,9 +1,8 @@
using System; using System;
using System.Collections.Generic;
namespace TicketManager.Models namespace TicketManager.Models
{ {
public class History public class Activity
{ {
public int Id { get; set; } public int Id { get; set; }
public string Description { get; set; } public string Description { get; set; }

View file

@ -38,12 +38,11 @@ namespace TicketManager.Models
// [Display(Name = "Avatar")] // [Display(Name = "Avatar")]
// public byte[] Picture { get; set; } // public byte[] Picture { get; set; }
// public Role Role { get; set; }
public List<Assignment> Assignments { get; set; } = new List<Assignment>(); public List<Assignment> Assignments { get; set; } = new List<Assignment>();
[Display(Name = "Activity")] [Display(Name = "Activity")]
public List<History> Edits { get; set; } = new List<History>(); public List<Activity> Activities { get; set; } = new List<Activity>();
// Methods // Methods
public List<Project> GetProjects() public List<Project> GetProjects()
@ -53,7 +52,7 @@ namespace TicketManager.Models
public List<Ticket> GetTickets() public List<Ticket> GetTickets()
{ {
List<Ticket> tickets = new List<Ticket>(); var tickets = new List<Ticket>();
GetProjects().ForEach(p => tickets.Concat(p.Tickets)); GetProjects().ForEach(p => tickets.Concat(p.Tickets));
return tickets; return tickets;
} }

View file

@ -5,8 +5,6 @@ namespace TicketManager.Models
{ {
public class File public class File
{ {
public int Id { get; set; } public int Id { get; set; }
public string FileName { get; set; } public string FileName { get; set; }

View file

@ -11,18 +11,18 @@ namespace TicketManager.Models
string Description { get; set; } string Description { get; set; }
DateTime CreatedAt { get; } DateTime CreatedAt { get; }
DateTime PlannedEnding { get; set; } DateTime PlannedEnding { get; set; }
List<History> Edits { get; set; } List<Activity> Activities { get; set; }
public virtual void AddLogEntry(string description)//, User user) public virtual void AddLogEntry(string description)//, User user)
{ {
History Edit = new History() Activity Activity = new Activity()
{ {
Description = description, Description = description,
ActivityType = ActivityType.Undefined, ActivityType = ActivityType.Undefined,
// User = user, // User = user,
UpdateDate = DateTime.Now UpdateDate = DateTime.Now
}; };
Edits.Add(Edit); Activities.Add(Activity);
} }
} }
} }

View file

@ -26,7 +26,6 @@ namespace TicketManager.Models
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime PlannedEnding { get; set; } public DateTime PlannedEnding { get; set; }
// private decimal _progression;
[Display(Name = "Progress")] [Display(Name = "Progress")]
public decimal Progression public decimal Progression
{ {
@ -67,7 +66,7 @@ namespace TicketManager.Models
public List<Ticket> Tickets { get; set; } = new List<Ticket>(); public List<Ticket> Tickets { get; set; } = new List<Ticket>();
public List<History> Edits { get; set; } = new List<History>(); public List<Activity> Activities { get; set; } = new List<Activity>();
public List<File> Files { get; set; } = new List<File>(); public List<File> Files { get; set; } = new List<File>();
@ -119,8 +118,7 @@ namespace TicketManager.Models
this.AddMembers(projectMembers); this.AddMembers(projectMembers);
} }
} }
// public int GetMembersCount() => this.GetMembers().Count();
// public void GetTicketsCount() => this.Tickets.Count();
public void GetTicketsUpdates() public void GetTicketsUpdates()
{ throw new NotImplementedException("Not Implemented"); } { throw new NotImplementedException("Not Implemented"); }

View file

@ -29,7 +29,7 @@ namespace TicketManager.Models
public Difficulty Difficulty { get; set; } = Difficulty.Undefined; public Difficulty Difficulty { get; set; } = Difficulty.Undefined;
public Category Category { get; set; } = Category.Undefined; public Category Category { get; set; } = Category.Undefined;
[Display(Name = "Created By")] // [Display(Name = "Created By")]
// public AppUser Creator { get; set; } // public AppUser Creator { get; set; }
public Guid CreatorId { get; set; } public Guid CreatorId { get; set; }
@ -38,7 +38,7 @@ namespace TicketManager.Models
// public int ProjectId { get; set; } // public int ProjectId { get; set; }
public List<Note> Notes = new List<Note>(); public List<Note> Notes = new List<Note>();
public List<History> Edits = new List<History>(); public List<Activity> Activities = new List<Activity>();
public List<File> Files = new List<File>(); public List<File> Files = new List<File>();

View file

@ -33,15 +33,17 @@
## TO DO ## TO DO
- Write API tests using Postman: request + test, environment variables, mock server - [ ] Write API tests using Postman: request + test, environment variables, mock server
- Annotate API request in controllers - [ ] Annotate API request in controllers
- Annotate Properties in Models - [ ] Annotate Properties in Models
- Write backend tests - [ ] Write backend tests
- Have a Look at typeahead component - [ ] Have a Look at typeahead component
- Ensure Tickets Edits belong to Project Edits - [ ] Ensure Tickets Edits belong to Project Edits
- Ensure Tickets Files belong to Project Files - [ ] Ensure Tickets Files belong to Project Files
- Async model methods ? - [ ] Async model methods ?
- setMembers & removeMembers from project api not working - [ ] update assignments automatically from context
- Write a query class to refactor code and optimize perf on get queries (AsNoTracking) - [ ] use PATCH instead of PUT
- repository + strategy to decouple controllers from DbContext. Easier testing - [ ] logging
- update assignments automatically from context - [ ] check useRef, useReducer, dispatch
- [ ] error page redirect when offline.
- [ ] ticket/files/activities list placeholders when empty

1
Scripts/apiQueries.sh Executable file
View file

@ -0,0 +1 @@
curl --insecure https://localhost:5001/api/v1/

1
Scripts/authentication.sh Executable file
View file

@ -0,0 +1 @@
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

View file

@ -1,5 +0,0 @@
rm -r Migrations
rm app.db
dotnet ef migrations add Migration1
dotnet ef database update
dotnet run

View file

@ -1,6 +0,0 @@
rm Controllers/AppUsersController.cs
rm Controllers/TicketsController.cs
rm Controllers/ProjectsController.cs
dotnet aspnet-codegenerator controller -name AppUsersController -async -api -m AppUser -dc AppDbContext -outDir Controllers
dotnet aspnet-codegenerator controller -name TicketsController -async -api -m Ticket -dc AppDbContext -outDir Controllers
dotnet aspnet-codegenerator controller -name ProjectsController -async -api -m Project -dc AppDbContext -outDir Controllers

View file

@ -19,6 +19,7 @@ using TicketManager.Data;
using TicketManager.Models; using TicketManager.Models;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson; using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using Newtonsoft.Json; using Newtonsoft.Json;
using Microsoft.AspNetCore.Authentication.JwtBearer;
[assembly: ApiController] [assembly: ApiController]
namespace TicketManager namespace TicketManager
@ -32,18 +33,33 @@ namespace TicketManager
public IConfiguration Configuration { get; } public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddDbContext<AppDbContext>(options => services.AddDbContext<AppDbContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("Sqlite"))); options.UseSqlite(Configuration.GetConnectionString("Sqlite")));
services.AddScoped<IProjectRepository, ProjectRepository>(); services.AddScoped<IProjectRepository, ProjectRepository>();
services.AddControllers() services.AddScoped<IAppUserRepository, AppUserRepository>();
.AddNewtonsoftJson(options => services.AddScoped<ITicketRepository, TicketRepository>();
services.AddAuthentication(options =>
{ {
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; // avoid cycle ref errors options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = "https://dev-fyjrvohx.auth0.com/";
options.Audience = "https://localhost:5001/api/V1/";
//options.Authority = $"https://{Configuration["Auth0:Domain"]}/";
//options.Audience = Configuration["Auth0:Audience"];
}); });
services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; // avoid cycle ref errors
}
);
services.AddSpaStaticFiles(configuration => services.AddSpaStaticFiles(configuration =>
{ {
configuration.RootPath = "client/build"; configuration.RootPath = "client/build";
@ -70,17 +86,12 @@ namespace TicketManager
services.AddSwaggerGenNewtonsoftSupport(); // explicit opt-in - needs to be placed after AddSwaggerGen() services.AddSwaggerGenNewtonsoftSupport(); // explicit opt-in - needs to be placed after AddSwaggerGen()
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider) public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
{ {
if (env.IsDevelopment()) if (env.IsDevelopment())
{ {
app.UseDeveloperExceptionPage(); app.UseDeveloperExceptionPage();
var repository = serviceProvider.GetRequiredService<IProjectRepository>(); // var repository = serviceProvider.GetRequiredService<IProjectRepository>();
// InitializeDatabaseAsync(repository).Wait()
} }
else else
{ {
@ -90,7 +101,6 @@ namespace TicketManager
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseDefaultFiles(); app.UseDefaultFiles();
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(c => app.UseSwaggerUI(c =>
@ -98,11 +108,9 @@ namespace TicketManager
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Ticket Manager API v1"); c.SwaggerEndpoint("/swagger/v1/swagger.json", "Ticket Manager API v1");
}); });
app.UseSpaStaticFiles(); app.UseSpaStaticFiles();
app.UseRouting(); app.UseRouting();
app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
@ -121,9 +129,4 @@ namespace TicketManager
}); });
} }
} }
} }

View file

@ -3,10 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup >
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<LangVersion>8.0</LangVersion> <LangVersion>8.0</LangVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
@ -16,17 +13,18 @@
<ItemGroup> <ItemGroup>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.0" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.0" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.1" />
<PackageReference Include="Microsoft.OpenApi" Version="1.1.4" /> <PackageReference Include="Microsoft.OpenApi" Version="1.1.4" />
<PackageReference Include="Moq" Version="4.13.1" /> <PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0" />

View file

@ -10,5 +10,9 @@
"AllowedHosts": "*", "AllowedHosts": "*",
"ConnectionStrings": { "ConnectionStrings": {
"Sqlite": "Data Source=app.db" "Sqlite": "Data Source=app.db"
},
"Auth0": {
"Domain": "https://dev-fyjrvohx.auth0.com/",
"Audience": "https://localhost:5001/api/V1/"
} }
} }

View file

@ -1,42 +0,0 @@
.panel {
padding-left: 0px;
padding-top: 10px;
}
.field {
padding-left: 10px;
padding-right: 10px;
}
.city {
display: flex;
background: linear-gradient(
90deg,
rgba(2, 0, 36, 1) 0%,
rgba(25, 112, 245, 0.6399510487788865) 0%,
rgba(0, 212, 255, 1) 100%
);
flex-direction: column;
height: 40vh;
justify-content: center;
align-items: center;
padding: 0px 20px 20px 20px;
margin: 0px 0px 50px 0px;
border: 1px solid;
border-radius: 5px;
box-shadow: 2px 2px #888888;
font-family: "Merriweather", serif;
}
.city h1 {
line-height: 1.2;
}
.city span {
padding-left: 20px;
}
.city .row {
padding-top: 20px;
}
.weatherError {
color: #f16051;
font-size: 20px;
letter-spacing: 1px;
font-weight: 200;
}

View file

@ -1,6 +1,4 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { AppRouter } from "./utils/router";
import "./App.css";
import Layout from "./pages/Layout"; import Layout from "./pages/Layout";
const App: FC = () => { const App: FC = () => {

View file

@ -0,0 +1,38 @@
import { Ticket } from "../types/Ticket";
import { Project } from "../types/Project";
import { AppFile } from "../types/AppFile";
import { Activity } from "../types/Activity";
import { User } from "../types/User";
import { getRemainingdays } from "../utils/methods";
export default class ProjectVM {
public id: number;
public title: string;
public description: string;
public value: number;
public tickets: Ticket[];
public users: User[];
public ticketsTotalCount: number;
public ticketsDone: number;
public remainingDays: number;
public files: AppFile[];
public activities: Activity[];
public constructor(project: Project) {
this.id = project.id;
this.title = project.title;
this.description = project.description;
this.users = project.users;
this.value = project.progression;
this.tickets = project.tickets;
this.ticketsTotalCount =
this.tickets === undefined ? 0 : this.tickets.length;
this.ticketsDone =
this.tickets === undefined
? 0
: this.tickets.filter(t => t.status === "Done").length;
this.files = project.files;
this.activities = project.activities;
this.remainingDays = getRemainingdays(project.plannedEnding);
}
}

View file

@ -0,0 +1,49 @@
import React, { FC } from "react";
import { Activity } from "../types/Activity";
type IProps = {
activities: Activity[];
filterText: string;
};
export const ActivityCollection: FC<IProps> = ({ activities, filterText }) => {
return activities === undefined ? (
<></>
) : (
<>
<ul className="collection">
{activities
.filter(
a =>
a.description.toLowerCase().includes(filterText.toLowerCase()) ||
a.user.firstName
.toLowerCase()
.includes(filterText.toLowerCase()) ||
a.ticket.title.toLowerCase().includes(filterText.toLowerCase())
)
.map((activity: Activity) => (
<li key={activity.id} className="collection-item avatar">
<ActivityEntry activity={activity} />
</li>
))}
</ul>
</>
);
};
type IFProps = {
activity: Activity;
};
export const ActivityEntry: FC<IFProps> = ({ activity }) => {
return (
<>
<img src={activity.user.picture} alt="" className="circle" />
{/* <i className="material-icons circle">folder</i> */}
<span className="title">
{activity.user.firstName} {activity.description} {activity.ticket.title}
</span>
<p>{activity.date.toDateString()}</p>
</>
);
};

View file

@ -0,0 +1,34 @@
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
import { ActivityCollection } from "./ActivityCollection";
import { Activity } from "../types/Activity";
import { FilterBar } from "./FilterBar";
type IProps = {
activities: Activity[];
};
export const ActivityList: FC<IProps> = ({ activities }) => {
const [filterText, setFilterText] = useState<string>("");
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
setFilterText("");
};
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
e: ChangeEvent<HTMLInputElement>
) => {
setFilterText(e.target.value);
};
return (
<>
<div className="row valign-wrapper">
<h3>Activity</h3>
<FilterBar
filterText={filterText}
handleChange={handleChange}
clearFilterText={clearFilterText}
/>
</div>
<ActivityCollection activities={activities} filterText={filterText} />
</>
);
};

View file

@ -0,0 +1,35 @@
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
import { AppFile } from "../types/AppFile";
import { FileCollection } from "./FileCollection";
import { InputFile } from "./InputFile";
import { FilterBar } from "./FilterBar";
type IProps = {
files: AppFile[];
};
export const FileList: FC<IProps> = ({ files }) => {
const [filterText, setFilterText] = useState<string>("");
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
setFilterText("");
};
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
e: ChangeEvent<HTMLInputElement>
) => {
setFilterText(e.target.value);
};
return (
<>
<div className="row valign-wrapper">
<h3>Files</h3>
<FilterBar
filterText={filterText}
handleChange={handleChange}
clearFilterText={clearFilterText}
/>
</div>
<InputFile />
<FileCollection files={files} filterText={filterText} />
</>
);
};

View file

@ -1,15 +1,24 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { FloatingButton } from "./FloatingButton"; import { User } from "../types/User";
interface AvatarListProps { interface AvatarListProps {
avatars: string[]; users: User[];
} }
export const AvatarList: FC<AvatarListProps> = ({ avatars }) => { export const AvatarList: FC<AvatarListProps> = ({ users }) => {
return ( return users === undefined ? (
<></>
) : (
<> <>
{avatars.map((avatar: string) => ( {users.map((user: User, i: number) => (
<img className="circle" src={avatar} width="32vh" height="32vh" /> <img
key={i}
className="circle"
src={user.picture}
width="32vh"
height="32vh"
alt={user.fullName}
/>
))} ))}
</> </>
); );

View file

@ -1,4 +1,4 @@
import React, { FC, Children } from "react"; import React, { FC, MouseEvent } from "react";
interface IProps { interface IProps {
icon?: string; icon?: string;
@ -6,18 +6,20 @@ interface IProps {
shape?: string; shape?: string;
color?: string; color?: string;
text?: string; text?: string;
onClick?: (e: MouseEvent) => void;
} }
export const Button: FC<IProps> = ({ export const Button: FC<IProps> = ({
size = "small", size = "small",
shape = "", shape = "",
color, color,
text, onClick,
children children
}) => { }) => {
return ( return (
<button <button
className={`waves-effect waves-light btn-${size} ${shape} ${color}`} className={`waves-effect waves-light btn-${size} ${shape} ${color}`}
onClick={onClick}
> >
{children} {children}
</button> </button>

View file

@ -0,0 +1,45 @@
import React, { FC } from "react";
import { AppFile } from "../types/AppFile";
type IProps = {
files: AppFile[];
filterText: string;
};
export const FileCollection: FC<IProps> = ({ files, filterText }) => {
return (
<>
<ul className="collection">
{files
.filter(
f =>
f.name.toLowerCase().includes(filterText.toLowerCase()) ||
f.format.toLowerCase().includes(filterText.toLowerCase())
)
.map((file: AppFile) => (
<FileEntry file={file} key={file.id} />
))}
</ul>
</>
);
};
type IFProps = {
file: AppFile;
};
export const FileEntry: FC<IFProps> = ({ file }) => {
return (
<li className="collection-item avatar">
{/* <img src={require("../images/user_1.jpg")} alt="" className="circle" /> */}
<i className="material-icons circle">folder</i>
<span className="title">{file.name}</span>
<p>
{file.size}kb {file.format}
</p>
<a href="#!" className="secondary-content">
<i className="material-icons">more_vert</i>
</a>
</li>
);
};

View file

@ -0,0 +1,41 @@
import React, { FC, ChangeEvent, MouseEvent } from "react";
import { useRouteMatch } from "react-router-dom";
type IProps = {
filterText: string;
handleChange: (e: ChangeEvent<HTMLInputElement>) => void;
clearFilterText: (e: MouseEvent<HTMLInputElement>) => void;
};
export const FilterBar: FC<IProps> = ({
filterText,
handleChange,
clearFilterText
}) => {
const { url } = useRouteMatch();
const placeholder: string = url.split("/")[3] || "users";
return (
<>
<div className="nav-wrapper">
<div className="input-field">
<input
// className="validate"
id="filter"
type="search"
required
name="filter"
value={filterText}
placeholder={`Filter ${placeholder}`}
onChange={handleChange}
/>
<label className="label-icon" htmlFor="search">
<i className="material-icons">filter_list</i>
</label>
<i className="material-icons" onClick={clearFilterText}>
close
</i>
</div>
</div>
</>
);
};

View file

@ -1,20 +1,22 @@
import React, { FC } from "react"; import React, { FC, MouseEvent } from "react";
import { Button } from "./Button"; import { Button } from "./Button";
interface IProps { interface IProps {
icon?: string; icon?: string;
size?: string; size?: string;
color?: string; color?: string;
onClick?: (e: MouseEvent) => void;
} }
export const FloatingButton: FC<IProps> = ({ export const FloatingButton: FC<IProps> = ({
icon = "add", icon = "add",
size = "small", size = "small",
color = "red" color = "red",
onClick
}) => { }) => {
const iconComponent = <i className="material-icons left">{icon}</i>; const iconComponent = <i className="material-icons left">{icon}</i>;
return ( return (
<Button color={color} size={size} shape="btn-floating"> <Button color={color} size={size} shape="btn-floating" onClick={onClick}>
{iconComponent} {iconComponent}
</Button> </Button>
); );

View file

@ -1,54 +1,44 @@
import React, { FC, MouseEvent } from "react"; import React, { FC, MouseEvent } from "react";
import { AvatarList } from "./AvatarList"; import { Link } from "react-router-dom";
import { getRemainingdays } from "../utils/methods";
interface IProps { interface IProps {
title: string; title: string;
tasksTotalCount?: number; remainingDays: string;
tasksDone?: number;
remainingDays?: number;
avatars: string[];
validateTicket: (event: MouseEvent) => void; validateTicket: (event: MouseEvent) => void;
archiveTicket: (event: MouseEvent) => void; archiveTicket: (event: MouseEvent) => void;
} }
export const HorizontalCard: FC<IProps> = ({ export const HorizontalCard: FC<IProps> = ({
title, title,
tasksDone,
tasksTotalCount,
remainingDays, remainingDays,
avatars,
archiveTicket, archiveTicket,
validateTicket validateTicket
}) => { }) => {
return ( return (
<div className="col s12"> <div className="card horizontal">
<div className="card horizontal"> <div className="card-stacked">
<div className="card-stacked"> <div className="card-content">
<div className="card-content"> <div className="row">
<div className="row"> <div className="card-title">
<div className="card-title"> <h6>
<h6>{title}</h6> <Link to="#">
</div> <b>{title}</b>
<span>Due {remainingDays} days</span> </Link>
{/* <AvatarList avatars={avatars} /> */} </h6>
<div className="right"> </div>
{/* <i className=" material-icons">playlist_add_check</i> <span>Due {getRemainingdays(remainingDays)} days</span>
<span> <div className="right">
{" "} <Link to="#">
{tasksDone}/{tasksTotalCount} <i className="material-icons" onClick={validateTicket}>
</span> */} check
</i>
<a> </Link>
<i className="material-icons" onClick={validateTicket}> <Link to="#">
check <i className="material-icons" onClick={archiveTicket}>
</i> archive
</a> </i>
<a> </Link>
<i className="material-icons" onClick={archiveTicket}>
archive
</i>
</a>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,29 @@
import React, { FC } from "react";
type IProps = {};
export const InputFile: FC<IProps> = () => {
return (
<>
<form action="/upload">
<div className="file-field input-field">
<div className="btn">
<i className="material-icons ">cloud_upload</i>
<input
type="file"
multiple
accept=".doc,.docx,.pdf,.md,.gdoc,.zip,image/*"
/>
</div>
<div className="file-path-wrapper">
<input
className="file-path validate"
type="text"
placeholder="Upload one or more files"
/>
</div>
</div>
</form>
</>
);
};

View file

@ -0,0 +1,24 @@
import React, { FC, useState, CSSProperties } from "react";
interface IProps {
handleClose: () => void;
show: boolean;
}
export const Modal: FC<IProps> = ({ handleClose, show, children }) => {
const showHideStyle: CSSProperties = show
? { display: "block", zIndex: 10 }
: { display: "none", zIndex: 10 };
return (
<div className="modal" style={showHideStyle}>
<div className="modal-content">{children}</div>
<div className="modal-footer">
<button
type="submit"
className="modal-close waves-effect waves-green btn"
>
Done
</button>
</div>
</div>
);
};

View file

@ -0,0 +1,55 @@
import React, { FC } from "react";
export const Preloader: FC = () => {
return (
<div className="preloader-wrapper big active">
<div className="spinner-layer spinner-blue">
<div className="circle-clipper left">
<div className="circle"></div>
</div>
<div className="gap-patch">
<div className="circle"></div>
</div>
<div className="circle-clipper right">
<div className="circle"></div>
</div>
</div>
<div className="spinner-layer spinner-red">
<div className="circle-clipper left">
<div className="circle"></div>
</div>
<div className="gap-patch">
<div className="circle"></div>
</div>
<div className="circle-clipper right">
<div className="circle"></div>
</div>
</div>
<div className="spinner-layer spinner-yellow">
<div className="circle-clipper left">
<div className="circle"></div>
</div>
<div className="gap-patch">
<div className="circle"></div>
</div>
<div className="circle-clipper right">
<div className="circle"></div>
</div>
</div>
<div className="spinner-layer spinner-green">
<div className="circle-clipper left">
<div className="circle"></div>
</div>
<div className="gap-patch">
<div className="circle"></div>
</div>
<div className="circle-clipper right">
<div className="circle"></div>
</div>
</div>
</div>
);
};

View file

@ -1,4 +1,4 @@
import React, { FC, HTMLAttributes, CSSProperties } from "react"; import React, { FC, CSSProperties } from "react";
type ProgressBarProps = { type ProgressBarProps = {
value: number; value: number;

View file

@ -1,52 +1,47 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { TabRouterHeader } from "./TabRouterHeader"; import { TabRouterHeader } from "./TabRouterHeader";
import { TicketList } from "./TicketList"; import { TicketList } from "./TicketList";
import { FileList } from "./AppFileList";
import { Ticket } from "../types/Ticket"; import { Ticket } from "../types/Ticket";
import { Switch, Route, useRouteMatch, Redirect } from "react-router-dom"; import { AppFile } from "../types/AppFile";
import { Route, useRouteMatch, Redirect } from "react-router-dom";
import { ActivityList } from "./ActivityList";
import { Activity } from "../types/Activity";
interface IProps { interface IProps {
tickets: Ticket[]; tickets: Ticket[];
tasksTotalCount?: number;
tasksDone?: number;
remainingDays?: number; remainingDays?: number;
avatars: string[]; tabNames: string[];
files: AppFile[];
activities: Activity[];
} }
export const TabRouter: FC<IProps> = ({ export const TabRouter: FC<IProps> = ({
tickets, tickets,
tasksDone, tabNames,
tasksTotalCount, files,
remainingDays, activities
avatars
}) => { }) => {
const { url } = useRouteMatch(); const { url } = useRouteMatch();
return ( return (
<> <>
<Switch> <div className="row">
<div className="row"> <TabRouterHeader tabNames={tabNames} />
<TabRouterHeader />
<Redirect from={url} to={`${url}/tickets`} /> <Redirect from={url} to={`${url}/tickets`} />
<Route path={`${url}/tickets`}> <Route path={`${url}/tickets`}>
<TicketList <TicketList tickets={tickets} />
tickets={tickets} </Route>
tasksDone={tasksDone}
tasksTotalCount={tasksTotalCount}
remainingDays={remainingDays}
avatars={avatars}
/>
</Route>
<Route path={`${url}/files`}> <Route path={`${url}/files`}>
{/* <TicketList tickets={tickets} /> */} <FileList files={files} />
</Route> </Route>
<Route path={`${url}/activity`}> <Route path={`${url}/activity`}>
{/* <TicketList tickets={tickets} /> */} <ActivityList activities={activities} />
</Route> </Route>
</div> </div>
</Switch>
</> </>
); );
}; };

View file

@ -1,12 +1,50 @@
import React, { FC, useState } from "react"; import React, { FC, useState } from "react";
import { Link, useRouteMatch } from "react-router-dom"; import { Link, useRouteMatch } from "react-router-dom";
interface IProps {
tabClass?: string;
tabNames: string[];
}
export const TabRouterHeader: FC<IProps> = ({
tabClass = "tab col s4",
tabNames
}) => {
const [isActive, setIsActive] = useState(0);
const nTabs = tabNames.length;
return (
<>
<ul className="tabs z-depth-1">
{tabNames.map((name, i) => (
<TabUnit
key={i}
text={name}
value={i.toString()}
tabClass={tabClass}
isActive={isActive}
setIsActive={setIsActive}
nTabs={nTabs}
/>
))}
<li
className="indicator"
style={{
left: `${(isActive / nTabs) * 100}%`,
right: `${(1 - (isActive + 1) / nTabs) * 100}%`
}}
></li>
</ul>
</>
);
};
interface TabUnitProps { interface TabUnitProps {
tabClass: string; tabClass: string;
isActive: number; isActive: number;
setIsActive: React.Dispatch<React.SetStateAction<number>>; setIsActive: React.Dispatch<React.SetStateAction<number>>;
text: string; text: string;
value: string; value: string;
nTabs: number;
} }
const TabUnit: FC<TabUnitProps> = ({ const TabUnit: FC<TabUnitProps> = ({
@ -14,15 +52,23 @@ const TabUnit: FC<TabUnitProps> = ({
isActive, isActive,
setIsActive, setIsActive,
text, text,
value value,
nTabs
}) => { }) => {
const { url } = useRouteMatch(); const { url } = useRouteMatch();
return ( return (
<li className={tabClass} key={value}> <li
className={tabClass}
key={value}
style={{
left: `${(isActive / nTabs) * 100}%`,
right: `${(1 - (isActive + 1) / nTabs) * 100}%`
}}
>
<Link <Link
to={`${url}/${text}`} to={`${url}/${text}`}
id={value} id={value}
className={isActive === parseInt(value) ? "active" : ""} className={isActive === parseInt(value) ? "active pink lighten-5" : ""}
onClick={() => setIsActive(parseInt(value))} onClick={() => setIsActive(parseInt(value))}
> >
{text} {text}
@ -30,50 +76,3 @@ const TabUnit: FC<TabUnitProps> = ({
</li> </li>
); );
}; };
interface IProps {
tabClass?: string;
}
export const TabRouterHeader: FC<IProps> = ({
tabClass = "tab col s3",
children
}) => {
const [isActive, setIsActive] = useState(1);
// const switchTab = (e: React.MouseEvent<HTMLAnchorElement>): void => {
// e.preventDefault();
// setIsActive(e.target.id);
// };
return (
<>
<div className="row col s12">
<ul className="tabs">
<TabUnit
text="Tickets"
value="1"
tabClass={tabClass}
isActive={isActive}
setIsActive={setIsActive}
/>
<TabUnit
text="Files"
value="2"
tabClass={tabClass}
isActive={isActive}
setIsActive={setIsActive}
/>
<TabUnit
text="Activity"
value="3"
tabClass={tabClass}
isActive={isActive}
setIsActive={setIsActive}
/>
</ul>
</div>
</>
);
};

View file

@ -1,52 +1,62 @@
import React, { FC } from "react"; import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
import { Ticket } from "../types/Ticket"; import { Ticket } from "../types/Ticket";
import { FloatingButton } from "./FloatingButton"; import { FloatingButton } from "./FloatingButton";
import { HorizontalCard } from "./HorizontalCard"; import { HorizontalCard } from "./HorizontalCard";
import { FilterBar } from "./FilterBar";
type TicketListProps = { type TicketListProps = {
tickets: Ticket[]; tickets: Ticket[];
tasksTotalCount?: number;
tasksDone?: number;
remainingDays?: number;
avatars: string[];
}; };
export const TicketList: FC<TicketListProps> = ({ export const TicketList: FC<TicketListProps> = ({ tickets }) => {
tickets, const [filterText, setFilterText] = useState<string>("");
tasksDone, const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
tasksTotalCount, setFilterText("");
remainingDays, };
avatars
}) => {
const archiveTicket = () => {}; const archiveTicket = () => {};
const validateTicket = () => {}; const validateTicket = () => {};
const onClick: (e: MouseEvent) => void = (e: MouseEvent) => {
e.preventDefault();
};
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
e: ChangeEvent<HTMLInputElement>
) => {
setFilterText(e.target.value);
};
return ( return (
<div className="col s12"> <>
<div className="row valign-wrapper"> <div className="row valign-wrapper">
<div className="col s6 m4"> <h3>Tickets</h3>
<h2>Tickets</h2> <FloatingButton
</div> color=" blue-grey lighten-4"
<div className="col s6 m8"> size="small"
<FloatingButton color="grey" size="big" /> onClick={onClick}
</div> />
<FilterBar
filterText={filterText}
handleChange={handleChange}
clearFilterText={clearFilterText}
/>
</div> </div>
<div className="col s12 grey">
<ul> <ul>
{tickets.map((t: Ticket) => ( {tickets
<li key={t.id}> .filter(t =>
<HorizontalCard t.title.toLowerCase().includes(filterText.toLowerCase())
title={t.title} )
tasksDone={tasksDone} .map((t: Ticket) => (
tasksTotalCount={tasksTotalCount} <li key={t.id}>
remainingDays={remainingDays} <HorizontalCard
avatars={avatars} title={t.title}
validateTicket={validateTicket} remainingDays={t.plannedEnding}
archiveTicket={archiveTicket} validateTicket={validateTicket}
/> archiveTicket={archiveTicket}
</li> />
))} </li>
</ul> ))}
</div> </ul>
</div>
</>
); );
}; };

View file

@ -0,0 +1,103 @@
import React, { FC, useState, ChangeEvent, useEffect } from "react";
import { Modal } from "./Modal";
import { AvatarList } from "./AvatarList";
import { User } from "../types/User";
import { FilterBar } from "./FilterBar";
import { HttpResponse } from "../types/HttpResponse";
import { get } from "../utils/http";
import { Constants } from "../utils/Constants";
interface IProps {
show: boolean;
handleClose: () => void;
users: User[];
}
export const UsersModal: FC<IProps> = ({ show, handleClose, users }) => {
const [filterText, setFilterText] = useState<string>("");
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
e: ChangeEvent<HTMLInputElement>
) => {
setFilterText(e.target.value);
};
const [allUsers, setAllUsers] = useState();
async function httpGet(): Promise<void> {
try {
const response: HttpResponse<User> = await get<User>(
`${Constants.usersURI}`
);
if (response.parsedBody !== undefined) {
setAllUsers(response.parsedBody);
// setIsLoading(false);
}
} catch (ex) {
// setHasError(true);
// setError(ex);
}
}
useEffect(() => {
// if (id !== undefined) {
httpGet();
// } else {
// setHasError(true);
// setError("Bad Request");
// }
}, []);
return (
<Modal show={show} handleClose={handleClose}>
<div className="row valign-wrapper blue">
<div className="col s10">
<h4 className="white-text">Manage users</h4>
</div>
<div className="col s2">
<i
className="right material-icons blue lighten-3 circle"
onClick={handleClose}
>
close
</i>
</div>
</div>
<div className="center">
<AvatarList users={users} />
<FilterBar
filterText={filterText}
clearFilterText={() => setFilterText("")}
handleChange={handleChange}
/>
</div>
{/* <div className="code">{allUsers}</div> */}
<form>
<ul>
{users.map((u: User) => (
<li key={u.id}>
<div className="row">
<input
id={u.id}
type="checkbox"
name="active"
value="true"
onChange={() => false}
// checked
/>
<span>
{u.fullName}
<img
className="circle"
src={u.picture}
width="32vh"
height="32vh"
alt={u.fullName}
/>
</span>
</div>
</li>
))}
</ul>
</form>
</Modal>
);
};

View file

@ -0,0 +1,19 @@
import React, { FC } from "react";
import { Redirect } from "react-router-dom";
interface IProps {
error: any;
}
export const ErrorController: FC<IProps> = ({ error }) => {
switch (error) {
case "Bad Request":
return <Redirect to="/400" />;
case "Not Found":
return <Redirect to="/404" />;
default:
return <Redirect to="/404" />;
}
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View file

@ -1,6 +1,5 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import "./index.css";
import App from "./App"; import App from "./App";
import * as serviceWorker from "./serviceWorker"; import * as serviceWorker from "./serviceWorker";

View file

@ -0,0 +1,10 @@
import React, { FC } from "react";
interface IProps {}
export const NotFoundPage: FC<IProps> = () => {
return (
<div className="section">
<p>error</p>
</div>
);
};

View file

@ -1,32 +1,50 @@
import React, { FC } from "react"; import React, { FC, useState } from "react";
import ProjectVM from "../VM/ProjectVM";
import { Header } from "../components/Header"; import { Header } from "../components/Header";
import { AvatarList } from "../components/AvatarList"; import { AvatarList } from "../components/AvatarList";
import { ProgressBar } from "../components/ProgressBar"; import { ProgressBar } from "../components/ProgressBar";
import ProjectVM from "../viewModels/ProjectVM";
import { TabRouter } from "../components/TabRouter"; import { TabRouter } from "../components/TabRouter";
import { FloatingButton } from "../components/FloatingButton"; import { FloatingButton } from "../components/FloatingButton";
import { UsersModal } from "../components/UsersModal";
interface IProps { interface IProps {
viewModel: ProjectVM; viewModel: ProjectVM;
} }
export const ProjectPage: FC<IProps> = ({ viewModel }) => { export const ProjectPage: FC<IProps> = ({ viewModel }) => {
const { const {
title, title,
description, description,
avatars, users,
value, value,
tickets, tickets,
ticketsDone, ticketsDone,
ticketsTotalCount, ticketsTotalCount,
remainingDays remainingDays,
files,
activities
} = viewModel; } = viewModel;
const tabNames: string[] = ["Tickets", "Files", "Activity"];
const [showModal, setShowModal] = useState<boolean>(false);
return ( return (
<div className="section"> <div className="section">
<div className="container"> <div className="container">
<Header title={title} description={description} /> <Header title={title} description={description} />
<div className="row valign-wrapper"> <div className="row valign-wrapper">
<AvatarList avatars={avatars} /> <AvatarList users={users} />
<FloatingButton icon="add" color="grey" size="small" /> <FloatingButton
icon="add"
color="grey"
size="small"
onClick={() => setShowModal(true)}
/>
<UsersModal
show={showModal}
users={users}
handleClose={() => setShowModal(false)}
/>
</div> </div>
<ProgressBar <ProgressBar
value={value} value={value}
@ -35,11 +53,10 @@ export const ProjectPage: FC<IProps> = ({ viewModel }) => {
remainingDays={remainingDays} remainingDays={remainingDays}
/> />
<TabRouter <TabRouter
tabNames={tabNames}
tickets={tickets} tickets={tickets}
tasksDone={ticketsDone} files={files}
tasksTotalCount={ticketsTotalCount} activities={activities}
remainingDays={remainingDays}
avatars={avatars}
/> />
</div> </div>
</div> </div>

View file

@ -10,7 +10,7 @@ export const TicketPage: FC = () => {
description="Research, ideate and present brand concepts for client consideration" description="Research, ideate and present brand concepts for client consideration"
title="Brand Concept and Design" title="Brand Concept and Design"
/> />
<AvatarList avatars={["../images/user_1.jpg", "../images/user_2.jpg"]} /> {/* <AvatarList users={["../images/user_1.jpg", "../images/user_2.jpg"]} /> */}
<ProgressBar value={60} /> <ProgressBar value={60} />
{/* // <TabView> {/* // <TabView>
// <ChildTicket/> // <ChildTicket/>

View file

@ -1 +0,0 @@
/// <reference types="react-scripts" />

View file

@ -0,0 +1,10 @@
import { User } from "./User";
import { Ticket } from "./Ticket";
export interface Activity {
id: number;
description: string;
date: Date;
user: User;
ticket: Ticket;
}

View file

@ -0,0 +1,9 @@
import { User } from "./User";
export interface AppFile {
id: number;
name: string;
description: string;
format: string;
size: number;
}

View file

@ -1,3 +0,0 @@
export interface File {
Id: number;
}

View file

@ -1,3 +0,0 @@
export interface History {
Id: number;
}

View file

@ -0,0 +1,3 @@
export interface HttpResponse<T> extends Response {
parsedBody?: T;
}

View file

@ -1,12 +1,19 @@
import { Ticket } from "./Ticket"; import { Ticket } from "./Ticket";
import { User } from "./User"; import { User } from "./User";
import { AppFile } from "./AppFile";
import { Activity } from "./Activity";
export interface Project { export interface Project {
id: number; id: number;
title: string; title: string;
description: string; description: string;
progression: number; createdAt: string;
tickets: Ticket[];
users: User[];
plannedEnding: string; plannedEnding: string;
progression: number;
status: string;
manager: User;
users: User[];
tickets: Ticket[];
files: AppFile[];
activities: Activity[];
} }

View file

@ -1,5 +1,7 @@
export interface Ticket { export interface Ticket {
id: number; id: number;
title: string; title: string;
description: string;
status: string; status: string;
plannedEnding: string;
} }

View file

@ -1,4 +1,6 @@
export interface User { export interface User {
id: string; id: string;
picture: string; picture: string;
firstName: string;
fullName?: string;
} }

View file

@ -1,3 +1,5 @@
export class Constants { export class Constants {
static getProjectURI: string = "/api/projects"; static projectsURI: string = "/api/v1/projects";
static ticketsURI: string = "/api/v1/tickets";
static usersURI: string = "/api/v1/users";
} }

35
client/src/utils/http.ts Normal file
View file

@ -0,0 +1,35 @@
import { HttpResponse } from "../types/HttpResponse";
export async function http<T>(request: RequestInfo): Promise<HttpResponse<T>> {
const response: HttpResponse<T> = await fetch(request);
try {
response.parsedBody = await response.json();
} catch (ex) {}
if (!response.ok) {
throw response.statusText;
}
return response;
}
export async function get<T>(
path: string,
args: RequestInit = { method: "get" }
): Promise<HttpResponse<T>> {
return await http<T>(new Request(path, args));
}
export async function post<T>(
path: string,
body: any,
args: RequestInit = { method: "post", body: JSON.stringify(body) }
): Promise<HttpResponse<T>> {
return await http<T>(new Request(path, args));
}
export async function put<T>(
path: string,
body: any,
args: RequestInit = { method: "put", body: JSON.stringify(body) }
): Promise<HttpResponse<T>> {
return await http<T>(new Request(path, args));
}

View file

@ -0,0 +1,7 @@
export const getRemainingdays: (endDate: string) => number = (
endDate: string
) => {
let endingDate: Date = new Date(endDate);
let today: Date = new Date();
return Math.abs(endingDate.getDate() - today.getDate());
};

View file

@ -1,19 +1,31 @@
import React from "react"; import React from "react";
import { Router, Route, Switch, Link, NavLink } from "react-router-dom"; import {
Router,
Route,
Switch
// Redirect
//Link, NavLink
} from "react-router-dom";
import * as creacteHistory from "history"; import * as creacteHistory from "history";
import { TicketPage } from "../pages/TicketPage"; // import { TicketPage } from "../pages/TicketPage";
import { HomeController } from "../controllers/HomeController"; // import { HomeController } from "../controllers/HomeController";
import { ProjectController } from "../controllers/ProjectController"; import { ProjectController } from "../controllers/ProjectController";
import { UserController } from "../controllers/UserController"; import { NotFoundPage } from "../pages/NotFoundPage";
import { TicketController } from "../controllers/TicketController"; import { TestPage } from "../pages/TestPage";
// import { UserController } from "../controllers/UserController";
// import { TicketController } from "../controllers/TicketController";
export const history = creacteHistory.createBrowserHistory(); export const history = creacteHistory.createBrowserHistory();
export const AppRouter = () => { export const AppRouter = () => {
return ( return (
<Router history={history}> <Router history={history}>
<div> <div className="grey lighten-4">
<Switch> <Switch>
<Route exact path="/">
<TestPage />
</Route>
{/* <Route path="/"> {/* <Route path="/">
<HomeController /> <HomeController />
</Route> </Route>
@ -26,6 +38,14 @@ export const AppRouter = () => {
{/* <Route path="/tickets/:id"> {/* <Route path="/tickets/:id">
<TicketController /> <TicketController />
</Route> */} </Route> */}
<Route path="/404">
<NotFoundPage />
</Route>
{/* <Route path="*">
<Redirect to="/error" />
</Route> */}
</Switch> </Switch>
</div> </div>
</Router> </Router>

View file

@ -1,46 +0,0 @@
import { Ticket } from "../types/Ticket";
import { Project } from "../types/Project";
import { Constants } from "../utils/Constants";
import { User } from "../types/User";
export default class ProjectVM {
public id: number;
public title: string;
public description: string;
public value: number;
public tickets: Ticket[];
public avatars: string[];
public ticketsTotalCount: number;
public ticketsDone: number;
public remainingDays: number;
/**
* getMembers
*/
// public getMembers(): string {
// let res: Promise<Response> = fetch(
// `${Constants.getProjectURI}/${this.id}/members`
// );
// return JSON.stringify(res);
// // res.json();
// }
public constructor(project: Project) {
this.id = project.id;
this.title = project.title;
this.description = project.description;
this.avatars = project.users.map(u => u.picture);
this.value = project.progression;
this.tickets = project.tickets;
this.ticketsTotalCount = this.tickets.length;
this.ticketsDone = this.tickets.filter(t => t.status === "Done").length;
let endingDate: Date = new Date(project.plannedEnding);
let today: Date = new Date();
let plannedEnding: number = Math.abs(
endingDate.getDate() - today.getDate()
);
this.remainingDays = plannedEnding;
}
}