From 0359e8154ab75e0d9c24226b41c26a17d9882c8f Mon Sep 17 00:00:00 2001 From: OoO Date: Wed, 13 May 2026 10:12:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E9=80=9F=E6=9C=88=E4=BB=BD=E7=B8=BD?= =?UTF-8?q?=E8=A1=A8=E7=89=B9=E6=AE=8A=E8=B6=A8=E5=8B=A2=E8=BC=89=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/monthly_routes.py | 82 +++++++++++++++++++++++++++ tests/test_frontend_v2_assets.py | 4 ++ web/static/js/page-monthly-summary.js | 5 +- 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/routes/monthly_routes.py b/routes/monthly_routes.py index c9583bd..87062d3 100644 --- a/routes/monthly_routes.py +++ b/routes/monthly_routes.py @@ -53,6 +53,37 @@ sys_log = SystemLogger("MonthlyRoutes").get_logger() monthly_bp = Blueprint('monthly', __name__) +def _split_csv_param(value): + if not value: + return [] + return [item.strip() for item in value.split(',') if item.strip()] + + +def _apply_monthly_filters(query, *, year=None, month=None, division=None, pm_name=None, + brand_name=None, vendor_name=None, area_name=None, + trade_type=None, ignore_year=False): + if year and not ignore_year: + query = query.filter(MonthlySummaryAnalysis.year == year) + if month: + query = query.filter(MonthlySummaryAnalysis.month == month) + if division: + query = query.filter(MonthlySummaryAnalysis.division == division) + if pm_name: + query = query.filter(MonthlySummaryAnalysis.pm_name == pm_name) + if brand_name: + query = query.filter(MonthlySummaryAnalysis.brand_name == brand_name) + if vendor_name: + query = query.filter(MonthlySummaryAnalysis.vendor_name == vendor_name) + area_values = _split_csv_param(area_name) + if len(area_values) > 1: + query = query.filter(MonthlySummaryAnalysis.area_name.in_(area_values)) + elif len(area_values) == 1: + query = query.filter(MonthlySummaryAnalysis.area_name == area_values[0]) + if trade_type: + query = query.filter(MonthlySummaryAnalysis.trade_type == trade_type) + return query + + # ========================================== # 頁面路由 # ========================================== @@ -71,6 +102,57 @@ def monthly_summary_analysis_page(): # API 路由 # ========================================== +@monthly_bp.route('/api/monthly_summary_trend') +@login_required +def get_monthly_summary_trend(): + """API: 取得月份總表輕量趨勢資料。""" + year = request.args.get('year', type=int) + month = request.args.get('month', type=int) + division = request.args.get('division') + pm_name = request.args.get('pm_name') + brand_name = request.args.get('brand_name') + vendor_name = request.args.get('vendor') + area_name = request.args.get('area_name') + trade_type = request.args.get('trade_type') + + db = DatabaseManager() + session = db.get_session() + try: + trend_query = session.query( + MonthlySummaryAnalysis.year, + MonthlySummaryAnalysis.month, + func.sum(MonthlySummaryAnalysis.sales_amt_curr).label('sales') + ).group_by( + MonthlySummaryAnalysis.year, MonthlySummaryAnalysis.month + ).order_by(MonthlySummaryAnalysis.year, MonthlySummaryAnalysis.month) + + trend_query = _apply_monthly_filters( + trend_query, + year=year, + month=month, + division=division, + pm_name=pm_name, + brand_name=brand_name, + vendor_name=vendor_name, + area_name=area_name, + trade_type=trade_type, + ) + trend_results = trend_query.all() + + return jsonify({ + 'status': 'success', + 'trend': [ + {'date': f"{r.year}/{r.month}", 'sales': int(r.sales or 0)} + for r in trend_results + ], + }) + except Exception as e: + sys_log.error(f"取得月份總表趨勢資料失敗: {e}") + return jsonify({'status': 'error', 'message': str(e)}), 500 + finally: + session.close() + + @monthly_bp.route('/api/monthly_summary_data') @login_required def get_monthly_summary_data(): diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index 317ec6e..6272bc7 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -231,6 +231,7 @@ def test_ai_recommend_uses_v2_shell_and_runtime_category_data(): def test_monthly_summary_analysis_uses_v2_shell_and_real_monthly_api(): template = (ROOT / "templates/monthly_summary_analysis.html").read_text(encoding="utf-8") route_source = (ROOT / "routes/monthly_routes.py").read_text(encoding="utf-8") + script = (ROOT / "web/static/js/page-monthly-summary.js").read_text(encoding="utf-8") assert "{% extends 'ewoooc_base.html' %}" in template assert "{% block ewooo_content %}" in template @@ -241,6 +242,9 @@ def test_monthly_summary_analysis_uses_v2_shell_and_real_monthly_api(): assert "monthly-analysis-hero" in template assert "monthly-analysis-page" in template assert "/api/monthly_summary_data" in template + assert "/api/monthly_summary_trend" in route_source + assert "/api/monthly_summary_trend" in script + assert "area_name=${encodeURIComponent(area)}&limit=1" not in script assert "monthly_summary_analysis" in route_source assert "active_page='monthly'" in route_source assert "MonthlySummaryAnalysis" in route_source diff --git a/web/static/js/page-monthly-summary.js b/web/static/js/page-monthly-summary.js index 3e3773f..ad84d2c 100644 --- a/web/static/js/page-monthly-summary.js +++ b/web/static/js/page-monthly-summary.js @@ -79,9 +79,10 @@ ['私密保養,嬰幼洗沐', privacyInfantChart, 'privacyInfantChartTableBody'] ]; pairs.forEach(([area, chart, tbody]) => { - fetch(`/api/monthly_summary_data?area_name=${encodeURIComponent(area)}&limit=1`) + fetch(`/api/monthly_summary_trend?area_name=${encodeURIComponent(area)}`) .then(r => r.json()) - .then(d => { if (d.status === 'success') renderExcelChart(chart, tbody, d.trend); }); + .then(d => { if (d.status === 'success') renderExcelChart(chart, tbody, d.trend); }) + .catch(err => console.error('monthly special trend failed', err)); }); }