From aec650dcef5fbe6d7e187b9bf5bf7a22a0ccee7f Mon Sep 17 00:00:00 2001 From: yumoqing Date: Tue, 28 Apr 2026 18:55:07 +0800 Subject: [PATCH] sync: local modifications to unified_dashboard - Updated core.py, init.py, mysql.ddl.sql - Added __init__.py - Added API files: dashboard_kpi, report_list - Added UI files: base.ui, dashboard.ui, reports.ui --- mysql.ddl.sql | 49 ++++++++++++++++++++ unified_dashboard/__init__.py | 1 + unified_dashboard/core.py | 11 +++-- unified_dashboard/init.py | 14 +++--- wwwroot/api/dashboard_kpi.dspy | 84 ++++++++++++++++++++++++++++++++++ wwwroot/api/report_list.dspy | 35 ++++++++++++++ wwwroot/base.ui | 44 ++++++++++++++++++ wwwroot/dashboard.ui | 83 +++++++++++++++++++++++++++++++++ wwwroot/reports.ui | 41 +++++++++++++++++ 9 files changed, 351 insertions(+), 11 deletions(-) create mode 100644 unified_dashboard/__init__.py create mode 100644 wwwroot/api/dashboard_kpi.dspy create mode 100644 wwwroot/api/report_list.dspy create mode 100644 wwwroot/base.ui create mode 100644 wwwroot/dashboard.ui create mode 100644 wwwroot/reports.ui diff --git a/mysql.ddl.sql b/mysql.ddl.sql index e69de29..17bc0d9 100644 --- a/mysql.ddl.sql +++ b/mysql.ddl.sql @@ -0,0 +1,49 @@ +-- Table from dashboard_config.json +CREATE TABLE IF NOT EXISTS `dashboard_config` ( + `id` VARCHAR(32) NOT NULL COMMENT '主键UUID', + `dashboard_name` VARCHAR(100) COMMENT '仪表板显示名称', + `dashboard_type` VARCHAR(50) COMMENT 'sales/finance/customer/executive', + `config_json` VARCHAR(2000) COMMENT '仪表板布局和组件配置', + `is_default` VARCHAR(1) COMMENT 'Y/N', + `org_id` VARCHAR(32) COMMENT '多租户组织隔离', + `created_by` VARCHAR(32) COMMENT '创建用户ID', + `created_at` TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='仪表板配置'; + +CREATE INDEX `idx_dashboard_org` ON `dashboard_config` (`org_id`); +CREATE INDEX `idx_dashboard_type` ON `dashboard_config` (`dashboard_type`); +CREATE UNIQUE INDEX `uk_dashboard_name_org` ON `dashboard_config` (`dashboard_name`, `org_id`); + +-- Table from report_template.json +CREATE TABLE IF NOT EXISTS `report_template` ( + `id` VARCHAR(32) NOT NULL COMMENT '主键UUID', + `template_name` VARCHAR(100) COMMENT '报表模板名称', + `report_type` VARCHAR(50) COMMENT 'sales/finance/customer/contract', + `sql_query` VARCHAR(2000) COMMENT '报表数据查询SQL', + `columns_config` VARCHAR(1000) NOT NULL COMMENT 'JSON格式的列配置', + `filters_config` VARCHAR(1000) NOT NULL COMMENT 'JSON格式的过滤器配置', + `chart_config` VARCHAR(1000) NOT NULL COMMENT 'JSON格式的图表配置', + `org_id` VARCHAR(32) COMMENT '多租户组织隔离', + `created_by` VARCHAR(32) COMMENT '创建用户ID', + `created_at` TIMESTAMP COMMENT '创建时间', + `is_active` VARCHAR(1) COMMENT 'Y/N', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='报表模板'; + +CREATE INDEX `idx_template_org` ON `report_template` (`org_id`); +CREATE INDEX `idx_template_type` ON `report_template` (`report_type`); + +-- Table from user_dashboard.json +CREATE TABLE IF NOT EXISTS `user_dashboard` ( + `id` VARCHAR(32) NOT NULL COMMENT '主键UUID', + `user_id` VARCHAR(32) COMMENT '关联用户', + `dashboard_config_id` VARCHAR(32) COMMENT '关联的仪表板配置', + `layout_json` VARCHAR(2000) NOT NULL COMMENT '用户自定义布局', + `is_favorite` VARCHAR(1) COMMENT 'Y/N', + `org_id` VARCHAR(32) COMMENT '多租户组织隔离', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户仪表板'; + +CREATE UNIQUE INDEX `idx_user_dashboard_user` ON `user_dashboard` (`user_id`, `dashboard_config_id`); +CREATE INDEX `idx_user_dashboard_org` ON `user_dashboard` (`org_id`); diff --git a/unified_dashboard/__init__.py b/unified_dashboard/__init__.py new file mode 100644 index 0000000..dd0e854 --- /dev/null +++ b/unified_dashboard/__init__.py @@ -0,0 +1 @@ +# Unified Dashboard Module diff --git a/unified_dashboard/core.py b/unified_dashboard/core.py index 4fd4193..b9c73d9 100644 --- a/unified_dashboard/core.py +++ b/unified_dashboard/core.py @@ -176,11 +176,12 @@ class DashboardCore: return approvals elif list_type == 'overdue_tasks': - tasks = await self.db.select('approval_task', - fields=['title', 'step_name', 'due_at'], - where={'org_id': org_id, 'status': 'pending', 'due_at < NOW()'}, - order_by='due_at ASC', - limit=10) + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + tasks = await self.db.sqlExe( + "SELECT title, step_name, due_at FROM approval_task WHERE org_id = ${org_id}$ AND status = ${status}$ AND due_at < ${now}$ ORDER BY due_at ASC", + {'org_id': org_id, 'status': 'pending', 'now': now}, + limit=10 + ) return tasks else: diff --git a/unified_dashboard/init.py b/unified_dashboard/init.py index 61fcc26..eda9c6e 100644 --- a/unified_dashboard/init.py +++ b/unified_dashboard/init.py @@ -1,6 +1,8 @@ from ahserver.serverenv import ServerEnv from appPublic.worker import awaitify from .core import DashboardCore, ReportCore +from sqlor.dbpools import DBPools + def load_unified_dashboard(): """加载统一仪表板和报表模块""" @@ -8,14 +10,14 @@ def load_unified_dashboard(): # 创建核心实例的工厂函数 async def create_dashboard_core(org_id): - from sqlor.dbp import getDBP - db = await getDBP(org_id) - return DashboardCore(db) + dbname = get_module_dbname('unified_dashboard') + sor = await DBPools().sqlorContext(dbname) + return DashboardCore(sor) async def create_report_core(org_id): - from sqlor.dbp import getDBP - db = await getDBP(org_id) - return ReportCore(db) + dbname = get_module_dbname('unified_dashboard') + sor = await DBPools().sqlorContext(dbname) + return ReportCore(sor) env.create_dashboard_core = create_dashboard_core env.create_report_core = create_report_core \ No newline at end of file diff --git a/wwwroot/api/dashboard_kpi.dspy b/wwwroot/api/dashboard_kpi.dspy new file mode 100644 index 0000000..79390b1 --- /dev/null +++ b/wwwroot/api/dashboard_kpi.dspy @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Dashboard KPI and chart data API""" +import json + +result = {'success': False, 'rows': [], 'total': 0} + +try: + kpi_type = params_kw.get('type', '').strip() + + if kpi_type == 'sales_funnel': + dbname = get_module_dbname('opportunity_management') + async with DBPools().sqlorContext(dbname) as sor: + stages = await sor.sqlExe("SELECT id, stage_name, stage_order FROM sales_stages ORDER BY stage_order ASC") + funnel_data = [] + for stage in stages: + rows = await sor.sqlExe("SELECT COUNT(*) as cnt, COALESCE(SUM(estimated_amount),0) as amt FROM opportunities WHERE current_stage = ${stage_id}$", {'stage_id': stage['id']}) + funnel_data.append({ + 'stage': stage['stage_name'], + 'count': rows[0]['cnt'] if rows else 0, + 'amount': float(rows[0]['amt'] or 0) if rows else 0 + }) + result['rows'] = funnel_data + result['total'] = len(funnel_data) + result['success'] = True + + elif kpi_type == 'recent_opportunities': + dbname = get_module_dbname('opportunity_management') + async with DBPools().sqlorContext(dbname) as sor: + rows = await sor.sqlExe("SELECT customer_name, estimated_amount, current_stage FROM opportunities ORDER BY created_at DESC LIMIT 10") + result['rows'] = [dict(r) for r in (rows or [])] + result['total'] = len(result['rows']) + result['success'] = True + + elif kpi_type == 'kpi_summary': + # Aggregate KPIs from all modules + kpis = {} + + # Customer count + try: + dbname = get_module_dbname('customer_management') + async with DBPools().sqlorContext(dbname) as sor: + rows = await sor.sqlExe("SELECT COUNT(*) as cnt FROM customers") + kpis['total_customers'] = rows[0]['cnt'] if rows else 0 + except: + kpis['total_customers'] = 0 + + # Active opportunities + try: + dbname = get_module_dbname('opportunity_management') + async with DBPools().sqlorContext(dbname) as sor: + rows = await sor.sqlExe("SELECT COUNT(*) as cnt FROM opportunities WHERE status = 'active'") + kpis['active_opportunities'] = rows[0]['cnt'] if rows else 0 + except: + kpis['active_opportunities'] = 0 + + # Contract count + try: + dbname = get_module_dbname('contract_management') + async with DBPools().sqlorContext(dbname) as sor: + rows = await sor.sqlExe("SELECT COUNT(*) as cnt FROM contract") + kpis['total_contracts'] = rows[0]['cnt'] if rows else 0 + except: + kpis['total_contracts'] = 0 + + # Total receivables + try: + dbname = get_module_dbname('financial_management') + async with DBPools().sqlorContext(dbname) as sor: + rows = await sor.sqlExe("SELECT COALESCE(SUM(receivable_amount),0) as total FROM receivables") + kpis['total_receivables'] = float(rows[0]['total'] or 0) if rows else 0 + except: + kpis['total_receivables'] = 0 + + result['success'] = True + result['data'] = kpis + + else: + result['error'] = f'Unknown KPI type: {kpi_type}' + +except Exception as e: + result['error'] = str(e) + +return json.dumps(result, ensure_ascii=False, default=str) diff --git a/wwwroot/api/report_list.dspy b/wwwroot/api/report_list.dspy new file mode 100644 index 0000000..983af04 --- /dev/null +++ b/wwwroot/api/report_list.dspy @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Report list API""" +import json + +result = {'success': False, 'rows': [], 'total': 0} + +try: + dbname = get_module_dbname('unified_dashboard') + ns = { + 'page': int(params_kw.get('page', 1)), + 'rows': int(params_kw.get('rows', 20)), + 'sort': 'created_at desc' + } + sql = "SELECT id, template_name, category, description, is_active, created_at FROM report_template WHERE 1=1" + + keyword = params_kw.get('keyword', '').strip() + if keyword: + sql += " AND template_name LIKE ${keyword}$" + ns['keyword'] = f'%{keyword}%' + + 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) diff --git a/wwwroot/base.ui b/wwwroot/base.ui new file mode 100644 index 0000000..63fdb94 --- /dev/null +++ b/wwwroot/base.ui @@ -0,0 +1,44 @@ +{ + "widgettype": "Page", + "options": { + "title": "统一仪表板", + "style": {"height": "100vh", "padding": "0"} + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": {"style": {"height": "100%"}}, + "subwidgets": [ + { + "widgettype": "VBox", + "options": {"style": {"width": "220px", "backgroundColor": "#1A1E2F", "padding": "8px"}}, + "subwidgets": [ + { + "widgettype": "Text", + "options": {"text": "数据看板", "style": {"color": "#fff", "fontSize": "16px", "fontWeight": "bold", "padding": "12px 8px"}} + }, + { + "widgettype": "Menu", + "options": { + "width": "100%", + "bgcolor": "#1A1E2F", + "items": [ + {"label": "高管视图", "icon": "fa fa-dashboard", "url": "{{entire_url('dashboard.ui')}}?type=executive", "target": "app.dashboard-content"}, + {"label": "销售视图", "icon": "fa fa-line-chart", "url": "{{entire_url('dashboard.ui')}}?type=sales", "target": "app.dashboard-content"}, + {"label": "财务视图", "icon": "fa fa-money", "url": "{{entire_url('dashboard.ui')}}?type=finance", "target": "app.dashboard-content"}, + {"label": "报表中心", "icon": "fa fa-file-text", "url": "{{entire_url('reports.ui')}}", "target": "app.dashboard-content"} + ], + "menuitem_css": "menuitem" + } + } + ] + }, + { + "widgettype": "VBox", + "id": "dashboard-content", + "options": {"style": {"flex": 1, "overflow": "auto", "backgroundColor": "#f5f5f5"}} + } + ] + } + ] +} diff --git a/wwwroot/dashboard.ui b/wwwroot/dashboard.ui new file mode 100644 index 0000000..a553bc9 --- /dev/null +++ b/wwwroot/dashboard.ui @@ -0,0 +1,83 @@ +{ + "widgettype": "Page", + "options": { + "title": "仪表板", + "style": {"height": "100%", "padding": "0"} + }, + "subwidgets": [ + { + "widgettype": "VBox", + "options": {"style": {"padding": "16px", "flex": 1, "overflow": "auto"}}, + "subwidgets": [ + { + "widgettype": "HBox", + "options": {"style": {"marginBottom": "16px", "gap": "12px", "flexWrap": "wrap"}}, + "subwidgets": [ + { + "widgettype": "VBox", + "options": {"style": {"flex": 1, "minWidth": "200px", "backgroundColor": "#fff", "borderRadius": "8px", "padding": "16px", "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"}}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "客户总数", "style": {"fontSize": "14px", "color": "#666"}}}, + {"widgettype": "Text", "id": "kpi_customers", "options": {"text": "--", "style": {"fontSize": "28px", "fontWeight": "bold", "color": "#1E40AF"}}} + ] + }, + { + "widgettype": "VBox", + "options": {"style": {"flex": 1, "minWidth": "200px", "backgroundColor": "#fff", "borderRadius": "8px", "padding": "16px", "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"}}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "活跃商机", "style": {"fontSize": "14px", "color": "#666"}}}, + {"widgettype": "Text", "id": "kpi_opportunities", "options": {"text": "--", "style": {"fontSize": "28px", "fontWeight": "bold", "color": "#059669"}}} + ] + }, + { + "widgettype": "VBox", + "options": {"style": {"flex": 1, "minWidth": "200px", "backgroundColor": "#fff", "borderRadius": "8px", "padding": "16px", "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"}}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "合同总数", "style": {"fontSize": "14px", "color": "#666"}}}, + {"widgettype": "Text", "id": "kpi_contracts", "options": {"text": "--", "style": {"fontSize": "28px", "fontWeight": "bold", "color": "#D97706"}}} + ] + }, + { + "widgettype": "VBox", + "options": {"style": {"flex": 1, "minWidth": "200px", "backgroundColor": "#fff", "borderRadius": "8px", "padding": "16px", "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"}}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "应收总额", "style": {"fontSize": "14px", "color": "#666"}}}, + {"widgettype": "Text", "id": "kpi_receivables", "options": {"text": "--", "style": {"fontSize": "28px", "fontWeight": "bold", "color": "#DC2626"}}} + ] + } + ] + }, + { + "widgettype": "HBox", + "options": {"style": {"gap": "16px", "flexWrap": "wrap"}}, + "subwidgets": [ + { + "widgettype": "VBox", + "options": {"style": {"flex": 2, "minWidth": "400px", "backgroundColor": "#fff", "borderRadius": "8px", "padding": "16px", "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"}}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "销售漏斗", "style": {"fontSize": "16px", "fontWeight": "bold", "marginBottom": "12px"}}}, + {"widgettype": "DataGrid", "id": "funnel_grid", "options": {"url": "{{entire_url('api/dashboard_kpi.dspy')}}?type=sales_funnel", "columns": [ + {"field": "stage", "header": "阶段", "width": 120}, + {"field": "count", "header": "数量", "width": 80}, + {"field": "amount", "header": "金额", "width": 120} + ]}} + ] + }, + { + "widgettype": "VBox", + "options": {"style": {"flex": 1, "minWidth": "300px", "backgroundColor": "#fff", "borderRadius": "8px", "padding": "16px", "boxShadow": "0 2px 4px rgba(0,0,0,0.1)"}}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "最新商机", "style": {"fontSize": "16px", "fontWeight": "bold", "marginBottom": "12px"}}}, + {"widgettype": "DataGrid", "id": "recent_opp_grid", "options": {"url": "{{entire_url('api/dashboard_kpi.dspy')}}?type=recent_opportunities", "columns": [ + {"field": "customer_name", "header": "客户", "width": 120}, + {"field": "estimated_amount", "header": "金额", "width": 100}, + {"field": "current_stage", "header": "阶段", "width": 80} + ]}} + ] + } + ] + } + ] + } + ] +} diff --git a/wwwroot/reports.ui b/wwwroot/reports.ui new file mode 100644 index 0000000..a1be178 --- /dev/null +++ b/wwwroot/reports.ui @@ -0,0 +1,41 @@ +{ + "widgettype": "Page", + "options": { + "title": "报表中心", + "style": {"height": "100%", "padding": "0"} + }, + "subwidgets": [ + { + "widgettype": "VBox", + "options": {"style": {"padding": "16px", "flex": 1, "overflow": "auto"}}, + "subwidgets": [ + { + "widgettype": "HBox", + "options": {"style": {"marginBottom": "16px", "gap": "8px"}}, + "subwidgets": [ + {"widgettype": "TextField", "id": "report_search", "options": {"label": "搜索报表", "placeholder": "报表名称", "style": {"flex": 1}}}, + {"widgettype": "Button", "id": "btn_search", "options": {"text": "搜索", "variant": "primary"}} + ] + }, + { + "widgettype": "DataGrid", + "id": "report_grid", + "options": { + "url": "{{entire_url('api/report_list.dspy')}}", + "style": {"flex": 1}, + "columns": [ + {"field": "template_name", "header": "报表名称", "width": 200}, + {"field": "category", "header": "分类", "width": 120}, + {"field": "description", "header": "说明", "width": 300}, + {"field": "is_active", "header": "状态", "width": 80}, + {"field": "created_at", "header": "创建时间", "width": 160} + ], + "toolbar": [ + {"type": "button", "text": "查看", "icon": "view", "action": "navigate('main/unified_dashboard/report_view.ui?id={% raw %}{{selectedRow.id}}{% endraw %}')"} + ] + } + } + ] + } + ] +}