using Billing.Models; using Billing.Store; using Billing.Themes; using Billing.UI; using Microcharts; using SkiaSharp; using SkiaSharp.Views.Forms; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Xamarin.Essentials; using Xamarin.Forms; using Resource = Billing.Languages.Resource; namespace Billing.Views { public enum DateType : int { Custom = 0, Monthly, Today, PastMonth, PastQuarter, PastSixMonths, PastYear, Total } public partial class RankPage : BillingPage { private static RankPage instance; public static RankPage Instance => instance; private static readonly DateTime today = DateTime.Today; private static readonly BindableProperty SegmentTypeProperty = Helper.Create(nameof(SegmentType), defaultValue: 0, propertyChanged: OnSegmentTypeChanged); private static readonly BindableProperty SegmentDateProperty = Helper.Create(nameof(SegmentDate), defaultValue: 1, propertyChanged: OnSegmentDateChanged); private static readonly BindableProperty StartDateProperty = Helper.Create(nameof(StartDate), defaultValue: today.AddDays(1 - today.Day), propertyChanged: OnDateChanged); private static readonly BindableProperty EndDateProperty = Helper.Create(nameof(EndDate), defaultValue: today.AddDays(DateTime.DaysInMonth(today.Year, today.Month) - today.Day).LastMoment(), propertyChanged: OnDateChanged); private static readonly BindableProperty StartPickerDateProperty = Helper.Create(nameof(StartPickerDate), defaultValue: today.AddDays(1 - today.Day), propertyChanged: OnPickerStartDateChanged); private static readonly BindableProperty EndPickerDateProperty = Helper.Create(nameof(EndPickerDate), defaultValue: today.AddDays(DateTime.DaysInMonth(today.Year, today.Month) - today.Day), propertyChanged: OnPickerEndDateChanged); private static readonly BindableProperty ChartProperty = Helper.Create(nameof(Chart)); private static readonly BindableProperty CategoryChartProperty = Helper.Create(nameof(CategoryChart)); private static readonly BindableProperty TopBillsProperty = Helper.Create, RankPage>(nameof(TopBills)); private static readonly BindableProperty NoResultChartProperty = Helper.Create(nameof(NoResultChart)); private static readonly BindableProperty NoResultCategoryChartProperty = Helper.Create(nameof(NoResultCategoryChart)); private static readonly BindableProperty NoResultTopBillsProperty = Helper.Create(nameof(NoResultTopBills)); private static readonly BindableProperty IncomeProperty = Helper.Create(nameof(Income)); private static readonly BindableProperty SpendingProperty = Helper.Create(nameof(Spending)); private static readonly BindableProperty BalanceProperty = Helper.Create(nameof(Balance)); private static void OnSegmentTypeChanged(RankPage page, int old, int @new) { page.type = @new switch { 1 => CategoryType.Income, _ => CategoryType.Spending }; page.LoadData(); } private static void OnSegmentDateChanged(RankPage page, int old, int @new) { page.OnDateTypeCommand((DateType)@new); } private static void OnDateChanged(RankPage page, DateTime old = default, DateTime @new = default) { page.isLocked = true; page.StartPickerDate = page.StartDate.Date; page.EndPickerDate = page.EndDate.Date; page.isLocked = false; if (!page.isFreezed) { var format = Resource.TitleDateFormat; page.Title = page.StartDate.ToString(format) + " ~ " + page.EndDate.ToString(format); page.LoadData(); } } private static void OnPickerStartDateChanged(RankPage page, DateTime _, DateTime @new) { if (!page.isLocked) { page.SegmentDate = 0; page.StartDate = @new.Date; } } private static void OnPickerEndDateChanged(RankPage page, DateTime _, DateTime @new) { if (!page.isLocked) { page.SegmentDate = 0; page.EndDate = @new.Date.LastMoment(); } } public int SegmentType { get => (int)GetValue(SegmentTypeProperty); set => SetValue(SegmentTypeProperty, value); } public int SegmentDate { get => (int)GetValue(SegmentDateProperty); set => SetValue(SegmentDateProperty, value); } public DateTime StartDate { get => (DateTime)GetValue(StartDateProperty); set => SetValue(StartDateProperty, value); } public DateTime EndDate { get => (DateTime)GetValue(EndDateProperty); set => SetValue(EndDateProperty, value); } public DateTime StartPickerDate { get => (DateTime)GetValue(StartPickerDateProperty); set => SetValue(StartPickerDateProperty, value); } public DateTime EndPickerDate { get => (DateTime)GetValue(EndPickerDateProperty); set => SetValue(EndPickerDateProperty, value); } public Chart Chart { get => (Chart)GetValue(ChartProperty); set => SetValue(ChartProperty, value); } public Chart CategoryChart { get => (Chart)GetValue(CategoryChartProperty); set => SetValue(CategoryChartProperty, value); } public IList TopBills { get => (IList)GetValue(TopBillsProperty); set => SetValue(TopBillsProperty, value); } public bool NoResultChart { get => (bool)GetValue(NoResultChartProperty); set => SetValue(NoResultChartProperty, value); } public bool NoResultCategoryChart { get => (bool)GetValue(NoResultCategoryChartProperty); set => SetValue(NoResultCategoryChartProperty, value); } public bool NoResultTopBills { get => (bool)GetValue(NoResultTopBillsProperty); set => SetValue(NoResultTopBillsProperty, value); } public decimal Income => (decimal)GetValue(IncomeProperty); public decimal Spending => (decimal)GetValue(SpendingProperty); public decimal Balance => (decimal)GetValue(BalanceProperty); public List DateTypes { get; } public Command LeftCommand { get; } public Command RightCommand { get; } public Command FilterCommand { get; } public Command EditBilling { get; } private IEnumerable bills; private CategoryType type = CategoryType.Spending; private bool isFilterToggled; private bool isFreezed; private bool isLocked; private bool needRefresh = true; private const int FILTER_HEIGHT = 100; private readonly SKTypeface font; public RankPage() { instance = this; LeftCommand = new Command(OnLeftCommand); RightCommand = new Command(OnRightCommand); FilterCommand = new Command(OnFilterCommand); EditBilling = new Command(OnEditBilling); var style = SKFontManager.Default.GetFontStyles("PingFang SC"); if (style != null) { font = style.CreateTypeface(SKFontStyle.Normal); } DateTypes = new List { Resource.Custom, Resource.Monthly, Resource.Today, Resource.PastMonth, Resource.PastQuarter, Resource.PastSixMonths, Resource.PastYear, Resource.Total }; InitializeComponent(); gridFilter.TranslationY = -FILTER_HEIGHT; panelFilter.TranslationY = -FILTER_HEIGHT; } public void SetNeedRefresh() { needRefresh = true; } protected override void OnAppearing() { if (needRefresh) { needRefresh = false; OnDateChanged(this); } } protected override void OnRefresh() { OnDateChanged(this); } private void OnDateTypeCommand(DateType index) { if (index < DateType.Monthly || index > DateType.Total) { return; } if (scroller.ScrollY > 0) { scroller.ScrollToAsync(0, 0, true); } isFreezed = true; var today = DateTime.Today; switch (index) { case DateType.Monthly: StartDate = today.AddDays(1 - today.Day); EndDate = today.AddDays(DateTime.DaysInMonth(today.Year, today.Month) - today.Day).LastMoment(); break; case DateType.Today: StartDate = today; EndDate = today.LastMoment(); break; case DateType.PastMonth: StartDate = today.AddMonths(-1).AddDays(1); EndDate = today.LastMoment(); break; case DateType.PastQuarter: StartDate = today.AddMonths(-3).AddDays(1); EndDate = today.LastMoment(); break; case DateType.PastSixMonths: StartDate = today.AddMonths(-6).AddDays(1); EndDate = today.LastMoment(); break; case DateType.PastYear: StartDate = today.AddYears(-1).AddDays(1); EndDate = today.LastMoment(); break; case DateType.Total: //StartDate = App.Bills.Min(b => b.CreateTime).Date; //EndDate = App.Bills.Max(b => b.CreateTime).Date.LastMoment(); DateTime min = DateTime.MaxValue; DateTime max = DateTime.MinValue; App.Bills.ForEach(b => { if (b.CreateTime < min) { min = b.CreateTime; } if (b.CreateTime > max) { max = b.CreateTime; } }); if (min == DateTime.MaxValue && max == DateTime.MinValue) { return; } StartDate = min.Date; EndDate = max.Date.LastMoment(); break; } isFreezed = false; OnDateChanged(this); } private bool IsPreset(DateTime start, DateTime end) { return start.Month == end.Month && start.Day == 1 && end.Day == DateTime.DaysInMonth(end.Year, end.Month); } private void OnLeftCommand() { var type = (DateType)SegmentDate; if (type < DateType.Monthly || type >= DateType.Total) { return; } if (scroller.ScrollY > 0) { scroller.ScrollToAsync(0, 0, true); } isFreezed = true; var start = StartDate; var end = EndDate; if (type == DateType.Monthly || IsPreset(start, end)) { start = start.AddMonths(-1); end = start.AddDays(DateTime.DaysInMonth(start.Year, start.Month) - 1).LastMoment(); } else if (type == DateType.PastMonth) { start = start.AddMonths(-1); end = end.AddMonths(-1); } else if (type == DateType.PastQuarter) { start = start.AddMonths(-3); end = end.AddMonths(-3); } else if (type == DateType.PastSixMonths) { start = start.AddMonths(-6); end = end.AddMonths(-6); } else if (type == DateType.PastYear) { start = start.AddYears(-1); end = end.AddYears(-1); } else { var days = (end.Date - start.Date).TotalDays + 1; start = start.AddDays(-days); end = end.AddDays(-days); } if (start.Year < 1900) { isFreezed = false; return; } StartDate = start; EndDate = end; isFreezed = false; OnDateChanged(this); } private void OnRightCommand() { var type = (DateType)SegmentDate; if (type < DateType.Monthly || type >= DateType.Total) { return; } if (scroller.ScrollY > 0) { scroller.ScrollToAsync(0, 0, true); } isFreezed = true; var start = StartDate; var end = EndDate; if (type == DateType.Monthly || IsPreset(start, end)) { start = start.AddMonths(1); end = start.AddDays(DateTime.DaysInMonth(start.Year, start.Month) - 1).LastMoment(); } else if (type == DateType.PastMonth) { start = start.AddMonths(1); end = end.AddMonths(1); } else if (type == DateType.PastQuarter) { start = start.AddMonths(3); end = end.AddMonths(3); } else if (type == DateType.PastSixMonths) { start = start.AddMonths(6); end = end.AddMonths(6); } else if (type == DateType.PastYear) { start = start.AddYears(1); end = end.AddYears(1); } else { var days = (end.Date - start.Date).TotalDays + 1; start = start.AddDays(days); end = end.AddDays(days); } if (end.Year > DateTime.Today.Year + 100) { isFreezed = false; return; } StartDate = start; EndDate = end; isFreezed = false; OnDateChanged(this); } private async void OnFilterCommand(object o) { if (o is bool flag) { isFilterToggled = flag; } else { isFilterToggled = !isFilterToggled; } ViewExtensions.CancelAnimations(gridFilter); ViewExtensions.CancelAnimations(panelFilter); if (isFilterToggled) { await scroller.ScrollToAsync(scroller.ScrollX, scroller.ScrollY, false); await Task.WhenAll( gridFilter.TranslateTo(0, 0, easing: Easing.CubicOut), gridFilter.FadeTo(1, easing: Easing.CubicOut), panelFilter.TranslateTo(0, 0, easing: Easing.CubicOut), panelFilter.FadeTo(1, easing: Easing.CubicOut)); } else { await Task.WhenAll( gridFilter.TranslateTo(0, -FILTER_HEIGHT, easing: Easing.CubicIn), gridFilter.FadeTo(0, easing: Easing.CubicIn), panelFilter.TranslateTo(0, -FILTER_HEIGHT, easing: Easing.CubicIn), panelFilter.FadeTo(0, easing: Easing.CubicIn)); } } private async void OnEditBilling(object o) { if (Tap.IsBusy) { return; } using (Tap.Start()) { if (o is UIBill bill) { var page = new AddBillPage(bill.Bill); page.BillChecked += OnBillChecked; await Navigation.PushAsync(page); } } } private async void RefreshBalance(DateTime start, DateTime end) { var bills = await Task.Run(() => App.Bills.Where(b => b.CreateTime >= start && b.CreateTime <= end)); var income = bills.Where(b => b.Amount > 0).Sum(b => b.Amount); var spending = -bills.Where(b => b.Amount < 0).Sum(b => b.Amount); SetValue(IncomeProperty, income); SetValue(SpendingProperty, spending); SetValue(BalanceProperty, income - spending); } private async void OnBillChecked(object sender, Bill e) { await StoreHelper.SaveBillItemAsync(e); LoadData(); } private async void LoadData() { var start = StartDate; var end = EndDate; var spending = type == CategoryType.Spending; bills = await Task.Run(() => App.Bills.Where(b => (b.Amount > 0 ^ spending) && b.CreateTime >= start && b.CreateTime <= end)); var primaryColor = BaseTheme.CurrentPrimaryColor.ToSKColor(); var textColor = BaseTheme.CurrentSecondaryTextColor.ToSKColor(); _ = Task.Run(() => LoadReportChart(primaryColor, textColor, start, end)); _ = Task.Run(() => LoadCategoryChart(primaryColor, textColor)); _ = Task.Run(LoadTopBills); RefreshBalance(start, end); } private void LoadReportChart(SKColor primaryColor, SKColor textColor, DateTime start, DateTime end) { var entries = new List(); for (var day = start; day <= end; day = day.AddDays(1)) { var daybills = bills.Where(b => Helper.IsSameDay(b.CreateTime, day)); decimal amount = Math.Abs(daybills.Sum(b => b.Amount)); if (amount > 0) { entries.Add(new((float)amount) { Label = day.ToString("MM-dd"), ValueLabel = amount.ToString("#,##0.##"), Color = primaryColor, TextColor = textColor, ValueLabelColor = textColor }); } } MainThread.BeginInvokeOnMainThread(() => { if (entries.Count > 0) { NoResultChart = false; Chart = new LineChart { BackgroundColor = SKColors.Transparent, LabelTextSize = 24, Entries = entries }; } else { Chart = null; NoResultChart = true; } }); } private void LoadCategoryChart(SKColor primaryColor, SKColor textColor) { var entries = new List(); var groups = bills.GroupBy(b => b.CategoryId); var dict = new Dictionary(); decimal all = 0m; foreach (var g in groups) { var categoryId = g.Key; var category = App.Categories.FirstOrDefault(c => c.Id == categoryId); if (category?.ParentId != null) { category = App.Categories.FirstOrDefault(c => c.Id == category.ParentId) ?? category; } if (category != null) { var total = Math.Abs(g.Sum(g => g.Amount)); all += total; if (dict.ContainsKey(category.Name)) { dict[category.Name] += total; } else { dict.Add(category.Name, total); } } } foreach (var kv in dict) { entries.Add(new((float)kv.Value) { Label = kv.Key, ValueLabel = (kv.Value * 100 / all).ToString("0.#") + "%", Color = primaryColor, TextColor = textColor, ValueLabelColor = textColor }); } MainThread.BeginInvokeOnMainThread(() => { if (entries.Count > 0) { NoResultCategoryChart = false; CategoryChart = new RadarChart { BackgroundColor = SKColors.Transparent, LabelTextSize = 30, Typeface = font, Entries = entries }; } else { CategoryChart = null; NoResultCategoryChart = true; } }); } private void LoadTopBills() { List topBills = bills.OrderByDescending(b => Math.Abs(b.Amount)).Take(10).Select(b => Helper.WrapBill(b)).ToList(); MainThread.BeginInvokeOnMainThread(() => { if (topBills.Count > 0) { NoResultTopBills = false; TopBills = topBills; } else { TopBills = null; NoResultTopBills = true; } }); } private void Scroller_Scrolled(object sender, ScrolledEventArgs e) { if (isFilterToggled) { OnFilterCommand(false); } } } }