635 lines
22 KiB
C#
635 lines
22 KiB
C#
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<int, RankPage>(nameof(SegmentType), defaultValue: 0, propertyChanged: OnSegmentTypeChanged);
|
|
private static readonly BindableProperty SegmentDateProperty = Helper.Create<int, RankPage>(nameof(SegmentDate), defaultValue: 1, propertyChanged: OnSegmentDateChanged);
|
|
private static readonly BindableProperty StartDateProperty = Helper.Create<DateTime, RankPage>(nameof(StartDate),
|
|
defaultValue: today.AddDays(1 - today.Day),
|
|
propertyChanged: OnDateChanged);
|
|
private static readonly BindableProperty EndDateProperty = Helper.Create<DateTime, RankPage>(nameof(EndDate),
|
|
defaultValue: today.AddDays(DateTime.DaysInMonth(today.Year, today.Month) - today.Day).LastMoment(),
|
|
propertyChanged: OnDateChanged);
|
|
private static readonly BindableProperty StartPickerDateProperty = Helper.Create<DateTime, RankPage>(nameof(StartPickerDate),
|
|
defaultValue: today.AddDays(1 - today.Day),
|
|
propertyChanged: OnPickerStartDateChanged);
|
|
private static readonly BindableProperty EndPickerDateProperty = Helper.Create<DateTime, RankPage>(nameof(EndPickerDate),
|
|
defaultValue: today.AddDays(DateTime.DaysInMonth(today.Year, today.Month) - today.Day),
|
|
propertyChanged: OnPickerEndDateChanged);
|
|
private static readonly BindableProperty ChartProperty = Helper.Create<Chart, RankPage>(nameof(Chart));
|
|
private static readonly BindableProperty CategoryChartProperty = Helper.Create<Chart, RankPage>(nameof(CategoryChart));
|
|
private static readonly BindableProperty TopBillsProperty = Helper.Create<IList<UIBill>, RankPage>(nameof(TopBills));
|
|
private static readonly BindableProperty NoResultChartProperty = Helper.Create<bool, RankPage>(nameof(NoResultChart));
|
|
private static readonly BindableProperty NoResultCategoryChartProperty = Helper.Create<bool, RankPage>(nameof(NoResultCategoryChart));
|
|
private static readonly BindableProperty NoResultTopBillsProperty = Helper.Create<bool, RankPage>(nameof(NoResultTopBills));
|
|
private static readonly BindableProperty IncomeProperty = Helper.Create<decimal, RankPage>(nameof(Income));
|
|
private static readonly BindableProperty SpendingProperty = Helper.Create<decimal, RankPage>(nameof(Spending));
|
|
private static readonly BindableProperty BalanceProperty = Helper.Create<decimal, RankPage>(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.TitleShortDateFormat;
|
|
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<UIBill> TopBills
|
|
{
|
|
get => (IList<UIBill>)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<string> DateTypes { get; }
|
|
|
|
public Command LeftCommand { get; }
|
|
public Command RightCommand { get; }
|
|
public Command FilterCommand { get; }
|
|
public Command EditBilling { get; }
|
|
|
|
private IEnumerable<Bill> 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);
|
|
|
|
#if __IOS__
|
|
var style = SKFontManager.Default.GetFontStyles("PingFang SC");
|
|
if (style != null)
|
|
{
|
|
font = style.CreateTypeface(SKFontStyle.Normal);
|
|
}
|
|
else
|
|
#endif
|
|
font = SKFontManager.Default.MatchCharacter(0x4e00);
|
|
|
|
DateTypes = new List<string>
|
|
{
|
|
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<ChartEntry>();
|
|
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<ChartEntry>();
|
|
var groups = bills.GroupBy(b => b.CategoryId);
|
|
var dict = new Dictionary<string, decimal>();
|
|
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<UIBill> 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);
|
|
}
|
|
}
|
|
}
|
|
} |