using JiDangJia.Core;
using System.Text.Json;
namespace JiDangJia.Views;
public sealed class RecordsPage : ContentPage
{
static RecordsPage(){Names["FundingAccount"]="资方资金";Names["FundingSettlement"]="资金成本结算";}
private readonly BusinessService service;
private readonly string table;
private readonly VerticalStackLayout list=Ui.Stack();
private readonly Entry search=new() {Placeholder="搜索姓名、电话、编号",ClearButtonVisibility=ClearButtonVisibility.WhileEditing,ReturnType=ReturnType.Search};
public RecordsPage(BusinessService service,string table)
{
this.service=service;this.table=table;Title=Names.GetValueOrDefault(table,table);
var body=Ui.Stack(Ui.Card(search,13,new Thickness(12,0),Ui.Paper));body.Padding=18;
if(table=="Customer")body.Add(Ui.Button("+ 新建客户",()=>Navigation.PushAsync(FormPage.Customer(service))));
body.Add(list);Content=new ScrollView {Content=body};search.TextChanged+=(_,_)=>Load();
}
public static readonly Dictionary<string,string> Names=new(){["Customer"]="客户档案",["PawnTicket"]="当单管理",["RecycleOrder"]="入库单",["RecycleItem"]="库存物品",["SaleRecord"]="销售记录",["BusinessOrder"]="业务记录",["FinanceRecord"]="财务流水",["OperationLog"]="操作日志",["Reminder"]="到期提醒",["User"]="员工管理",["PawnItemTypeRate"]="当物类型与费率",["FinanceCategory"]="财务分类",["PrintTemplate"]="打印模板",["Advertisement"]="广告管理"};
protected override void OnAppearing() {base.OnAppearing();Load();}
private void Load()
{
list.Clear();
var records=table=="Reminder"?service.Db.Query("SELECT * FROM PawnTicket WHERE status='ACTIVE' AND deletedAt IS NULL AND dueDate<=@p0 ORDER BY dueDate",Display.Day(DateTime.Today.AddDays(7))):service.Db.Query($"SELECT * FROM [{table}] ORDER BY createdAt DESC");
records=records.Where(r=>r.Text("deletedAt").Length==0&&(string.IsNullOrWhiteSpace(search.Text)||r.Values.Any(v=>v?.ToString()?.Contains(search.Text,StringComparison.OrdinalIgnoreCase)==true))).ToList();
list.Add(Ui.Text(records.Count+" 条记录",12,Ui.Muted));
if(records.Count==0){list.Add(Ui.Card(Ui.Empty("暂无记录","有新数据时会在这里显示")));return;}
foreach(var record in records)
{
string name=record.Text("name",record.Text("itemName",record.Text("label",record.Text("targetName",service.Category(record.Text("category"))))));
string code=record.Text("code",record.Text("ticketNo",record.Text("orderNo",record.Text("saleNo",record.Text("occurDate",record.Text("createdAt"))))));
var main=Ui.Stack(Ui.Text(name,17,bold:true),Ui.Text(code,11,Ui.Muted));
var amount=record.Money("principal")!=0?record.Money("principal"):record.Money("amount");
var title=Ui.Spread(main,table=="Customer"?Ui.Pill(record.Int("isBlacklist")==1?"黑名单":record.Text("type")=="COMPANY"?"企业":"个人"):amount!=0?Ui.Text(Display.Money(amount),17,Ui.Bronze,true):Ui.Icon("chevron",Ui.Muted,15));
var body=Ui.Stack(title);
if(table=="Customer") {body.Add(Ui.Text(record.Text("phone","未填写电话"),12,Ui.Muted));var stats=service.Db.Query("SELECT COUNT(*) cnt,COALESCE(SUM(principal),0) total FROM PawnTicket WHERE customerId=@p0 AND deletedAt IS NULL",record.Id).First();body.Add(Ui.Divider());body.Add(Ui.Spread(Ui.Text($"累计 {stats.Text("cnt")} 笔 · {Display.Money(stats.Money("total"))}",12,Ui.Bronze),Ui.Button("+ 开当",()=>Navigation.PushAsync(FormPage.Order(service,"PAWN",record.Id)))));}
if(table=="Reminder")body.Add(Ui.Text("到期 "+record.Text("dueDate")+" · "+record.Text("customerName"),12,record.Date("dueDate")<DateTime.Today?Ui.Red:Ui.Bronze));
string target=table=="Reminder"?"PawnTicket":table;
var card=Ui.Card(body);if(target=="PawnTicket"){var state=PawnState.State(record,DateTime.Today,service.Shop.Int("nearExpireDays"));body.Add(Ui.Text(Display.Status(state),12,state=="OVERDUE"?Ui.Red:state=="DEAD"?Ui.Ink:Ui.Bronze));Ui.PawnOutline(card,state);}list.Add(Ui.Tap(card,()=>Navigation.PushAsync(new DetailPage(service,target,record.Id))));
}
}
}
public sealed class DetailPage : ContentPage
{
private readonly BusinessService service;
private readonly string table,id;
private readonly VerticalStackLayout body=Ui.Stack();
public DetailPage(BusinessService service,string table,string id)
{
this.service=service;this.table=table;this.id=id;Title=RecordsPage.Names.GetValueOrDefault(table,"记录")+"详情";body.Padding=18;Content=new ScrollView {Content=body};
}
protected override void OnAppearing() {base.OnAppearing();Load();}
private void Load()
{
body.Clear();var row=service.Db.Find(table,id);if(row is null) {body.Add(Ui.Empty("记录不存在","记录可能已经被删除"));return;}
if(row.Text("deletedAt").Length>0){body.Add(Ui.Empty("记录在回收站中","请在设置的回收站中还原或永久删除"));return;}
var state=table=="PawnTicket"?PawnState.State(row,DateTime.Today,service.Shop.Int("nearExpireDays")):row.Text("status");
if(table=="RecycleItem" && row.Text("source")=="RENTAL")state=RentalInventory.Status(row,service.Db.Find("BusinessOrder",row.Text("rentalOrderId")),DateTime.Today);
var name=row.Text("name",row.Text("itemName",row.Text("label",row.Text("title",RecordsPage.Names.GetValueOrDefault(table,"记录")))));
var head=Ui.Stack(Ui.Spread(Ui.Text(name,24,bold:true),Ui.Pill(Display.Status(state),state=="OVERDUE"?Ui.Red:Ui.Bronze)),Ui.Text(row.Text("ticketNo",row.Text("orderNo",row.Text("saleNo",row.Text("code")))),12,Ui.Muted));
if(row.Money("principal")>0||row.Money("amount")>0)head.Add(Ui.Text(Display.Money(row.Money(table=="PawnTicket"?"principal":"amount")),30,Ui.Bronze,true));body.Add(Ui.Card(head));
var details=Ui.Stack(Ui.Section("详细信息"));
foreach(var pair in row)
{
if(pair.Value is null || !FieldLabels.TryGetValue(pair.Key,out var label)||pair.Key is "images" or "contractImages" or "idCardImages" || row.Text(pair.Key).Length==0)continue;
if(table=="BusinessOrder" && (row.Text("kind")=="CONSIGN"&&pair.Key=="deposit" || row.Text("kind")!="CONSIGN"&&pair.Key is "commission" or "serviceFeeMode" or "serviceFeeValue" or "storageFee" or "expectedAmount"))continue;
if(table=="RecycleItem" && row.Text("type") is "PROPERTY" or "VEHICLE" && pair.Key is "weight" or "spec" or "unitPrice")continue;
if(table=="BusinessOrder" && row.Text("kind")=="CONSIGN")label=pair.Key switch{"amount"=>"实际成交金额","commission"=>"实收服务费","serviceFeeValue"=>row.Text("serviceFeeMode")=="PERCENT"?"服务费比例(%)":"固定服务费(元)",_=>label};
string value=pair.Key switch {"majorClass"=>FinanceAnalytics.Majors.GetValueOrDefault(row.Text(pair.Key),row.Text(pair.Key)),"detailClass"=>FinanceAnalytics.DetailLabel(row.Text("majorClass"),row.Text(pair.Key)),"type" when table=="Customer"=>row.Text("type")=="COMPANY"?"企业":"个人","type" when table=="FinanceRecord"=>row.Text("type")=="INCOME"?"收入":"支出","itemType" or "type"=>service.ItemType(row.Text(pair.Key)),"material" when ItemCatalog.Category(row.Text("type"))=="METAL"=>ItemCatalog.MaterialName(row.Text(pair.Key)),"serviceFeeMode"=>row.Text(pair.Key)=="PERCENT"?"百分比":"固定金额","status"=>Display.Status(state),"category"=>service.Category(row.Text(pair.Key)),"action"=>Display.Action(row.Text(pair.Key)),"creditLevel"=>new Dictionary<string,string>{{"NORMAL","普通"},{"GOOD","良好"},{"VIP","VIP"},{"RISK","风险"}}.GetValueOrDefault(row.Text(pair.Key),row.Text(pair.Key)),"isBlacklist"=>row.Int(pair.Key)==1?"是":"否","createdAt" or "updatedAt"=>row.Date(pair.Key).ToLocalTime().ToString("yyyy-MM-dd HH:mm"),"payMethod"=>FormPage.PaymentMethods.GetValueOrDefault(row.Text(pair.Key),row.Text(pair.Key)),"feeChargeMode"=>row.Text(pair.Key)=="POSTPAID"?"按期结算":"开当收取",_=>row.Text(pair.Key)};
details.Add(Ui.Spread(Ui.Text(label,12,Ui.Muted),Ui.Text(value,13)));details.Add(Ui.Divider());
}
AddAttributes(details,row);body.Add(Ui.Card(details));
foreach(var key in new[]{"images","contractImages","idCardImages"})
{
var photos=MediaFiles.Parse(row.Text(key));if(photos.Count==0)continue;var images=Ui.Stack(Ui.Section(FieldLabels[key]));
foreach(var photo in photos)try {images.Add(new Image {Source=MediaFiles.Source(photo),Aspect=Aspect.AspectFit,HeightRequest=220});}catch {images.Add(Ui.Text("此图片来自旧设备,需同时迁移原图片文件",12,Ui.Muted));}body.Add(Ui.Card(images));
}
if(table=="FinanceRecord"&&row.Text("category")=="FUNDING_COST")
{
var settlement=service.Db.Find("FundingSettlement",row.Text("ticketId"));
if(settlement is not null)body.Add(Ui.Button("查看关联资方资金",()=>Navigation.PushAsync(new FundingDetailPage(service,settlement.Text("accountId"))),false));
}
if(table=="Customer")
{
body.Add(Ui.Button("编辑客户",()=>Navigation.PushAsync(FormPage.Customer(service,id))));body.Add(Ui.Button("+ 新建当单",()=>Navigation.PushAsync(FormPage.Order(service,"PAWN",id)),false));
body.Add(Ui.Button("+ 新建回收单",()=>Navigation.PushAsync(FormPage.Order(service,"RECYCLE",id)),false));
var history=service.Db.Query("SELECT * FROM PawnTicket WHERE customerId=@p0 AND deletedAt IS NULL ORDER BY createdAt DESC",id);var items=Ui.Stack(Ui.Section("客户业务记录"));
foreach(var ticket in history) items.Add(Ui.Tap(Ui.Spread(Ui.Text(ticket.Text("ticketNo")+" · "+Display.Status(ticket.Text("status")),12),Ui.Text(Display.Money(ticket.Money("principal")),14,Ui.Bronze)),()=>Navigation.PushAsync(new DetailPage(service,"PawnTicket",ticket.Id))));body.Add(Ui.Card(items));
}
if(table is "PawnTicket" or "RecycleOrder")
{
var parts=table=="PawnTicket"?service.Db.Query("SELECT * FROM PawnItem WHERE ticketId=@p0",id):service.Db.Query("SELECT * FROM RecycleItem WHERE orderId=@p0",id);
var items=Ui.Stack(Ui.Section("物品清单"));foreach(var item in parts)AddItem(items,item);body.Add(Ui.Card(items));
body.Add(Ui.Button("预览 / 打印凭证",()=>Navigation.PushAsync(new PrintPage(service,table,row)),false));
}
if(table=="BusinessOrder" && row.Text("items").Length>0)
{
var parts=Ui.Stack(Ui.Section(row.Text("kind")=="RENT"?"租赁物品清单":"寄卖物品清单"));
try{using var doc=JsonDocument.Parse(row.Text("items"));foreach(var item in doc.RootElement.EnumerateArray())AddItem(parts,DbRow.FromJson(item));}catch(JsonException){parts.Add(Ui.Text("物品资料读取失败",12,Ui.Muted));}
body.Add(Ui.Card(parts));
}
if(table=="PawnTicket"&&row.Text("status")=="ACTIVE")
{
body.Add(Ui.Button("续当",()=>Navigation.PushAsync(FormPage.Renew(service,id))));
body.Add(Ui.Button("赎当结算",()=>Navigation.PushAsync(FormPage.Redeem(service,id)),false));
body.Add(Ui.Button("绝当转入库存",async()=> {if(await DisplayAlertAsync("确认绝当","当物将进入库存,可上架销售。","转入库存","取消")){await Task.Run(()=>service.Dead(id));Load();}},false));
}
if(table=="PawnTicket")
{
var logs=service.Db.Query("SELECT * FROM OperationLog WHERE targetId=@p0 ORDER BY createdAt DESC",id);var renews=service.Db.Query("SELECT * FROM RenewRecord WHERE ticketId=@p0 ORDER BY createdAt DESC",id);
var timeline=Ui.Stack(Ui.Section("业务轨迹"));foreach(var r in renews)timeline.Add(Ui.Text($"{r.Text("renewDate")} 续当 · 费用 {Display.Money(r.Money("fee"))} · 到期 {r.Text("newDueDate")}",12,Ui.Muted));foreach(var r in service.Db.Query("SELECT * FROM RedeemRecord WHERE ticketId=@p0 ORDER BY createdAt DESC",id))timeline.Add(Ui.Text($"{r.Text("redeemDate")} 赎当 · 实收 {Display.Money(r.Money("paid"))}",12,Ui.Muted));foreach(var log in logs)timeline.Add(Ui.Text(log.Text("createdAt")[..Math.Min(10,log.Text("createdAt").Length)]+" "+log.Text("action"),12,Ui.Muted));body.Add(Ui.Card(timeline));
}
if(table=="RecycleItem" && row.Text("source")=="RENTAL" && row.Int("costKnown")==0)
{
body.Add(Ui.Text("旧租赁物品缺少购货成本,销售结算前请补录,避免利润失真。",12,Ui.Red));
body.Add(Ui.Button("补录历史购货成本",async()=>{var text=await DisplayPromptAsync("补录购货成本","仅补录历史成本,不新增当前现金支出。赠送物品可填 0。","保存","取消",keyboard:Keyboard.Numeric);if(text is null)return;if(!decimal.TryParse(text,out var cost))throw new ArgumentException("请填写正确的成本");await Task.Run(()=>service.SetRentalCost(id,cost));Load();},false));
}
if(table=="RecycleItem"&&row.Text("status") is "IN_STOCK" or "LISTED")
{
if(row.Text("source")=="RENTAL")body.Add(Ui.Button("出租此物品",()=>Navigation.PushAsync(FormPage.Order(service,"RENT",stockId:id))));
body.Add(Ui.Button("上架 / 修改标价",async()=> {var text=await DisplayPromptAsync("上架销售","请输入销售标价(元)","确认","取消",keyboard:Keyboard.Numeric,initialValue:row.Money("listedPrice").ToString("F2"));if(text is null)return;if(!decimal.TryParse(text,out var value))throw new ArgumentException("请输入正确价格");await Task.Run(()=>service.ListStock(id,value));Load();},false));
body.Add(Ui.Button("销售出库",async()=> {var text=await DisplayPromptAsync("销售出库","请输入实际成交价(元)","下一步","取消",keyboard:Keyboard.Numeric,initialValue:row.Money("listedPrice").ToString("F2"));if(text is null)return;if(!decimal.TryParse(text,out var value))throw new ArgumentException("请输入正确价格");if(await DisplayAlertAsync("确认销售",$"成交 {Display.Money(value)}\n成本 {Display.Money(row.Money("amount"))}\n利润 {Display.Money(value-row.Money("amount"))}","确认收款","取消")){await Task.Run(()=>service.Sell(id,value,"","CASH",DateTime.Today));Load();}}));
}
if(table=="RecycleItem" && row.Text("status")=="RENTED")
{
body.Add(Ui.Text(state=="OVERDUE_RENT"?"逾期未归还,可在租赁单中办理归还或销售结算。":"租赁中,暂不能销售或再次出租。",13,state=="OVERDUE_RENT"?Ui.Red:Ui.Muted));
body.Add(Ui.Button("查看当前租赁单",()=>Navigation.PushAsync(new DetailPage(service,"BusinessOrder",row.Text("rentalOrderId"))),false));
if(state=="OVERDUE_RENT")body.Add(Ui.Button("未归还,按销售结算",()=>Navigation.PushAsync(FormPage.LostRental(service,row.Text("rentalOrderId")))));
}
if(table=="BusinessOrder"&&row.Text("status")=="OPEN")
{
if(row.Text("kind")=="CONSIGN")body.Add(Ui.Button("录入成交并结算",()=>Navigation.PushAsync(FormPage.SettleConsignment(service,id))));
else body.Add(Ui.Button(row.Text("kind")=="RENT"?"归还并结算":"完成业务",async()=> {var text=row.Text("kind")=="RENT"?$"收取租金 {Display.Money(row.Money("amount"))},退还押金 {Display.Money(row.Money("deposit"))}。":"确认本次款项已收妥?";if(await DisplayAlertAsync("确认结算",text,"确认已结清","取消")){service.CompleteBusiness(id);Load();}}));
if(row.Text("kind")=="RENT" && RentalInventory.IsOverdue(row,DateTime.Today))body.Add(Ui.Button("未归还,保留押金并按销售结算",()=>Navigation.PushAsync(FormPage.LostRental(service,id)),false));
body.Add(Ui.Button("作废业务",async()=>{if(await DisplayAlertAsync("确认作废","确认已归还物品和押金?作废后保留业务记录。","确认作废","取消")){service.VoidBusiness(id);Load();}},false));
}
if(table=="SaleRecord"&&row.Text("status")=="DONE")body.Add(Ui.Button("作废销售并退回库存",async()=>{if(await DisplayAlertAsync("确认作废","确认已退回成交款?物品将恢复在库,退款保留在财务流水中。","已退款,作废","取消")){await Task.Run(()=>service.VoidSale(id));Load();}},false));
if(table=="RecycleOrder"&&row.Text("status")=="DONE")body.Add(Ui.Button("作废入库单",async()=>{if(await DisplayAlertAsync("确认作废","仅允许整单物品仍在库或在售时作废。确认已退回物品并收回入库款?","确认作废","取消")){await Task.Run(()=>service.VoidIntake(id));Load();}},false));
if(table=="RecycleItem"&&row.Text("orderId").Length>0)body.Add(Ui.Button("查看来源入库单",()=>Navigation.PushAsync(new DetailPage(service,"RecycleOrder",row.Text("orderId"))),false));
if(table=="RecycleItem"&&row.Text("status")=="SOLD"){var sale=service.Db.Query("SELECT id FROM SaleRecord WHERE itemId=@p0 AND status='DONE' AND deletedAt IS NULL ORDER BY createdAt DESC LIMIT 1",id).FirstOrDefault();if(sale is not null)body.Add(Ui.Button("查看销售记录",()=>Navigation.PushAsync(new DetailPage(service,"SaleRecord",sale.Id)),false));}
if(RecordLifecycle.Tables.Contains(table))body.Add(Ui.Button("移入回收站",async()=> {if(await DisplayAlertAsync("移入回收站","该记录及关联业务、库存和流水将退出正常列表与统计,可在回收站一起还原。此操作不代表退款或实物归还。","移入","取消")){await Task.Run(()=>service.SoftDelete(table,id));await Navigation.PopAsync();}},false));
var phone=row.Text("customerPhone",row.Text("phone"));if(phone.Length>0)body.Add(Ui.Button("拨打客户电话",()=> {PhoneDialer.Default.Open(phone);return Task.CompletedTask;},false));
}
private void AddItem(VerticalStackLayout target,DbRow item)
{
target.Add(Ui.Text(item.Text("name")+" · "+ItemCatalog.ItemName(item),15,bold:true));
foreach(var key in new[]{"itemCode","weight","spec","unitPrice","brand","serialNo","accessory","amount","quotedPrice","appraisalPrice"})
{
if(item.Text(key).Length==0 || item.Text("type") is "PROPERTY" or "VEHICLE" && key is "weight" or "spec" or "unitPrice")continue;
if(key is "amount" or "appraisalPrice" && item.Money(key)==0)continue;
target.Add(Ui.Spread(Ui.Text(FieldLabels[key],12,Ui.Muted),Ui.Text(item.Text(key),13)));
}
AddAttributes(target,item);
foreach(var photo in MediaFiles.Parse(item.Text("images")))try{target.Add(new Image{Source=MediaFiles.Source(photo),HeightRequest=150,Aspect=Aspect.AspectFit});}catch{ }
target.Add(Ui.Divider());
}
private void AddAttributes(VerticalStackLayout target,DbRow row)
{
if(string.IsNullOrWhiteSpace(row.Text("extraAttributes")))return;
var labels=new Dictionary<string,string>(ItemCatalog.AttributeLabels);
var config=service.Db.Query("SELECT attributes FROM PawnItemTypeRate WHERE itemType=@p0",row.Text("type")).FirstOrDefault()?.Text("attributes");
if(!string.IsNullOrWhiteSpace(config))try
{
using var definition=JsonDocument.Parse(config);
if(definition.RootElement.ValueKind==JsonValueKind.Array)foreach(var attr in definition.RootElement.EnumerateArray())
if(attr.ValueKind==JsonValueKind.Object&&attr.TryGetProperty("key",out var key))labels[key.ToString()]=attr.TryGetProperty("label",out var label)?label.ToString():key.ToString();
}catch(JsonException){}
try
{
using var values=JsonDocument.Parse(row.Text("extraAttributes"));if(values.RootElement.ValueKind!=JsonValueKind.Object)return;
foreach(var value in values.RootElement.EnumerateObject())if(value.Value.ValueKind!=JsonValueKind.Null&&value.Value.ToString().Length>0)
target.Add(Ui.Spread(Ui.Text(labels.GetValueOrDefault(value.Name,value.Name),12,Ui.Muted),Ui.Text(value.Value.ToString(),13)));
}catch(JsonException){}
}
public static readonly Dictionary<string,string> FieldLabels=new()
{
["majorClass"]="财务大类",["detailClass"]="财务细类",["saleAmount"]="未归还设备售价",
["material"]="物品材质",["unitPrice"]="入库单价(元/克)",["itemCode"]="物品编码",["expectedAmount"]="客户期望金额",["serviceFeeMode"]="寄卖服务费方式",["serviceFeeValue"]="寄卖服务费",["storageFee"]="保管费(元)",
["name"]="名称",["customerName"]="客户姓名",["customerPhone"]="联系电话",["phone"]="联系电话",["gender"]="性别",["idCard"]="证件号码",["address"]="地址",["type"]="类型",["creditLevel"]="信用等级",["isBlacklist"]="黑名单",["tags"]="标签",["itemType"]="物品类型",["itemName"]="物品名称",["principal"]="当金",["appraisalPrice"]="估价",["weight"]="重量(克)",["spec"]="规格 / 成色",["brand"]="品牌",["serialNo"]="序列号",["accessory"]="附件",["monthlyRate"]="月利率(%)",["feeRate"]="综合费率(%)",["storageFeeRate"]="保管费率(%)",["storageFixedFee"]="固定保管费",["fixedInterest"]="固定利息",["pawnDate"]="开当日期",["dueDate"]="到期日期",["status"]="状态",["feeChargeMode"]="收费方式",["remark"]="备注",["amount"]="金额",["cost"]="成本",["profit"]="利润",["quotedPrice"]="客户报价",["dealDate"]="成交日期",["payMethod"]="付款方式",["occurDate"]="发生日期",["category"]="财务分类",["listedPrice"]="销售标价",["soldAt"]="售出日期",["startDate"]="开始日期",["endDate"]="结束日期",["commission"]="佣金",["deposit"]="押金",["images"]="物品照片",["contractImages"]="合同照片",["idCardImages"]="证件照片",["createdAt"]="创建时间",["updatedAt"]="更新时间",["action"]="操作",["targetType"]="记录类型",["targetName"]="记录名称",["detail"]="操作内容",["role"]="角色",["baseSalary"]="基本工资",["content"]="模板内容"
};
}
using JiDangJia.Core;
using System.Text.Json;
namespace JiDangJia.Views;
public sealed class RecordsPage : ContentPage
{
static RecordsPage(){Names["FundingAccount"]="资方资金";Names["FundingSettlement"]="资金成本结算";}
private readonly BusinessService service;
private readonly string table;
private readonly VerticalStackLayout list=Ui.Stack();
private readonly Entry search=new() {Placeholder="搜索姓名、电话、编号",ClearButtonVisibility=ClearButtonVisibility.WhileEditing,ReturnType=ReturnType.Search};
public RecordsPage(BusinessService service,string table)
{
this.service=service;this.table=table;Title=Names.GetValueOrDefault(table,table);
var body=Ui.Stack(Ui.Card(search,13,new Thickness(12,0),Ui.Paper));body.Padding=18;
if(table=="Customer")body.Add(Ui.Button("+ 新建客户",()=>Navigation.PushAsync(FormPage.Customer(service))));
body.Add(list);Content=new ScrollView {Content=body};search.TextChanged+=(_,_)=>Load();
}
public static readonly Dictionary<string,string> Names=new(){["Customer"]="客户档案",["PawnTicket"]="当单管理",["RecycleOrder"]="入库单",["RecycleItem"]="库存物品",["SaleRecord"]="销售记录",["BusinessOrder"]="业务记录",["FinanceRecord"]="财务流水",["OperationLog"]="操作日志",["Reminder"]="到期提醒",["User"]="员工管理",["PawnItemTypeRate"]="当物类型与费率",["FinanceCategory"]="财务分类",["PrintTemplate"]="打印模板",["Advertisement"]="广告管理"};
protected override void OnAppearing() {base.OnAppearing();Load();}
private void Load()
{
list.Clear();
var records=table=="Reminder"?service.Db.Query("SELECT * FROM PawnTicket WHERE status='ACTIVE' AND deletedAt IS NULL AND dueDate<=@p0 ORDER BY dueDate",Display.Day(DateTime.Today.AddDays(7))):service.Db.Query($"SELECT * FROM [{table}] ORDER BY createdAt DESC");
records=records.Where(r=>r.Text("deletedAt").Length==0&&(string.IsNullOrWhiteSpace(search.Text)||r.Values.Any(v=>v?.ToString()?.Contains(search.Text,StringComparison.OrdinalIgnoreCase)==true))).ToList();
list.Add(Ui.Text(records.Count+" 条记录",12,Ui.Muted));
if(records.Count==0){list.Add(Ui.Card(Ui.Empty("暂无记录","有新数据时会在这里显示")));return;}
foreach(var record in records)
{
string name=record.Text("name",record.Text("itemName",record.Text("label",record.Text("targetName",service.Category(record.Text("category"))))));
string code=record.Text("code",record.Text("ticketNo",record.Text("orderNo",record.Text("saleNo",record.Text("occurDate",record.Text("createdAt"))))));
var main=Ui.Stack(Ui.Text(name,17,bold:true),Ui.Text(code,11,Ui.Muted));
var amount=record.Money("principal")!=0?record.Money("principal"):record.Money("amount");
var title=Ui.Spread(main,table=="Customer"?Ui.Pill(record.Int("isBlacklist")==1?"黑名单":record.Text("type")=="COMPANY"?"企业":"个人"):amount!=0?Ui.Text(Display.Money(amount),17,Ui.Bronze,true):Ui.Icon("chevron",Ui.Muted,15));
var body=Ui.Stack(title);
if(table=="Customer") {body.Add(Ui.Text(record.Text("phone","未填写电话"),12,Ui.Muted));var stats=service.Db.Query("SELECT COUNT(*) cnt,COALESCE(SUM(principal),0) total FROM PawnTicket WHERE customerId=@p0 AND deletedAt IS NULL",record.Id).First();body.Add(Ui.Divider());body.Add(Ui.Spread(Ui.Text($"累计 {stats.Text("cnt")} 笔 · {Display.Money(stats.Money("total"))}",12,Ui.Bronze),Ui.Button("+ 开当",()=>Navigation.PushAsync(FormPage.Order(service,"PAWN",record.Id)))));}
if(table=="Reminder")body.Add(Ui.Text("到期 "+record.Text("dueDate")+" · "+record.Text("customerName"),12,record.Date("dueDate")<DateTime.Today?Ui.Red:Ui.Bronze));
string target=table=="Reminder"?"PawnTicket":table;
var card=Ui.Card(body);if(target=="PawnTicket"){var state=PawnState.State(record,DateTime.Today,service.Shop.Int("nearExpireDays"));body.Add(Ui.Text(Display.Status(state),12,state=="OVERDUE"?Ui.Red:state=="DEAD"?Ui.Ink:Ui.Bronze));Ui.PawnOutline(card,state);}list.Add(Ui.Tap(card,()=>Navigation.PushAsync(new DetailPage(service,target,record.Id))));
}
}
}
public sealed class DetailPage : ContentPage
{
private readonly BusinessService service;
private readonly string table,id;
private readonly VerticalStackLayout body=Ui.Stack();
public DetailPage(BusinessService service,string table,string id)
{
this.service=service;this.table=table;this.id=id;Title=RecordsPage.Names.GetValueOrDefault(table,"记录")+"详情";body.Padding=18;Content=new ScrollView {Content=body};
}
protected override void OnAppearing() {base.OnAppearing();Load();}
private void Load()
{
body.Clear();var row=service.Db.Find(table,id);if(row is null) {body.Add(Ui.Empty("记录不存在","记录可能已经被删除"));return;}
if(row.Text("deletedAt").Length>0){body.Add(Ui.Empty("记录在回收站中","请在设置的回收站中还原或永久删除"));return;}
var state=table=="PawnTicket"?PawnState.State(row,DateTime.Today,service.Shop.Int("nearExpireDays")):row.Text("status");
if(table=="RecycleItem" && row.Text("source")=="RENTAL")state=RentalInventory.Status(row,service.Db.Find("BusinessOrder",row.Text("rentalOrderId")),DateTime.Today);
var name=row.Text("name",row.Text("itemName",row.Text("label",row.Text("title",RecordsPage.Names.GetValueOrDefault(table,"记录")))));
var head=Ui.Stack(Ui.Spread(Ui.Text(name,24,bold:true),Ui.Pill(Display.Status(state),state=="OVERDUE"?Ui.Red:Ui.Bronze)),Ui.Text(row.Text("ticketNo",row.Text("orderNo",row.Text("saleNo",row.Text("code")))),12,Ui.Muted));
if(row.Money("principal")>0||row.Money("amount")>0)head.Add(Ui.Text(Display.Money(row.Money(table=="PawnTicket"?"principal":"amount")),30,Ui.Bronze,true));body.Add(Ui.Card(head));
var details=Ui.Stack(Ui.Section("详细信息"));
foreach(var pair in row)
{
if(pair.Value is null || !FieldLabels.TryGetValue(pair.Key,out var label)||pair.Key is "images" or "contractImages" or "idCardImages" || row.Text(pair.Key).Length==0)continue;
if(table=="BusinessOrder" && (row.Text("kind")=="CONSIGN"&&pair.Key=="deposit" || row.Text("kind")!="CONSIGN"&&pair.Key is "commission" or "serviceFeeMode" or "serviceFeeValue" or "storageFee" or "expectedAmount"))continue;
if(table=="RecycleItem" && row.Text("type") is "PROPERTY" or "VEHICLE" && pair.Key is "weight" or "spec" or "unitPrice")continue;
if(table=="BusinessOrder" && row.Text("kind")=="CONSIGN")label=pair.Key switch{"amount"=>"实际成交金额","commission"=>"实收服务费","serviceFeeValue"=>row.Text("serviceFeeMode")=="PERCENT"?"服务费比例(%)":"固定服务费(元)",_=>label};
string value=pair.Key switch {"majorClass"=>FinanceAnalytics.Majors.GetValueOrDefault(row.Text(pair.Key),row.Text(pair.Key)),"detailClass"=>FinanceAnalytics.DetailLabel(row.Text("majorClass"),row.Text(pair.Key)),"type" when table=="Customer"=>row.Text("type")=="COMPANY"?"企业":"个人","type" when table=="FinanceRecord"=>row.Text("type")=="INCOME"?"收入":"支出","itemType" or "type"=>service.ItemType(row.Text(pair.Key)),"material" when ItemCatalog.Category(row.Text("type"))=="METAL"=>ItemCatalog.MaterialName(row.Text(pair.Key)),"serviceFeeMode"=>row.Text(pair.Key)=="PERCENT"?"百分比":"固定金额","status"=>Display.Status(state),"category"=>service.Category(row.Text(pair.Key)),"action"=>Display.Action(row.Text(pair.Key)),"creditLevel"=>new Dictionary<string,string>{{"NORMAL","普通"},{"GOOD","良好"},{"VIP","VIP"},{"RISK","风险"}}.GetValueOrDefault(row.Text(pair.Key),row.Text(pair.Key)),"isBlacklist"=>row.Int(pair.Key)==1?"是":"否","createdAt" or "updatedAt"=>row.Date(pair.Key).ToLocalTime().ToString("yyyy-MM-dd HH:mm"),"payMethod"=>FormPage.PaymentMethods.GetValueOrDefault(row.Text(pair.Key),row.Text(pair.Key)),"feeChargeMode"=>row.Text(pair.Key)=="POSTPAID"?"按期结算":"开当收取",_=>row.Text(pair.Key)};
details.Add(Ui.Spread(Ui.Text(label,12,Ui.Muted),Ui.Text(value,13)));details.Add(Ui.Divider());
}
AddAttributes(details,row);body.Add(Ui.Card(details));
foreach(var key in new[]{"images","contractImages","idCardImages"})
{
var photos=MediaFiles.Parse(row.Text(key));if(photos.Count==0)continue;var images=Ui.Stack(Ui.Section(FieldLabels[key]));
foreach(var photo in photos)try {images.Add(new Image {Source=MediaFiles.Source(photo),Aspect=Aspect.AspectFit,HeightRequest=220});}catch {images.Add(Ui.Text("此图片来自旧设备,需同时迁移原图片文件",12,Ui.Muted));}body.Add(Ui.Card(images));
}
if(table=="FinanceRecord"&&row.Text("category")=="FUNDING_COST")
{
var settlement=service.Db.Find("FundingSettlement",row.Text("ticketId"));
if(settlement is not null)body.Add(Ui.Button("查看关联资方资金",()=>Navigation.PushAsync(new FundingDetailPage(service,settlement.Text("accountId"))),false));
}
if(table=="Customer")
{
body.Add(Ui.Button("编辑客户",()=>Navigation.PushAsync(FormPage.Customer(service,id))));body.Add(Ui.Button("+ 新建当单",()=>Navigation.PushAsync(FormPage.Order(service,"PAWN",id)),false));
body.Add(Ui.Button("+ 新建回收单",()=>Navigation.PushAsync(FormPage.Order(service,"RECYCLE",id)),false));
var history=service.Db.Query("SELECT * FROM PawnTicket WHERE customerId=@p0 AND deletedAt IS NULL ORDER BY createdAt DESC",id);var items=Ui.Stack(Ui.Section("客户业务记录"));
foreach(var ticket in history) items.Add(Ui.Tap(Ui.Spread(Ui.Text(ticket.Text("ticketNo")+" · "+Display.Status(ticket.Text("status")),12),Ui.Text(Display.Money(ticket.Money("principal")),14,Ui.Bronze)),()=>Navigation.PushAsync(new DetailPage(service,"PawnTicket",ticket.Id))));body.Add(Ui.Card(items));
}
if(table is "PawnTicket" or "RecycleOrder")
{
var parts=table=="PawnTicket"?service.Db.Query("SELECT * FROM PawnItem WHERE ticketId=@p0",id):service.Db.Query("SELECT * FROM RecycleItem WHERE orderId=@p0",id);
var items=Ui.Stack(Ui.Section("物品清单"));foreach(var item in parts)AddItem(items,item);body.Add(Ui.Card(items));
body.Add(Ui.Button("预览 / 打印凭证",()=>Navigation.PushAsync(new PrintPage(service,table,row)),false));
}
if(table=="BusinessOrder" && row.Text("items").Length>0)
{
var parts=Ui.Stack(Ui.Section(row.Text("kind")=="RENT"?"租赁物品清单":"寄卖物品清单"));
try{using var doc=JsonDocument.Parse(row.Text("items"));foreach(var item in doc.RootElement.EnumerateArray())AddItem(parts,DbRow.FromJson(item));}catch(JsonException){parts.Add(Ui.Text("物品资料读取失败",12,Ui.Muted));}
body.Add(Ui.Card(parts));
}
if(table=="PawnTicket"&&row.Text("status")=="ACTIVE")
{
body.Add(Ui.Button("续当",()=>Navigation.PushAsync(FormPage.Renew(service,id))));
body.Add(Ui.Button("赎当结算",()=>Navigation.PushAsync(FormPage.Redeem(service,id)),false));
body.Add(Ui.Button("绝当转入库存",async()=> {if(await DisplayAlertAsync("确认绝当","当物将进入库存,可上架销售。","转入库存","取消")){await Task.Run(()=>service.Dead(id));Load();}},false));
}
if(table=="PawnTicket")
{
var logs=service.Db.Query("SELECT * FROM OperationLog WHERE targetId=@p0 ORDER BY createdAt DESC",id);var renews=service.Db.Query("SELECT * FROM RenewRecord WHERE ticketId=@p0 ORDER BY createdAt DESC",id);
var timeline=Ui.Stack(Ui.Section("业务轨迹"));foreach(var r in renews)timeline.Add(Ui.Text($"{r.Text("renewDate")} 续当 · 费用 {Display.Money(r.Money("fee"))} · 到期 {r.Text("newDueDate")}",12,Ui.Muted));foreach(var r in service.Db.Query("SELECT * FROM RedeemRecord WHERE ticketId=@p0 ORDER BY createdAt DESC",id))timeline.Add(Ui.Text($"{r.Text("redeemDate")} 赎当 · 实收 {Display.Money(r.Money("paid"))}",12,Ui.Muted));foreach(var log in logs)timeline.Add(Ui.Text(log.Text("createdAt")[..Math.Min(10,log.Text("createdAt").Length)]+" "+log.Text("action"),12,Ui.Muted));body.Add(Ui.Card(timeline));
}
if(table=="RecycleItem" && row.Text("source")=="RENTAL" && row.Int("costKnown")==0)
{
body.Add(Ui.Text("旧租赁物品缺少购货成本,销售结算前请补录,避免利润失真。",12,Ui.Red));
body.Add(Ui.Button("补录历史购货成本",async()=>{var text=await DisplayPromptAsync("补录购货成本","仅补录历史成本,不新增当前现金支出。赠送物品可填 0。","保存","取消",keyboard:Keyboard.Numeric);if(text is null)return;if(!decimal.TryParse(text,out var cost))throw new ArgumentException("请填写正确的成本");await Task.Run(()=>service.SetRentalCost(id,cost));Load();},false));
}
if(table=="RecycleItem"&&row.Text("status") is "IN_STOCK" or "LISTED")
{
if(row.Text("source")=="RENTAL")body.Add(Ui.Button("出租此物品",()=>Navigation.PushAsync(FormPage.Order(service,"RENT",stockId:id))));
body.Add(Ui.Button("上架 / 修改标价",async()=> {var text=await DisplayPromptAsync("上架销售","请输入销售标价(元)","确认","取消",keyboard:Keyboard.Numeric,initialValue:row.Money("listedPrice").ToString("F2"));if(text is null)return;if(!decimal.TryParse(text,out var value))throw new ArgumentException("请输入正确价格");await Task.Run(()=>service.ListStock(id,value));Load();},false));
body.Add(Ui.Button("销售出库",async()=> {var text=await DisplayPromptAsync("销售出库","请输入实际成交价(元)","下一步","取消",keyboard:Keyboard.Numeric,initialValue:row.Money("listedPrice").ToString("F2"));if(text is null)return;if(!decimal.TryParse(text,out var value))throw new ArgumentException("请输入正确价格");if(await DisplayAlertAsync("确认销售",$"成交 {Display.Money(value)}\n成本 {Display.Money(row.Money("amount"))}\n利润 {Display.Money(value-row.Money("amount"))}","确认收款","取消")){await Task.Run(()=>service.Sell(id,value,"","CASH",DateTime.Today));Load();}}));
}
if(table=="RecycleItem" && row.Text("status")=="RENTED")
{
body.Add(Ui.Text(state=="OVERDUE_RENT"?"逾期未归还,可在租赁单中办理归还或销售结算。":"租赁中,暂不能销售或再次出租。",13,state=="OVERDUE_RENT"?Ui.Red:Ui.Muted));
body.Add(Ui.Button("查看当前租赁单",()=>Navigation.PushAsync(new DetailPage(service,"BusinessOrder",row.Text("rentalOrderId"))),false));
if(state=="OVERDUE_RENT")body.Add(Ui.Button("未归还,按销售结算",()=>Navigation.PushAsync(FormPage.LostRental(service,row.Text("rentalOrderId")))));
}
if(table=="BusinessOrder"&&row.Text("status")=="OPEN")
{
if(row.Text("kind")=="CONSIGN")body.Add(Ui.Button("录入成交并结算",()=>Navigation.PushAsync(FormPage.SettleConsignment(service,id))));
else body.Add(Ui.Button(row.Text("kind")=="RENT"?"归还并结算":"完成业务",async()=> {var text=row.Text("kind")=="RENT"?$"收取租金 {Display.Money(row.Money("amount"))},退还押金 {Display.Money(row.Money("deposit"))}。":"确认本次款项已收妥?";if(await DisplayAlertAsync("确认结算",text,"确认已结清","取消")){service.CompleteBusiness(id);Load();}}));
if(row.Text("kind")=="RENT" && RentalInventory.IsOverdue(row,DateTime.Today))body.Add(Ui.Button("未归还,保留押金并按销售结算",()=>Navigation.PushAsync(FormPage.LostRental(service,id)),false));
body.Add(Ui.Button("作废业务",async()=>{if(await DisplayAlertAsync("确认作废","确认已归还物品和押金?作废后保留业务记录。","确认作废","取消")){service.VoidBusiness(id);Load();}},false));
}
if(table=="SaleRecord"&&row.Text("status")=="DONE")body.Add(Ui.Button("作废销售并退回库存",async()=>{if(await DisplayAlertAsync("确认作废","确认已退回成交款?物品将恢复在库,退款保留在财务流水中。","已退款,作废","取消")){await Task.Run(()=>service.VoidSale(id));Load();}},false));
if(table=="RecycleOrder"&&row.Text("status")=="DONE")body.Add(Ui.Button("作废入库单",async()=>{if(await DisplayAlertAsync("确认作废","仅允许整单物品仍在库或在售时作废。确认已退回物品并收回入库款?","确认作废","取消")){await Task.Run(()=>service.VoidIntake(id));Load();}},false));
if(table=="RecycleItem"&&row.Text("orderId").Length>0)body.Add(Ui.Button("查看来源入库单",()=>Navigation.PushAsync(new DetailPage(service,"RecycleOrder",row.Text("orderId"))),false));
if(table=="RecycleItem"&&row.Text("status")=="SOLD"){var sale=service.Db.Query("SELECT id FROM SaleRecord WHERE itemId=@p0 AND status='DONE' AND deletedAt IS NULL ORDER BY createdAt DESC LIMIT 1",id).FirstOrDefault();if(sale is not null)body.Add(Ui.Button("查看销售记录",()=>Navigation.PushAsync(new DetailPage(service,"SaleRecord",sale.Id)),false));}
if(RecordLifecycle.Tables.Contains(table))body.Add(Ui.Button("移入回收站",async()=> {if(await DisplayAlertAsync("移入回收站","该记录及关联业务、库存和流水将退出正常列表与统计,可在回收站一起还原。此操作不代表退款或实物归还。","移入","取消")){await Task.Run(()=>service.SoftDelete(table,id));await Navigation.PopAsync();}},false));
var phone=row.Text("customerPhone",row.Text("phone"));if(phone.Length>0)body.Add(Ui.Button("拨打客户电话",()=> {PhoneDialer.Default.Open(phone);return Task.CompletedTask;},false));
}
private void AddItem(VerticalStackLayout target,DbRow item)
{
target.Add(Ui.Text(item.Text("name")+" · "+ItemCatalog.ItemName(item),15,bold:true));
foreach(var key in new[]{"itemCode","weight","spec","unitPrice","brand","serialNo","accessory","amount","quotedPrice","appraisalPrice"})
{
if(item.Text(key).Length==0 || item.Text("type") is "PROPERTY" or "VEHICLE" && key is "weight" or "spec" or "unitPrice")continue;
if(key is "amount" or "appraisalPrice" && item.Money(key)==0)continue;
target.Add(Ui.Spread(Ui.Text(FieldLabels[key],12,Ui.Muted),Ui.Text(item.Text(key),13)));
}
AddAttributes(target,item);
foreach(var photo in MediaFiles.Parse(item.Text("images")))try{target.Add(new Image{Source=MediaFiles.Source(photo),HeightRequest=150,Aspect=Aspect.AspectFit});}catch{ }
target.Add(Ui.Divider());
}
private void AddAttributes(VerticalStackLayout target,DbRow row)
{
if(string.IsNullOrWhiteSpace(row.Text("extraAttributes")))return;
var labels=new Dictionary<string,string>(ItemCatalog.AttributeLabels);
var config=service.Db.Query("SELECT attributes FROM PawnItemTypeRate WHERE itemType=@p0",row.Text("type")).FirstOrDefault()?.Text("attributes");
if(!string.IsNullOrWhiteSpace(config))try
{
using var definition=JsonDocument.Parse(config);
if(definition.RootElement.ValueKind==JsonValueKind.Array)foreach(var attr in definition.RootElement.EnumerateArray())
if(attr.ValueKind==JsonValueKind.Object&&attr.TryGetProperty("key",out var key))labels[key.ToString()]=attr.TryGetProperty("label",out var label)?label.ToString():key.ToString();
}catch(JsonException){}
try
{
using var values=JsonDocument.Parse(row.Text("extraAttributes"));if(values.RootElement.ValueKind!=JsonValueKind.Object)return;
foreach(var value in values.RootElement.EnumerateObject())if(value.Value.ValueKind!=JsonValueKind.Null&&value.Value.ToString().Length>0)
target.Add(Ui.Spread(Ui.Text(labels.GetValueOrDefault(value.Name,value.Name),12,Ui.Muted),Ui.Text(value.Value.ToString(),13)));
}catch(JsonException){}
}
public static readonly Dictionary<string,string> FieldLabels=new()
{
["majorClass"]="财务大类",["detailClass"]="财务细类",["saleAmount"]="未归还设备售价",
["material"]="物品材质",["unitPrice"]="入库单价(元/克)",["itemCode"]="物品编码",["expectedAmount"]="客户期望金额",["serviceFeeMode"]="寄卖服务费方式",["serviceFeeValue"]="寄卖服务费",["storageFee"]="保管费(元)",
["name"]="名称",["customerName"]="客户姓名",["customerPhone"]="联系电话",["phone"]="联系电话",["gender"]="性别",["idCard"]="证件号码",["address"]="地址",["type"]="类型",["creditLevel"]="信用等级",["isBlacklist"]="黑名单",["tags"]="标签",["itemType"]="物品类型",["itemName"]="物品名称",["principal"]="当金",["appraisalPrice"]="估价",["weight"]="重量(克)",["spec"]="规格 / 成色",["brand"]="品牌",["serialNo"]="序列号",["accessory"]="附件",["monthlyRate"]="月利率(%)",["feeRate"]="综合费率(%)",["storageFeeRate"]="保管费率(%)",["storageFixedFee"]="固定保管费",["fixedInterest"]="固定利息",["pawnDate"]="开当日期",["dueDate"]="到期日期",["status"]="状态",["feeChargeMode"]="收费方式",["remark"]="备注",["amount"]="金额",["cost"]="成本",["profit"]="利润",["quotedPrice"]="客户报价",["dealDate"]="成交日期",["payMethod"]="付款方式",["occurDate"]="发生日期",["category"]="财务分类",["listedPrice"]="销售标价",["soldAt"]="售出日期",["startDate"]="开始日期",["endDate"]="结束日期",["commission"]="佣金",["deposit"]="押金",["images"]="物品照片",["contractImages"]="合同照片",["idCardImages"]="证件照片",["createdAt"]="创建时间",["updatedAt"]="更新时间",["action"]="操作",["targetType"]="记录类型",["targetName"]="记录名称",["detail"]="操作内容",["role"]="角色",["baseSalary"]="基本工资",["content"]="模板内容"
};
}