sync: local modifications to customer_management
- Updated core.py - Updated base.ui - Added new API files: check_tables, customer_pool_list, customers CRUD, handover_list - Added new UI/DSPY files: customer_edit, customer_list, customer_pool, handover_list
This commit is contained in:
parent
12e2fb6978
commit
469255afe7
@ -5,7 +5,7 @@ import re
|
||||
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from appPublic.worker import awaitify
|
||||
from sqlor.dbp import DBP
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
|
||||
async def create_customer(
|
||||
@ -21,47 +21,48 @@ async def create_customer(
|
||||
region: str = None
|
||||
) -> Dict:
|
||||
"""创建客户档案"""
|
||||
dbp = DBP()
|
||||
customer_id = str(uuid.uuid4()).replace('-', '')
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('default') as sor:
|
||||
customer_id = str(uuid.uuid4()).replace('-', '')
|
||||
|
||||
# 数据校验
|
||||
await validate_customer_data(dbp, phone, tax_id, customer_type)
|
||||
# 数据校验
|
||||
await validate_customer_data(sor, phone, tax_id, customer_type)
|
||||
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
customer_data = {
|
||||
"id": customer_id,
|
||||
"customer_name": customer_name,
|
||||
"customer_type": customer_type,
|
||||
"phone": phone,
|
||||
"email": email,
|
||||
"tax_id": tax_id,
|
||||
"industry": industry,
|
||||
"customer_level": customer_level,
|
||||
"address": address,
|
||||
"owner_id": owner_id or get_current_user_id(),
|
||||
"region": region,
|
||||
"last_follow_up": now,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
"status": "active"
|
||||
}
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
customer_data = {
|
||||
"id": customer_id,
|
||||
"customer_name": customer_name,
|
||||
"customer_type": customer_type,
|
||||
"phone": phone,
|
||||
"email": email,
|
||||
"tax_id": tax_id,
|
||||
"industry": industry,
|
||||
"customer_level": customer_level,
|
||||
"address": address,
|
||||
"owner_id": owner_id or get_current_user_id(),
|
||||
"region": region,
|
||||
"last_follow_up": now,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
"status": "active"
|
||||
}
|
||||
|
||||
await dbp.insert("customers", customer_data)
|
||||
return customer_data
|
||||
await sor.C("customers", customer_data)
|
||||
return customer_data
|
||||
|
||||
|
||||
async def validate_customer_data(dbp, phone: str, tax_id: str, customer_type: str):
|
||||
async def validate_customer_data(sor, phone: str, tax_id: str, customer_type: str):
|
||||
"""验证客户数据唯一性"""
|
||||
# 手机号唯一性校验
|
||||
if phone:
|
||||
existing_phone = await dbp.select_one("customers", {"phone": phone})
|
||||
if existing_phone:
|
||||
existing_phone = await sor.R("customers", {"filters": [{"field": "phone", "op": "=", "value": phone}]})
|
||||
if existing_phone and len(existing_phone) > 0:
|
||||
raise ValueError(f"手机号 {phone} 已存在,不能重复建档")
|
||||
|
||||
# 企业税号唯一性校验(仅对企业客户)
|
||||
if customer_type == "enterprise" and tax_id:
|
||||
existing_tax = await dbp.select_one("customers", {"tax_id": tax_id})
|
||||
if existing_tax:
|
||||
existing_tax = await sor.R("customers", {"filters": [{"field": "tax_id", "op": "=", "value": tax_id}]})
|
||||
if existing_tax and len(existing_tax) > 0:
|
||||
raise ValueError(f"企业税号 {tax_id} 已存在,不能重复建档")
|
||||
|
||||
|
||||
@ -72,39 +73,40 @@ async def initiate_handover(
|
||||
reviewer_id: str = None
|
||||
) -> Dict:
|
||||
"""发起客户交接流程"""
|
||||
dbp = DBP()
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('default') as sor:
|
||||
# 获取客户信息
|
||||
customer_records = await sor.R("customers", {"filters": [{"field": "id", "op": "=", "value": customer_id}]})
|
||||
if not customer_records or len(customer_records) == 0:
|
||||
raise ValueError("客户不存在")
|
||||
|
||||
# 获取客户信息
|
||||
customer = await dbp.select_one("customers", {"id": customer_id})
|
||||
if not customer:
|
||||
raise ValueError("客户不存在")
|
||||
customer = customer_records[0]
|
||||
if customer["status"] != "active":
|
||||
raise ValueError("只能交接活跃状态的客户")
|
||||
|
||||
if customer["status"] != "active":
|
||||
raise ValueError("只能交接活跃状态的客户")
|
||||
# 创建交接记录
|
||||
handover_id = str(uuid.uuid4()).replace('-', '')
|
||||
handover_data = {
|
||||
"id": handover_id,
|
||||
"customer_id": customer_id,
|
||||
"from_owner_id": customer["owner_id"],
|
||||
"to_owner_id": to_owner_id,
|
||||
"handover_reason": handover_reason,
|
||||
"current_stage": "preparation",
|
||||
"reviewer_id": reviewer_id,
|
||||
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
# 创建交接记录
|
||||
handover_id = str(uuid.uuid4()).replace('-', '')
|
||||
handover_data = {
|
||||
"id": handover_id,
|
||||
"customer_id": customer_id,
|
||||
"from_owner_id": customer["owner_id"],
|
||||
"to_owner_id": to_owner_id,
|
||||
"handover_reason": handover_reason,
|
||||
"current_stage": "preparation",
|
||||
"reviewer_id": reviewer_id,
|
||||
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
await sor.C("customer_handover", handover_data)
|
||||
|
||||
await dbp.insert("customer_handover", handover_data)
|
||||
# 自动生成交接清单
|
||||
await generate_handover_items(sor, handover_id, customer_id)
|
||||
|
||||
# 自动生成交接清单
|
||||
await generate_handover_items(dbp, handover_id, customer_id)
|
||||
|
||||
return handover_data
|
||||
return handover_data
|
||||
|
||||
|
||||
async def generate_handover_items(dbp, handover_id: str, customer_id: str):
|
||||
async def generate_handover_items(sor, handover_id: str, customer_id: str):
|
||||
"""自动生成交接清单"""
|
||||
items = []
|
||||
|
||||
@ -120,10 +122,12 @@ async def generate_handover_items(dbp, handover_id: str, customer_id: str):
|
||||
})
|
||||
|
||||
# 未结商机
|
||||
opportunities = await dbp.query(
|
||||
"SELECT id, customer_name, estimated_amount, current_stage FROM opportunities WHERE customer_name = (SELECT customer_name FROM customers WHERE id = %(customer_id)s) AND status = 'active'",
|
||||
{"customer_id": customer_id}
|
||||
)
|
||||
opportunities = await sor.R("opportunities", {
|
||||
"filters": [
|
||||
{"field": "customer_name", "op": "=", "value": f"(SELECT customer_name FROM customers WHERE id = '{customer_id}')"},
|
||||
{"field": "status", "op": "=", "value": "active"}
|
||||
]
|
||||
})
|
||||
if opportunities:
|
||||
for opp in opportunities:
|
||||
items.append({
|
||||
@ -138,10 +142,7 @@ async def generate_handover_items(dbp, handover_id: str, customer_id: str):
|
||||
})
|
||||
|
||||
# 历史合同(假设合同管理模块存在)
|
||||
contracts = await dbp.query(
|
||||
"SELECT id, contract_no, amount, status FROM contracts WHERE customer_id = %(customer_id)s",
|
||||
{"customer_id": customer_id}
|
||||
)
|
||||
contracts = await sor.R("contracts", {"filters": [{"field": "customer_id", "op": "=", "value": customer_id}]})
|
||||
if contracts:
|
||||
for contract in contracts:
|
||||
items.append({
|
||||
@ -156,10 +157,12 @@ async def generate_handover_items(dbp, handover_id: str, customer_id: str):
|
||||
})
|
||||
|
||||
# 服务工单(假设服务模块存在)
|
||||
service_tickets = await dbp.query(
|
||||
"SELECT id, ticket_no, subject, status FROM service_tickets WHERE customer_id = %(customer_id)s AND status != 'closed'",
|
||||
{"customer_id": customer_id}
|
||||
)
|
||||
service_tickets = await sor.R("service_tickets", {
|
||||
"filters": [
|
||||
{"field": "customer_id", "op": "=", "value": customer_id},
|
||||
{"field": "status", "op": "!=", "value": "closed"}
|
||||
]
|
||||
})
|
||||
if service_tickets:
|
||||
for ticket in service_tickets:
|
||||
items.append({
|
||||
@ -174,10 +177,12 @@ async def generate_handover_items(dbp, handover_id: str, customer_id: str):
|
||||
})
|
||||
|
||||
# 未解决回款问题
|
||||
payment_issues = await dbp.query(
|
||||
"SELECT id, invoice_no, amount, due_date FROM payment_records WHERE customer_id = %(customer_id)s AND status = 'overdue'",
|
||||
{"customer_id": customer_id}
|
||||
)
|
||||
payment_issues = await sor.R("payment_records", {
|
||||
"filters": [
|
||||
{"field": "customer_id", "op": "=", "value": customer_id},
|
||||
{"field": "status", "op": "=", "value": "overdue"}
|
||||
]
|
||||
})
|
||||
if payment_issues:
|
||||
for issue in payment_issues:
|
||||
items.append({
|
||||
@ -193,233 +198,246 @@ async def generate_handover_items(dbp, handover_id: str, customer_id: str):
|
||||
|
||||
# 批量插入交接项目
|
||||
for item in items:
|
||||
await dbp.insert("customer_handover_items", item)
|
||||
await sor.C("customer_handover_items", item)
|
||||
|
||||
|
||||
async def complete_handover_preparation(handover_id: str) -> Dict:
|
||||
"""完成交接准备阶段"""
|
||||
dbp = DBP()
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('default') as sor:
|
||||
handover_records = await sor.R("customer_handover", {"filters": [{"field": "id", "op": "=", "value": handover_id}]})
|
||||
if not handover_records or len(handover_records) == 0:
|
||||
raise ValueError("交接记录不存在")
|
||||
|
||||
handover = await dbp.select_one("customer_handover", {"id": handover_id})
|
||||
if not handover:
|
||||
raise ValueError("交接记录不存在")
|
||||
handover = handover_records[0]
|
||||
if handover["current_stage"] != "preparation":
|
||||
raise ValueError("当前不在准备阶段")
|
||||
|
||||
if handover["current_stage"] != "preparation":
|
||||
raise ValueError("当前不在准备阶段")
|
||||
# 更新为审核阶段
|
||||
await sor.U(
|
||||
"customer_handover",
|
||||
{
|
||||
"current_stage": "review",
|
||||
"prepared_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
{"filters": [{"field": "id", "op": "=", "value": handover_id}]}
|
||||
)
|
||||
|
||||
# 更新为审核阶段
|
||||
await dbp.update(
|
||||
"customer_handover",
|
||||
{
|
||||
"current_stage": "review",
|
||||
"prepared_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
{"id": handover_id}
|
||||
)
|
||||
|
||||
return {"handover_id": handover_id, "stage": "review"}
|
||||
return {"handover_id": handover_id, "stage": "review"}
|
||||
|
||||
|
||||
async def approve_handover(handover_id: str, approver_id: str = None) -> Dict:
|
||||
"""审核交接清单"""
|
||||
dbp = DBP()
|
||||
approver_id = approver_id or get_current_user_id()
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('default') as sor:
|
||||
approver_id = approver_id or get_current_user_id()
|
||||
|
||||
handover = await dbp.select_one("customer_handover", {"id": handover_id})
|
||||
if not handover:
|
||||
raise ValueError("交接记录不存在")
|
||||
handover_records = await sor.R("customer_handover", {"filters": [{"field": "id", "op": "=", "value": handover_id}]})
|
||||
if not handover_records or len(handover_records) == 0:
|
||||
raise ValueError("交接记录不存在")
|
||||
|
||||
if handover["current_stage"] != "review":
|
||||
raise ValueError("当前不在审核阶段")
|
||||
handover = handover_records[0]
|
||||
if handover["current_stage"] != "review":
|
||||
raise ValueError("当前不在审核阶段")
|
||||
|
||||
# 更新为确认阶段
|
||||
await dbp.update(
|
||||
"customer_handover",
|
||||
{
|
||||
"current_stage": "confirmation",
|
||||
"reviewed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
{"id": handover_id}
|
||||
)
|
||||
# 更新为确认阶段
|
||||
await sor.U(
|
||||
"customer_handover",
|
||||
{
|
||||
"current_stage": "confirmation",
|
||||
"reviewed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
{"filters": [{"field": "id", "op": "=", "value": handover_id}]}
|
||||
)
|
||||
|
||||
return {"handover_id": handover_id, "stage": "confirmation"}
|
||||
return {"handover_id": handover_id, "stage": "confirmation"}
|
||||
|
||||
|
||||
async def confirm_handover(handover_id: str, confirm_by: str = None) -> Dict:
|
||||
"""确认接收客户"""
|
||||
dbp = DBP()
|
||||
confirm_by = confirm_by or get_current_user_id()
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('default') as sor:
|
||||
confirm_by = confirm_by or get_current_user_id()
|
||||
|
||||
handover = await dbp.select_one("customer_handover", {"id": handover_id})
|
||||
if not handover:
|
||||
raise ValueError("交接记录不存在")
|
||||
handover_records = await sor.R("customer_handover", {"filters": [{"field": "id", "op": "=", "value": handover_id}]})
|
||||
if not handover_records or len(handover_records) == 0:
|
||||
raise ValueError("交接记录不存在")
|
||||
|
||||
if handover["current_stage"] != "confirmation":
|
||||
raise ValueError("当前不在确认阶段")
|
||||
handover = handover_records[0]
|
||||
if handover["current_stage"] != "confirmation":
|
||||
raise ValueError("当前不在确认阶段")
|
||||
|
||||
# 更新客户负责人
|
||||
await dbp.update(
|
||||
"customers",
|
||||
{
|
||||
"owner_id": handover["to_owner_id"],
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
{"id": handover["customer_id"]}
|
||||
)
|
||||
# 更新客户负责人
|
||||
await sor.U(
|
||||
"customers",
|
||||
{
|
||||
"owner_id": handover["to_owner_id"],
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
{"filters": [{"field": "id", "op": "=", "value": handover["customer_id"]}]}
|
||||
)
|
||||
|
||||
# 完成交接流程
|
||||
await dbp.update(
|
||||
"customer_handover",
|
||||
{
|
||||
"current_stage": "completed",
|
||||
"confirmed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"completed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
{"id": handover_id}
|
||||
)
|
||||
# 完成交接流程
|
||||
await sor.U(
|
||||
"customer_handover",
|
||||
{
|
||||
"current_stage": "completed",
|
||||
"confirmed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"completed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
{"filters": [{"field": "id", "op": "=", "value": handover_id}]}
|
||||
)
|
||||
|
||||
# 发送客户通知(模拟)
|
||||
await send_customer_notification(handover["customer_id"], handover["to_owner_id"])
|
||||
# 发送客户通知(模拟)
|
||||
await send_customer_notification(sor, handover["customer_id"], handover["to_owner_id"])
|
||||
|
||||
return {"handover_id": handover_id, "stage": "completed", "customer_id": handover["customer_id"]}
|
||||
return {"handover_id": handover_id, "stage": "completed", "customer_id": handover["customer_id"]}
|
||||
|
||||
|
||||
async def send_customer_notification(customer_id: str, new_owner_id: str):
|
||||
async def send_customer_notification(sor, customer_id: str, new_owner_id: str):
|
||||
"""发送客户对接人变更通知"""
|
||||
# 这里应该集成短信/邮件服务
|
||||
# 模拟实现
|
||||
dbp = DBP()
|
||||
customer = await dbp.select_one("customers", {"id": customer_id})
|
||||
new_owner = await dbp.select_one("users", {"id": new_owner_id}) # 假设用户表存在
|
||||
customer_records = await sor.R("customers", {"filters": [{"field": "id", "op": "=", "value": customer_id}]})
|
||||
customer = customer_records[0] if customer_records else None
|
||||
|
||||
notification_content = f"尊敬的{customer['customer_name']},您的客户经理已变更为{new_owner.get('name', '新经理')},联系方式:{new_owner.get('phone', '待更新')}。"
|
||||
new_owner_records = await sor.R("users", {"filters": [{"field": "id", "op": "=", "value": new_owner_id}]})
|
||||
new_owner = new_owner_records[0] if new_owner_records else {"name": "新经理", "phone": "待更新"}
|
||||
|
||||
# 记录通知日志(实际应调用短信/邮件API)
|
||||
print(f"客户通知已发送: {notification_content}")
|
||||
if customer:
|
||||
notification_content = f"尊敬的{customer['customer_name']},您的客户经理已变更为{new_owner.get('name', '新经理')},联系方式:{new_owner.get('phone', '待更新')}。"
|
||||
|
||||
# 记录通知日志(实际应调用短信/邮件API)
|
||||
print(f"客户通知已发送: {notification_content}")
|
||||
|
||||
|
||||
async def recycle_to_pool(customer_id: str, inactive_days: int = None, reason: str = "inactive_days"):
|
||||
"""回收客户到公海池"""
|
||||
dbp = DBP()
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('default') as sor:
|
||||
customer_records = await sor.R("customers", {"filters": [{"field": "id", "op": "=", "value": customer_id}]})
|
||||
if not customer_records or len(customer_records) == 0:
|
||||
raise ValueError("客户不存在")
|
||||
|
||||
customer = await dbp.select_one("customers", {"id": customer_id})
|
||||
if not customer:
|
||||
raise ValueError("客户不存在")
|
||||
customer = customer_records[0]
|
||||
if customer["status"] == "in_pool":
|
||||
raise ValueError("客户已在公海池中")
|
||||
|
||||
if customer["status"] == "in_pool":
|
||||
raise ValueError("客户已在公海池中")
|
||||
# 创建公海记录
|
||||
pool_id = str(uuid.uuid4()).replace('-', '')
|
||||
pool_data = {
|
||||
"id": pool_id,
|
||||
"customer_id": customer_id,
|
||||
"original_owner_id": customer["owner_id"],
|
||||
"recycle_reason": reason,
|
||||
"inactive_days": inactive_days,
|
||||
"recycled_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"pool_status": "available",
|
||||
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
# 创建公海记录
|
||||
pool_id = str(uuid.uuid4()).replace('-', '')
|
||||
pool_data = {
|
||||
"id": pool_id,
|
||||
"customer_id": customer_id,
|
||||
"original_owner_id": customer["owner_id"],
|
||||
"recycle_reason": reason,
|
||||
"inactive_days": inactive_days,
|
||||
"recycled_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"pool_status": "available",
|
||||
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
await sor.C("customer_pool", pool_data)
|
||||
|
||||
await dbp.insert("customer_pool", pool_data)
|
||||
# 更新客户状态
|
||||
await sor.U(
|
||||
"customers",
|
||||
{
|
||||
"status": "in_pool",
|
||||
"owner_id": "", # 清空负责人
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
{"filters": [{"field": "id", "op": "=", "value": customer_id}]}
|
||||
)
|
||||
|
||||
# 更新客户状态
|
||||
await dbp.update(
|
||||
"customers",
|
||||
{
|
||||
"status": "in_pool",
|
||||
"owner_id": "", # 清空负责人
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
{"id": customer_id}
|
||||
)
|
||||
|
||||
return pool_data
|
||||
return pool_data
|
||||
|
||||
|
||||
async def claim_from_pool(pool_id: str, new_owner_id: str = None):
|
||||
"""从公海池认领客户"""
|
||||
dbp = DBP()
|
||||
new_owner_id = new_owner_id or get_current_user_id()
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('default') as sor:
|
||||
new_owner_id = new_owner_id or get_current_user_id()
|
||||
|
||||
pool_record = await dbp.select_one("customer_pool", {"id": pool_id})
|
||||
if not pool_record:
|
||||
raise ValueError("公海记录不存在")
|
||||
pool_records = await sor.R("customer_pool", {"filters": [{"field": "id", "op": "=", "value": pool_id}]})
|
||||
if not pool_records or len(pool_records) == 0:
|
||||
raise ValueError("公海记录不存在")
|
||||
|
||||
if pool_record["pool_status"] != "available":
|
||||
raise ValueError("该客户已被认领或分配")
|
||||
pool_record = pool_records[0]
|
||||
if pool_record["pool_status"] != "available":
|
||||
raise ValueError("该客户已被认领或分配")
|
||||
|
||||
# 更新公海记录
|
||||
await dbp.update(
|
||||
"customer_pool",
|
||||
{
|
||||
"assigned_to": new_owner_id,
|
||||
"assigned_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"pool_status": "claimed",
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
{"id": pool_id}
|
||||
)
|
||||
# 更新公海记录
|
||||
await sor.U(
|
||||
"customer_pool",
|
||||
{
|
||||
"assigned_to": new_owner_id,
|
||||
"assigned_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"pool_status": "claimed",
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
{"filters": [{"field": "id", "op": "=", "value": pool_id}]}
|
||||
)
|
||||
|
||||
# 更新客户状态和负责人
|
||||
await dbp.update(
|
||||
"customers",
|
||||
{
|
||||
"status": "active",
|
||||
"owner_id": new_owner_id,
|
||||
"last_follow_up": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
{"id": pool_record["customer_id"]}
|
||||
)
|
||||
# 更新客户状态和负责人
|
||||
await sor.U(
|
||||
"customers",
|
||||
{
|
||||
"status": "active",
|
||||
"owner_id": new_owner_id,
|
||||
"last_follow_up": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
{"filters": [{"field": "id", "op": "=", "value": pool_record["customer_id"]}]}
|
||||
)
|
||||
|
||||
return {"customer_id": pool_record["customer_id"], "new_owner_id": new_owner_id}
|
||||
return {"customer_id": pool_record["customer_id"], "new_owner_id": new_owner_id}
|
||||
|
||||
|
||||
async def get_customer_360_view(customer_id: str) -> Dict:
|
||||
"""获取客户360度视图"""
|
||||
dbp = DBP()
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('default') as sor:
|
||||
# 客户基本信息
|
||||
customer_records = await sor.R("customers", {"filters": [{"field": "id", "op": "=", "value": customer_id}]})
|
||||
if not customer_records or len(customer_records) == 0:
|
||||
raise ValueError("客户不存在")
|
||||
|
||||
# 客户基本信息
|
||||
customer = await dbp.select_one("customers", {"id": customer_id})
|
||||
if not customer:
|
||||
raise ValueError("客户不存在")
|
||||
customer = customer_records[0]
|
||||
|
||||
# 商机记录
|
||||
opportunities = await dbp.query(
|
||||
"SELECT id, estimated_amount, current_stage, expected_close_date, status FROM opportunities WHERE customer_name = %(customer_name)s ORDER BY created_at DESC",
|
||||
{"customer_name": customer["customer_name"]}
|
||||
)
|
||||
# 商机记录
|
||||
opportunities = await sor.R("opportunities", {
|
||||
"filters": [{"field": "customer_name", "op": "=", "value": customer["customer_name"]}],
|
||||
"sortby": [{"field": "created_at", "direction": "desc"}]
|
||||
})
|
||||
|
||||
# 合同历史(假设合同管理模块存在)
|
||||
contracts = await dbp.query(
|
||||
"SELECT id, contract_no, amount, start_date, end_date, status FROM contracts WHERE customer_id = %(customer_id)s ORDER BY created_at DESC",
|
||||
{"customer_id": customer_id}
|
||||
)
|
||||
# 合同历史(假设合同管理模块存在)
|
||||
contracts = await sor.R("contracts", {
|
||||
"filters": [{"field": "customer_id", "op": "=", "value": customer_id}],
|
||||
"sortby": [{"field": "created_at", "direction": "desc"}]
|
||||
})
|
||||
|
||||
# 服务工单(假设服务模块存在)
|
||||
service_tickets = await dbp.query(
|
||||
"SELECT id, ticket_no, subject, priority, status, created_at FROM service_tickets WHERE customer_id = %(customer_id)s ORDER BY created_at DESC",
|
||||
{"customer_id": customer_id}
|
||||
)
|
||||
# 服务工单(假设服务模块存在)
|
||||
service_tickets = await sor.R("service_tickets", {
|
||||
"filters": [{"field": "customer_id", "op": "=", "value": customer_id}],
|
||||
"sortby": [{"field": "created_at", "direction": "desc"}]
|
||||
})
|
||||
|
||||
# 回款情况
|
||||
payments = await dbp.query(
|
||||
"SELECT id, invoice_no, amount, paid_amount, due_date, status FROM payment_records WHERE customer_id = %(customer_id)s ORDER BY due_date DESC",
|
||||
{"customer_id": customer_id}
|
||||
)
|
||||
# 回款情况
|
||||
payments = await sor.R("payment_records", {
|
||||
"filters": [{"field": "customer_id", "op": "=", "value": customer_id}],
|
||||
"sortby": [{"field": "due_date", "direction": "desc"}]
|
||||
})
|
||||
|
||||
return {
|
||||
"customer": customer,
|
||||
"opportunities": opportunities,
|
||||
"contracts": contracts,
|
||||
"service_tickets": service_tickets,
|
||||
"payments": payments
|
||||
}
|
||||
return {
|
||||
"customer": customer,
|
||||
"opportunities": opportunities,
|
||||
"contracts": contracts,
|
||||
"service_tickets": service_tickets,
|
||||
"payments": payments
|
||||
}
|
||||
|
||||
|
||||
def get_current_user_id() -> str:
|
||||
|
||||
19
wwwroot/api/check_tables.dspy
Normal file
19
wwwroot/api/check_tables.dspy
Normal file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
result = {'keys': [], 'rows': []}
|
||||
try:
|
||||
dbname = get_module_dbname('customer_management')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
ns = {'page': 1, 'rows': 50, 'sort': 'TABLE_NAME'}
|
||||
sql = "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA='crm_db'"
|
||||
rows = await sor.sqlExe(sql, ns)
|
||||
if isinstance(rows, dict):
|
||||
rows = rows.get('rows', [])
|
||||
if rows:
|
||||
result['keys'] = list(dict(rows[0]).keys())
|
||||
result['rows'] = [list(dict(r).values()) for r in rows]
|
||||
result['success'] = True
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
30
wwwroot/api/customer_pool_list.dspy
Normal file
30
wwwroot/api/customer_pool_list.dspy
Normal file
@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Customer pool list API"""
|
||||
import json
|
||||
|
||||
result = {'success': False, 'rows': [], 'total': 0}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('customer_management')
|
||||
ns = {
|
||||
'page': int(params_kw.get('page', 1)),
|
||||
'rows': int(params_kw.get('rows', 20)),
|
||||
'sort': 'created_at desc'
|
||||
}
|
||||
sql = "SELECT id, customer_id, original_owner_id, recycle_reason, inactive_days, recycled_at, pool_status, created_at FROM customer_pool"
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
data = await sor.sqlExe(sql, ns)
|
||||
if isinstance(data, dict):
|
||||
result['total'] = data.get('total', 0)
|
||||
result['rows'] = [dict(r) for r in data.get('rows', [])]
|
||||
else:
|
||||
result['rows'] = [dict(r) for r in (data or [])]
|
||||
result['total'] = len(result['rows'])
|
||||
result['success'] = True
|
||||
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
48
wwwroot/api/customers_create.dspy
Normal file
48
wwwroot/api/customers_create.dspy
Normal file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Customer create API"""
|
||||
import json, uuid, time
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
customer_name = params_kw.get('customer_name', '').strip()
|
||||
customer_type = params_kw.get('customer_type', '').strip()
|
||||
owner_id = params_kw.get('owner_id', '').strip()
|
||||
|
||||
if not customer_name:
|
||||
result['options'] = {'title': 'Error', 'message': '客户名称不能为空', 'type': 'error'}
|
||||
elif not customer_type:
|
||||
result['options'] = {'title': 'Error', 'message': '客户类型不能为空', 'type': 'error'}
|
||||
elif not owner_id:
|
||||
result['options'] = {'title': 'Error', 'message': '负责人不能为空', 'type': 'error'}
|
||||
else:
|
||||
dbname = get_module_dbname('customer_management')
|
||||
new_id = str(uuid.uuid4()).replace('-', '')
|
||||
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
phone = params_kw.get('phone', '').strip()
|
||||
email = params_kw.get('email', '').strip()
|
||||
tax_id = params_kw.get('tax_id', '').strip()
|
||||
industry = params_kw.get('industry', '').strip()
|
||||
customer_level = params_kw.get('customer_level', 'potential').strip()
|
||||
address = params_kw.get('address', '').strip()
|
||||
region = params_kw.get('region', '').strip()
|
||||
status = params_kw.get('status', 'active').strip()
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.sqlExe("""INSERT INTO customers (id, customer_name, customer_type, phone, email, tax_id, industry, customer_level, address, owner_id, region, status, created_at, updated_at)
|
||||
VALUES (${id}$, ${customer_name}$, ${customer_type}$, ${phone}$, ${email}$, ${tax_id}$, ${industry}$, ${customer_level}$, ${address}$, ${owner_id}$, ${region}$, ${status}$, ${created_at}$, ${updated_at}$)""", {
|
||||
'id': new_id, 'customer_name': customer_name, 'customer_type': customer_type,
|
||||
'phone': phone, 'email': email, 'tax_id': tax_id, 'industry': industry,
|
||||
'customer_level': customer_level, 'address': address, 'owner_id': owner_id,
|
||||
'region': region, 'status': status,
|
||||
'created_at': now, 'updated_at': now
|
||||
})
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '客户创建成功', 'type': 'success'}}
|
||||
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': '创建失败: ' + str(e), 'type': 'error'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
25
wwwroot/api/customers_delete.dspy
Normal file
25
wwwroot/api/customers_delete.dspy
Normal file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Customer delete API"""
|
||||
import json
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
record_id = params_kw.get('id', '').strip()
|
||||
if not record_id:
|
||||
result['options'] = {'title': 'Error', 'message': '记录ID不能为空', 'type': 'error'}
|
||||
else:
|
||||
dbname = get_module_dbname('customer_management')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
existing = await sor.sqlExe("SELECT id FROM customers WHERE id = ${id}$", {'id': record_id})
|
||||
if not existing:
|
||||
result['options'] = {'title': 'Error', 'message': '客户不存在', 'type': 'error'}
|
||||
else:
|
||||
await sor.sqlExe("DELETE FROM customers WHERE id = ${id}$", {'id': record_id})
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '删除成功', 'type': 'success'}}
|
||||
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'删除失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
30
wwwroot/api/customers_list.dspy
Normal file
30
wwwroot/api/customers_list.dspy
Normal file
@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Customer list API - uses sqlor built-in pagination"""
|
||||
import json
|
||||
|
||||
result = {'success': False, 'rows': [], 'total': 0}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('customer_management')
|
||||
ns = {
|
||||
'page': int(params_kw.get('page', 1)),
|
||||
'rows': int(params_kw.get('rows', 20)),
|
||||
'sort': 'created_at desc'
|
||||
}
|
||||
sql = "SELECT id, customer_name, customer_type, phone, email, industry, customer_level, region, status, created_at FROM customers"
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
data = await sor.sqlExe(sql, ns)
|
||||
if isinstance(data, dict):
|
||||
result['total'] = data.get('total', 0)
|
||||
result['rows'] = [dict(r) for r in data.get('rows', [])]
|
||||
else:
|
||||
result['rows'] = [dict(r) for r in (data or [])]
|
||||
result['total'] = len(result['rows'])
|
||||
result['success'] = True
|
||||
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
39
wwwroot/api/customers_update.dspy
Normal file
39
wwwroot/api/customers_update.dspy
Normal file
@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Customer update API"""
|
||||
import json, time
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
record_id = params_kw.get('id', '').strip()
|
||||
if not record_id:
|
||||
result['options'] = {'title': 'Error', 'message': '记录ID不能为空', 'type': 'error'}
|
||||
else:
|
||||
dbname = get_module_dbname('customer_management')
|
||||
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
fields = ['customer_name', 'customer_type', 'phone', 'email', 'tax_id', 'industry', 'customer_level', 'address', 'owner_id', 'region', 'status']
|
||||
set_parts = []
|
||||
params = {'id': record_id}
|
||||
for f in fields:
|
||||
val = params_kw.get(f, None)
|
||||
if val is not None:
|
||||
set_parts.append(f + " = ${" + f + "}$")
|
||||
params[f] = val
|
||||
|
||||
set_parts.append("updated_at = ${updated_at}$")
|
||||
params['updated_at'] = now
|
||||
|
||||
if len(set_parts) <= 1:
|
||||
result['options'] = {'title': 'Error', 'message': '没有可更新的字段', 'type': 'error'}
|
||||
else:
|
||||
sql = "UPDATE customers SET " + ", ".join(set_parts) + " WHERE id = ${id}$"
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.sqlExe(sql, params)
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '更新成功', 'type': 'success'}}
|
||||
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': '更新失败: ' + str(e), 'type': 'error'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
30
wwwroot/api/handover_list.dspy
Normal file
30
wwwroot/api/handover_list.dspy
Normal file
@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Customer handover list API"""
|
||||
import json
|
||||
|
||||
result = {'success': False, 'rows': [], 'total': 0}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('customer_management')
|
||||
ns = {
|
||||
'page': int(params_kw.get('page', 1)),
|
||||
'rows': int(params_kw.get('rows', 20)),
|
||||
'sort': 'created_at desc'
|
||||
}
|
||||
sql = "SELECT id, customer_id, from_owner_id, to_owner_id, current_stage, handover_reason, created_at FROM customer_handover"
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
data = await sor.sqlExe(sql, ns)
|
||||
if isinstance(data, dict):
|
||||
result['total'] = data.get('total', 0)
|
||||
result['rows'] = [dict(r) for r in data.get('rows', [])]
|
||||
else:
|
||||
result['rows'] = [dict(r) for r in (data or [])]
|
||||
result['total'] = len(result['rows'])
|
||||
result['success'] = True
|
||||
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
108
wwwroot/base.ui
108
wwwroot/base.ui
@ -1,51 +1,67 @@
|
||||
{
|
||||
"widgettype": "TabPanel",
|
||||
"options": {
|
||||
"title": "客户管理"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "CRUD",
|
||||
"options": {
|
||||
"title": "客户档案",
|
||||
"url": "{{entire_url(customers_list)}}"
|
||||
}
|
||||
"type": "Page",
|
||||
"title": "Customer Management",
|
||||
"content": {
|
||||
"type": "VBox",
|
||||
"style": {"padding": "16px", "height": "100%"},
|
||||
"children": [
|
||||
{
|
||||
"type": "HBox",
|
||||
"justify": "space-between",
|
||||
"style": {"marginBottom": "16px"},
|
||||
"children": [
|
||||
{"type": "Text", "content": "Customer List", "style": {"fontSize": "20px", "fontWeight": "bold"}},
|
||||
{"type": "Button", "id": "addCustomerBtn", "text": "Add Customer", "variant": "primary", "leadingIcon": "add", "onclick": "openDialog('addCustomerDialog')"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "CRUD",
|
||||
"id": "customerCRUD",
|
||||
"api": {
|
||||
"list": "main/customer_management/api/customers_list.dspy",
|
||||
"create": "main/customer_management/api/customers_create.dspy",
|
||||
"update": "main/customer_management/api/customers_update.dspy",
|
||||
"delete": "main/customer_management/api/customers_delete.dspy"
|
||||
},
|
||||
{
|
||||
"widgettype": "CRUD",
|
||||
"options": {
|
||||
"title": "客户交接",
|
||||
"url": "{{entire_url(handover_list)}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "CRUD",
|
||||
"options": {
|
||||
"title": "客户公海池",
|
||||
"url": "{{entire_url(customer_pool_list)}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Panel",
|
||||
"options": {
|
||||
"title": "客户360度视图"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Form",
|
||||
"options": {
|
||||
"title": "客户查询",
|
||||
"fields": [
|
||||
{
|
||||
"name": "customer_id",
|
||||
"label": "客户ID",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"onSubmit": "get_customer_360_view"
|
||||
}
|
||||
}
|
||||
]
|
||||
"columns": [
|
||||
{"field": "customer_name", "title": "Customer Name", "width": 180},
|
||||
{"field": "customer_type", "title": "Type", "width": 100, "formatter": "code:customer_type"},
|
||||
{"field": "phone", "title": "Phone", "width": 130},
|
||||
{"field": "email", "title": "Email", "width": 180},
|
||||
{"field": "industry", "title": "Industry", "width": 120},
|
||||
{"field": "customer_level", "title": "Level", "width": 100, "formatter": "code:customer_level"},
|
||||
{"field": "region", "title": "Region", "width": 100},
|
||||
{"field": "status", "title": "Status", "width": 80, "formatter": "code:customer_status"},
|
||||
{"field": "created_at", "title": "Created", "width": 160}
|
||||
],
|
||||
"style": {"flex": 1}
|
||||
},
|
||||
{
|
||||
"type": "Dialog",
|
||||
"id": "addCustomerDialog",
|
||||
"title": "Add Customer",
|
||||
"width": 600,
|
||||
"content": {
|
||||
"type": "Form",
|
||||
"id": "addCustomerForm",
|
||||
"fields": [
|
||||
{"field": "customer_name", "title": "Customer Name", "uitype": "TextField", "required": true},
|
||||
{"field": "customer_type", "title": "Type", "uitype": "SelectField", "required": true, "options": "code:customer_type"},
|
||||
{"field": "phone", "title": "Phone", "uitype": "TextField"},
|
||||
{"field": "email", "title": "Email", "uitype": "TextField"},
|
||||
{"field": "tax_id", "title": "Tax ID", "uitype": "TextField"},
|
||||
{"field": "industry", "title": "Industry", "uitype": "TextField"},
|
||||
{"field": "customer_level", "title": "Level", "uitype": "SelectField", "options": "code:customer_level", "value": "potential"},
|
||||
{"field": "address", "title": "Address", "uitype": "TextField"},
|
||||
{"field": "region", "title": "Region", "uitype": "TextField"},
|
||||
{"field": "owner_id", "title": "Owner ID", "uitype": "TextField", "required": true}
|
||||
],
|
||||
"actions": [
|
||||
{"type": "Button", "text": "Cancel", "onclick": "closeDialog('addCustomerDialog')"},
|
||||
{"type": "Button", "text": "Submit", "variant": "primary", "onclick": "submitForm('addCustomerForm', 'main/customer_management/api/customers_create.dspy', 'addCustomerDialog', 'customerCRUD')"}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
65
wwwroot/customer_edit.ui
Normal file
65
wwwroot/customer_edit.ui
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"widgettype": "Page",
|
||||
"options": {
|
||||
"title": "客户编辑",
|
||||
"style": {"height": "100vh", "padding": "0"}
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"style": {"padding": "16px", "flex": 1, "overflow": "auto"}},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Form",
|
||||
"id": "customer_form",
|
||||
"options": {
|
||||
"submit_url": "{{entire_url('api/customers_create.dspy')}}",
|
||||
"method": "POST",
|
||||
"layout": "vertical",
|
||||
"style": {"maxWidth": "600px"},
|
||||
"fields": [
|
||||
{"name": "customer_name", "label": "客户名称", "uitype": "text", "required": true},
|
||||
{"name": "customer_type", "label": "客户类型", "uitype": "code", "required": true, "data": [
|
||||
{"value": "enterprise", "text": "企业客户"},
|
||||
{"value": "individual", "text": "个人客户"},
|
||||
{"value": "government", "text": "政府客户"}
|
||||
]},
|
||||
{"name": "contact_person", "label": "联系人", "uitype": "text"},
|
||||
{"name": "phone", "label": "电话", "uitype": "text"},
|
||||
{"name": "email", "label": "邮箱", "uitype": "text"},
|
||||
{"name": "tax_id", "label": "税号", "uitype": "text"},
|
||||
{"name": "industry", "label": "行业", "uitype": "code", "data": [
|
||||
{"value": "manufacturing", "text": "制造业"},
|
||||
{"value": "technology", "text": "科技"},
|
||||
{"value": "finance", "text": "金融"},
|
||||
{"value": "retail", "text": "零售"},
|
||||
{"value": "other", "text": "其他"}
|
||||
]},
|
||||
{"name": "customer_level", "label": "客户等级", "uitype": "code", "data": [
|
||||
{"value": "potential", "text": "潜在客户"},
|
||||
{"value": "active", "text": "活跃客户"},
|
||||
{"value": "vip", "text": "VIP客户"}
|
||||
]},
|
||||
{"name": "address", "label": "地址", "uitype": "textarea"},
|
||||
{"name": "owner_id", "label": "负责人", "uitype": "text", "required": true},
|
||||
{"name": "region", "label": "区域", "uitype": "code", "data": [
|
||||
{"value": "east", "text": "华东"},
|
||||
{"value": "south", "text": "华南"},
|
||||
{"value": "west", "text": "华西"},
|
||||
{"value": "north", "text": "华北"}
|
||||
]},
|
||||
{"name": "status", "label": "状态", "uitype": "code", "data": [
|
||||
{"value": "active", "text": "有效"},
|
||||
{"value": "inactive", "text": "无效"}
|
||||
]}
|
||||
],
|
||||
"buttons": [
|
||||
{"type": "submit", "text": "保存", "variant": "primary"},
|
||||
{"type": "button", "text": "取消", "action": "navigate('main/customer_management/customer_list.ui')"}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
45
wwwroot/customer_list.ui
Normal file
45
wwwroot/customer_list.ui
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"widgettype": "Page",
|
||||
"options": {
|
||||
"title": "客户列表",
|
||||
"style": {"height": "100vh", "padding": "0"}
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"style": {"padding": "16px", "flex": 1, "overflow": "hidden"}},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
"options": {"style": {"marginBottom": "16px", "gap": "8px"}},
|
||||
"subwidgets": [
|
||||
{"widgettype": "TextField", "id": "search_keyword", "options": {"label": "搜索", "placeholder": "客户名称/编码", "style": {"flex": 1}}},
|
||||
{"widgettype": "Button", "id": "btn_search", "options": {"text": "搜索", "variant": "primary"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "DataGrid",
|
||||
"id": "customer_grid",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/customers_list.dspy')}}",
|
||||
"style": {"flex": 1},
|
||||
"columns": [
|
||||
{"field": "customer_code", "header": "客户编码", "width": 120},
|
||||
{"field": "customer_name", "header": "客户名称", "width": 200},
|
||||
{"field": "customer_type", "header": "类型", "width": 100},
|
||||
{"field": "industry", "header": "行业", "width": 120},
|
||||
{"field": "contact_person", "header": "联系人", "width": 100},
|
||||
{"field": "phone", "header": "电话", "width": 120},
|
||||
{"field": "status", "header": "状态", "width": 80}
|
||||
],
|
||||
"toolbar": [
|
||||
{"type": "button", "text": "新增", "icon": "add", "action": "navigate('main/customer_management/customer_edit.ui')"},
|
||||
{"type": "button", "text": "编辑", "icon": "edit", "action": "navigate('main/customer_management/customer_edit.ui?id={% raw %}{{selectedRow.id}}{% endraw %}')"},
|
||||
{"type": "button", "text": "删除", "icon": "delete", "action": "doDelete('{% raw %}{{selectedRow.id}}{% endraw %}')"}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
34
wwwroot/customer_pool.ui
Normal file
34
wwwroot/customer_pool.ui
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"widgettype": "Page",
|
||||
"options": {
|
||||
"title": "客户公海",
|
||||
"style": {"height": "100vh", "padding": "0"}
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"style": {"padding": "16px", "flex": 1, "overflow": "hidden"}},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "DataGrid",
|
||||
"id": "pool_grid",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/customer_pool_list.dspy')}}",
|
||||
"style": {"flex": 1},
|
||||
"columns": [
|
||||
{"field": "customer_code", "header": "客户编码", "width": 120},
|
||||
{"field": "customer_name", "header": "客户名称", "width": 200},
|
||||
{"field": "industry", "header": "行业", "width": 120},
|
||||
{"field": "recycle_reason", "header": "回收原因", "width": 150},
|
||||
{"field": "pool_status", "header": "状态", "width": 80},
|
||||
{"field": "recycle_time", "header": "回收时间", "width": 150}
|
||||
],
|
||||
"toolbar": [
|
||||
{"type": "button", "text": "领取", "icon": "get_app", "action": "claimCustomer('{% raw %}{{selectedRow.id}}{% endraw %}')"}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
31
wwwroot/handover_list.ui
Normal file
31
wwwroot/handover_list.ui
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"widgettype": "Page",
|
||||
"options": {
|
||||
"title": "客户交接",
|
||||
"style": {"height": "100vh", "padding": "0"}
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"style": {"padding": "16px", "flex": 1, "overflow": "hidden"}},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "DataGrid",
|
||||
"id": "handover_grid",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/handover_list.dspy')}}",
|
||||
"style": {"flex": 1},
|
||||
"columns": [
|
||||
{"field": "customer_name", "header": "客户名称", "width": 200},
|
||||
{"field": "from_owner_name", "header": "原负责人", "width": 120},
|
||||
{"field": "to_owner_name", "header": "新负责人", "width": 120},
|
||||
{"field": "handover_reason", "header": "交接原因", "width": 200},
|
||||
{"field": "status", "header": "状态", "width": 80},
|
||||
{"field": "created_at", "header": "创建时间", "width": 150}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user