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
This commit is contained in:
parent
1d4a6b2893
commit
aec650dcef
@ -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`);
|
||||
1
unified_dashboard/__init__.py
Normal file
1
unified_dashboard/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Unified Dashboard Module
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
84
wwwroot/api/dashboard_kpi.dspy
Normal file
84
wwwroot/api/dashboard_kpi.dspy
Normal file
@ -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)
|
||||
35
wwwroot/api/report_list.dspy
Normal file
35
wwwroot/api/report_list.dspy
Normal file
@ -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)
|
||||
44
wwwroot/base.ui
Normal file
44
wwwroot/base.ui
Normal file
@ -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"}}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
83
wwwroot/dashboard.ui
Normal file
83
wwwroot/dashboard.ui
Normal file
@ -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}
|
||||
]}}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
41
wwwroot/reports.ui
Normal file
41
wwwroot/reports.ui
Normal file
@ -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 %}')"}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user