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; 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 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.ViewSchedulePersonalWorkOrder.Where(x => x.SchedUserId == UserId && ViewStart <= x.StopDate && x.StartDate <= ViewEnd) .Select(x => MakeWOSchedItem(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 PostPersonalSchedule([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()); 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 PersonalScheduleListItem MakeWOSchedItem(ViewSchedulePersonalWorkOrder v, PersonalScheduleParams p) { var s = new PersonalScheduleListItem(); s.Id = v.WoItemSchedUserId; s.Color = ColorFromWOItem(v, p); s.TextColor = TextColor(s.Color); s.Start = (DateTime)v.StartDate; s.End = (DateTime)v.StopDate; s.Type = AyaType.WorkOrderItemScheduledUser; s.Name = NameFromWOItem(v, p); 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(ViewSchedulePersonalWorkOrder v, PersonalScheduleParams p) { switch (p.WisuColorSource) { case PersonalScheduleWorkOrderColorSource.WorkOrderStatus: return v.WorkOrderStatusColor; case PersonalScheduleWorkOrderColorSource.WorkOrderItemStatus: return v.WorkOrderItemStatusColor; case PersonalScheduleWorkOrderColorSource.WorkOrderItemPriority: return v.WorkOrderItemPriorityColor; } return string.Empty; } 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(ViewSchedulePersonalWorkOrder v, PersonalScheduleParams p) { // Name=[wonumber customername] return v.Serial.ToString() + " " + v.CustomerName; } public enum PersonalScheduleWorkOrderColorSource : int { None = 0, WorkOrderStatus = 2, WorkOrderItemStatus = 3, WorkOrderItemPriority = 4 } public enum ScheduleView : int { Day = 1, Week = 2, Month = 3, Day4 = 4 } public class PersonalScheduleParams { public ScheduleView View { get; set; } public DateTime Start { get; set; } public DateTime End { get; set; } public PersonalScheduleWorkOrderColorSource WisuColorSource { get; set; } public bool Wisu { get; set; } public bool Reviews { get; set; } public bool Reminders { get; set; } 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 } 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 bool Timed { get { return true; } } 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; } } //------------ }//eoc }//eons