using System.Threading.Tasks; using System; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Logging; using AyaNova.Models; using AyaNova.Api.ControllerHelpers; using AyaNova.Biz; using Microsoft.EntityFrameworkCore; using System.Linq; using System.Collections.Generic; using AyaNova.Util; using System.ComponentModel.DataAnnotations; namespace AyaNova.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); //WORKORDERS { //Note: query for *overlapping* ranges, not *contained* entirely in view range r.AddRange(await ct.ViewScheduleWorkOrder.Where(x => userIdList.Contains(x.SchedUserId) && ViewStart <= x.StopDate && x.StartDate <= ViewEnd) .Select(x => MakeServiceWOSchedItem(x, p)) .ToListAsync()); } 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 ScheduleWorkOrderColorSource WisuColorSource { get; set; } // [Required] // public List Users { get; set; }//user id's to display and in order to display [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 /// 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; //WORKORDERS { //Note: query for *overlapping* ranges, not *contained* entirely in view range r.AddRange(await ct.ViewScheduleWorkOrder.Where(x => x.SchedUserId == actualUserId && ViewStart <= x.StopDate && x.StartDate <= ViewEnd) .Select(x => MakePersonalWOSchedItem(x, p)) .ToListAsync()); } 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); } //WORKORDERS if (p.Wisu && (UType == UserType.Service || UType == UserType.ServiceContractor)) { //Note: query for *overlapping* ranges, not *contained* entirely in view range r.AddRange(await ct.ViewScheduleWorkOrder.Where(x => x.SchedUserId == UserId && ViewStart <= x.StopDate && x.StartDate <= ViewEnd) .Select(x => MakePersonalWOSchedItem(x, p)) .ToListAsync()); } //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 AyaType.WorkOrderItemScheduledUser: { WorkOrderBiz biz = WorkOrderBiz.GetBiz(ct, HttpContext); if (!Authorized.HasModifyRole(HttpContext.Items, AyaType.WorkOrderItemScheduledUser) || biz.UserIsRestrictedType) return StatusCode(403, new ApiNotAuthorizedResponse()); //svc-schedule-user uses 0 to mean unassigned user but here it's null //and it can't be a required parameter and provide null for some reason so zero is the token indicator if (ad.UserId == 0) ad.UserId = null; var o = await biz.ScheduledUserPutNewScheduleTimeAsync(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 AyaType.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 AyaType.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 ServiceScheduleListItem MakeServiceWOSchedItem(ViewScheduleWorkOrder v, ServiceScheduleParams p) { var s = new ServiceScheduleListItem(); s.Id = v.WoItemSchedUserId; s.Color = ColorFromWOItem(v, p.WisuColorSource); s.TextColor = TextColor(s.Color); s.Start = (DateTime)v.StartDate; s.End = (DateTime)v.StopDate; s.Type = AyaType.WorkOrderItemScheduledUser; s.Name = NameFromWOItem(v); s.Editable = v.WorkOrderStatusCompleted != true && v.WorkOrderStatusLocked != true;//could be null as well which we'll consider open as it's no status set s.UserId = v.SchedUserId ?? 0;//0 signifies to client schedule that it's an unassigned woitem return s; } private static PersonalScheduleListItem MakePersonalWOSchedItem(ViewScheduleWorkOrder v, PersonalScheduleParams p) { var s = new PersonalScheduleListItem(); s.Id = v.WoItemSchedUserId; s.Color = ColorFromWOItem(v, p.WisuColorSource); s.TextColor = TextColor(s.Color); s.Start = (DateTime)v.StartDate; s.End = (DateTime)v.StopDate; s.Type = AyaType.WorkOrderItemScheduledUser; s.Name = NameFromWOItem(v); s.Editable = v.WorkOrderStatusCompleted != true && v.WorkOrderStatusLocked != true;//could be null as well which we'll consider open as it's no status set return s; } 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 = AyaType.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 = AyaType.Review; s.Name = v.Name; s.Editable = v.CompletedDate == null;//not completed yet so can still be changed return s; } private static string ColorFromWOItem(ViewScheduleWorkOrder v, ScheduleWorkOrderColorSource src) { switch (src) { case ScheduleWorkOrderColorSource.None: return GRAY_NEUTRAL_HEXA; case ScheduleWorkOrderColorSource.WorkOrderStatus: return string.IsNullOrWhiteSpace(v.WorkOrderStatusColor) ? GRAY_NEUTRAL_HEXA : v.WorkOrderStatusColor; case ScheduleWorkOrderColorSource.WorkOrderItemStatus: return string.IsNullOrWhiteSpace(v.WorkOrderItemStatusColor) ? GRAY_NEUTRAL_HEXA : v.WorkOrderItemStatusColor; case ScheduleWorkOrderColorSource.WorkOrderItemPriority: return string.IsNullOrWhiteSpace(v.WorkOrderItemPriorityColor) ? GRAY_NEUTRAL_HEXA : v.WorkOrderItemPriorityColor; } return GRAY_NEUTRAL_HEXA; } 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 } private static string NameFromWOItem(ViewScheduleWorkOrder v) { // Name=[wonumber customername] return v.Serial.ToString() + " " + v.CustomerName; } public enum ScheduleWorkOrderColorSource : int { None = 0, WorkOrderStatus = 2, WorkOrderItemStatus = 3, WorkOrderItemPriority = 4 } 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 ScheduleWorkOrderColorSource WisuColorSource { get; set; } [Required] public bool Wisu { 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 AyaType 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 AyaType Type { get; set; } public long Id { get; set; } public bool Editable { get; set; } public long UserId { get; set; } } //------------ // deprecated as not used but by tag instead // /// // /// Get active Scheduleable user list // /// // /// List of all scheduleable users // [HttpGet("scheduleable-user-list")] // public async Task GetScheduleableUserList() // { // if (!serverState.IsOpen) // return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); // if (!Authorized.HasReadFullRole(HttpContext.Items, AyaType.WorkOrder))//WorkOrder right applies to all svc-schedule ops // return StatusCode(403, new ApiNotAuthorizedResponse()); // var o = 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 }).ToListAsync(); // return Ok(ApiOkResponse.Response(o)); // } }//eoc }//eons