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

STHApplication

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

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

public sealed class FormPage : ContentPage
{
    private readonly VerticalStackLayout body=Ui.Stack();
    private Func<Task>? save;
    internal Func<PhotoField,Services.OrderCameraDraft?>? CameraDraft;
    private Func<Services.OrderCameraDraft>? draftSnapshot;
    private Action? discardDraft;
    private bool draftFinished;
    internal void PreserveDraft()
    {
        if(draftFinished||draftSnapshot is null)return;
        try{var draft=draftSnapshot();if(Services.AuthClient.Current.AccountId!=draft.AccountId||Services.AuthClient.Current.Locked||Services.OrderCameraRecovery.Capturing(draft.AccountId)||Services.AccountDataClear.InProgress)return;Services.OrderCameraRecovery.Save(draft);}
        catch(Exception ex)when(ex is IOException or InvalidOperationException or JsonException or ArgumentException){}
    }
    private void FinishDraft(){draftFinished=true;discardDraft?.Invoke();}
    protected override void OnDisappearing(){PreserveDraft();base.OnDisappearing();}
    protected override void OnNavigatedFrom(NavigatedFromEventArgs args){base.OnNavigatedFrom(args);if(!Navigation.NavigationStack.Contains(this))FinishDraft();}
    private FormPage(string title)
    {
        Title=title;body.Padding=new Thickness(18,20,18,30);body.Spacing=18;
        Content=new ScrollView {Content=body};
    }
    private void Footer(string label="保存")
    {
        body.Add(Ui.Button(label,async()=> {if(save is null)return;await save();FinishDraft();await DisplayAlertAsync("已保存","数据已保存到本机。","完成");await Navigation.PopAsync();}));
    }
    public static FormPage Customer(BusinessService s,string? id=null)
    {
        var p=new FormPage(id is null?"新建客户":"编辑客户");
        var row=id is not null?s.Db.Find("Customer",id)??new():new DbRow { ["type"]="PERSON",["creditLevel"]="NORMAL" };
        var fields=new FieldSet(row,[new("name","客户姓名",Required:true),new("phone","联系电话"),new("gender","性别",FieldKind.Choice,Choices:new(){["男"]="男",["女"]="女",["未知"]="未填写"}),new("type","客户类型",FieldKind.Choice,true,new(){["PERSON"]="个人",["COMPANY"]="企业"}),new("creditLevel","信用等级",FieldKind.Choice,true,new(){["NORMAL"]="普通",["GOOD"]="良好",["VIP"]="VIP",["RISK"]="风险"}),new("isBlacklist","黑名单",FieldKind.Toggle),new("idCard","证件号码"),new("idCardImages","证件照片",FieldKind.Photos),new("address","联系地址"),new("tags","客户标签",Hint:"多个标签用逗号分隔"),new("remark","备注",FieldKind.Notes)]);
        p.body.Add(Ui.Card(Ui.Stack(Ui.Section("客户档案"),fields.View)));p.save=()=>{var value=fields.Read();return Task.Run(()=>s.Customer(value));};p.Footer("保存客户");return p;
    }
    public static Page Finance(BusinessService s)=>new ManualFinancePage(s);
    public static FormPage Settings(BusinessService s,string table,DbRow initial,string title,IEnumerable<Field> fields)
    {
        if(table=="Advertisement")throw new InvalidOperationException("广告由服务器管理员统一管理,请进入管理员后台");
        var p=new FormPage(title);var controls=new FieldSet(initial,fields);p.body.Add(Ui.Card(controls.View));
        p.save=()=>
        {
            var row=controls.Read();
            if(row.Id.Length==0){row["id"]=Database.Id(table.ToLowerInvariant());row["createdAt"]=Database.Now;}
            row["updatedAt"]=Database.Now;
            if(row.Text("name").Length==0&&row.Text("label").Length==0&&row.Text("title").Length==0)throw new ArgumentException("请填写名称");
            foreach(var key in new[]{"monthlyRate","serviceFeeRate","comprehensiveFeeRate","storageFeeRate","penaltyRate","commission"})
                if(row.Money(key)<0||row.Money(key)>100)throw new ArgumentException("费率和比例须在 0% 至 100% 之间");
            if(row.ContainsKey("defaultPeriodMonths")&&(row.Money("defaultPeriodMonths")%1!=0||row.Int("defaultPeriodMonths")<1||row.Int("defaultPeriodMonths")>120))throw new ArgumentException("默认当期应为 1 至 120 整月");
            if(row.Text("attributes").Length>0)
            {
                try{using var d=JsonDocument.Parse(row.Text("attributes"));if(d.RootElement.ValueKind!=JsonValueKind.Array)throw new ArgumentException("属性配置应为数组");}
                catch(JsonException){throw new ArgumentException("属性配置格式错误");}
            }
            if(table=="PrintTemplate"&&row.Int("isDefault")==1)
                s.Db.Write(c=>{Database.Execute(c,"UPDATE PrintTemplate SET isDefault=0 WHERE type=@p0",row.Text("type"));Database.Upsert(c,table,row);Database.Log(c,"SAVE",table,row.Id,row.Text("name"));return 0;});
            else s.Db.Save(table,row);
            return Task.CompletedTask;
        };
        p.Footer();return p;
    }
    public static FormPage Order(BusinessService s,string kind,string? customerId=null,string? stockId=null,Services.OrderCameraDraft? draft=null)
    {
        var p=new FormPage(kind switch {"PAWN"=>"新建当单","RECYCLE"=>"新建回收单","PURCHASE"=>"新货铺货入库","RENTAL"=>"租赁物品入库","STOCK_OTHER"=>"新建入库单","CONSIGN"=>"新建寄卖单","RENT"=>"新建租赁单",_=>"新建业务"});
        var customers=s.Db.Query("SELECT id,name,phone,code FROM Customer WHERE deletedAt IS NULL AND isBlacklist=0 ORDER BY createdAt DESC").ToDictionary(r=>r.Id,r=>r.Text("name")+"  "+r.Text("phone")+"  "+r.Text("code"));
        var staff=s.Db.Query("SELECT id,name FROM User WHERE active=1").ToDictionary(r=>r.Id,r=>r.Text("name"));
        bool intake=kind is "RECYCLE" or "PURCHASE" or "RENTAL" or "STOCK_OTHER";
        if(intake)customers=new Dictionary<string,string>{{"","不选择客户 / 供应商"}}.Concat(customers).ToDictionary(p=>p.Key,p=>p.Value);
        var headerFields=new List<Field>{new("customerId",kind=="PURCHASE"?"已有供应商 / 客户":"选择客户",FieldKind.SearchChoice,!intake,customers)};
        if(intake){headerFields.Add(new("customerName",kind=="PURCHASE"?"供应商名称(选填)":"客户名称(选填)"));headerFields.Add(new("customerPhone","联系电话(选填)"));}
        headerFields.Add(new("operatorId","经办人",FieldKind.Choice,true,staff));
        var header=new FieldSet(draft?.Header??new DbRow { ["customerId"]=customerId??"",["operatorId"]=s.Owner.Id },headerFields);
        var customerBox=Ui.Stack(Ui.Section(kind=="PURCHASE"?"01  供货信息":"01  客户信息"),header.View);
        if(!intake)customerBox.Add(Ui.Button("+ 新建客户",()=>p.Navigation.PushAsync(Customer(s)),false));
        else customerBox.Add(Ui.Text("直接填写客户信息,入库时自动建立或关联客户档案。",12,Ui.Muted));
        p.body.Add(Ui.Card(customerBox));
        var customerSelect=(SearchChoiceInput)header.Inputs["customerId"];
        void CustomerVisibility(){if(intake){header.SetVisible("customerName",customerSelect.Value.Length==0);header.SetVisible("customerPhone",customerSelect.Value.Length==0);}}
        customerSelect.Changed+=CustomerVisibility;CustomerVisibility();
        p.Appearing+=(_,_)=>
        {
            var updated=s.Db.Query("SELECT id,name,phone,code FROM Customer WHERE deletedAt IS NULL AND isBlacklist=0 ORDER BY createdAt DESC");
            var selected=updated.FirstOrDefault(r=>!customers.ContainsKey(r.Id))?.Id??customerSelect.Value;
            customers.Clear();if(intake)customers[""]="不选择客户 / 供应商";foreach(var r in updated)customers[r.Id]=r.Text("name")+"  "+r.Text("phone")+"  "+r.Text("code");customerSelect.SetValue(selected);
        };
        // Multiple items stay in one order and enter inventory individually.
        var itemTypes=ItemCatalog.Choices(kind);
        var itemArea=Ui.Stack(Ui.Section(kind=="RENT"?"02  租赁物品信息":"02  物品信息"));var items=new List<ItemEditor>();
        var rentalItems=new List<DbRow>();var rentalArea=Ui.Stack();
        void RenderRentalItems()
        {
            rentalArea.Clear();foreach(var item in rentalItems)
            {
                var current=item;rentalArea.Add(Ui.Card(Ui.Stack(Ui.Spread(Ui.Text(item.Text("name"),16,bold:true),Ui.Button("移除",()=>{rentalItems.Remove(current);RenderRentalItems();return Task.CompletedTask;},false)),Ui.Text(ItemCatalog.ItemName(item)+" · 物品编码 "+item.Text("itemCode"),12,Ui.Bronze),Ui.Text("品牌 "+item.Text("brand")+" · 序列号 "+item.Text("serialNo"),11,Ui.Muted)),14,new Thickness(14),Ui.Paper));
            }
        }
        async Task SelectRentalItem()
        {
            var available=s.Db.Query("SELECT * FROM RecycleItem WHERE source='RENTAL' AND deletedAt IS NULL AND status IN ('IN_STOCK','LISTED') ORDER BY createdAt DESC").Where(r=>rentalItems.All(i=>i.Text("stockId")!=r.Id)).ToList();
            if(available.Count==0)throw new InvalidOperationException("没有可出租的在库物品,请先租赁入库");
            await p.Navigation.PushAsync(new SelectionPage("选择租赁库存",available.ToDictionary(r=>r.Id,r=>r.Text("name")+" · "+r.Text("itemCode")+" · "+r.Text("serialNo")),"",id=>{var stock=available.First(r=>r.Id==id);rentalItems.Add(new DbRow(stock){["stockId"]=id});RenderRentalItems();},inventory:true));
        }
        void AddItem(DbRow? initial=null)
        {
            var fs=new ItemEditor(s,kind,initial);items.Add(fs);Border? card=null;card=Ui.Card(Ui.Stack(Ui.Spread(Ui.Text("物品 "+items.Count,15,bold:true),Ui.Tap(Ui.Text("移除",12,Ui.Red),()=> {if(items.Count<=1)throw new ArgumentException("至少保留一件物品");items.Remove(fs);if(card is not null)itemArea.Children.Remove(card);return Task.CompletedTask;})),fs.View),14,new Thickness(14),Ui.Paper);
            itemArea.Add(card);
        }
        FieldSet? terms=null;
        if(kind=="RENT")
        {
            if(stockId is not null && s.Db.Find("RecycleItem",stockId) is { } selected){rentalItems.Add(new DbRow(selected){["stockId"]=stockId});RenderRentalItems();}
            itemArea.Add(Ui.Text("从租赁库存选择具体物品,物品编码和详细资料随单保存。",12,Ui.Muted));itemArea.Add(rentalArea);itemArea.Add(Ui.Button("+ 选择租赁库存物品",SelectRentalItem,false));itemArea.Add(Ui.Button("+ 租赁物品入库",()=>p.Navigation.PushAsync(Order(s,"RENTAL")),false));p.body.Add(Ui.Card(itemArea));
        }
        else if(kind is "PAWN" or "RECYCLE" or "PURCHASE" or "RENTAL" or "STOCK_OTHER" or "CONSIGN") {if(draft?.Items.Count>0)foreach(var item in draft.Items)AddItem(item);else AddItem();itemArea.Add(Ui.Button("+ 添加物品",()=> {AddItem();return Task.CompletedTask;},false));p.body.Add(Ui.Card(itemArea));}
        var today=Display.Day(DateTime.Today);var shop=s.Shop;
        if(kind=="PAWN")
        {
            var rate=s.Db.Query("SELECT * FROM PawnItemTypeRate WHERE enabled=1 ORDER BY sortOrder LIMIT 1").FirstOrDefault()??shop;
            var row=new DbRow { ["pawnDate"]=today,["principal"]="",["periodDays"]=Math.Max(1,rate.Int("defaultPeriodMonths"))*30,["monthlyRate"]=rate.Money("monthlyRate"),["feeRate"]=rate.Money("serviceFeeRate"),["storageFeeRate"]=rate.Money("storageFeeRate"),["storageFixedFee"]=rate.Money("storageFixedFee"),["fixedInterest"]=0,["feeChargeMode"]="PREPAID" };
            if(draft is not null)row=draft.Terms;
            terms=new FieldSet(row,[new("principal","当金(元)",FieldKind.Number,true),new("pawnDate","开当日期",FieldKind.Date,true),new("periodDays","当期(天)",FieldKind.Number,true),new("monthlyRate","月利率(%)",FieldKind.Number,true),new("fixedInterest","整期固定利息(元,0 使用费率)",FieldKind.Number),new("storageFeeRate","月保管费率(%)",FieldKind.Number),new("storageFixedFee","每期固定保管费(元)",FieldKind.Number),new("feeRate","月综合服务费率(%)",FieldKind.Number),new("feeChargeMode","费用收取方式",FieldKind.Choice,true,new(){["PREPAID"]="开当收取",["POSTPAID"]="按期结算"}),new("contractImages","合同照片",FieldKind.Photos),new("remark","备注",FieldKind.Notes)]);
        }
        else if(intake)terms=new FieldSet(new DbRow { ["dealDate"]=today,["payMethod"]="BANK" },[new("dealDate","成交日期",FieldKind.Date,true),new("payMethod","付款方式",FieldKind.Choice,true,PaymentMethods),new("remark","备注",FieldKind.Notes)]);
        else if(kind=="CONSIGN")terms=ConsignmentFields(new DbRow{["kind"]=kind,["amount"]=0,["serviceFeeMode"]="PERCENT",["serviceFeeValue"]=0,["storageFee"]=0,["startDate"]=today,["dueDate"]=Display.Day(DateTime.Today.AddDays(30))},false);
        else if(kind=="RENT")terms=new FieldSet(new DbRow{["kind"]=kind,["deposit"]=0,["startDate"]=today,["dueDate"]=Display.Day(DateTime.Today.AddDays(30))},[new("amount","租金(元)",FieldKind.Number,true),new("deposit","押金(元)",FieldKind.Number),new("startDate","开始日期",FieldKind.Date,true),new("dueDate","约定到期日",FieldKind.Date),new("remark","备注",FieldKind.Notes)]);
        else terms=new FieldSet(new DbRow{["kind"]=kind,["itemType"]="OTHER",["startDate"]=today},[new("itemName","业务名称",Required:true),new("itemType","物品类型",FieldKind.Choice,true,itemTypes),new("amount","金额(元)",FieldKind.Number,true),new("startDate","开始日期",FieldKind.Date,true),new("images","物品照片",FieldKind.Photos),new("remark","备注",FieldKind.Notes)]);
        p.body.Add(Ui.Card(Ui.Stack(Ui.Section("03  结算信息"),terms.View)));
        if(kind=="PAWN")
        {
            void ApplyRate(){var r=s.Db.Query("SELECT * FROM PawnItemTypeRate WHERE itemType=@p0",items[0].RateType).FirstOrDefault()??s.Db.Query("SELECT * FROM PawnItemTypeRate WHERE itemType=@p0",items[0].Type).FirstOrDefault()??s.Shop;foreach(var pair in new[]{("monthlyRate","monthlyRate"),("storageFeeRate","storageFeeRate"),("storageFixedFee","storageFixedFee") ,("feeRate","serviceFeeRate")})if(terms.Inputs[pair.Item1] is Entry e)e.Text=r.Text(pair.Item2);if(terms.Inputs["periodDays"] is Entry period)period.Text=(Math.Max(1,r.Int("defaultPeriodMonths"))*30).ToString();}
            items[0].TypeChanged+=ApplyRate;if(draft is null)ApplyRate();
            var draftAccount=Services.AuthClient.Current.AccountId;var draftId=string.IsNullOrEmpty(draft?.DraftId)?Guid.NewGuid().ToString("N"):draft.DraftId;
            p.draftSnapshot=()=>new Services.OrderCameraDraft(draftAccount,header.Read(false),terms.Read(false),items.Select(item=>item.Read(false)).ToList(),"",DraftId:draftId);
            p.discardDraft=()=>Services.OrderCameraRecovery.Discard(draftAccount,draftId);
            p.CameraDraft=photo=>
            {
                var field=terms.Photos.FirstOrDefault(pair=>ReferenceEquals(pair.Value,photo)).Key;int index=-1;
                if(field is null)for(int i=0;i<items.Count;i++){var itemField=items[i].Photos.FirstOrDefault(pair=>ReferenceEquals(pair.Value,photo)).Key;if(itemField is not null){field=itemField;index=i;break;}}
                return field is null?null:p.draftSnapshot() with{Field=field,ItemIndex=index};
            };
        }
        var preview=Ui.Text("",13,Ui.Bronze);var termsRef=terms;
        p.body.Add(Ui.Button("查看结算预览",()=> {var t=termsRef.Read();if(kind=="PAWN"){var f=Fees.Period(t,t.Int("periodDays"));preview.Text=$"当金 {Display.Money(t.Money("principal"))}\n利息 {Display.Money(f.Interest)} · 保管费 {Display.Money(f.Storage)}\n服务费 {Display.Money(f.Service)} · 当期费用 {Display.Money(f.Total)}\n到期日 {Display.Day(t.Date("pawnDate").AddDays(t.Int("periodDays")))}";}else if(intake)preview.Text="入库实付合计 "+Display.Money(items.Sum(i=>i.Read().Money("amount")));else if(kind=="CONSIGN"){Consignment.ValidateExpected(t.Text("expectedAmount"));preview.Text=ConsignmentPreview(t);}else preview.Text=(kind=="RENT"?"租金 ":"金额 ")+Display.Money(t.Money("amount"));return Task.CompletedTask;},false));p.body.Add(preview);
        p.save=async()=> {var row=termsRef.Read();foreach(var pair in header.Read())row[pair.Key]=pair.Value;var itemRows=kind=="RENT"?rentalItems:items.Select(i=>i.Read()).ToList();if(kind=="RENT" && itemRows.Count==0)throw new ArgumentException("请选择租赁库存中的物品");if(kind=="PAWN")await Task.Run(()=>s.OpenPawn(row,itemRows,row.Int("periodDays")));else if(kind=="RECYCLE")await Task.Run(()=>s.Recycle(row,itemRows));else if(kind=="PURCHASE")await Task.Run(()=>s.PurchaseStock(row,itemRows));else if(kind=="RENTAL")await Task.Run(()=>s.RentalStock(row,itemRows));else if(kind=="STOCK_OTHER")await Task.Run(()=>s.OtherStock(row,itemRows));else await Task.Run(()=>s.Business(row,itemRows.Count>0?itemRows:null));};p.Footer(intake?"确认收货入库":"确认保存");return p;
    }
    public static Dictionary<string,string> PaymentMethods=>new(){["BANK"]="银行转账",["WECHAT"]="微信",["ALIPAY"]="支付宝",["CASH"]="现金",["OTHER"]="其他"};
    private static FieldSet ConsignmentFields(DbRow row,bool settlement)
    {
        var definitions=new List<Field>();
        if(!settlement)definitions.Add(new("expectedAmount","客户期望金额(元)",Required:true,Hint:"例如 4500 或 4500-5000"));
        definitions.AddRange([new("serviceFeeMode","寄卖服务费方式",FieldKind.Choice,true,new(){["PERCENT"]="百分比",["FIXED"]="固定金额"}),new("serviceFeeValue","寄卖服务费",FieldKind.Number,true),new("storageFee","保管费(元)",FieldKind.Number),new("amount","实际成交金额(元)",FieldKind.Number,settlement,Hint:settlement?"填写实际成交金额":"未成交可填 0,成交后再结算"),new("commission","实收服务费(元)",FieldKind.Number)]);
        if(!settlement)definitions.AddRange([new("startDate","开始日期",FieldKind.Date,true),new("dueDate","约定到期日",FieldKind.Date),new("remark","备注",FieldKind.Notes)]);
        var fields=new FieldSet(row,definitions);var actual=(Entry)fields.Inputs["commission"];actual.IsReadOnly=true;
        bool calculating=false;
        void Update()
        {
            if(calculating)return;calculating=true;
            try{var values=fields.Read(false);fields.SetLabel("serviceFeeValue",values.Text("serviceFeeMode")=="PERCENT"?"寄卖服务费比例(%) *":"固定寄卖服务费(元) *");actual.Text=Consignment.Calculate(values).Service.ToString("0.##",CultureInfo.InvariantCulture);}
            catch(ArgumentException){actual.Text="";}
            finally{calculating=false;}
        }
        fields.Changed+=Update;Update();return fields;
    }
    private static string ConsignmentPreview(DbRow row)
    {
        var fees=Consignment.Calculate(row);return row.Money("amount")<=0?"尚未成交,保存后可在详情中录入成交并结算。":$"实际成交 {Display.Money(row.Money("amount"))}\n实收服务费 {Display.Money(fees.Service)} · 保管费 {Display.Money(fees.Storage)}\n支付客户 {Display.Money(fees.Proceeds)}";
    }
    public static FormPage SettleConsignment(BusinessService s,string id)
    {
        var row=s.Db.Find("BusinessOrder",id)??throw new ArgumentException("寄卖单不存在");var p=new FormPage("寄卖成交结算");
        if(row.Text("serviceFeeMode").Length==0){row["serviceFeeMode"]="FIXED";row["serviceFeeValue"]=row.Money("commission");}
        var fields=ConsignmentFields(row,true);
        p.body.Add(Ui.Card(Ui.Stack(Ui.Section(row.Text("itemName")),Ui.Text("客户期望金额:"+row.Text("expectedAmount","未填写"),12,Ui.Muted),fields.View)));
        p.save=async()=>{var values=fields.Read();Consignment.Calculate(values,true);if(!await p.DisplayAlertAsync("确认寄卖结算",ConsignmentPreview(values),"已收款并支付客户","取消"))throw new InvalidOperationException("尚未结算");await Task.Run(()=>s.CompleteBusiness(id,values));};p.Footer("确认成交并结算");return p;
    }
    public static FormPage Renew(BusinessService s,string id)
    {
        var ticket=s.Db.Find("PawnTicket",id)??throw new ArgumentException("当单不存在");var p=new FormPage("续当结算");
        var fields=new FieldSet(new DbRow{["days"]=30,["principal"]=ticket.Money("principal"),["renewDate"]=Display.Day(DateTime.Today)},[new("renewDate","实际续当日期",FieldKind.Date,true),new("days","续当周期(天)",FieldKind.Number,true),new("principal","续当后的当金(元)",FieldKind.Number,true)]);
        var preview=Ui.Text("",14,Ui.Bronze);
        string Preview(DbRow r){var t=new DbRow(ticket);t["principal"]=r.Money("principal");var fees=Fees.Period(t,r.Int("days"),Fees.CurrentPeriod(ticket,s.Db.Query("SELECT * FROM RenewRecord WHERE ticketId=@p0",id)));var difference=r.Money("principal")-ticket.Money("principal");return $"实际续当日期 {Display.Day(r.Date("renewDate"))}\n新到期日 {Display.Day(Fees.RenewDue(ticket.Date("dueDate"),r.Int("days")))}\n本次费用 {Display.Money(fees.Total)}\n"+(difference>0?"增加放款 "+Display.Money(difference):difference<0?"收回本金 "+Display.Money(-difference):"当金不变");}
        p.body.Add(Ui.Card(Ui.Stack(Ui.Section(ticket.Text("ticketNo")),fields.View)));p.body.Add(Ui.Button("预览结算",()=>{preview.Text=Preview(fields.Read());return Task.CompletedTask;},false));p.body.Add(preview);
        p.save=async()=>{var row=fields.Read();if(row.Money("days")%1!=0)throw new ArgumentException("续当天数须为整数");if(!await p.DisplayAlertAsync("确认续当结算",Preview(row),"已结清,续当","取消"))throw new InvalidOperationException("结算已取消,尚未续当");await Task.Run(()=>s.Renew(id,row.Int("days"),row.Money("principal"),row.Date("renewDate")));};p.Footer("办理续当");return p;
    }
    public static FormPage Redeem(BusinessService s,string id)
    {
        var ticket=s.Db.Find("PawnTicket",id)??throw new ArgumentException("当单不存在");var p=new FormPage("赎当结算");
        decimal previousDue=ticket.Money("principal");
        try{previousDue+=s.RedeemPreview(id,DateTime.Today).Total;}catch(ArgumentException){}
        var fields=new FieldSet(new DbRow{["redeemDate"]=Display.Day(DateTime.Today),["paid"]=previousDue},[new("redeemDate","实际赎当日期",FieldKind.Date,true),new("paid","本次实收金额(元)",FieldKind.Number,true)]);
        var preview=Ui.Text("",13,Ui.Bronze);
        void Preview()
        {
            var date=((DatePicker)fields.Inputs["redeemDate"]).Date;if(date is null){preview.Text="请选择实际赎当日期";return;}
            try{var fees=s.RedeemPreview(id,date.Value);decimal due=ticket.Money("principal")+fees.Total;var paid=(Entry)fields.Inputs["paid"];if(decimal.TryParse(paid.Text,NumberStyles.Number,CultureInfo.InvariantCulture,out var value)&&value==previousDue)paid.Text=due.ToString("F2",CultureInfo.InvariantCulture);previousDue=due;preview.Text=$"本金 {Display.Money(ticket.Money("principal"))}\n费用 {Display.Money(fees.Total)}\n应收合计 {Display.Money(due)}";}catch(ArgumentException ex){preview.Text=ex.Message;}
        }
        ((DatePicker)fields.Inputs["redeemDate"]).DateSelected+=(_,_)=>Preview();Preview();
        p.body.Add(Ui.Card(Ui.Stack(Ui.Section(ticket.Text("ticketNo")),fields.View,preview)));
        p.save=async()=>{var row=fields.Read();var date=row.Date("redeemDate");var fees=s.RedeemPreview(id,date);if(!await p.DisplayAlertAsync("确认赎当",$"实际赎当日期 {Display.Day(date)}\n本金 {Display.Money(ticket.Money("principal"))}\n费用 {Display.Money(fees.Total)}\n实收 {Display.Money(row.Money("paid"))}\n确认收款并交还当物?","确认赎当","取消"))throw new InvalidOperationException("结算已取消,尚未赎当");await Task.Run(()=>s.Redeem(id,date,row.Money("paid")));};p.Footer("办理赎当");return p;
    }
    public static FormPage LostRental(BusinessService s,string id)
    {
        var row=s.Db.Find("BusinessOrder",id)??throw new ArgumentException("租赁单不存在");var p=new FormPage("租赁未归还结算");
        var fields=new FieldSet(new DbRow{["saleAmount"]=row.Money("deposit")},[new("saleAmount","设备售价(元)",FieldKind.Number,true)]);
        var preview=Ui.Text("",13,Ui.Bronze);
        void Preview(){var amount=fields.Read(false).Money("saleAmount");preview.Text=$"保留押金 {Display.Money(row.Money("deposit"))}\n押金抵作售价 {Display.Money(row.Money("deposit"))}\n另收售价补款 {Display.Money(Math.Max(0,amount-row.Money("deposit")))}\n另收租金 {Display.Money(row.Money("amount"))}";}
        fields.Changed+=Preview;Preview();p.body.Add(Ui.Card(Ui.Stack(Ui.Section(row.Text("itemName")),Ui.Text("设备未归还,结算后物品记为已售。押金已在开单时记账,此处仅新增补款和租金流水。",12,Ui.Muted),fields.View,preview)));
        p.save=async()=>{var amount=fields.Read().Money("saleAmount");if(!await p.DisplayAlertAsync("确认设备未归还",preview.Text+"\n确认不再等待归还,按售价结算全部租赁物品?","保留押金并结算","取消"))throw new InvalidOperationException("未归还结算已取消");await Task.Run(()=>s.LostRental(id,amount));};p.Footer("确认未归还并按销售结算");return p;
    }
}