diff --git a/discount/init.py b/discount/init.py index d45e1ab..da99a04 100644 --- a/discount/init.py +++ b/discount/init.py @@ -881,6 +881,11 @@ def load_discount(): # Marketing plan discount setting env.get_marketing_discount_products = get_marketing_discount_products env.get_marketing_info = get_marketing_info + # Customer assignment + env.get_free_customers = get_free_customers + env.get_bound_customers = get_bound_customers + env.save_customer_sale = save_customer_sale + env.get_reseller_sales = get_reseller_sales async def get_discount_setting_products(request): @@ -1020,3 +1025,109 @@ async def get_marketing_info(request): if recs: return {'name': recs[0].name or ''} return {} + + +async def get_free_customers(request): + """Return unbound customers for the current reseller. + Used in Jinja2 template: {% set customers = get_free_customers(request) %} + """ + env = request._run_ns + userorgid = await env.get_userorgid() + if not userorgid or userorgid == '0': + return [] + dbname = env.get_module_dbname('discount') + async with DBPools().sqlorContext(dbname) as sor: + # Find customers of this reseller that are unbound (no sale_id) + rows = await sor.sqlExe( + """SELECT dcb.id as bind_id, dcb.customerid, + o.orgname as customer_name, dcb.sale_id + FROM discount_customer_bind dcb + JOIN organization o ON o.id = dcb.customerid + WHERE dcb.resellerid = ${rid}$ AND dcb.sale_id IS NULL + ORDER BY o.orgname""", + {'rid': userorgid} + ) + return rows or [] + + +async def get_bound_customers(request): + """Return bound customers with their current sale for the current reseller. + Used in Jinja2 template: {% set customers = get_bound_customers(request) %} + """ + env = request._run_ns + userorgid = await env.get_userorgid() + if not userorgid or userorgid == '0': + return [] + dbname = env.get_module_dbname('discount') + async with DBPools().sqlorContext(dbname) as sor: + rows = await sor.sqlExe( + """SELECT dcb.id as bind_id, dcb.customerid, + o.orgname as customer_name, dcb.sale_id, + u.nick_name as sale_name + FROM discount_customer_bind dcb + JOIN organization o ON o.id = dcb.customerid + LEFT JOIN users u ON u.id = dcb.sale_id + WHERE dcb.resellerid = ${rid}$ AND dcb.sale_id IS NOT NULL + ORDER BY o.orgname""", + {'rid': userorgid} + ) + return rows or [] + + +async def save_customer_sale(request, params_kw): + """Save/update sale assignment for a customer (upsert on discount_customer_bind). + Called from free_customer_assign and customer_reassign pages on blur. + """ + env = request._run_ns + userorgid = await env.get_userorgid() + bind_id = params_kw.get('bind_id') + customerid = params_kw.get('customerid') + sale_id = params_kw.get('sale_id') + + if not customerid: + return {'status': 'error', 'message': 'Missing customerid'} + + dbname = env.get_module_dbname('discount') + async with DBPools().sqlorContext(dbname) as sor: + if bind_id: + # Update existing binding + await sor.U('discount_customer_bind', { + 'id': bind_id, + 'sale_id': sale_id or None, + }) + else: + # Create new binding for free customer + new_id = getID() + await sor.C('discount_customer_bind', { + 'id': new_id, + 'customerid': customerid, + 'resellerid': userorgid, + 'sale_id': sale_id or None, + 'promo_code_id': None, + 'bind_type': '2', # manual assignment + }) + bind_id = new_id + + return {'status': 'success', 'bind_id': bind_id} + + +async def get_reseller_sales(request): + """Return sales users list for the current reseller (for dropdown). + Used in Jinja2 template: {% set sales = get_reseller_sales(request) %} + """ + env = request._run_ns + userorgid = await env.get_userorgid() + if not userorgid or userorgid == '0': + return [] + async with DBPools().sqlorContext('sage') as sor: + rows = await sor.sqlExe( + """SELECT DISTINCT u.id as sale_id, u.nick_name as sale_name + FROM users u + JOIN userrole ur ON ur.userid = u.id + JOIN role r ON r.id = ur.roleid + WHERE r.name = 'sale' AND u.orgid = ${oid}$ + ORDER BY u.nick_name""", + {'oid': userorgid} + ) + return [{'value': r.sale_id, 'text': r.sale_name} for r in rows] + diff --git a/scripts/load_path.py b/scripts/load_path.py index ae0373c..11f7e31 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -177,9 +177,17 @@ for tblname in SALE_TABLES: # sale 相关 API for api_path in get_api_files(): basename = os.path.basename(api_path) - if any(k in basename for k in ["promo_code", "customer_bind"]): + if any(k in basename for k in ["promo_code", "customer_bind", "customer_sale"]): PATHS_SALE.append(api_path) +# sale 客户分配页面 +PATHS_SALE.extend([ + f"/{MOD}/free_customer_assign", + f"/{MOD}/free_customer_assign/index.ui", + f"/{MOD}/customer_reassign", + f"/{MOD}/customer_reassign/index.ui", +]) + # ============================================================ # 去重 # ============================================================ diff --git a/wwwroot/api/get_customer_sale_list.dspy b/wwwroot/api/get_customer_sale_list.dspy index 097dde9..acdecf4 100644 --- a/wwwroot/api/get_customer_sale_list.dspy +++ b/wwwroot/api/get_customer_sale_list.dspy @@ -1,15 +1,19 @@ -# 获取机构下所有客户及销售归属 +# 获取当前机构下的客户及销售归属 +userorgid = await get_userorgid() +if not userorgid: + return [] + db = DBPools() -resellerid = params_kw.get('resellerid') or '0' +resellerid = userorgid sql = """ select o.id as customerid, o.orgname as customer_name, dcb.id as bind_id, dcb.sale_id, - u.name as sale_name + u.nick_name as sale_name from organization o left join discount_customer_bind dcb on dcb.customerid = o.id and dcb.resellerid = ${resellerid}$ -left join user u on u.id = dcb.sale_id -where 1=1 +left join users u on u.id = dcb.sale_id +where o.org_type = 'customer' and o.parentid = ${resellerid}$ order by o.orgname """ diff --git a/wwwroot/api/save_customer_sale.dspy b/wwwroot/api/save_customer_sale.dspy new file mode 100644 index 0000000..2b29a72 --- /dev/null +++ b/wwwroot/api/save_customer_sale.dspy @@ -0,0 +1,17 @@ +# Save/update customer-sale assignment (called on blur from customer assign pages) +from discount.init import save_customer_sale + +ret = await save_customer_sale(request, params_kw) +if ret.get('status') == 'success': + return { + 'widgettype': 'Message', + 'options': { + 'title': '成功', 'message': '已更新', 'type': 'success', 'timeout': 2 + } + } +return { + 'widgettype': 'Error', + 'options': { + 'title': '失败', 'message': ret.get('message', '未知错误'), 'timeout': 3 + } +} diff --git a/wwwroot/customer_reassign/index.ui b/wwwroot/customer_reassign/index.ui new file mode 100644 index 0000000..7709f50 --- /dev/null +++ b/wwwroot/customer_reassign/index.ui @@ -0,0 +1,69 @@ +{% set customers = get_bound_customers(request) %} +{% set sales = get_reseller_sales(request) %} +{ + "widgettype": "VBox", + "options": {"width": "100%", "height": "100%", "css": "filler"}, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "客户再分配 - 更换销售", + "fontSize": "16px", "fontWeight": "600", "color": "#F1F5F9", "padding": "8px 12px" + } + }, + { + "widgettype": "HBox", + "options": {"bgcolor": "#0F172A", "padding": "8px 0", "borderRadius": "6px 6px 0 0", "alignItems": "center", "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "客户名称", "width": "30%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8"}}, + {"widgettype": "Text", "options": {"text": "当前销售", "width": "25%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#22C55E"}}, + {"widgettype": "Text", "options": {"text": "更换为", "width": "35%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#F59E0B"}}, + {"widgettype": "Text", "options": {"text": "", "width": "10%", "padding": "0 8px"}} + ] + }, + { + "widgettype": "VScrollPanel", + "options": {"width": "100%", "flex": "1"}, + "subwidgets": [ + { + "widgettype": "VBox", + "options": {"width": "100%"}, + "subwidgets": [ +{% for c in customers %} + { + "widgettype": "HBox", + "options": {"padding": "6px 0", "border": "0 0 1px 0", "borderColor": "#334155", "alignItems": "center", "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": {{json.dumps(c.customer_name or '')}}, "width": "30%", "padding": "0 8px", "fontSize": "12px", "color": "#E2E8F0"}}, + {"widgettype": "Text", "options": {"text": {{json.dumps(c.sale_name or '-')}}, "width": "25%", "padding": "0 8px", "fontSize": "12px", "color": "#22C55E"}}, + { + "widgettype": "UiCode", + "options": { + "width": "35%", "padding": "0 8px", + "name": "sale_id", + "value": "", + "placeholder": "选择新销售...", + "data": {{json.dumps(sales or [], ensure_ascii=False)}} + }, + "binds": [{ + "wid": "self", "event": "blur", + "actiontype": "urlwidget", "datawidget": "self", "target": "self", + "options": { + "url": "{{entire_url('/discount/api/save_customer_sale.dspy')}}", + "params": { + "bind_id": {{json.dumps(c.bind_id or '')}}, + "customerid": {{json.dumps(c.customerid)}} + } + } + }] + }, + {"widgettype": "Text", "options": {"text": "", "width": "10%", "padding": "0 8px"}} + ] + }{% if not loop.last %},{% endif %} +{% endfor %} + ] + } + ] + } + ] +} diff --git a/wwwroot/free_customer_assign/index.ui b/wwwroot/free_customer_assign/index.ui new file mode 100644 index 0000000..52f41d2 --- /dev/null +++ b/wwwroot/free_customer_assign/index.ui @@ -0,0 +1,67 @@ +{% set customers = get_free_customers(request) %} +{% set sales = get_reseller_sales(request) %} +{ + "widgettype": "VBox", + "options": {"width": "100%", "height": "100%", "css": "filler"}, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "自由客户分配 - 选择销售", + "fontSize": "16px", "fontWeight": "600", "color": "#F1F5F9", "padding": "8px 12px" + } + }, + { + "widgettype": "HBox", + "options": {"bgcolor": "#0F172A", "padding": "8px 0", "borderRadius": "6px 6px 0 0", "alignItems": "center", "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "客户名称", "width": "45%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8"}}, + {"widgettype": "Text", "options": {"text": "分配销售", "width": "45%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#F59E0B"}}, + {"widgettype": "Text", "options": {"text": "状态", "width": "10%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8"}} + ] + }, + { + "widgettype": "VScrollPanel", + "options": {"width": "100%", "flex": "1"}, + "subwidgets": [ + { + "widgettype": "VBox", + "options": {"width": "100%"}, + "subwidgets": [ +{% for c in customers %} + { + "widgettype": "HBox", + "options": {"padding": "6px 0", "border": "0 0 1px 0", "borderColor": "#334155", "alignItems": "center", "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": {{json.dumps(c.customer_name or '')}}, "width": "45%", "padding": "0 8px", "fontSize": "12px", "color": "#E2E8F0"}}, + { + "widgettype": "UiCode", + "options": { + "width": "45%", "padding": "0 8px", + "name": "sale_id", + "value": "", + "placeholder": "选择销售...", + "data": {{json.dumps(sales or [], ensure_ascii=False)}} + }, + "binds": [{ + "wid": "self", "event": "blur", + "actiontype": "urlwidget", "datawidget": "self", "target": "self", + "options": { + "url": "{{entire_url('/discount/api/save_customer_sale.dspy')}}", + "params": { + "bind_id": {{json.dumps(c.bind_id or '')}}, + "customerid": {{json.dumps(c.customerid)}} + } + } + }] + }, + {"widgettype": "Text", "options": {"text": "未分配", "width": "10%", "padding": "0 8px", "fontSize": "11px", "color": "#475569"}} + ] + }{% if not loop.last %},{% endif %} +{% endfor %} + ] + } + ] + } + ] +}