XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

STHApplication

【MAUI】寄当家——安卓端交易管理系统

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Globalization;
using System.Text;
using JiDangJia.Core;

namespace JiDangJia.Views;

public sealed class FinancePanel
{
    private readonly BusinessService service;
    private readonly Action<FinanceFilter>? changed;
    private readonly DatePicker start=new(){Format="yyyy-MM-dd"},end=new(){Format="yyyy-MM-dd"};
    private readonly Picker major=new(){Title="业务大类"},detail=new(){Title="细类"},direction=new(){Title="收支方向"};
    private readonly Entry search=new(){Placeholder="搜索摘要或财务分类",ReturnType=ReturnType.Search,ClearButtonVisibility=ClearButtonVisibility.WhileEditing};
    private readonly Switch includeVoided=new(){OnColor=Ui.Bronze};
    private readonly VerticalStackLayout results=Ui.Stack(),flows=Ui.Stack();
    private readonly Dictionary<string,string> majors=new Dictionary<string,string>{{"ALL","全部大类"}}.Concat(FinanceAnalytics.Majors).ToDictionary(p=>p.Key,p=>p.Value);
    private readonly Dictionary<string,string> directions=new(){["ALL"]="全部收支",["INCOME"]="收入",["EXPENSE"]="支出"};
    private Dictionary<string,string> details=new();
    private FinanceResult current=new([],[],0);
    private bool updating;
    private int limit=25;
    public VerticalStackLayout View {get;}
    private FinanceFilter Filter=>new(start.Date??DateTime.Today,end.Date??DateTime.Today,majors.Keys.ElementAtOrDefault(major.SelectedIndex)??"ALL",details.Keys.ElementAtOrDefault(detail.SelectedIndex)??"ALL",directions.Keys.ElementAtOrDefault(direction.SelectedIndex)??"ALL",search.Text?.Trim()??"",includeVoided.IsToggled);
    public FinancePanel(BusinessService service,FinanceFilter? initial=null,Action<FinanceFilter>? changed=null)
    {
        this.service=service;this.changed=changed;initial??=new(new DateTime(DateTime.Today.Year,DateTime.Today.Month,1),DateTime.Today);
        updating=true;start.Date=initial.Start;end.Date=initial.End;search.Text=initial.Query;major.ItemsSource=majors.Values.ToList();major.SelectedIndex=majors.Keys.ToList().IndexOf(initial.Major);
        direction.ItemsSource=directions.Values.ToList();direction.SelectedIndex=directions.Keys.ToList().IndexOf(initial.Direction);includeVoided.IsToggled=initial.IncludeVoided;SetDetails(initial.Detail);updating=false;
        includeVoided.Toggled+=(_,_)=>{if(!updating){var selected=Filter.Detail;updating=true;SetDetails(selected);updating=false;Reload();}};
        var dates=new Grid{ColumnDefinitions=[new(GridLength.Star),new(GridLength.Auto),new(GridLength.Star)],ColumnSpacing=8};dates.Add(start);dates.Add(Ui.Text("至",12,Ui.Muted),1);dates.Add(end,2);
        var quick=new HorizontalStackLayout{Spacing=8};
        foreach(var name in new[]{"今日","近7天","本月","本年","全部日期"})quick.Add(Ui.Tap(Ui.Pill(name),()=>{Range(name);return Task.CompletedTask;}));
        var selectors=new Grid{ColumnDefinitions=[new(GridLength.Star),new(GridLength.Star)],ColumnSpacing=10};selectors.Add(Ui.Stack(Ui.Text("大类",11,Ui.Muted),major));selectors.Add(Ui.Stack(Ui.Text("细类",11,Ui.Muted),detail),1);
        var filters=Ui.Stack(Ui.Section("财务筛选"),Ui.Text("日期区间(含起止日期)",11,Ui.Muted),dates,new ScrollView{Orientation=ScrollOrientation.Horizontal,Content=quick,HorizontalScrollBarVisibility=ScrollBarVisibility.Never},selectors,Ui.Stack(Ui.Text("收支方向",11,Ui.Muted),direction),search,Ui.Spread(Ui.Button("查询",()=>{Reload();return Task.CompletedTask;}),Ui.Button("重置筛选",()=>{Reset();return Task.CompletedTask;},false)));
        filters.Add(Ui.Spread(Ui.Text("包含作废单据与退款流水",12,Ui.Muted),includeVoided));
        View=Ui.Stack(Ui.Card(filters),Ui.Button("资金成本 · 资方与利息结算",()=>Application.Current!.Windows[0].Page!.Navigation.PushAsync(new FundingPage(service))),Ui.Spread(Ui.Button("+ 手工记账",()=>Application.Current!.Windows[0].Page!.Navigation.PushAsync(FormPage.Finance(service))),Ui.Button("导出筛选结果",Export,false)),results);
        start.DateSelected+=(_,_)=>AutoReload();end.DateSelected+=(_,_)=>AutoReload();direction.SelectedIndexChanged+=(_,_)=>AutoReload();detail.SelectedIndexChanged+=(_,_)=>AutoReload();
        major.SelectedIndexChanged+=(_,_)=>{if(updating)return;updating=true;SetDetails("ALL");updating=false;Reload();};search.Completed+=(_,_)=>Reload();Reload();
    }
    private void SetDetails(string selected)
    {
        var key=majors.Keys.ElementAtOrDefault(major.SelectedIndex)??"ALL";
        details=new Dictionary<string,string>{{"ALL","全部细类"}}.Concat(key=="ALL"?new Dictionary<string,string>():FinanceAnalytics.AvailableDetails(service.Db,key,includeVoided.IsToggled)).ToDictionary(p=>p.Key,p=>p.Value);
        detail.ItemsSource=details.Values.ToList();detail.SelectedIndex=Math.Max(0,details.Keys.ToList().IndexOf(selected));detail.IsEnabled=key!="ALL"&&details.Count>1;
    }
    private void AutoReload(){if(!updating)Reload();}
    private void Range(string name)
    {
        updating=true;end.Date=DateTime.Today;start.Date=name switch{"今日"=>DateTime.Today,"近7天"=>DateTime.Today.AddDays(-6),"本月"=>new(DateTime.Today.Year,DateTime.Today.Month,1),"本年"=>new(DateTime.Today.Year,1,1),_=>service.Db.Query("SELECT MIN(occurDate) first FROM FinanceRecord WHERE deletedAt IS NULL").First().Date("first")};updating=false;Reload();
    }
    private void Reset(){updating=true;major.SelectedIndex=0;direction.SelectedIndex=0;search.Text="";includeVoided.IsToggled=false;SetDetails("ALL");updating=false;Range("本月");}
    private void Reload()
    {
        var filter=Filter;results.Clear();
        if(filter.Start.Date>filter.End.Date){current=new([],[],0);flows.Clear();results.Add(Ui.Card(Ui.Text("结束日期不能早于开始日期,请调整日期区间。",13,Ui.Red)));return;}
        current=FinanceAnalytics.Read(service.Db,filter);changed?.Invoke(filter);limit=25;
        var summary=new Grid{ColumnDefinitions=[new(GridLength.Star),new(GridLength.Star)],ColumnSpacing=8};summary.Add(Ui.Stack(Ui.Text("筛选收入",12,Ui.Muted),Ui.Text(Display.Money(current.Income),19,Ui.Green,true)));summary.Add(Ui.Stack(Ui.Text("筛选支出",12,Ui.Muted),Ui.Text(Display.Money(current.Expense),19,Ui.Red,true)),1);
        results.Add(Ui.Card(Ui.Stack(Ui.Section("区间收支"),Ui.Text(Display.Day(filter.Start)+" 至 "+Display.Day(filter.End),11,Ui.Muted),summary,Ui.Divider(),Ui.Spread(Ui.Text("收支净额",12,Ui.Muted),Ui.Text(Display.Money(current.Income-current.Expense),18,Ui.Bronze,true)),Ui.Spread(Ui.Text("经营利润",12,Ui.Muted),Ui.Text(Display.Money(current.Profit),18,Ui.Bronze,true)),Ui.Text("净额含本金、采购款与押金;经营利润扣除销售成本,排除本金和押金往来。",11,Ui.Muted))));
        var chart=Ui.Stack(Ui.Section(filter.Major=="ALL"?"大类收支对比":"细类收支对比"),Ui.Row(Ui.Pill("收入",Ui.Green),Ui.Pill("支出",Ui.Red)));
        decimal max=current.Groups.SelectMany(g=>new[]{g.Income,g.Expense}).DefaultIfEmpty(0).Max();
        foreach(var group in current.Groups)
        {
            var bars=Ui.Stack(Ui.Text(group.Label,14,bold:true));bars.Spacing=7;
            foreach(var p in new[]{("收入",group.Income,Ui.Green),("支出",group.Expense,Ui.Red)})
            {
                var line=new Grid{ColumnDefinitions=[new(GridLength.Auto),new(GridLength.Star),new(GridLength.Auto)],ColumnSpacing=9};line.Add(Ui.Text(p.Item1,10,p.Item3));line.Add(new ProgressBar{Progress=max>0?(double)(p.Item2/max):0,ProgressColor=p.Item3,BackgroundColor=Ui.Paper,HeightRequest=8,VerticalOptions=LayoutOptions.Center},1);line.Add(Ui.Text(Display.Money(p.Item2),12,p.Item3,true),2);bars.Add(line);
            }
            chart.Add(bars);chart.Add(Ui.Divider());
        }
        if(current.Groups.Count==0)chart.Add(Ui.Empty("该筛选暂无收支","调整日期、大类或细类后查看"));results.Add(Ui.Card(chart));results.Add(flows);RenderFlows();
    }
    private void RenderFlows()
    {
        flows.Clear();var grouped=current.Lines.GroupBy(l=>l.Record.Id).ToList();flows.Add(Ui.Section("筛选流水",grouped.Count+" 笔"));
        foreach(var group in grouped.Take(limit))
        {
            var row=group.First().Record;var income=row.Text("type")=="INCOME";
            var labels=string.Join("、",group.Select(l=>FinanceAnalytics.Majors.GetValueOrDefault(l.Major,l.Major)+" · "+DetailLabel(l)).Distinct());
            var amount=group.Sum(l=>l.Amount);var title=Ui.Text(service.Category(row.Text("category")),14,bold:true);
            var body=Ui.Stack(Ui.Spread(title,Ui.Text((income?"+":"−")+Display.Money(amount),16,income?Ui.Green:Ui.Red,true)),Ui.Text(labels,11,Ui.Bronze),Ui.Text(row.Text("occurDate")+"  "+row.Text("remark"),11,Ui.Muted));
            if(amount!=row.Money("amount"))body.Add(Ui.Text("金额为所选细类分配额;详情显示整笔流水。",10,Ui.Muted));
            flows.Add(Ui.Tap(Ui.Card(body),()=>Application.Current!.Windows[0].Page!.Navigation.PushAsync(new DetailPage(service,"FinanceRecord",row.Id))));
        }
        if(grouped.Count>limit)flows.Add(Ui.Button("加载更多流水",()=>{limit+=25;RenderFlows();return Task.CompletedTask;},false));
    }
    private async Task Export()
    {
        var data=FinanceAnalytics.Read(service.Db,Filter);
        string Csv(string value)=>"\""+(value.StartsWith('=')||value.StartsWith('+')||value.StartsWith('-')||value.StartsWith('@')?"'":"")+value.Replace("\"","\"\"")+"\"";
        var text="日期,方向,大类,细类,财务分类,金额,摘要\r\n"+string.Join("\r\n",data.Lines.Select(l=>string.Join(",",new[]{l.Record.Text("occurDate"),l.Income?"收入":"支出",FinanceAnalytics.Majors.GetValueOrDefault(l.Major,l.Major),DetailLabel(l),service.Category(l.Record.Text("category")),l.Amount.ToString("F2",CultureInfo.InvariantCulture),l.Record.Text("remark")}.Select(Csv))));
        var path=Services.AccountDataClear.CacheFile("寄当家-财务筛选报表.csv");await File.WriteAllTextAsync(path,text,new UTF8Encoding(true));await Share.RequestAsync(new ShareFileRequest("财务筛选报表",new ShareFile(path)));
    }
    private string DetailLabel(FinanceLine line)=>line.Detail.StartsWith("CUSTOM_",StringComparison.Ordinal)?service.Category(line.Detail):FinanceAnalytics.DetailLabel(line.Major,line.Detail);
}