using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Xamarin.Forms; using InABox.Core; using InABox.Clients; using InABox.Mobile; using Comal.Classes; using InABox.Configuration; using XF.Material.Forms.UI.Dialogs; namespace comal.timesheets { public partial class MainPage : BasePage { #region Fields bool bUpdatingTimesheet = false; bool midnightTimerOn = false; DateTime oneSecondBeforeMidnight = DateTime.Today.AddSeconds(864399); bool clockedOffInLast5Seconds = false; bool bRecentlyUpdatedTiles = false; string matchedDeviceName = ""; int notCount = 1; private TimeSheet _currenttimesheet = null; #endregion #region Constructor public MainPage() : base() { MobileLogging.Log("MainPage.Create"); InitializeComponent(); MobileLogging.Log("MainPage.InitNotificationCentre"); InitNotificationCentre(); MobileLogging.Log("MainPage.Subscribe"); MessagingCenter.Subscribe(this, App.MessageOnResume, (o) => { if (!App.GPS.RecentlyLocated) App.GPS.GetLocation(); RefreshScreen(); } ); MobileLogging.Log("MainPage.NotifyChanges"); NotifyChanges(); MobileLogging.Log("MainPage.Create Done"); } private void NotifyChanges() { Task.Run(() => { string changes = NotifyMobileChanges.Notifiy(); if (!string.IsNullOrWhiteSpace(changes)) Device.BeginInvokeOnMainThread(() => { DisplayAlert("Latest changes", changes, "OK"); }); }); } #endregion #region OnAppearing and Display private SelectionPage _selectionpage = null; protected override void OnAppearing() { MobileLogging.Log("MainPage.Appearing"); if (_selectionpage == null) { BackButtonEnabled = App.IsSharedDevice; EnableModules(); StartMonitoringNotifications(); StartPromptingForUpdates(); App.Data.GPSLocationUpdated += OnGPSLocationUpdated; App.Data.BluetoothScanFinished += OnBluetothScanFinished; //LoadHRToDos(); to be uncommented when ready for roll out CheckTimeSheet(); } _selectionpage = null; MobileLogging.Log("MainPage.refreshScreen"); RefreshScreen(); MobileLogging.Log("MainPage.Base.OnAppearing"); base.OnAppearing(); MobileLogging.Log("MainPage.OnAppearing Done"); } private IMaterialModalPage _snackbar = null; protected override void OnDisappearing() { base.OnDisappearing(); //App.Data.GPSLocationUpdated -= OnGPSLocationUpdated; //App.Data.BluetoothScanFinished -= OnBluetothScanFinished; _canceltasks.Cancel(); } private void OnBluetothScanFinished(BluetoothEventArgs args) { Dispatcher.BeginInvokeOnMainThread(RefreshScreen); } private void OnGPSLocationUpdated(GPSEventArgs args) { Dispatcher.BeginInvokeOnMainThread(RefreshScreen); } private void EnableModules() { void SetupClockOnButton(bool disable) { LogoRow.Height = disable ? 150 : 0; ClockOnRow.Height = disable ? 0 : new GridLength(5.5, GridUnitType.Star); JobRow.Height = disable ? 0 : 55; TaskRow.Height = disable ? 0 : 55; } SetupClockOnButton(InABox.Core.Security.IsAllowed()); Assignments.IsVisible = InABox.Core.Security.CanView(); Deliveries.IsVisible = InABox.Core.Security.CanView(); Forms.IsVisible = InABox.Core.Security.CanView(); Equipment.IsVisible = InABox.Core.Security.CanView(); InOut.IsVisible = InABox.Core.Security.IsAllowed(); Manufacturing.IsVisible = InABox.Core.Security.CanView(); Products.IsVisible = InABox.Core.Security.CanView(); PurchaseOrders.IsVisible = InABox.Core.Security.CanView(); StoreRequis.IsVisible = InABox.Core.Security.CanView(); MyTasks.IsVisible = InABox.Core.Security.CanView(); Warehousing.IsVisible = InABox.Core.Security.CanView(); } protected override void UpdateTransportStatus() { base.UpdateTransportStatus(); RefreshScreen(); } private CancellationTokenSource _canceltasks = new CancellationTokenSource(); private int _notificationcount = 0; private void StartMonitoringNotifications() { //notifications are allowed to upload once every 30 seconds // This will eventually be replaced with websocket pushes // but we're not ready for that yet :-( var token = _canceltasks.Token; Task.Run( () => { while (!_canceltasks.Token.IsCancellationRequested) { App.Data.Notifications.Refresh(true); if (App.Data.Notifications.Items.Count != _notificationcount) { Dispatcher.BeginInvokeOnMainThread(() => { _notificationcount = App.Data.Notifications.Items.Count; Notifications.Indicator = _notificationcount.ToString(); }); } Task.Delay(TimeSpan.FromSeconds(30), token) .Wait(token); } }, token ); } private void StartPromptingForUpdates() { var token = _canceltasks.Token; Task.Run( () => { while (!token.IsCancellationRequested) { bool isLatest = MobileUtils.AppVersion.IsUsingLatestVersion().Result; if (!isLatest) { string latestVersionNumber = MobileUtils.AppVersion.GetLatestVersionNumber().Result; string chosenOption = DisplayActionSheet( $"Version {latestVersionNumber} Available. Update now?", "You will be reminded again in 10 minutes.", null, "Yes", "No").Result; if (String.Equals(chosenOption,"Yes")) Dispatcher.BeginInvokeOnMainThread(() => { MobileUtils.AppVersion.OpenAppInStore(); }); } Task.Delay(TimeSpan.FromMinutes(10), token) .Wait(token); } } , token ); } private void RefreshScreen() { bool GateReady = CheckLocation(); var location = GetAddress(); Title = App.Data.Me != null ? App.Data.Me.Name : "(Not Logged In)"; ClockOnButton.IsEnabled = App.Data.IsConnected() && GateReady; ClockOnOffLabel.Text = GateReady ? _currenttimesheet == null ? "CLOCK ON" : "CLOCK OFF" : "PLEASE WAIT"; CurrentLocation.Text = location.ToUpper().Contains("ERROR") ? "Unknown Address" : location; ClockOnButton.BackgroundColor = App.Data.IsConnected() && GateReady ? _currenttimesheet == null ? Color.FromHex("#e6e6fa") : Color.FromHex("#15C7C1") : Color.Silver; ClockOnButton.BorderColor = App.Data.IsConnected() && GateReady ? _currenttimesheet == null ? Color.FromHex("#96969a") : Color.FromHex("#059791") : Color.Gray; addNoteBtn.IsEnabled = App.Data.IsConnected() && _currenttimesheet != null; jobBtn.IsEnabled = App.Data.IsConnected() && _currenttimesheet != null; taskBtn.IsEnabled = App.Data.IsConnected() && _currenttimesheet != null; jobBtn.Text = ((_currenttimesheet?.JobID ?? Guid.Empty) != Guid.Empty) ? $"{_currenttimesheet.JobLink.JobNumber}: {_currenttimesheet.JobLink.Name}" : "No Job Selected"; Assignments.IsEnabled = App.Data.IsConnected(); Deliveries.IsEnabled = App.Data.IsConnected(); Forms.IsEnabled = App.Data.IsConnected(); //Equipment.IsEnabled = App.Data.IsConnected(); InOut.IsEnabled = App.Data.IsConnected(); Manufacturing.IsEnabled = App.Data.IsConnected(); MyHR.IsEnabled = App.Data.IsConnected(); Notifications.IsEnabled = App.Data.IsConnected(); Products.IsEnabled = App.Data.IsConnected(); PurchaseOrders.IsEnabled = App.Data.IsConnected(); Site.IsEnabled = App.Data.IsConnected(); StoreRequis.IsEnabled = App.Data.IsConnected(); MyTasks.IsEnabled = App.Data.IsConnected(); Warehousing.IsEnabled = App.Data.IsConnected(); } #endregion #region Clock on/off private void CheckTimeSheet() { if (App.Data.IsConnected()) { var client = new Client(); var filter = new Filter(x => x.EmployeeLink.ID).IsEqualTo(App.Data.Me.ID) .And(x => x.Date).IsEqualTo(DateTime.Today) .And(x => x.Finish).IsEqualTo(TimeSpan.Zero); var table = client.Query(filter); _currenttimesheet = table.Rows.FirstOrDefault()?.ToObject(); } var token = _canceltasks.Token; Task.Run( () => { while (!token.IsCancellationRequested) { if (_currenttimesheet != null) { if (_currenttimesheet.Date != DateTime.Today) { InABox.Core.Location here = new InABox.Core.Location() { Latitude = App.GPS.Latitude, Longitude = App.GPS.Longitude, Timestamp = DateTime.Now }; _currenttimesheet.FinishLocation = here; _currenttimesheet.Finish = TimeSpan.FromDays(1).Subtract(TimeSpan.FromSeconds(1)); new Client().Save(_currenttimesheet, "Closing Timesheet at Modnight on Mobile Device"); CreateTimeSheet(_currenttimesheet.JobLink.ID, _currenttimesheet.JobLink.JobNumber, _currenttimesheet.JobLink.Name, here, here.Address, "Creating Timeheet at Midnight on Mobile Device"); } } Task.Delay(TimeSpan.FromMinutes(1), token).Wait(token); } }, token ); } private DateTime _debounce = DateTime.MinValue; async void ClockOnOff_Clicked(object sender, System.EventArgs e) { if (_debounce > DateTime.Now.Subtract(TimeSpan.FromMilliseconds(500))) return; _debounce = DateTime.MaxValue; string chosenOption = _currenttimesheet != null ? await DisplayActionSheet("Clock off?", "Cancel", null, "Continue", "Cancel") : "Continue"; if (!String.Equals(chosenOption, "Continue")) { _debounce = DateTime.Now; return; } try { using (await MaterialDialog.Instance.LoadingDialogAsync(message: $"Clocking {(_currenttimesheet == null ? "On" : "Off")}")) { InABox.Core.Location here = new InABox.Core.Location() { Latitude = App.GPS.Latitude, Longitude = App.GPS.Longitude, Timestamp = DateTime.Now }; if (_currenttimesheet != null) { FinishTimeSheet(here); } else { if (!InABox.Core.Security.IsAllowed()) { var tracker = App.Data.BluetoothGates.FirstOrDefault(x => App.Bluetooth.Devices.Contains(x.DeviceID)); if (tracker != null) CreateTimeSheet(tracker.JobID, tracker.JobNumber, tracker.JobName, here, App.GPS.Address, "Clocking On"); } else { if ((!App.GPS.Latitude.Equals(0.0F)) && (!App.GPS.Longitude.Equals(0.0F))) { var job = await ChooseNearbyJob(here); CreateTimeSheet(job?.ID ?? Guid.Empty, job?.JobNumber ?? "", job?.Name ?? "" , here, App.GPS.Address, "Clocking On"); } } } Dispatcher.BeginInvokeOnMainThread(RefreshScreen); } } catch (Exception ex) { InABox.Mobile.MobileLogging.Log(ex); } _debounce = DateTime.Now; } private void FinishTimeSheet(InABox.Core.Location here) { if ((_currenttimesheet == null) || (_currenttimesheet.ID == Guid.Empty)) { _currenttimesheet = null; return; } try { if (ZeroLengthTimesheet()) new Client().Delete(_currenttimesheet, "Deleted due to zero duration timesheet"); else { _currenttimesheet.Finish = new TimeSpan(DateTime.Now.TimeOfDay.Hours, DateTime.Now.TimeOfDay.Minutes, 0); _currenttimesheet.FinishLocation = here; bUpdatingTimesheet = true; new Client().Save(_currenttimesheet, "Clocking Off"); } _currenttimesheet = null; } catch (Exception ex) { InABox.Mobile.MobileLogging.Log(ex); } } #endregion #region Utilities private void InitNotificationCentre() { // LocalNotificationCenter.Current.NotificationActionTapped += (Plugin.LocalNotification.EventArgs.NotificationActionEventArgs e) => // { // if (MainPageUtils.DetermineCorrectPage(e) != null) // { // Device.BeginInvokeOnMainThread(() => // { // Navigation.PushAsync(MainPageUtils.DetermineCorrectPage(e)); // }); // } // }; // // MainPageUtils.OnMainPageNotificationsChanged += RefreshOnNotificationsChange; } private void CheckNotificationsPushed(CoreTable table) { // try // { // if (!Application.Current.Properties.ContainsKey("LastPushedNotifications")) // { // Application.Current.Properties.Add("LastPushedNotifications", DateTime.Now); // } // DateTime lastPushed = DateTime.Parse(Application.Current.Properties["LastPushedNotifications"].ToString()); // List toNotify = new List(); // foreach (CoreRow row in table.Rows) // { // List list = row.Values; // DateTime created = DateTime.Parse(list[3].ToString()); // if (created > new DateTime(2022, 8, 22)) // prevent spam from buildup of old notifications before this is released // { // if (created > lastPushed) // { // if (list[1] == null) list[1] = ""; // if (list[2] == null) list[2] = ""; // if (list[3] == null) list[3] = DateTime.MinValue; // if (list[4] == null) list[4] = ""; // if (list[5] == null) list[5] = ""; // if (list[6] == null) list[6] = Guid.Empty; // // NotificationShell shell = new NotificationShell // { // ID = Guid.Parse(list[0].ToString()), // Sender = list[1].ToString(), // Title = list[2].ToString(), // Created = DateTime.Parse(list[3].ToString()), // EntityType = list[5].ToString(), // EntityID = Guid.Parse(list[6].ToString()) // }; // toNotify.Add(shell); //add notification to be pushed // } // } // } // if (toNotify.Count > 0) // PushNotificationsAsync(toNotify); // } // catch { } } private async Task PushNotificationsAsync(List shells) { // try // { // int count = 1; // // foreach (NotificationShell shell in shells) // { // var notification = new NotificationRequest // { // BadgeNumber = 1, // Description = shell.Title, // Title = "New PRS Notification: ", // ReturningData = shell.EntityID.ToString() + "$" + shell.EntityType, // NotificationId = count, // }; // count++; // NotificationImage img = new NotificationImage(); // img.ResourceName = "icon16.png"; // notification.Image = img; // // await LocalNotificationCenter.Current.Show(notification); // } // Application.Current.Properties["LastPushedNotifications"] = DateTime.Now; // } // catch { } } private async Task ChooseNearbyJob(InABox.Core.Location here) { var nearbyjobs = App.Data.Jobs.Where(x => here.DistanceTo(x.Location, UnitOfLength.Kilometers) < 1.0F) .ToArray(); if (nearbyjobs.Length > 1) { Dictionary dict = new Dictionary(); foreach (var job in nearbyjobs) dict[job.DisplayName] = job; string chosenOption = await DisplayActionSheet("Choose job site", "Cancel", null, dict.Keys.ToArray()); if (string.IsNullOrEmpty(chosenOption) || chosenOption.Equals("Cancel")) return null; return dict[chosenOption]; } return nearbyjobs.FirstOrDefault(); } void AddNote_Tapped(System.Object sender, System.EventArgs e) { if (_currenttimesheet == null) return; var notepage = new TimeSheetNotePage(_currenttimesheet); Navigation.PushAsync(notepage); } private void TaskBtn_Tapped(object sender, EventArgs e) { _selectionpage = new TaskSelectionPage( (task) => { if (_currenttimesheet != null) { // Not sure hwat to do here... } } ); Navigation.PushAsync(_selectionpage); } // private async void RequestUserInput(Guid taskID) // { // const string addtask = "Change current assignment task"; // const string newassignment = "Start a new assignment with this task"; // string chosenOption = await DisplayActionSheet("Choose an option", "Cancel", null, addtask, newassignment); // switch (chosenOption) // { // case addtask: // MainPageUtils.ChangeAssignmentTask(taskID); // break; // case newassignment: // MainPageUtils.SaveCurrentAssignment("PRS Mobile main screen - saving assignment on task change", true); // MainPageUtils.CreateNewAssignment(Guid.Empty, taskID); // break; // default: // break; // } // } private void JobBtn_Tapped(object sender, EventArgs e) { _selectionpage = new JobSelectionPage( (job) => { if (_currenttimesheet != null) { if (ZeroLengthTimesheet()) { _currenttimesheet.JobLink.ID = job?.ID ?? Guid.Empty; _currenttimesheet.JobLink.JobNumber = job?.JobNumber ?? ""; _currenttimesheet.JobLink.Name = job?.Name ?? ""; new Client().Save(_currenttimesheet,"Updated Job Number via mobile device"); } else { var here = new Location() { Latitude = App.GPS.Latitude, Longitude = App.GPS.Longitude, Address = App.GPS.Address, Timestamp = App.GPS.TimeStamp }; FinishTimeSheet(here); CreateTimeSheet(job?.ID ?? Guid.Empty, job?.JobNumber ?? "", job?.Name ?? "", here, here.Address, "Switched Jobs via Mobile Device"); } } } ); Navigation.PushAsync(_selectionpage); } // private bool CheckTimeSheetAgainstGates(TimeSheet timesheet) // { // DateTime now = DateTime.Now; // // //var timesheet = CurrentTimeSheet(); // // //Can't confirm if there is no timesheet // if (timesheet == null) // return false; // // // Can't confirm if there are no devices // if (App.Bluetooth.Devices.Length == 0) // return false; // // if (App.Data.Gates == null) // return false; // // long tsTicks = timesheet.Date.Add(timesheet.Start).Ticks; // long btTicks = App.Bluetooth.TimeStamp.Ticks; // // if (Math.Abs(tsTicks - btTicks) > new TimeSpan(0, 2, 0).Ticks) // return false; // // CoreRow firstgate = null; // List gates = new List(); // // Scan every located d // foreach (var device in App.Bluetooth.Devices) // { // CoreRow gate = App.Data.Gates?.Rows.FirstOrDefault(r => r.Get(c => c.TrackerLink.DeviceID) == device); // if (gate != null) // { // // if ((gate.Get(x => x.IsJobSite) == true) && (firstgate == null)) // firstgate = gate; // // gates.Add(gate.Get(x => x.Gate)); // } // } // if (gates.Any()) // { // timesheet.Gate = String.Join(", ", gates.OrderBy(x => x)); // if (firstgate != null) // { // timesheet.JobLink.ID = firstgate.Get(x => x.JobLink.ID); // timesheet.JobLink.JobNumber = firstgate.Get(x => x.JobLink.JobNumber); // timesheet.JobLink.Name = firstgate.Get(x => x.JobLink.Name); // } // return true; // //new Client().Save(timesheet, "Confirmed Gate Entry by Bluetooth Tracker", (o, e) => { }); // } // // return false; // // } private bool ZeroLengthTimesheet() { if (_currenttimesheet == null) return true; if (!String.IsNullOrWhiteSpace(_currenttimesheet.Notes)) return false; if (_currenttimesheet.Date.Equals(DateTime.Today)) { var diff = (DateTime.Now.TimeOfDay - _currenttimesheet.Start).TotalSeconds; if (Math.Abs(diff) < 120.0F) return true; } return false; } private async void CreateTimeSheet(Guid jobid, string jobnumber, String jobname, InABox.Core.Location location, String address, String auditmessage) { try { _currenttimesheet = new TimeSheet(); _currenttimesheet.EmployeeLink.ID = App.Data.Me.ID; _currenttimesheet.Date = DateTime.Today; TimeSpan tod = DateTime.Now - DateTime.Today; tod = new TimeSpan(tod.Hours, tod.Minutes, 0); _currenttimesheet.Start = tod; _currenttimesheet.StartLocation = location; _currenttimesheet.JobLink.ID = jobid; _currenttimesheet.JobLink.JobNumber = jobnumber; _currenttimesheet.JobLink.Name = jobname; _currenttimesheet.Address = address; _currenttimesheet.SoftwareVersion = MobileUtils.AppVersion.InstalledVersionNumber + MobileUtils.GetDeviceID(); //if (ClientFactory.IsAllowed()) CheckTimeSheetAgainstGates(timesheet); bUpdatingTimesheet = true; new Client().Save(_currenttimesheet, auditmessage); } catch (Exception ex) { InABox.Mobile.MobileLogging.Log(ex); } } private bool CheckLocation() { // a 15 minute timeout is awfully long for this process. // The App times tick over every 30 seconds, so surely we can // drop this to max 1 or 2 minutes.. // Also, we would expect the GPS / Bluetooth subsystems to take care // of purging stale data if (InABox.Core.Security.IsAllowed()) return App.GPS.TimeStamp > DateTime.Now.Subtract(new TimeSpan(0, 15, 0)); if (App.Bluetooth.TimeStamp < DateTime.Now.Subtract(new TimeSpan(0, 15, 0))) return false; return App.Data.BluetoothGates.Any(x => App.Bluetooth.DetectedBlueToothMACAddresses.Contains(x.DeviceID)); } private String GetAddress() { if (InABox.Core.Security.IsAllowed()) { if (App.GPS.TimeStamp < DateTime.Now.Subtract(new TimeSpan(0, 5, 0))) { App.GPS.GetLocation(true); return "Searching for GPS"; } return App.GPS.Address; } else { var gate = App.Data.BluetoothGates.FirstOrDefault(x => App.Bluetooth.DetectedBlueToothMACAddresses.Contains(x.DeviceID)); return gate?.Gate ?? "Looking for Gate"; } } private async void LoadHRToDos() { try { await Task.Run(() => { Thread.Sleep(10000); if (App.Data.UpdateHRItemsNeedingAttention()) { string message = "You have HR Items needing attention. Open My HR now?"; Device.BeginInvokeOnMainThread(async () => { string chosenOption = await DisplayActionSheet(message, "Cancel", null, "Yes", "No"); switch (chosenOption) { case "Cancel": break; case "No": break; default: break; case "Yes": MyHRHome myHRHome = new MyHRHome(); Navigation.PushAsync(myHRHome); break; } }); } }); } catch { } } #endregion private void Settings_OnTapped(object sender, EventArgs e) { Navigation.PushAsync(new SettingsPage()); } private void Assignments_OnTapped(object sender, EventArgs e) { Navigation.PushAsync(new AssignmentList()); } private void Deliveries_OnTapped(object sender, EventArgs e) { Navigation.PushAsync(new DeliveryModule()); } private void Equipment_OnTapped(object sender, EventArgs e) { Navigation.PushAsync(new EquipmentModule()); } private void Forms_OnTapped(object sender, EventArgs e) { Navigation.PushAsync(new KanbanForms()); //Navigation.PushAsync(new DigitalFormsPicker("Kanban"); } private void InOut_OnTapped(object sender, EventArgs e) { Navigation.PushAsync(new StaffStatusPage()); } private void Manufacturing_OnTapped(object sender, EventArgs e) { Navigation.PushAsync(new ManufacturingScreen()); } private void MyHR_OnTapped(object sender, EventArgs e) { Navigation.PushAsync(new MyHRHome()); } private void Notifications_OnTapped(object sender, EventArgs e) { Navigation.PushAsync(new NotificationList()); } private void Products_OnTapped(object sender, EventArgs e) { Navigation.PushAsync(new ProductListTwo()); } private void PurchaseOrders_OnTapped(object sender, EventArgs e) { Navigation.PushAsync(new PurchaseOrderModule()); } private void Site_OnTapped(object sender, EventArgs e) { Guid jobid = _currenttimesheet?.JobLink.ID ?? new LocalConfiguration().Load().JobID; Navigation.PushAsync( new SiteModule() { JobID = jobid } ); } private void StoreRequis_OnTapped(object sender, EventArgs e) { Navigation.PushAsync(new StoreRequiList()); } private void MyTasks_OnTapped(object sender, EventArgs e) { Navigation.PushAsync(new TasksList()); } private void Warehousing_OnTapped(object sender, EventArgs e) { Navigation.PushAsync(new Warehousing2()); } } }