aboutsummaryrefslogtreecommitdiffstats
path: root/code/api/src/Endpoints/V1
diff options
context:
space:
mode:
Diffstat (limited to 'code/api/src/Endpoints/V1')
-rw-r--r--code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs30
-rw-r--r--code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs52
-rw-r--r--code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs32
-rw-r--r--code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs71
-rw-r--r--code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs60
-rw-r--r--code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs52
-rw-r--r--code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs62
-rw-r--r--code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs63
-rw-r--r--code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs107
-rw-r--r--code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs54
-rw-r--r--code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs282
-rw-r--r--code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs52
-rw-r--r--code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs102
-rw-r--r--code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs70
-rw-r--r--code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs51
-rw-r--r--code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs49
-rw-r--r--code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs58
-rw-r--r--code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs80
-rw-r--r--code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs43
-rw-r--r--code/api/src/Endpoints/V1/Projects/_calls.http12
-rw-r--r--code/api/src/Endpoints/V1/RouteBaseAsync.cs4
21 files changed, 784 insertions, 602 deletions
diff --git a/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs b/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs
index 60b00ff..6bc2fdc 100644
--- a/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs
+++ b/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs
@@ -4,13 +4,12 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens;
public class CreateTokenRoute : RouteBaseSync.WithRequest<ApiAccessToken.ApiAccessTokenDto>.WithActionResult
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
private readonly AppConfiguration _configuration;
private readonly ILogger<CreateTokenRoute> _logger;
- public CreateTokenRoute(AppDbContext context, VaultService vaultService, ILogger<CreateTokenRoute> logger)
- {
- _context = context;
+ public CreateTokenRoute(MainAppDatabase database, VaultService vaultService, ILogger<CreateTokenRoute> logger) {
+ _database = database;
_configuration = vaultService.GetCurrentAppConfiguration();
_logger = logger;
}
@@ -24,24 +23,19 @@ public class CreateTokenRoute : RouteBaseSync.WithRequest<ApiAccessToken.ApiAcce
[HttpPost("~/v{version:apiVersion}/api-tokens/create")]
[ProducesResponseType(200, Type = typeof(string))]
[ProducesResponseType(404, Type = typeof(KnownProblemModel))]
- public override ActionResult Handle(ApiAccessToken.ApiAccessTokenDto request)
- {
- var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id);
- if (user == default)
- {
+ public override ActionResult Handle(ApiAccessToken.ApiAccessTokenDto request) {
+ var user = _database.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id);
+ if (user == default) {
return NotFound(new KnownProblemModel("User does not exist"));
}
var token_entropy = _configuration.APP_AES_KEY;
- if (token_entropy.IsNullOrWhiteSpace())
- {
+ if (token_entropy.IsNullOrWhiteSpace()) {
_logger.LogWarning("No token entropy is available, Basic auth is disabled");
return NotFound();
}
- var access_token = new ApiAccessToken()
- {
- Id = Guid.NewGuid(),
+ var accessToken = new ApiAccessToken() {
User = user,
ExpiryDate = request.ExpiryDate.ToUniversalTime(),
AllowCreate = request.AllowCreate,
@@ -50,8 +44,8 @@ public class CreateTokenRoute : RouteBaseSync.WithRequest<ApiAccessToken.ApiAcce
AllowUpdate = request.AllowUpdate
};
- _context.AccessTokens.Add(access_token);
- _context.SaveChanges();
- return Ok(Convert.ToBase64String(Encoding.UTF8.GetBytes(access_token.Id.ToString().EncryptWithAes(token_entropy))));
+ _database.AccessTokens.Add(accessToken);
+ _database.SaveChanges();
+ return Ok(Convert.ToBase64String(Encoding.UTF8.GetBytes(accessToken.Id.ToString().EncryptWithAes(token_entropy))));
}
-}
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs b/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs
index a90b4c0..116a814 100644
--- a/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs
+++ b/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs
@@ -2,32 +2,32 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens;
public class DeleteTokenRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult
{
- private readonly AppDbContext _context;
- private readonly ILogger<DeleteTokenRoute> _logger;
+ private readonly MainAppDatabase _database;
+ private readonly ILogger<DeleteTokenRoute> _logger;
- public DeleteTokenRoute(AppDbContext context, ILogger<DeleteTokenRoute> logger) {
- _context = context;
- _logger = logger;
- }
+ public DeleteTokenRoute(MainAppDatabase database, ILogger<DeleteTokenRoute> logger) {
+ _database = database;
+ _logger = logger;
+ }
- /// <summary>
- /// Delete an api token, rendering it unusable
- /// </summary>
- /// <param name="id">Id of the token to delete</param>
- /// <returns>Nothing</returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [HttpDelete("~/v{version:apiVersion}/api-tokens/delete")]
- [ProducesResponseType(200)]
- [ProducesResponseType(404)]
- public override ActionResult Handle(Guid id) {
- var token = _context.AccessTokens.SingleOrDefault(c => c.Id == id);
- if (token == default) {
- _logger.LogWarning("A deletion request of an already deleted (maybe) api token was received.");
- return NotFound();
- }
+ /// <summary>
+ /// Delete an api token, rendering it unusable
+ /// </summary>
+ /// <param name="id">Id of the token to delete</param>
+ /// <returns>Nothing</returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [HttpDelete("~/v{version:apiVersion}/api-tokens/delete")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ public override ActionResult Handle(Guid id) {
+ var token = _database.AccessTokens.SingleOrDefault(c => c.Id == id);
+ if (token == default) {
+ _logger.LogWarning("A deletion request of an already deleted (maybe) api token was received.");
+ return NotFound();
+ }
- _context.AccessTokens.Remove(token);
- _context.SaveChanges();
- return Ok();
- }
-}
+ _database.AccessTokens.Remove(token);
+ _database.SaveChanges();
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs b/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs
index 59fd077..19790e4 100644
--- a/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs
+++ b/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs
@@ -2,21 +2,21 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens;
public class GetTokensRoute : RouteBaseSync.WithoutRequest.WithResult<ActionResult<List<ApiAccessToken.ApiAccessTokenDto>>>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public GetTokensRoute(AppDbContext context) {
- _context = context;
- }
+ public GetTokensRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Get all tokens, both active and inactive.
- /// </summary>
- /// <returns>A list of tokens</returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [HttpGet("~/v{version:apiVersion}/api-tokens")]
- [ProducesResponseType(200, Type = typeof(List<ApiAccessToken.ApiAccessTokenDto>))]
- [ProducesResponseType(204)]
- public override ActionResult<List<ApiAccessToken.ApiAccessTokenDto>> Handle() {
- return Ok(_context.AccessTokens.Where(c => c.User.Id == LoggedInUser.Id).Select(c => c.AsDto));
- }
-}
+ /// <summary>
+ /// Get all tokens, both active and inactive.
+ /// </summary>
+ /// <returns>A list of tokens</returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [HttpGet("~/v{version:apiVersion}/api-tokens")]
+ [ProducesResponseType(200, Type = typeof(List<ApiAccessToken.ApiAccessTokenDto>))]
+ [ProducesResponseType(204)]
+ public override ActionResult<List<ApiAccessToken.ApiAccessTokenDto>> Handle() {
+ return Ok(_database.AccessTokens.Where(c => c.User.Id == LoggedInUser.Id).Select(c => c.AsDto));
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs b/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs
index fac2b5e..0471637 100644
--- a/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs
+++ b/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs
@@ -2,42 +2,43 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Categories;
public class CreateCategoryRoute : RouteBaseSync.WithRequest<TimeCategory.TimeCategoryDto>.WithActionResult<TimeCategory.TimeCategoryDto>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public CreateCategoryRoute(AppDbContext context) {
- _context = context;
- }
+ public CreateCategoryRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Create a new time entry category.
- /// </summary>
- /// <param name="categoryTimeCategoryDto"></param>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)]
- [HttpPost("~/v{version:apiVersion}/categories/create")]
- [ProducesResponseType(200, Type = typeof(TimeCategory.TimeCategoryDto))]
- public override ActionResult<TimeCategory.TimeCategoryDto> Handle(TimeCategory.TimeCategoryDto categoryTimeCategoryDto) {
- var duplicate = _context.TimeCategories
- .Where(c => c.UserId == LoggedInUser.Id)
- .Any(c => c.Name.Trim() == categoryTimeCategoryDto.Name.Trim());
- if (duplicate) {
- var category = _context.TimeCategories
- .Where(c => c.UserId == LoggedInUser.Id)
- .SingleOrDefault(c => c.Name.Trim() == categoryTimeCategoryDto.Name.Trim());
- if (category != default) {
- return Ok(category.AsDto);
- }
- }
+ /// <summary>
+ /// Create a new time entry category.
+ /// </summary>
+ /// <param name="categoryTimeCategoryDto"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)]
+ [HttpPost("~/v{version:apiVersion}/categories/create")]
+ [ProducesResponseType(200, Type = typeof(TimeCategory.TimeCategoryDto))]
+ public override ActionResult<TimeCategory.TimeCategoryDto> Handle(TimeCategory.TimeCategoryDto categoryTimeCategoryDto) {
+ var duplicate = _database.TimeCategories
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .Any(c => c.Name.Trim() == categoryTimeCategoryDto.Name.Trim());
+ if (duplicate) {
+ var category = _database.TimeCategories
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Name.Trim() == categoryTimeCategoryDto.Name.Trim());
+ if (category != default) {
+ return Ok(category.AsDto);
+ }
+ }
- var newCategory = new TimeCategory(LoggedInUser.Id) {
- Name = categoryTimeCategoryDto.Name.Trim(),
- Color = categoryTimeCategoryDto.Color
- };
+ var newCategory = new TimeCategory(LoggedInUser) {
+ Name = categoryTimeCategoryDto.Name.Trim(),
+ Color = categoryTimeCategoryDto.Color
+ };
+ newCategory.SetOwnerIds(LoggedInUser.Id);
- _context.TimeCategories.Add(newCategory);
- _context.SaveChanges();
- categoryTimeCategoryDto.Id = newCategory.Id;
- return Ok(categoryTimeCategoryDto);
- }
-}
+ _database.TimeCategories.Add(newCategory);
+ _database.SaveChanges();
+ categoryTimeCategoryDto.Id = newCategory.Id;
+ return Ok(categoryTimeCategoryDto);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs b/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs
index 3d438a0..582ec7d 100644
--- a/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs
+++ b/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs
@@ -2,37 +2,37 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Categories;
public class DeleteCategoryRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public DeleteCategoryRoute(AppDbContext context) {
- _context = context;
- }
+ public DeleteCategoryRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Delete a time entry category.
- /// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)]
- [HttpDelete("~/v{version:apiVersion}/categories/{id:guid}/delete")]
- [ProducesResponseType(200)]
- [ProducesResponseType(404)]
- public override ActionResult Handle(Guid id) {
- var category = _context.TimeCategories
- .Where(c => c.UserId == LoggedInUser.Id)
- .SingleOrDefault(c => c.Id == id);
+ /// <summary>
+ /// Delete a time entry category.
+ /// </summary>
+ /// <param name="id"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)]
+ [HttpDelete("~/v{version:apiVersion}/categories/{id:guid}/delete")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ public override ActionResult Handle(Guid id) {
+ var category = _database.TimeCategories
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Id == id);
- if (category == default) {
- return NotFound();
- }
+ if (category == default) {
+ return NotFound();
+ }
- var entries = _context.TimeEntries
- .Include(c => c.Category)
- .Where(c => c.Category.Id == category.Id);
- _context.TimeEntries.RemoveRange(entries);
- _context.TimeCategories.Remove(category);
- _context.SaveChanges();
- return Ok();
- }
-}
+ var entries = _database.TimeEntries
+ .Include(c => c.Category)
+ .Where(c => c.Category.Id == category.Id);
+ _database.TimeEntries.RemoveRange(entries);
+ _database.TimeCategories.Remove(category);
+ _database.SaveChanges();
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs b/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs
index a40a832..937f8e3 100644
--- a/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs
+++ b/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs
@@ -1,35 +1,33 @@
namespace IOL.GreatOffice.Api.Endpoints.V1.Categories;
-/// <inheritdoc />
public class GetCategoriesRoute : RouteBaseSync.WithoutRequest.WithActionResult<List<TimeCategory.TimeCategoryDto>>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- /// <inheritdoc />
- public GetCategoriesRoute(AppDbContext context) {
- _context = context;
- }
+ public GetCategoriesRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Get a minimal list of time entry categories.
- /// </summary>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [ProducesResponseType(200, Type = typeof(List<TimeCategory.TimeCategoryDto>))]
- [ProducesResponseType(204)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)]
- [HttpGet("~/v{version:apiVersion}/categories")]
- public override ActionResult<List<TimeCategory.TimeCategoryDto>> Handle() {
- var categories = _context.TimeCategories
- .Where(c => c.UserId == LoggedInUser.Id)
- .OrderByDescending(c => c.CreatedAt)
- .Select(c => c.AsDto)
- .ToList();
+ /// <summary>
+ /// Get a minimal list of time entry categories.
+ /// </summary>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [ProducesResponseType(200, Type = typeof(List<TimeCategory.TimeCategoryDto>))]
+ [ProducesResponseType(204)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)]
+ [HttpGet("~/v{version:apiVersion}/categories")]
+ public override ActionResult<List<TimeCategory.TimeCategoryDto>> Handle() {
+ var categories = _database.TimeCategories
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .OrderByDescending(c => c.CreatedAt)
+ .Select(c => c.AsDto)
+ .ToList();
- if (categories.Count == 0) {
- return NoContent();
- }
+ if (categories.Count == 0) {
+ return NoContent();
+ }
- return Ok(categories);
- }
-}
+ return Ok(categories);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs b/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs
index ca7dfdf..096132d 100644
--- a/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs
+++ b/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs
@@ -2,38 +2,38 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Categories;
public class UpdateCategoryRoute : RouteBaseSync.WithRequest<TimeCategory.TimeCategoryDto>.WithActionResult
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public UpdateCategoryRoute(AppDbContext context) {
- _context = context;
- }
+ public UpdateCategoryRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Update a time entry category.
- /// </summary>
- /// <param name="categoryTimeCategoryDto"></param>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)]
- [HttpPost("~/v{version:apiVersion}/categories/update")]
- [ProducesResponseType(200)]
- [ProducesResponseType(404)]
- [ProducesResponseType(403)]
- public override ActionResult Handle(TimeCategory.TimeCategoryDto categoryTimeCategoryDto) {
- var category = _context.TimeCategories
- .Where(c => c.UserId == LoggedInUser.Id)
- .SingleOrDefault(c => c.Id == categoryTimeCategoryDto.Id);
- if (category == default) {
- return NotFound();
- }
+ /// <summary>
+ /// Update a time entry category.
+ /// </summary>
+ /// <param name="categoryTimeCategoryDto"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)]
+ [HttpPost("~/v{version:apiVersion}/categories/update")]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(404)]
+ [ProducesResponseType(403)]
+ public override ActionResult Handle(TimeCategory.TimeCategoryDto categoryTimeCategoryDto) {
+ var category = _database.TimeCategories
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Id == categoryTimeCategoryDto.Id);
+ if (category == default) {
+ return NotFound();
+ }
- if (LoggedInUser.Id != category.UserId) {
- return Forbid();
- }
+ if (LoggedInUser.Id != category.UserId) {
+ return Forbid();
+ }
- category.Name = categoryTimeCategoryDto.Name;
- category.Color = categoryTimeCategoryDto.Color;
- _context.SaveChanges();
- return Ok();
- }
-}
+ category.Name = categoryTimeCategoryDto.Name;
+ category.Color = categoryTimeCategoryDto.Color;
+ _database.SaveChanges();
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs b/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs
new file mode 100644
index 0000000..eb69f7f
--- /dev/null
+++ b/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs
@@ -0,0 +1,63 @@
+using Microsoft.Extensions.Localization;
+
+namespace IOL.GreatOffice.Api.Endpoints.V1.Customers;
+
+public class CreateCustomerRoute : RouteBaseAsync.WithRequest<CreateCustomerPayload>.WithActionResult
+{
+ private readonly MainAppDatabase _database;
+ private readonly ILogger<CreateCustomerRoute> _logger;
+ private readonly IStringLocalizer<SharedResources> _localizer;
+
+ public CreateCustomerRoute(MainAppDatabase database, ILogger<CreateCustomerRoute> logger, IStringLocalizer<SharedResources> localizer) {
+ _database = database;
+ _logger = logger;
+ _localizer = localizer;
+ }
+
+ [HttpPost("~/v{version:apiVersion}/customers/create")]
+ public override async Task<ActionResult> HandleAsync(CreateCustomerPayload request, CancellationToken cancellationToken = default) {
+ var errors = new Dictionary<string, string>();
+ if (request.Name.Trim().IsNullOrEmpty()) errors.Add("name", _localizer["Name is a required field"]);
+ if (errors.Any()) return KnownProblem(_localizer["Invalid form"], _localizer["One or more fields is invalid"], errors);
+ var customer = new Customer(LoggedInUser) {
+ CustomerNumber = request.CustomerNumber,
+ Name = request.Name,
+ Description = request.Description,
+ Address1 = request.Address1,
+ Address2 = request.Address2,
+ Country = request.Country,
+ Currency = request.Currency,
+ Email = request.Email,
+ Phone = request.Phone,
+ PostalCity = request.PostalCity,
+ PostalCode = request.PostalCode,
+ VATNumber = request.VATNumber,
+ ORGNumber = request.ORGNumber,
+ DefaultReference = request.DefaultReference,
+ Website = request.Website
+ };
+ customer.SetOwnerIds(default, LoggedInUser.TenantId);
+ _database.Customers.Add(customer);
+ await _database.SaveChangesAsync(cancellationToken);
+ return Ok();
+ }
+}
+
+public class CreateCustomerPayload
+{
+ public string CustomerNumber { get; set; }
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public string Address1 { get; set; }
+ public string Address2 { get; set; }
+ public string PostalCode { get; set; }
+ public string PostalCity { get; set; }
+ public string Country { get; set; }
+ public string Phone { get; set; }
+ public string Email { get; set; }
+ public string VATNumber { get; set; }
+ public string ORGNumber { get; set; }
+ public string DefaultReference { get; set; }
+ public string Website { get; set; }
+ public string Currency { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs
index 854ff59..45d8f32 100644
--- a/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs
+++ b/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs
@@ -2,65 +2,66 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Entries;
public class CreateEntryRoute : RouteBaseSync.WithRequest<TimeEntry.TimeEntryDto>.WithActionResult<TimeEntry.TimeEntryDto>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public CreateEntryRoute(AppDbContext context) {
- _context = context;
- }
+ public CreateEntryRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Create a time entry.
- /// </summary>
- /// <param name="timeEntryTimeEntryDto"></param>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)]
- [ProducesResponseType(200)]
- [ProducesResponseType(400, Type = typeof(KnownProblemModel))]
- [ProducesResponseType(404, Type = typeof(KnownProblemModel))]
- [HttpPost("~/v{version:apiVersion}/entries/create")]
- public override ActionResult<TimeEntry.TimeEntryDto> Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) {
- if (timeEntryTimeEntryDto.Stop == default) {
- return BadRequest(new KnownProblemModel("Invalid form", "A stop date is required"));
- }
+ /// <summary>
+ /// Create a time entry.
+ /// </summary>
+ /// <param name="timeEntryTimeEntryDto"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(400, Type = typeof(KnownProblemModel))]
+ [ProducesResponseType(404, Type = typeof(KnownProblemModel))]
+ [HttpPost("~/v{version:apiVersion}/entries/create")]
+ public override ActionResult<TimeEntry.TimeEntryDto> Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) {
+ if (timeEntryTimeEntryDto.Stop == default) {
+ return BadRequest(new KnownProblemModel("Invalid form", "A stop date is required"));
+ }
- if (timeEntryTimeEntryDto.Start == default) {
- return BadRequest(new KnownProblemModel("Invalid form", "A start date is required"));
- }
+ if (timeEntryTimeEntryDto.Start == default) {
+ return BadRequest(new KnownProblemModel("Invalid form", "A start date is required"));
+ }
- if (timeEntryTimeEntryDto.Category == default) {
- return BadRequest(new KnownProblemModel("Invalid form", "A category is required"));
- }
+ if (timeEntryTimeEntryDto.Category == default) {
+ return BadRequest(new KnownProblemModel("Invalid form", "A category is required"));
+ }
- var category = _context.TimeCategories
- .Where(c => c.UserId == LoggedInUser.Id)
- .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Category.Id);
-
- if (category == default) {
- return NotFound(new KnownProblemModel("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}"));
- }
+ var category = _database.TimeCategories
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Category.Id);
- var entry = new TimeEntry(LoggedInUser.Id) {
- Category = category,
- Start = timeEntryTimeEntryDto.Start.ToUniversalTime(),
- Stop = timeEntryTimeEntryDto.Stop.ToUniversalTime(),
- Description = timeEntryTimeEntryDto.Description,
- };
+ if (category == default) {
+ return NotFound(new KnownProblemModel("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}"));
+ }
- if (timeEntryTimeEntryDto.Labels?.Count > 0) {
- var labels = _context.TimeLabels
- .Where(c => c.UserId == LoggedInUser.Id)
- .Where(c => timeEntryTimeEntryDto.Labels.Select(p => p.Id).Contains(c.Id))
- .ToList();
- if (labels.Count != timeEntryTimeEntryDto.Labels.Count) {
- return NotFound(new KnownProblemModel("Not found", "Could not find all of the specified labels"));
- }
+ var entry = new TimeEntry(LoggedInUser) {
+ Category = category,
+ Start = timeEntryTimeEntryDto.Start.ToUniversalTime(),
+ Stop = timeEntryTimeEntryDto.Stop.ToUniversalTime(),
+ Description = timeEntryTimeEntryDto.Description,
+ };
+ entry.SetOwnerIds(LoggedInUser.Id);
- entry.Labels = labels;
- }
+ if (timeEntryTimeEntryDto.Labels?.Count > 0) {
+ var labels = _database.TimeLabels
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .Where(c => timeEntryTimeEntryDto.Labels.Select(p => p.Id).Contains(c.Id))
+ .ToList();
+ if (labels.Count != timeEntryTimeEntryDto.Labels.Count) {
+ return NotFound(new KnownProblemModel("Not found", "Could not find all of the specified labels"));
+ }
- _context.TimeEntries.Add(entry);
- _context.SaveChanges();
- return Ok(entry.AsDto);
- }
-}
+ entry.Labels = labels;
+ }
+
+ _database.TimeEntries.Add(entry);
+ _database.SaveChanges();
+ return Ok(entry.AsDto);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs
index 0850af0..c488ed5 100644
--- a/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs
+++ b/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs
@@ -1,35 +1,33 @@
namespace IOL.GreatOffice.Api.Endpoints.V1.Entries;
-/// <inheritdoc />
public class DeleteEntryRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- /// <inheritdoc />
- public DeleteEntryRoute(AppDbContext context) {
- _context = context;
- }
+ public DeleteEntryRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Delete a time entry.
- /// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)]
- [HttpDelete("~/v{version:apiVersion}/entries/{id:guid}/delete")]
- [ProducesResponseType(404)]
- [ProducesResponseType(200)]
- public override ActionResult Handle(Guid id) {
- var entry = _context.TimeEntries
- .Where(c => c.UserId == LoggedInUser.Id)
- .SingleOrDefault(c => c.Id == id);
- if (entry == default) {
- return NotFound();
- }
+ /// <summary>
+ /// Delete a time entry.
+ /// </summary>
+ /// <param name="id"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)]
+ [HttpDelete("~/v{version:apiVersion}/entries/{id:guid}/delete")]
+ [ProducesResponseType(404)]
+ [ProducesResponseType(200)]
+ public override ActionResult Handle(Guid id) {
+ var entry = _database.TimeEntries
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Id == id);
+ if (entry == default) {
+ return NotFound();
+ }
- _context.TimeEntries.Remove(entry);
- _context.SaveChanges();
- return Ok();
- }
-}
+ _database.TimeEntries.Remove(entry);
+ _database.SaveChanges();
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs b/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs
index ec6003c..03a64d2 100644
--- a/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs
+++ b/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs
@@ -2,185 +2,185 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Entries;
public class EntryQueryRoute : RouteBaseSync.WithRequest<EntryQueryPayload>.WithActionResult<EntryQueryResponse>
{
- private readonly ILogger<EntryQueryRoute> _logger;
- private readonly AppDbContext _context;
+ private readonly ILogger<EntryQueryRoute> _logger;
+ private readonly MainAppDatabase _database;
- public EntryQueryRoute(ILogger<EntryQueryRoute> logger, AppDbContext context) {
- _logger = logger;
- _context = context;
- }
+ public EntryQueryRoute(ILogger<EntryQueryRoute> logger, MainAppDatabase database) {
+ _logger = logger;
+ _database = database;
+ }
- /// <summary>
- /// Get a list of entries based on a given query.
- /// </summary>
- /// <param name="entryQuery"></param>
- /// <returns></returns>
- /// <exception cref="ArgumentOutOfRangeException"></exception>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)]
- [HttpPost("~/v{version:apiVersion}/entries/query")]
- [ProducesResponseType(204)]
- [ProducesResponseType(400, Type = typeof(KnownProblemModel))]
- [ProducesResponseType(200, Type = typeof(EntryQueryResponse))]
- public override ActionResult<EntryQueryResponse> Handle(EntryQueryPayload entryQuery) {
- var result = new TimeQueryDto();
+ /// <summary>
+ /// Get a list of entries based on a given query.
+ /// </summary>
+ /// <param name="entryQuery"></param>
+ /// <returns></returns>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)]
+ [HttpPost("~/v{version:apiVersion}/entries/query")]
+ [ProducesResponseType(204)]
+ [ProducesResponseType(400, Type = typeof(KnownProblemModel))]
+ [ProducesResponseType(200, Type = typeof(EntryQueryResponse))]
+ public override ActionResult<EntryQueryResponse> Handle(EntryQueryPayload entryQuery) {
+ var result = new TimeQueryDto();
- Request.Headers.TryGetValue(AppHeaders.BROWSER_TIME_ZONE, out var timeZoneHeader);
- var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneHeader.ToString().HasValue() ? timeZoneHeader.ToString() : "UTC");
- var offsetInHours = tz.BaseUtcOffset.Hours;
+ Request.Headers.TryGetValue(AppHeaders.BROWSER_TIME_ZONE, out var timeZoneHeader);
+ var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneHeader.ToString().HasValue() ? timeZoneHeader.ToString() : "UTC");
+ var offsetInHours = tz.BaseUtcOffset.Hours;
- // this is fine as long as the client is not connecting from Australia: Lord Howe Island
- // according to https://en.wikipedia.org/wiki/Daylight_saving_time_by_country
- if (tz.IsDaylightSavingTime(AppDateTime.UtcNow)) {
- offsetInHours++;
- }
+ // this is fine as long as the client is not connecting from Australia: Lord Howe Island
+ // according to https://en.wikipedia.org/wiki/Daylight_saving_time_by_country
+ if (tz.IsDaylightSavingTime(AppDateTime.UtcNow)) {
+ offsetInHours++;
+ }
- _logger.LogInformation("Request time zone (" + tz.Id + ") offset is: " + offsetInHours + " hours");
- var requestDateTime = TimeZoneInfo.ConvertTimeFromUtc(AppDateTime.UtcNow, tz);
- _logger.LogInformation("Querying data with date time: " + requestDateTime.ToString("u"));
+ _logger.LogInformation("Request time zone (" + tz.Id + ") offset is: " + offsetInHours + " hours");
+ var requestDateTime = TimeZoneInfo.ConvertTimeFromUtc(AppDateTime.UtcNow, tz);
+ _logger.LogInformation("Querying data with date time: " + requestDateTime.ToString("u"));
- var skipCount = 0;
- if (entryQuery.Page > 1) {
- skipCount = entryQuery.PageSize * entryQuery.Page;
- }
+ var skipCount = 0;
+ if (entryQuery.Page > 1) {
+ skipCount = entryQuery.PageSize * entryQuery.Page;
+ }
- result.Page = entryQuery.Page;
- result.PageSize = entryQuery.PageSize;
+ result.Page = entryQuery.Page;
+ result.PageSize = entryQuery.PageSize;
- var baseQuery = _context.TimeEntries
- .AsNoTracking()
- .Include(c => c.Category)
- .Include(c => c.Labels)
- .Where(c => c.UserId == LoggedInUser.Id)
- .ConditionalWhere(entryQuery.Categories?.Any() ?? false, c => entryQuery.Categories.Any(p => p.Id == c.Category.Id))
- .ConditionalWhere(entryQuery.Labels?.Any() ?? false, c => c.Labels.Any(l => entryQuery.Labels.Any(p => p.Id == l.Id)))
- .OrderByDescending(c => c.Start);
+ var baseQuery = _database.TimeEntries
+ .AsNoTracking()
+ .Include(c => c.Category)
+ .Include(c => c.Labels)
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .ConditionalWhere(entryQuery.Categories?.Any() ?? false, c => entryQuery.Categories.Any(p => p.Id == c.Category.Id))
+ .ConditionalWhere(entryQuery.Labels?.Any() ?? false, c => c.Labels.Any(l => entryQuery.Labels.Any(p => p.Id == l.Id)))
+ .OrderByDescending(c => c.Start);
- switch (entryQuery.Duration) {
- case TimeEntryQueryDuration.TODAY:
- var baseTodaysEntries = baseQuery
- .Where(c => DateTime.Compare(c.Start.AddHours(offsetInHours).Date, AppDateTime.UtcNow.Date) == 0);
- var baseTodaysEntriesCount = baseTodaysEntries.Count();
+ switch (entryQuery.Duration) {
+ case TimeEntryQueryDuration.TODAY:
+ var baseTodaysEntries = baseQuery
+ .Where(c => DateTime.Compare(c.Start.AddHours(offsetInHours).Date, AppDateTime.UtcNow.Date) == 0);
+ var baseTodaysEntriesCount = baseTodaysEntries.Count();
- if (baseTodaysEntriesCount == 0) {
- return NoContent();
- }
+ if (baseTodaysEntriesCount == 0) {
+ return NoContent();
+ }
- result.TotalSize = baseTodaysEntriesCount;
- result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseTodaysEntriesCount / entryQuery.PageSize));
+ result.TotalSize = baseTodaysEntriesCount;
+ result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseTodaysEntriesCount / entryQuery.PageSize));
- var pagedTodaysEntries = baseTodaysEntries.Skip(skipCount).Take(entryQuery.PageSize);
+ var pagedTodaysEntries = baseTodaysEntries.Skip(skipCount).Take(entryQuery.PageSize);
- result.Results.AddRange(pagedTodaysEntries.Select(c => c.AsDto));
- break;
- case TimeEntryQueryDuration.THIS_WEEK:
- var lastMonday = AppDateTime.UtcNow.StartOfWeek(DayOfWeek.Monday);
+ result.Results.AddRange(pagedTodaysEntries.Select(c => c.AsDto));
+ break;
+ case TimeEntryQueryDuration.THIS_WEEK:
+ var lastMonday = AppDateTime.UtcNow.StartOfWeek(DayOfWeek.Monday);
- var baseEntriesThisWeek = baseQuery
- .Where(c => c.Start.AddHours(offsetInHours).Date >= lastMonday.Date && c.Start.AddHours(offsetInHours).Date <= AppDateTime.UtcNow.Date);
+ var baseEntriesThisWeek = baseQuery
+ .Where(c => c.Start.AddHours(offsetInHours).Date >= lastMonday.Date && c.Start.AddHours(offsetInHours).Date <= AppDateTime.UtcNow.Date);
- var baseEntriesThisWeekCount = baseEntriesThisWeek.Count();
+ var baseEntriesThisWeekCount = baseEntriesThisWeek.Count();
- if (baseEntriesThisWeekCount == 0) {
- return NoContent();
- }
+ if (baseEntriesThisWeekCount == 0) {
+ return NoContent();
+ }
- result.TotalSize = baseEntriesThisWeekCount;
- result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisWeekCount / entryQuery.PageSize));
+ result.TotalSize = baseEntriesThisWeekCount;
+ result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseEntriesThisWeekCount / entryQuery.PageSize));
- var pagedEntriesThisWeek = baseEntriesThisWeek.Skip(skipCount).Take(entryQuery.PageSize);
+ var pagedEntriesThisWeek = baseEntriesThisWeek.Skip(skipCount).Take(entryQuery.PageSize);
- result.Results.AddRange(pagedEntriesThisWeek.Select(c => c.AsDto));
- break;
- case TimeEntryQueryDuration.THIS_MONTH:
- var baseEntriesThisMonth = baseQuery
- .Where(c => c.Start.AddHours(offsetInHours).Month == AppDateTime.UtcNow.Month
- && c.Start.AddHours(offsetInHours).Year == AppDateTime.UtcNow.Year);
- var baseEntriesThisMonthCount = baseEntriesThisMonth.Count();
- if (baseEntriesThisMonthCount == 0) {
- return NoContent();
- }
+ result.Results.AddRange(pagedEntriesThisWeek.Select(c => c.AsDto));
+ break;
+ case TimeEntryQueryDuration.THIS_MONTH:
+ var baseEntriesThisMonth = baseQuery
+ .Where(c => c.Start.AddHours(offsetInHours).Month == AppDateTime.UtcNow.Month
+ && c.Start.AddHours(offsetInHours).Year == AppDateTime.UtcNow.Year);
+ var baseEntriesThisMonthCount = baseEntriesThisMonth.Count();
+ if (baseEntriesThisMonthCount == 0) {
+ return NoContent();
+ }
- result.TotalSize = baseEntriesThisMonthCount;
- result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisMonthCount / entryQuery.PageSize));
+ result.TotalSize = baseEntriesThisMonthCount;
+ result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseEntriesThisMonthCount / entryQuery.PageSize));
- var pagedEntriesThisMonth = baseEntriesThisMonth.Skip(skipCount).Take(entryQuery.PageSize);
+ var pagedEntriesThisMonth = baseEntriesThisMonth.Skip(skipCount).Take(entryQuery.PageSize);
- result.Results.AddRange(pagedEntriesThisMonth.Select(c => c.AsDto));
- break;
- case TimeEntryQueryDuration.THIS_YEAR:
- var baseEntriesThisYear = baseQuery
- .Where(c => c.Start.AddHours(offsetInHours).Year == AppDateTime.UtcNow.Year);
+ result.Results.AddRange(pagedEntriesThisMonth.Select(c => c.AsDto));
+ break;
+ case TimeEntryQueryDuration.THIS_YEAR:
+ var baseEntriesThisYear = baseQuery
+ .Where(c => c.Start.AddHours(offsetInHours).Year == AppDateTime.UtcNow.Year);
- var baseEntriesThisYearCount = baseEntriesThisYear.Count();
- if (baseEntriesThisYearCount == 0) {
- return NoContent();
- }
+ var baseEntriesThisYearCount = baseEntriesThisYear.Count();
+ if (baseEntriesThisYearCount == 0) {
+ return NoContent();
+ }
- result.TotalSize = baseEntriesThisYearCount;
- result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisYearCount / entryQuery.PageSize));
+ result.TotalSize = baseEntriesThisYearCount;
+ result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseEntriesThisYearCount / entryQuery.PageSize));
- var pagedEntriesThisYear = baseEntriesThisYear.Skip(skipCount).Take(entryQuery.PageSize);
+ var pagedEntriesThisYear = baseEntriesThisYear.Skip(skipCount).Take(entryQuery.PageSize);
- result.Results.AddRange(pagedEntriesThisYear.Select(c => c.AsDto));
- break;
- case TimeEntryQueryDuration.SPECIFIC_DATE:
- var date = DateTime.SpecifyKind(entryQuery.SpecificDate, DateTimeKind.Utc);
- var baseEntriesOnThisDate = baseQuery.Where(c => c.Start.AddHours(offsetInHours).Date == date.Date);
- var baseEntriesOnThisDateCount = baseEntriesOnThisDate.Count();
+ result.Results.AddRange(pagedEntriesThisYear.Select(c => c.AsDto));
+ break;
+ case TimeEntryQueryDuration.SPECIFIC_DATE:
+ var date = DateTime.SpecifyKind(entryQuery.SpecificDate, DateTimeKind.Utc);
+ var baseEntriesOnThisDate = baseQuery.Where(c => c.Start.AddHours(offsetInHours).Date == date.Date);
+ var baseEntriesOnThisDateCount = baseEntriesOnThisDate.Count();
- if (baseEntriesOnThisDateCount == 0) {
- return NoContent();
- }
+ if (baseEntriesOnThisDateCount == 0) {
+ return NoContent();
+ }
- result.TotalSize = baseEntriesOnThisDateCount;
- result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesOnThisDateCount / entryQuery.PageSize));
+ result.TotalSize = baseEntriesOnThisDateCount;
+ result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseEntriesOnThisDateCount / entryQuery.PageSize));
- var pagedEntriesOnThisDate = baseEntriesOnThisDate.Skip(skipCount).Take(entryQuery.PageSize);
+ var pagedEntriesOnThisDate = baseEntriesOnThisDate.Skip(skipCount).Take(entryQuery.PageSize);
- result.Results.AddRange(pagedEntriesOnThisDate.Select(c => c.AsDto));
- break;
- case TimeEntryQueryDuration.DATE_RANGE:
- if (entryQuery.DateRange.From == default) {
- return BadRequest(new KnownProblemModel("Invalid query", "From date cannot be empty"));
- }
+ result.Results.AddRange(pagedEntriesOnThisDate.Select(c => c.AsDto));
+ break;
+ case TimeEntryQueryDuration.DATE_RANGE:
+ if (entryQuery.DateRange.From == default) {
+ return BadRequest(new KnownProblemModel("Invalid query", "From date cannot be empty"));
+ }
- var fromDate = DateTime.SpecifyKind(entryQuery.DateRange.From, DateTimeKind.Utc);
+ var fromDate = DateTime.SpecifyKind(entryQuery.DateRange.From, DateTimeKind.Utc);
- if (entryQuery.DateRange.To == default) {
- return BadRequest(new KnownProblemModel("Invalid query", "To date cannot be empty"));
- }
+ if (entryQuery.DateRange.To == default) {
+ return BadRequest(new KnownProblemModel("Invalid query", "To date cannot be empty"));
+ }
- var toDate = DateTime.SpecifyKind(entryQuery.DateRange.To, DateTimeKind.Utc);
+ var toDate = DateTime.SpecifyKind(entryQuery.DateRange.To, DateTimeKind.Utc);
- if (DateTime.Compare(fromDate, toDate) > 0) {
- return BadRequest(new KnownProblemModel("Invalid query", "To date cannot be less than From date"));
- }
+ if (DateTime.Compare(fromDate, toDate) > 0) {
+ return BadRequest(new KnownProblemModel("Invalid query", "To date cannot be less than From date"));
+ }
- var baseDateRangeEntries = baseQuery
- .Where(c => c.Start.AddHours(offsetInHours).Date > fromDate && c.Start.AddHours(offsetInHours).Date <= toDate);
+ var baseDateRangeEntries = baseQuery
+ .Where(c => c.Start.AddHours(offsetInHours).Date > fromDate && c.Start.AddHours(offsetInHours).Date <= toDate);
- var baseDateRangeEntriesCount = baseDateRangeEntries.Count();
- if (baseDateRangeEntriesCount == 0) {
- return NoContent();
- }
+ var baseDateRangeEntriesCount = baseDateRangeEntries.Count();
+ if (baseDateRangeEntriesCount == 0) {
+ return NoContent();
+ }
- result.TotalSize = baseDateRangeEntriesCount;
- result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseDateRangeEntriesCount / entryQuery.PageSize));
+ result.TotalSize = baseDateRangeEntriesCount;
+ result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseDateRangeEntriesCount / entryQuery.PageSize));
- var pagedDateRangeEntries = baseDateRangeEntries.Skip(skipCount).Take(entryQuery.PageSize);
+ var pagedDateRangeEntries = baseDateRangeEntries.Skip(skipCount).Take(entryQuery.PageSize);
- result.Results.AddRange(pagedDateRangeEntries.Select(c => c.AsDto));
- break;
- default:
- throw new ArgumentOutOfRangeException(nameof(entryQuery), "Unknown duration for query");
- }
+ result.Results.AddRange(pagedDateRangeEntries.Select(c => c.AsDto));
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(entryQuery), "Unknown duration for query");
+ }
- if (result.Results.Any() && result.Page == 0) {
- result.Page = 1;
- result.TotalPageCount = 1;
- }
+ if (result.Results.Any() && result.Page == 0) {
+ result.Page = 1;
+ result.TotalPageCount = 1;
+ }
- return Ok(result);
- }
-}
+ return Ok(result);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs
index 87038db..6e064c7 100644
--- a/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs
+++ b/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs
@@ -2,33 +2,33 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Entries;
public class GetEntryRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult<TimeEntry.TimeEntryDto>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public GetEntryRoute(AppDbContext context) {
- _context = context;
- }
+ public GetEntryRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Get a spesific time entry.
- /// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)]
- [HttpGet("~/v{version:apiVersion}/entries/{id:guid}")]
- [ProducesResponseType(404)]
- [ProducesResponseType(200, Type = typeof(TimeEntry.TimeEntryDto))]
- public override ActionResult<TimeEntry.TimeEntryDto> Handle(Guid id) {
- var entry = _context.TimeEntries
- .Where(c => c.UserId == LoggedInUser.Id)
- .Include(c => c.Category)
- .Include(c => c.Labels)
- .SingleOrDefault(c => c.Id == id);
+ /// <summary>
+ /// Get a spesific time entry.
+ /// </summary>
+ /// <param name="id"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)]
+ [HttpGet("~/v{version:apiVersion}/entries/{id:guid}")]
+ [ProducesResponseType(404)]
+ [ProducesResponseType(200, Type = typeof(TimeEntry.TimeEntryDto))]
+ public override ActionResult<TimeEntry.TimeEntryDto> Handle(Guid id) {
+ var entry = _database.TimeEntries
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .Include(c => c.Category)
+ .Include(c => c.Labels)
+ .SingleOrDefault(c => c.Id == id);
- if (entry == default) {
- return NotFound();
- }
+ if (entry == default) {
+ return NotFound();
+ }
- return Ok(entry);
- }
-}
+ return Ok(entry);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs
index 09e3b9c..2254214 100644
--- a/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs
+++ b/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs
@@ -2,65 +2,65 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Entries;
public class UpdateEntryRoute : RouteBaseSync.WithRequest<TimeEntry.TimeEntryDto>.WithActionResult<TimeEntry.TimeEntryDto>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public UpdateEntryRoute(AppDbContext context) {
- _context = context;
- }
+ public UpdateEntryRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Update a time entry.
- /// </summary>
- /// <param name="timeEntryTimeEntryDto"></param>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)]
- [HttpPost("~/v{version:apiVersion}/entries/update")]
- [ProducesResponseType(404, Type = typeof(KnownProblemModel))]
- [ProducesResponseType(200, Type = typeof(TimeEntry.TimeEntryDto))]
- public override ActionResult<TimeEntry.TimeEntryDto> Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) {
- var entry = _context.TimeEntries
- .Where(c => c.UserId == LoggedInUser.Id)
- .Include(c => c.Labels)
- .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Id);
+ /// <summary>
+ /// Update a time entry.
+ /// </summary>
+ /// <param name="timeEntryTimeEntryDto"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)]
+ [HttpPost("~/v{version:apiVersion}/entries/update")]
+ [ProducesResponseType(404, Type = typeof(KnownProblemModel))]
+ [ProducesResponseType(200, Type = typeof(TimeEntry.TimeEntryDto))]
+ public override ActionResult<TimeEntry.TimeEntryDto> Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) {
+ var entry = _database.TimeEntries
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .Include(c => c.Labels)
+ .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Id);
- if (entry == default) {
- return NotFound();
- }
+ if (entry == default) {
+ return NotFound();
+ }
- var category = _context.TimeCategories
- .Where(c => c.UserId == LoggedInUser.Id)
- .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Category.Id);
- if (category == default) {
- return NotFound(new KnownProblemModel("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}"));
- }
+ var category = _database.TimeCategories
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Category.Id);
+ if (category == default) {
+ return NotFound(new KnownProblemModel("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}"));
+ }
- entry.Start = timeEntryTimeEntryDto.Start.ToUniversalTime();
- entry.Stop = timeEntryTimeEntryDto.Stop.ToUniversalTime();
- entry.Description = timeEntryTimeEntryDto.Description;
- entry.Category = category;
+ entry.Start = timeEntryTimeEntryDto.Start.ToUniversalTime();
+ entry.Stop = timeEntryTimeEntryDto.Stop.ToUniversalTime();
+ entry.Description = timeEntryTimeEntryDto.Description;
+ entry.Category = category;
- if (timeEntryTimeEntryDto.Labels?.Count > 0) {
- var labels = new List<TimeLabel>();
+ if (timeEntryTimeEntryDto.Labels?.Count > 0) {
+ var labels = new List<TimeLabel>();
- foreach (var labelDto in timeEntryTimeEntryDto.Labels) {
- var label = _context.TimeLabels
- .Where(c => c.UserId == LoggedInUser.Id)
- .SingleOrDefault(c => c.Id == labelDto.Id);
+ foreach (var labelDto in timeEntryTimeEntryDto.Labels) {
+ var label = _database.TimeLabels
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Id == labelDto.Id);
- if (label == default) {
- continue;
- }
+ if (label == default) {
+ continue;
+ }
- labels.Add(label);
- }
+ labels.Add(label);
+ }
- entry.Labels = labels;
- } else {
- entry.Labels = default;
- }
+ entry.Labels = labels;
+ } else {
+ entry.Labels = default;
+ }
- _context.SaveChanges();
- return Ok(entry.AsDto);
- }
-}
+ _database.SaveChanges();
+ return Ok(entry.AsDto);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs
index 4fe418b..d6106aa 100644
--- a/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs
+++ b/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs
@@ -1,44 +1,44 @@
-
namespace IOL.GreatOffice.Api.Endpoints.V1.Labels;
public class CreateLabelRoute : RouteBaseSync.WithRequest<TimeLabel.TimeLabelDto>.WithActionResult<TimeLabel.TimeLabelDto>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- public CreateLabelRoute(AppDbContext context) {
- _context = context;
- }
+ public CreateLabelRoute(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Create a time entry label.
- /// </summary>
- /// <param name="labelTimeLabelDto"></param>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)]
- [HttpPost("~/v{version:apiVersion}/labels/create")]
- public override ActionResult<TimeLabel.TimeLabelDto> Handle(TimeLabel.TimeLabelDto labelTimeLabelDto) {
- var duplicate = _context.TimeLabels
- .Where(c => c.UserId == LoggedInUser.Id)
- .Any(c => c.Name.Trim() == labelTimeLabelDto.Name.Trim());
- if (duplicate) {
- var label = _context.TimeLabels
- .Where(c => c.UserId == LoggedInUser.Id)
- .SingleOrDefault(c => c.Name.Trim() == labelTimeLabelDto.Name.Trim());
+ /// <summary>
+ /// Create a time entry label.
+ /// </summary>
+ /// <param name="labelTimeLabelDto"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)]
+ [HttpPost("~/v{version:apiVersion}/labels/create")]
+ public override ActionResult<TimeLabel.TimeLabelDto> Handle(TimeLabel.TimeLabelDto labelTimeLabelDto) {
+ var duplicate = _database.TimeLabels
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .Any(c => c.Name.Trim() == labelTimeLabelDto.Name.Trim());
+ if (duplicate) {
+ var label = _database.TimeLabels
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Name.Trim() == labelTimeLabelDto.Name.Trim());
- if (label != default) {
- return Ok(label.AsDto);
- }
- }
+ if (label != default) {
+ return Ok(label.AsDto);
+ }
+ }
- var newLabel = new TimeLabel(LoggedInUser.Id) {
- Name = labelTimeLabelDto.Name.Trim(),
- Color = labelTimeLabelDto.Color
- };
+ var newLabel = new TimeLabel(LoggedInUser) {
+ Name = labelTimeLabelDto.Name.Trim(),
+ Color = labelTimeLabelDto.Color
+ };
+ newLabel.SetOwnerIds(LoggedInUser.Id);
- _context.TimeLabels.Add(newLabel);
- _context.SaveChanges();
- labelTimeLabelDto.Id = newLabel.Id;
- return Ok(labelTimeLabelDto);
- }
-}
+ _database.TimeLabels.Add(newLabel);
+ _database.SaveChanges();
+ labelTimeLabelDto.Id = newLabel.Id;
+ return Ok(labelTimeLabelDto);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs
index d845a6f..1baf4ef 100644
--- a/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs
+++ b/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs
@@ -1,35 +1,32 @@
-
namespace IOL.GreatOffice.Api.Endpoints.V1.Labels;
-/// <inheritdoc />
public class DeleteLabelEndpoint : RouteBaseSync.WithRequest<Guid>.WithActionResult
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- /// <inheritdoc />
- public DeleteLabelEndpoint(AppDbContext context) {
- _context = context;
- }
+ public DeleteLabelEndpoint(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Delete a time entry label.
- /// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)]
- [HttpDelete("~/v{version:apiVersion}/labels/{id:guid}/delete")]
- public override ActionResult Handle(Guid id) {
- var label = _context.TimeLabels
- .Where(c => c.UserId == LoggedInUser.Id)
- .SingleOrDefault(c => c.Id == id);
+ /// <summary>
+ /// Delete a time entry label.
+ /// </summary>
+ /// <param name="id"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)]
+ [HttpDelete("~/v{version:apiVersion}/labels/{id:guid}/delete")]
+ public override ActionResult Handle(Guid id) {
+ var label = _database.TimeLabels
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Id == id);
- if (label == default) {
- return NotFound();
- }
+ if (label == default) {
+ return NotFound();
+ }
- _context.TimeLabels.Remove(label);
- _context.SaveChanges();
- return Ok();
- }
-}
+ _database.TimeLabels.Remove(label);
+ _database.SaveChanges();
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs
index c9ccef3..09d453b 100644
--- a/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs
+++ b/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs
@@ -1,34 +1,31 @@
-
namespace IOL.GreatOffice.Api.Endpoints.V1.Labels;
-/// <inheritdoc />
public class GetEndpoint : RouteBaseSync.WithoutRequest.WithActionResult<List<TimeLabel.TimeLabelDto>>
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- /// <inheritdoc />
- public GetEndpoint(AppDbContext context) {
- _context = context;
- }
+ public GetEndpoint(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Get a minimal list of time entry labels.
- /// </summary>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)]
- [HttpGet("~/v{version:apiVersion}/labels")]
- public override ActionResult<List<TimeLabel.TimeLabelDto>> Handle() {
- var labels = _context.TimeLabels
- .Where(c => c.UserId == LoggedInUser.Id)
- .OrderByDescending(c => c.CreatedAt)
- .Select(c => c.AsDto)
- .ToList();
+ /// <summary>
+ /// Get a minimal list of time entry labels.
+ /// </summary>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)]
+ [HttpGet("~/v{version:apiVersion}/labels")]
+ public override ActionResult<List<TimeLabel.TimeLabelDto>> Handle() {
+ var labels = _database.TimeLabels
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .OrderByDescending(c => c.CreatedAt)
+ .Select(c => c.AsDto)
+ .ToList();
- if (labels.Count == 0) {
- return NoContent();
- }
+ if (labels.Count == 0) {
+ return NoContent();
+ }
- return Ok(labels);
- }
-}
+ return Ok(labels);
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs
index 30d72ec..9857b7d 100644
--- a/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs
+++ b/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs
@@ -1,38 +1,36 @@
namespace IOL.GreatOffice.Api.Endpoints.V1.Labels;
-/// <inheritdoc />
public class UpdateLabelEndpoint : RouteBaseSync.WithRequest<TimeLabel.TimeLabelDto>.WithActionResult
{
- private readonly AppDbContext _context;
+ private readonly MainAppDatabase _database;
- /// <inheritdoc />
- public UpdateLabelEndpoint(AppDbContext context) {
- _context = context;
- }
+ public UpdateLabelEndpoint(MainAppDatabase database) {
+ _database = database;
+ }
- /// <summary>
- /// Update a time entry label.
- /// </summary>
- /// <param name="labelTimeLabelDto"></param>
- /// <returns></returns>
- [ApiVersion(ApiSpecV1.VERSION_STRING)]
- [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)]
- [HttpPost("~/v{version:apiVersion}/labels/update")]
- public override ActionResult Handle(TimeLabel.TimeLabelDto labelTimeLabelDto) {
- var label = _context.TimeLabels
- .Where(c => c.UserId == LoggedInUser.Id)
- .SingleOrDefault(c => c.Id == labelTimeLabelDto.Id);
- if (label == default) {
- return NotFound();
- }
+ /// <summary>
+ /// Update a time entry label.
+ /// </summary>
+ /// <param name="labelTimeLabelDto"></param>
+ /// <returns></returns>
+ [ApiVersion(ApiSpecV1.VERSION_STRING)]
+ [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)]
+ [HttpPost("~/v{version:apiVersion}/labels/update")]
+ public override ActionResult Handle(TimeLabel.TimeLabelDto labelTimeLabelDto) {
+ var label = _database.TimeLabels
+ .Where(c => c.UserId == LoggedInUser.Id)
+ .SingleOrDefault(c => c.Id == labelTimeLabelDto.Id);
+ if (label == default) {
+ return NotFound();
+ }
- if (LoggedInUser.Id != label.UserId) {
- return Forbid();
- }
+ if (LoggedInUser.Id != label.UserId) {
+ return Forbid();
+ }
- label.Name = labelTimeLabelDto.Name;
- label.Color = labelTimeLabelDto.Color;
- _context.SaveChanges();
- return Ok();
- }
-}
+ label.Name = labelTimeLabelDto.Name;
+ label.Color = labelTimeLabelDto.Color;
+ _database.SaveChanges();
+ return Ok();
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs b/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs
new file mode 100644
index 0000000..5c78e27
--- /dev/null
+++ b/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs
@@ -0,0 +1,80 @@
+using Microsoft.Extensions.Localization;
+
+namespace IOL.GreatOffice.Api.Endpoints.V1.Projects;
+
+public class CreateProjectRoute : RouteBaseAsync.WithRequest<CreateProjectPayload>.WithActionResult
+{
+ private readonly MainAppDatabase _database;
+ private readonly IStringLocalizer<SharedResources> _localizer;
+
+ public CreateProjectRoute(MainAppDatabase database, IStringLocalizer<SharedResources> localizer) {
+ _database = database;
+ _localizer = localizer;
+ }
+
+ [HttpPost("~/v{version:apiVersion}/projects/create")]
+ public override async Task<ActionResult> HandleAsync(CreateProjectPayload request, CancellationToken cancellationToken = default) {
+ var errors = new Dictionary<string, string>();
+
+ if (request.Name.IsNullOrEmpty()) {
+ errors.Add("name", _localizer["Name is a required field"]);
+ }
+
+ var project = new Project(LoggedInUser) {
+ Name = request.Name,
+ Description = request.Description,
+ Start = request.Start,
+ Stop = request.Stop,
+ };
+ project.SetOwnerIds(default, LoggedInUser.TenantId);
+
+ foreach (var customerId in request.CustomerIds) {
+ var customer = _database.Customers.FirstOrDefault(c => c.Id == customerId);
+ if (customer == default) {
+ errors.Add("customer_" + customerId, _localizer["Customer not found"]);
+ continue;
+ }
+
+ project.Customers.Add(customer);
+ }
+
+ foreach (var member in request.Members) {
+ var user = _database.Users.FirstOrDefault(c => c.Id == member.UserId);
+ if (user == default) {
+ errors.Add("members_" + member.UserId, _localizer["User not found"]);
+ continue;
+ }
+
+ project.Members.Add(new ProjectMember() {
+ Project = project,
+ User = user,
+ Role = member.Role
+ });
+ }
+
+ if (errors.Any()) return KnownProblem(_localizer["Invalid form"], _localizer["One or more fields is invalid"], errors);
+
+ _database.Projects.Add(project);
+ await _database.SaveChangesAsync(cancellationToken);
+ return Ok();
+ }
+}
+
+public class CreateProjectResponse
+{ }
+
+public class CreateProjectPayload
+{
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public DateTime Start { get; set; }
+ public DateTime Stop { get; set; }
+ public List<Guid> CustomerIds { get; set; }
+ public List<ProjectMember> Members { get; set; }
+
+ public class ProjectMember
+ {
+ public Guid UserId { get; set; }
+ public ProjectRole Role { get; set; }
+ }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs b/code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs
new file mode 100644
index 0000000..d60f974
--- /dev/null
+++ b/code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs
@@ -0,0 +1,43 @@
+using MR.AspNetCore.Pagination;
+
+namespace IOL.GreatOffice.Api.Endpoints.V1.Projects;
+
+public class GetProjectsRoute : RouteBaseAsync.WithRequest<GetProjectsQueryParameters>.WithActionResult<KeysetPaginationResult<GetProjectsResponseDto>>
+{
+ private readonly MainAppDatabase _database;
+ private readonly PaginationService _pagination;
+
+ public GetProjectsRoute(MainAppDatabase database, PaginationService pagination) {
+ _database = database;
+ _pagination = pagination;
+ }
+
+ [HttpGet("~/v{version:apiVersion}/projects")]
+ public override async Task<ActionResult<KeysetPaginationResult<GetProjectsResponseDto>>> HandleAsync(GetProjectsQueryParameters request, CancellationToken cancellationToken = default) {
+ var result = await _pagination.KeysetPaginateAsync(
+ _database.Projects.ForTenant(LoggedInUser),
+ b => b.Descending(x => x.CreatedAt),
+ async id => await _database.Projects.FindAsync(id),
+ query => query.Select(p => new GetProjectsResponseDto() {
+ Id = p.Id,
+ Name = p.Name
+ })
+ );
+ return Ok(result);
+ }
+}
+
+public class GetProjectsResponseDto
+{
+ public Guid Id { get; set; }
+ public string Name { get; set; }
+}
+
+public class GetProjectsQueryParameters
+{
+ [FromQuery]
+ public int Page { get; set; }
+
+ [FromQuery]
+ public int Size { get; set; }
+} \ No newline at end of file
diff --git a/code/api/src/Endpoints/V1/Projects/_calls.http b/code/api/src/Endpoints/V1/Projects/_calls.http
new file mode 100644
index 0000000..af0eba6
--- /dev/null
+++ b/code/api/src/Endpoints/V1/Projects/_calls.http
@@ -0,0 +1,12 @@
+### Create Project
+GET https://localhost:5001/v1/projects/create
+Accept: application/json
+Content-Type: application/json
+Cookie: ""
+
+{
+ "": ""
+}
+
+### Get Projects
+POST http://localhost
diff --git a/code/api/src/Endpoints/V1/RouteBaseAsync.cs b/code/api/src/Endpoints/V1/RouteBaseAsync.cs
index a75e9da..33b6f5f 100644
--- a/code/api/src/Endpoints/V1/RouteBaseAsync.cs
+++ b/code/api/src/Endpoints/V1/RouteBaseAsync.cs
@@ -3,9 +3,9 @@ namespace IOL.GreatOffice.Api.Endpoints.V1;
/// <summary>
/// A base class for an endpoint that accepts parameters.
/// </summary>
-public static class RouteBaseAsync
+public class RouteBaseAsync
{
- public static class WithRequest<TRequest>
+ public class WithRequest<TRequest>
{
public abstract class WithResult<TResponse> : V1_EndpointBase
{