using System.Threading.Tasks; using System; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Logging; using Sockeye.Models; using Sockeye.Api.ControllerHelpers; using Sockeye.Biz; using Microsoft.EntityFrameworkCore; using System.Linq; using System.Collections.Generic; using Sockeye.Util; using System.ComponentModel.DataAnnotations; using Newtonsoft.Json.Linq; namespace Sockeye.Api.Controllers { [ApiController] [ApiVersion("8.0")] [Route("api/v{version:apiVersion}/schedule")] [Produces("application/json")] [Authorize] public class ScheduleController : ControllerBase { private const string WHITE_HEXA = "#FFFFFFFF"; private const string BLACK_HEXA = "#000000FF"; private const string GRAY_NEUTRAL_HEXA = "#CACACAFF"; private readonly AyContext ct; private readonly ILogger log; private readonly ApiServerState serverState; /// /// ctor /// /// /// /// public ScheduleController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState) { ct = dbcontext; log = logger; serverState = apiServerState; } /// /// Get service management schedule for parameters specified /// time zone UTC offset in minutes is required to be passed in /// timestamps returned are in Unix Epoch milliseconds converted for local time display /// /// Service schedule parameters /// From route path /// [HttpPost("svc")] public async Task PostServiceSchedule([FromBody] ServiceScheduleParams p, ApiVersion apiVersion) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); List r = new List(); //Note: query will return records that fall within viewed range even if they start or end outside of it //However in month view (only, rest are as is) we can see up to 6 days before or after the month so in the interest of filling those voids: //Adjust query dates to encompass actual potential view range DateTime ViewStart = p.Start; DateTime ViewEnd = p.End; //this covers the largest possible window that could display due to nearly a week of the last or next month showing if (p.View == ScheduleView.Month) { ViewStart = p.Start.AddDays(-6); ViewEnd = p.End.AddDays(6); } //Tags to Users List Users = null; if (p.Tags.Count == 0) Users = await ct.User.AsNoTracking() .Where(x => x.Active == true && (x.UserType == UserType.ServiceContractor || x.UserType == UserType.Service)) .OrderBy(x => x.Name) .Select(x => new NameIdItem { Name = x.Name, Id = x.Id }) .ToListAsync(); else { Users = new List(); //add users that match any of the tags, to match they must have at least one of the tags //iterate available users var availableUsers = await ct.User.AsNoTracking().Where(x => x.Active == true && (x.UserType == UserType.ServiceContractor || x.UserType == UserType.Service)).OrderBy(x => x.Name).Select(x => new { x.Name, x.Id, x.Tags }).ToListAsync(); //if user has any of the tags in the list then include them foreach (var u in availableUsers) { //any of the inclusive tags in contact tags? if (p.Tags.Intersect(u.Tags).Any()) Users.Add(new NameIdItem { Name = u.Name, Id = u.Id }); } } List userIdList = Users.Select(x => x.Id as long?).ToList(); userIdList.Add(null); var HasUnAssigned = r.Any(x => x.UserId == 0); return Ok(ApiOkResponse.Response(new { items = r, users = Users, hasUnassigned = HasUnAssigned })); } public class ServiceScheduleParams { [Required] public ScheduleView View { get; set; } [Required] public DateTime Start { get; set; } [Required] public DateTime End { get; set; } [Required] public List Tags { get; set; } [Required] public bool Dark { get; set; }//indicate if Client is set to dark mode or not, used for colorless types to display as black or white } //############################################################### //USER - svc-schedule-user //############################################################### // /// // /// Get User schedule for parameters specified // /// This is called when drilling down into specific user from service schedule form and is not the personal schedule // /// time zone UTC offset in minutes is required to be passed in // /// timestamps returned are in Unix Epoch milliseconds converted for local time display // /// // /// User schedule parameters // /// From route path // /// // [HttpPost("user")] // public async Task PostUserSchedule([FromBody] PersonalScheduleParams p, ApiVersion apiVersion) // { // if (!serverState.IsOpen) // return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); // if (!ModelState.IsValid) // return BadRequest(new ApiErrorResponse(ModelState)); // List r = new List(); // //Note: query will return records that fall within viewed range even if they start or end outside of it // //However in month view (only, rest are as is) we can see up to 6 days before or after the month so in the interest of filling those voids: // //Adjust query dates to encompass actual potential view range // DateTime ViewStart = p.Start; // DateTime ViewEnd = p.End; // //this covers the largest possible window that could display due to nearly a week of the last or next month showing // if (p.View == ScheduleView.Month) // { // ViewStart = p.Start.AddDays(-6); // ViewEnd = p.End.AddDays(6); // } // long? actualUserId = p.UserId == 0 ? null : p.UserId; // return Ok(ApiOkResponse.Response(r)); // } //############################################################### //PERSONAL //############################################################### /// /// Get personal schedule for parameters specified /// time zone UTC offset in minutes is required to be passed in /// timestamps returned are in Unix Epoch milliseconds converted for local time display /// /// Personal schedule parameters /// From route path /// [HttpPost("personal")] public async Task PostPersonalSchedule([FromBody] PersonalScheduleParams p, ApiVersion apiVersion) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); List r = new List(); var UserId = UserIdFromContext.Id(HttpContext.Items); var UType = UserTypeFromContext.Type(HttpContext.Items); //Note: query will return records that fall within viewed range even if they start or end outside of it //However in month view (only, rest are as is) we can see up to 6 days before or after the month so in the interest of filling those voids: //Adjust query dates to encompass actual potential view range DateTime ViewStart = p.Start; DateTime ViewEnd = p.End; //this covers the largest possible window that could display due to nearly a week of the last or next month showing if (p.View == ScheduleView.Month) { ViewStart = p.Start.AddDays(-6); ViewEnd = p.End.AddDays(6); } //REMINDERS if (p.Reminders) { r.AddRange(await ct.Reminder.Where(x => x.UserId == UserId && ViewStart <= x.StopDate && x.StartDate <= ViewEnd).Select(x => MakeReminderSchedItem(x, p)).ToListAsync()); } //REVIEWS if (p.Reviews) { r.AddRange(await ct.Review.Where(x => x.UserId == UserId && ViewStart <= x.ReviewDate && x.ReviewDate <= ViewEnd).Select(x => MakeReviewSchedItem(x, p)).ToListAsync()); } return Ok(ApiOkResponse.Response(r)); } /// /// Adjust a schedule item's start / end timestamp /// /// Adjustment parameters parameters /// From route path /// Error or OK response [HttpPost("adjust")] public async Task AdjustSchedule([FromBody] ScheduleItemAdjustParams ad, ApiVersion apiVersion) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); switch (ad.Type) { case SockType.Reminder: { ReminderBiz biz = ReminderBiz.GetBiz(ct, HttpContext); var o = await biz.PutNewScheduleTimeAsync(ad); if (o == false) { if (biz.Errors.Exists(z => z.Code == ApiErrorCode.CONCURRENCY_CONFLICT)) return StatusCode(409, new ApiErrorResponse(biz.Errors)); else return BadRequest(new ApiErrorResponse(biz.Errors)); } } break; case SockType.Review: { ReviewBiz biz = ReviewBiz.GetBiz(ct, HttpContext); var o = await biz.PutNewScheduleTimeAsync(ad); if (o == false) { if (biz.Errors.Exists(z => z.Code == ApiErrorCode.CONCURRENCY_CONFLICT)) return StatusCode(409, new ApiErrorResponse(biz.Errors)); else return BadRequest(new ApiErrorResponse(biz.Errors)); } } break; default: return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, "Type", "Type not supported for adjustment")); } //a-ok response return Ok(ApiOkResponse.Response(true)); } //#### UTILITY METHODS ############## private static PersonalScheduleListItem MakeReminderSchedItem(Reminder v, PersonalScheduleParams p) { var s = new PersonalScheduleListItem(); s.Id = v.Id; s.Color = v.Color; s.TextColor = TextColor(v.Color); s.Start = (DateTime)v.StartDate; s.End = (DateTime)v.StopDate; s.Type = SockType.Reminder; s.Name = v.Name; s.Editable = true;//personal reminders are always editable return s; } private static PersonalScheduleListItem MakeReviewSchedItem(Review v, PersonalScheduleParams p) { var s = new PersonalScheduleListItem(); s.Id = v.Id; s.Color = p.Dark ? WHITE_HEXA : BLACK_HEXA; s.TextColor = p.Dark ? "black" : "white"; s.Start = (DateTime)v.ReviewDate; s.End = (DateTime)v.ReviewDate.AddMinutes(30);//just something to show in schedule as not supporting all day or unscheduled type stuff s.Type = SockType.Review; s.Name = v.Name; s.Editable = v.CompletedDate == null;//not completed yet so can still be changed return s; } private static string TextColor(string hexcolor) { //Note: we use HEXA format which is 8 hex digits //this here works even though it's considering as 6 digits because in hexA the last two //digits are the opacity which this can ignore if (string.IsNullOrWhiteSpace(hexcolor) || hexcolor.Length < 6) return GRAY_NEUTRAL_HEXA;//gray neutral hexcolor = hexcolor.Replace("#", ""); var r = StringUtil.HexToInt(hexcolor.Substring(0, 2)); var g = StringUtil.HexToInt(hexcolor.Substring(2, 2)); var b = StringUtil.HexToInt(hexcolor.Substring(4, 2)); var yiq = (r * 299 + g * 587 + b * 114) / 1000; //return yiq >= 128 ? WHITE_HEXA : BLACK_HEXA; return yiq >= 128 ? "black" : "white";//<---NOTE: this MUST be a named color due to how the style is applied at client } public enum ScheduleView : int { Day = 1, Week = 2, Month = 3, Day4 = 4, Category = 5 } public class PersonalScheduleParams { [Required] public ScheduleView View { get; set; } [Required] public DateTime Start { get; set; } [Required] public DateTime End { get; set; } [Required] public bool Reviews { get; set; } [Required] public bool Reminders { get; set; } [Required] public bool Dark { get; set; }//indicate if Client is set to dark mode or not, used for colorless types to display as black or white [Required] public long UserId { get; set; }//required due to dual use from home-schedule and svc-schedule-user if it's a 0 zero then it's actually meant to be null not assigned userid } public class PersonalScheduleListItem { //Never be null dates in here even though source records might be have null dates because they are not queried for and //can't be displayed on a calendar anyway //user can simply filter a data table by null dates to see them //we shouldn't have allowed null dates in the first place in v7 but here we are :) public DateTime Start { get; set; } public DateTime End { get; set; } public string Name { get; set; } public string Color { get; set; } public string TextColor { get; set; } public SockType Type { get; set; } public long Id { get; set; } public bool Editable { get; set; } } public class ServiceScheduleListItem { //Never be null dates in here even though source records might be have null dates because they are not queried for and //can't be displayed on a calendar anyway //user can simply filter a data table by null dates to see them //we shouldn't have allowed null dates in the first place in v7 but here we are :) public DateTime Start { get; set; } public DateTime End { get; set; } public string Name { get; set; } public string Color { get; set; } public string TextColor { get; set; } public SockType Type { get; set; } public long Id { get; set; } public bool Editable { get; set; } public long UserId { get; set; } } //------------ }//eoc }//eons