namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; public class EntryQueryRoute : RouteBaseSync.WithRequest.WithActionResult { private readonly ILogger _logger; private readonly AppDbContext _context; public EntryQueryRoute(ILogger logger, AppDbContext context) { _logger = logger; _context = context; } /// /// Get a list of entries based on a given query. /// /// /// /// [ApiVersion(ApiSpecV1.VERSION_STRING)] [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)] [HttpPost("~/v{version:apiVersion}/entries/query")] [ProducesResponseType(204)] [ProducesResponseType(400, Type = typeof(ErrorResult))] [ProducesResponseType(200, Type = typeof(EntryQueryResponse))] public override ActionResult 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; // 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(DateTime.UtcNow)) { offsetInHours++; } _logger.LogInformation("Request time zone (" + tz.Id + ") offset is: " + offsetInHours + " hours"); var requestDateTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz); _logger.LogInformation("Querying data with date time: " + requestDateTime.ToString("u")); var skipCount = 0; if (entryQuery.Page > 1) { skipCount = entryQuery.PageSize * entryQuery.Page; } 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); switch (entryQuery.Duration) { case TimeEntryQueryDuration.TODAY: var baseTodaysEntries = baseQuery .Where(c => DateTime.Compare(c.Start.AddHours(offsetInHours).Date, DateTime.UtcNow.Date) == 0); var baseTodaysEntriesCount = baseTodaysEntries.Count(); if (baseTodaysEntriesCount == 0) { return NoContent(); } result.TotalSize = baseTodaysEntriesCount; result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseTodaysEntriesCount / 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 = DateTime.UtcNow.StartOfWeek(DayOfWeek.Monday); var baseEntriesThisWeek = baseQuery .Where(c => c.Start.AddHours(offsetInHours).Date >= lastMonday.Date && c.Start.AddHours(offsetInHours).Date <= DateTime.UtcNow.Date); var baseEntriesThisWeekCount = baseEntriesThisWeek.Count(); if (baseEntriesThisWeekCount == 0) { return NoContent(); } result.TotalSize = baseEntriesThisWeekCount; result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisWeekCount / 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 == DateTime.UtcNow.Month && c.Start.AddHours(offsetInHours).Year == DateTime.UtcNow.Year); var baseEntriesThisMonthCount = baseEntriesThisMonth.Count(); if (baseEntriesThisMonthCount == 0) { return NoContent(); } result.TotalSize = baseEntriesThisMonthCount; result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisMonthCount / 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 == DateTime.UtcNow.Year); var baseEntriesThisYearCount = baseEntriesThisYear.Count(); if (baseEntriesThisYearCount == 0) { return NoContent(); } result.TotalSize = baseEntriesThisYearCount; result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisYearCount / 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(); if (baseEntriesOnThisDateCount == 0) { return NoContent(); } result.TotalSize = baseEntriesOnThisDateCount; result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesOnThisDateCount / 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 ErrorResult("Invalid query", "From date cannot be empty")); } var fromDate = DateTime.SpecifyKind(entryQuery.DateRange.From, DateTimeKind.Utc); if (entryQuery.DateRange.To == default) { return BadRequest(new ErrorResult("Invalid query", "To date cannot be empty")); } var toDate = DateTime.SpecifyKind(entryQuery.DateRange.To, DateTimeKind.Utc); if (DateTime.Compare(fromDate, toDate) > 0) { return BadRequest(new ErrorResult("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 baseDateRangeEntriesCount = baseDateRangeEntries.Count(); if (baseDateRangeEntriesCount == 0) { return NoContent(); } result.TotalSize = baseDateRangeEntriesCount; result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseDateRangeEntriesCount / 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"); } if (result.Results.Any() && result.Page == 0) { result.Page = 1; result.TotalPageCount = 1; } return Ok(result); } }