using JiDangJia.Core;
using System.Globalization;
using System.Text.Json;
namespace JiDangJia.Views;
public sealed class ItemEditor
{
private readonly BusinessService service;
private readonly string kind;
private readonly Dictionary<string,string> types;
private readonly FieldSet identity;
private readonly FieldSet tail;
private readonly VerticalStackLayout detailArea=Ui.Stack();
private readonly Dictionary<string,(FieldSet fields,HashSet<string> extra)> cache=new();
private string current="";
private bool updatingAmount;
private bool amountOverridden;
private readonly Button recalculate;
private readonly DbRow restored;
public Dictionary<string,PhotoField> Photos=>tail.Photos;
public VerticalStackLayout View {get;}
public Picker TypePicker => (Picker)identity.Inputs["type"];
public string Type => types.Keys.ElementAtOrDefault(TypePicker.SelectedIndex)??"OTHER";
public string RateType=>Type=="METAL"?cache[current].fields.Read(false).Text("material","GOLD"):Type;
public event Action? TypeChanged;
private bool Intake => kind is "RECYCLE" or "PURCHASE" or "RENTAL" or "STOCK_OTHER";
public ItemEditor(BusinessService service,string kind,DbRow? initial=null)
{
this.service=service;this.kind=kind;types=ItemCatalog.Choices(kind);
restored=initial is null?new():new(initial);
if(restored.Text("extraAttributes").Length>0)try{using var attributes=JsonDocument.Parse(restored.Text("extraAttributes"));foreach(var pair in attributes.RootElement.EnumerateObject())restored[pair.Name]=pair.Value.Clone();}catch(JsonException){}
identity=new FieldSet(initial??new DbRow{["type"]=types.Keys.First()},[new("name","物品名称",Required:true),new("type","物品类型",FieldKind.Choice,true,types)]);
var definitions=new List<Field>();
if(kind=="PAWN")definitions.Add(new("appraisalPrice","估价(元)",FieldKind.Number));
if(Intake)definitions.Add(new("amount",kind=="RECYCLE"?"回收金额(元)":"进货成本(元)",FieldKind.Number,true));
if(kind=="RECYCLE")definitions.Add(new("quotedPrice","客户报价(元)",FieldKind.Number));
if(kind is "RENT" or "RENTAL" or "STOCK_OTHER")definitions.Add(new("itemCode","物品编码",Hint:"用于识别具体物品,可填写门店现有编码"));
definitions.Add(new("images","物品照片",FieldKind.Photos));
tail=new FieldSet(initial??new DbRow{["amount"]=0,["appraisalPrice"]=0,["itemCode"]=ItemCatalog.NewCode()},definitions);
recalculate=Ui.Button("按克重和单价重算",()=>{amountOverridden=false;CalculateAmount();return Task.CompletedTask;},false);
if(Intake && tail.Inputs["amount"] is Entry amount)amount.TextChanged+=(_,_)=>{if(!updatingAmount)amountOverridden=true;};
View=Ui.Stack(identity.View,detailArea,tail.View,recalculate);TypePicker.SelectedIndexChanged+=(_,_)=>{Render();TypeChanged?.Invoke();};Render();
}
private void Render()
{
current=Type;detailArea.Clear();
if(!cache.TryGetValue(current,out var entry))
{
var definitions=Definitions(current);var extra=definitions.Where(f=>!Standard.Contains(f.Key)).Select(f=>f.Key).ToHashSet();
AddCustom(definitions,extra,current);
var initial=restored.Text("type")==current?new DbRow(restored):new DbRow();if(current=="METAL"){if(!initial.ContainsKey("weight"))initial["weight"]=0;if(!initial.ContainsKey("unitPrice"))initial["unitPrice"]=0;if(!initial.ContainsKey("material"))initial["material"]="GOLD";}else if(current=="JEWELRY"&&!initial.ContainsKey("weight"))initial["weight"]=0;
var fields=new FieldSet(initial,definitions);
if(current=="METAL"&&fields.Inputs["material"] is Picker material)material.SelectedIndexChanged+=(_,_)=>TypeChanged?.Invoke();
fields.Changed+=CalculateAmount;cache[current]=entry=(fields,extra);
}
detailArea.Add(entry.fields.View);recalculate.IsVisible=Intake&¤t=="METAL";
CalculateAmount();
}
private List<Field> Definitions(string type) => type switch
{
"METAL"=>new List<Field>{new("material","物品材质",FieldKind.Choice,true,ItemCatalog.Materials),new("weight","克重(克)",FieldKind.Number),new("spec","规格 / 成色")}
.Concat(Intake?new[]{new Field("unitPrice",kind=="RECYCLE"?"回收单价(元/克)":"进货单价(元/克)",FieldKind.Number)}:Array.Empty<Field>())
.Concat(new Field[]{new("brand","品牌"),new("serialNo","序列号"),new("accessory","附件")}).ToList(),
"PROPERTY"=>[new("address","房产地址"),new("area","面积(平方米)",FieldKind.Number),new("certNo","产权证号")],
"VEHICLE"=>[new("plateNo","车牌号"),new("vin","车架号"),new("mileage","里程(公里)",FieldKind.Number),new("brand","品牌"),new("model","车型"),new("accessory","随车资料 / 附件")],
"WATCH"=>[new("brand","品牌"),new("model","型号"),new("serialNo","序列号"),new("accessory","附件")],
"LUXURY"=>[new("brand","品牌"),new("model","型号"),new("serialNo","序列号"),new("condition","品相"),new("accessory","附件")],
"JEWELRY"=>[new("material","材质"),new("weight","重量(克)",FieldKind.Number),new("spec","规格 / 成色"),new("certificate","证书编号"),new("brand","品牌"),new("serialNo","序列号"),new("accessory","附件")],
"ELECTRONICS"=>[new("brand","品牌"),new("model","型号"),new("serialNo","序列号 / IMEI"),new("storage","存储容量"),new("accessory","附件")],
_=>[new("brand","品牌"),new("model","型号"),new("serialNo","序列号"),new("accessory","附件")]
};
private static readonly HashSet<string> Standard=["name","type","material","weight","unitPrice","spec","brand","serialNo","accessory","images","amount","appraisalPrice","quotedPrice","itemCode","extraAttributes"];
private void AddCustom(List<Field> definitions,HashSet<string> extra,string type)
{
var config=service.Db.Query("SELECT attributes FROM PawnItemTypeRate WHERE itemType=@p0",type).FirstOrDefault()?.Text("attributes");
if(type=="METAL"&&string.IsNullOrWhiteSpace(config))config=service.Db.Query("SELECT attributes FROM PawnItemTypeRate WHERE itemType='GOLD'").FirstOrDefault()?.Text("attributes");
if(string.IsNullOrWhiteSpace(config))return;
try
{
using var doc=JsonDocument.Parse(config);if(doc.RootElement.ValueKind!=JsonValueKind.Array)return;
foreach(var attr in doc.RootElement.EnumerateArray())
{
if(attr.ValueKind!=JsonValueKind.Object || !attr.TryGetProperty("key",out var k))continue;var key=k.ToString();
if(key.Length==0 || Standard.Contains(key) || definitions.Any(f=>f.Key==key) || key=="purity"&&definitions.Any(f=>f.Key=="spec"))continue;
var kind=attr.TryGetProperty("type",out var t)?t.ToString():"text";
var fieldKind=kind switch{"number"=>FieldKind.Number,"date"=>FieldKind.Date,"select"=>FieldKind.Choice,"textarea"=>FieldKind.Notes,_=>FieldKind.Text};
Dictionary<string,string>? options=null;
if(fieldKind==FieldKind.Choice)
{
options=new();if(attr.TryGetProperty("options",out var values)&&values.ValueKind==JsonValueKind.Array)foreach(var v in values.EnumerateArray())
{var value=v.ValueKind==JsonValueKind.Object&&v.TryGetProperty("value",out var ov)?ov.ToString():v.ToString();options[value]=v.ValueKind==JsonValueKind.Object&&v.TryGetProperty("label",out var ol)?ol.ToString():value;}
}
definitions.Add(new(key,attr.TryGetProperty("label",out var label)?label.ToString():key,fieldKind,attr.TryGetProperty("required",out var required)&&required.ValueKind==JsonValueKind.True,options));extra.Add(key);
}
}catch(JsonException){ }
}
private void CalculateAmount()
{
if(!Intake || current!="METAL" || amountOverridden || !cache.TryGetValue(current,out var entry))return;
var row=entry.fields.Read(false);var weight=row.Money("weight");var price=row.Money("unitPrice");
if(weight<0 || weight>999999999 || price<0 || price>999999999)return;
updatingAmount=true;
try{((Entry)tail.Inputs["amount"]).Text=Fees.Round(weight*price).ToString("0.##",CultureInfo.InvariantCulture);}
finally{updatingAmount=false;}
}
public DbRow Read(bool validate=true)
{
var row=identity.Read(validate);var detail=cache[current].fields.Read(validate);foreach(var pair in detail)row[pair.Key]=pair.Value;
foreach(var pair in tail.Read(validate))row[pair.Key]=pair.Value;
row["extraAttributes"]=JsonSerializer.Serialize(detail.Where(p=>cache[current].extra.Contains(p.Key)).ToDictionary(p=>p.Key,p=>p.Value));
return row;
}
}
using JiDangJia.Core;
using System.Globalization;
using System.Text.Json;
namespace JiDangJia.Views;
public sealed class ItemEditor
{
private readonly BusinessService service;
private readonly string kind;
private readonly Dictionary<string,string> types;
private readonly FieldSet identity;
private readonly FieldSet tail;
private readonly VerticalStackLayout detailArea=Ui.Stack();
private readonly Dictionary<string,(FieldSet fields,HashSet<string> extra)> cache=new();
private string current="";
private bool updatingAmount;
private bool amountOverridden;
private readonly Button recalculate;
private readonly DbRow restored;
public Dictionary<string,PhotoField> Photos=>tail.Photos;
public VerticalStackLayout View {get;}
public Picker TypePicker => (Picker)identity.Inputs["type"];
public string Type => types.Keys.ElementAtOrDefault(TypePicker.SelectedIndex)??"OTHER";
public string RateType=>Type=="METAL"?cache[current].fields.Read(false).Text("material","GOLD"):Type;
public event Action? TypeChanged;
private bool Intake => kind is "RECYCLE" or "PURCHASE" or "RENTAL" or "STOCK_OTHER";
public ItemEditor(BusinessService service,string kind,DbRow? initial=null)
{
this.service=service;this.kind=kind;types=ItemCatalog.Choices(kind);
restored=initial is null?new():new(initial);
if(restored.Text("extraAttributes").Length>0)try{using var attributes=JsonDocument.Parse(restored.Text("extraAttributes"));foreach(var pair in attributes.RootElement.EnumerateObject())restored[pair.Name]=pair.Value.Clone();}catch(JsonException){}
identity=new FieldSet(initial??new DbRow{["type"]=types.Keys.First()},[new("name","物品名称",Required:true),new("type","物品类型",FieldKind.Choice,true,types)]);
var definitions=new List<Field>();
if(kind=="PAWN")definitions.Add(new("appraisalPrice","估价(元)",FieldKind.Number));
if(Intake)definitions.Add(new("amount",kind=="RECYCLE"?"回收金额(元)":"进货成本(元)",FieldKind.Number,true));
if(kind=="RECYCLE")definitions.Add(new("quotedPrice","客户报价(元)",FieldKind.Number));
if(kind is "RENT" or "RENTAL" or "STOCK_OTHER")definitions.Add(new("itemCode","物品编码",Hint:"用于识别具体物品,可填写门店现有编码"));
definitions.Add(new("images","物品照片",FieldKind.Photos));
tail=new FieldSet(initial??new DbRow{["amount"]=0,["appraisalPrice"]=0,["itemCode"]=ItemCatalog.NewCode()},definitions);
recalculate=Ui.Button("按克重和单价重算",()=>{amountOverridden=false;CalculateAmount();return Task.CompletedTask;},false);
if(Intake && tail.Inputs["amount"] is Entry amount)amount.TextChanged+=(_,_)=>{if(!updatingAmount)amountOverridden=true;};
View=Ui.Stack(identity.View,detailArea,tail.View,recalculate);TypePicker.SelectedIndexChanged+=(_,_)=>{Render();TypeChanged?.Invoke();};Render();
}
private void Render()
{
current=Type;detailArea.Clear();
if(!cache.TryGetValue(current,out var entry))
{
var definitions=Definitions(current);var extra=definitions.Where(f=>!Standard.Contains(f.Key)).Select(f=>f.Key).ToHashSet();
AddCustom(definitions,extra,current);
var initial=restored.Text("type")==current?new DbRow(restored):new DbRow();if(current=="METAL"){if(!initial.ContainsKey("weight"))initial["weight"]=0;if(!initial.ContainsKey("unitPrice"))initial["unitPrice"]=0;if(!initial.ContainsKey("material"))initial["material"]="GOLD";}else if(current=="JEWELRY"&&!initial.ContainsKey("weight"))initial["weight"]=0;
var fields=new FieldSet(initial,definitions);
if(current=="METAL"&&fields.Inputs["material"] is Picker material)material.SelectedIndexChanged+=(_,_)=>TypeChanged?.Invoke();
fields.Changed+=CalculateAmount;cache[current]=entry=(fields,extra);
}
detailArea.Add(entry.fields.View);recalculate.IsVisible=Intake&¤t=="METAL";
CalculateAmount();
}
private List<Field> Definitions(string type) => type switch
{
"METAL"=>new List<Field>{new("material","物品材质",FieldKind.Choice,true,ItemCatalog.Materials),new("weight","克重(克)",FieldKind.Number),new("spec","规格 / 成色")}
.Concat(Intake?new[]{new Field("unitPrice",kind=="RECYCLE"?"回收单价(元/克)":"进货单价(元/克)",FieldKind.Number)}:Array.Empty<Field>())
.Concat(new Field[]{new("brand","品牌"),new("serialNo","序列号"),new("accessory","附件")}).ToList(),
"PROPERTY"=>[new("address","房产地址"),new("area","面积(平方米)",FieldKind.Number),new("certNo","产权证号")],
"VEHICLE"=>[new("plateNo","车牌号"),new("vin","车架号"),new("mileage","里程(公里)",FieldKind.Number),new("brand","品牌"),new("model","车型"),new("accessory","随车资料 / 附件")],
"WATCH"=>[new("brand","品牌"),new("model","型号"),new("serialNo","序列号"),new("accessory","附件")],
"LUXURY"=>[new("brand","品牌"),new("model","型号"),new("serialNo","序列号"),new("condition","品相"),new("accessory","附件")],
"JEWELRY"=>[new("material","材质"),new("weight","重量(克)",FieldKind.Number),new("spec","规格 / 成色"),new("certificate","证书编号"),new("brand","品牌"),new("serialNo","序列号"),new("accessory","附件")],
"ELECTRONICS"=>[new("brand","品牌"),new("model","型号"),new("serialNo","序列号 / IMEI"),new("storage","存储容量"),new("accessory","附件")],
_=>[new("brand","品牌"),new("model","型号"),new("serialNo","序列号"),new("accessory","附件")]
};
private static readonly HashSet<string> Standard=["name","type","material","weight","unitPrice","spec","brand","serialNo","accessory","images","amount","appraisalPrice","quotedPrice","itemCode","extraAttributes"];
private void AddCustom(List<Field> definitions,HashSet<string> extra,string type)
{
var config=service.Db.Query("SELECT attributes FROM PawnItemTypeRate WHERE itemType=@p0",type).FirstOrDefault()?.Text("attributes");
if(type=="METAL"&&string.IsNullOrWhiteSpace(config))config=service.Db.Query("SELECT attributes FROM PawnItemTypeRate WHERE itemType='GOLD'").FirstOrDefault()?.Text("attributes");
if(string.IsNullOrWhiteSpace(config))return;
try
{
using var doc=JsonDocument.Parse(config);if(doc.RootElement.ValueKind!=JsonValueKind.Array)return;
foreach(var attr in doc.RootElement.EnumerateArray())
{
if(attr.ValueKind!=JsonValueKind.Object || !attr.TryGetProperty("key",out var k))continue;var key=k.ToString();
if(key.Length==0 || Standard.Contains(key) || definitions.Any(f=>f.Key==key) || key=="purity"&&definitions.Any(f=>f.Key=="spec"))continue;
var kind=attr.TryGetProperty("type",out var t)?t.ToString():"text";
var fieldKind=kind switch{"number"=>FieldKind.Number,"date"=>FieldKind.Date,"select"=>FieldKind.Choice,"textarea"=>FieldKind.Notes,_=>FieldKind.Text};
Dictionary<string,string>? options=null;
if(fieldKind==FieldKind.Choice)
{
options=new();if(attr.TryGetProperty("options",out var values)&&values.ValueKind==JsonValueKind.Array)foreach(var v in values.EnumerateArray())
{var value=v.ValueKind==JsonValueKind.Object&&v.TryGetProperty("value",out var ov)?ov.ToString():v.ToString();options[value]=v.ValueKind==JsonValueKind.Object&&v.TryGetProperty("label",out var ol)?ol.ToString():value;}
}
definitions.Add(new(key,attr.TryGetProperty("label",out var label)?label.ToString():key,fieldKind,attr.TryGetProperty("required",out var required)&&required.ValueKind==JsonValueKind.True,options));extra.Add(key);
}
}catch(JsonException){ }
}
private void CalculateAmount()
{
if(!Intake || current!="METAL" || amountOverridden || !cache.TryGetValue(current,out var entry))return;
var row=entry.fields.Read(false);var weight=row.Money("weight");var price=row.Money("unitPrice");
if(weight<0 || weight>999999999 || price<0 || price>999999999)return;
updatingAmount=true;
try{((Entry)tail.Inputs["amount"]).Text=Fees.Round(weight*price).ToString("0.##",CultureInfo.InvariantCulture);}
finally{updatingAmount=false;}
}
public DbRow Read(bool validate=true)
{
var row=identity.Read(validate);var detail=cache[current].fields.Read(validate);foreach(var pair in detail)row[pair.Key]=pair.Value;
foreach(var pair in tail.Read(validate))row[pair.Key]=pair.Value;
row["extraAttributes"]=JsonSerializer.Serialize(detail.Where(p=>cache[current].extra.Contains(p.Key)).ToDictionary(p=>p.Key,p=>p.Value));
return row;
}
}