diff --git a/config.py b/config.py index c8ad7ee..0db4bd1 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.162" +SYSTEM_VERSION = "V10.163" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/routes/daily_sales_routes.py b/routes/daily_sales_routes.py index 3025bd2..7c7125a 100644 --- a/routes/daily_sales_routes.py +++ b/routes/daily_sales_routes.py @@ -12,7 +12,7 @@ import os import pickle from datetime import datetime, timezone, timedelta from urllib.parse import quote -from flask import Blueprint, request, render_template, send_file +from flask import Blueprint, request, render_template, send_file, url_for from auth import login_required from sqlalchemy import inspect, text import pandas as pd @@ -46,6 +46,7 @@ _VIEW_CACHE_EXPIRY_SECONDS = 120 _SHARED_VIEW_CACHE_EXPIRY_SECONDS = 1800 _DAILY_SALES_VIEW_CACHE = {} _DAILY_SALES_VIEW_CACHE_MAX = 24 +_CATEGORY_TABLE_DEFAULT_LIMIT = 120 def _get_daily_view_cache(cache_key): @@ -112,6 +113,15 @@ def _set_shared_daily_view_cache(cache_key, context): pass +def _daily_sales_url_with_detail(detail_value): + args = request.args.to_dict(flat=True) + if detail_value == 'all': + args['detail'] = 'all' + else: + args.pop('detail', None) + return url_for('daily_sales.daily_sales', **args) + + def clear_daily_sales_cache(): """清除當日業績緩存(供匯入服務調用)""" _clear_daily_sales_cache() @@ -462,6 +472,8 @@ def prepare_category_summary(df, date_str=None, is_month_view=False, month_start if 'profit' not in category_df.columns: category_df['profit'] = 0 + if 'revenue' in category_df.columns: + category_df = category_df.sort_values(by='revenue', ascending=False) return category_df.to_dict('records') @@ -514,6 +526,7 @@ def daily_sales(): is_month_view = not selected_date_param and not request.args.get('month') if selected_month_param and not selected_date_param: is_month_view = True + show_all_categories = request.args.get('detail') == 'all' month_start = selected_month.replace(day=1) month_end = (month_start + pd.DateOffset(months=1)) - pd.Timedelta(days=1) @@ -531,6 +544,7 @@ def daily_sales(): selected_date.strftime('%Y-%m-%d'), selected_month.strftime('%Y-%m'), 'month' if is_month_view else 'day', + 'category_all' if show_all_categories else f'category_top{_CATEGORY_TABLE_DEFAULT_LIMIT}', str(current_fingerprint), ]) cached_context = _get_daily_view_cache(view_cache_key) or _get_shared_daily_view_cache(view_cache_key) @@ -601,6 +615,11 @@ def daily_sales(): month_start=month_start if is_month_view else None, month_end=month_end if is_month_view else None ) + category_total_count = len(category_list) + if show_all_categories: + visible_categories = category_list + else: + visible_categories = category_list[:_CATEGORY_TABLE_DEFAULT_LIMIT] calendar_data = prepare_calendar_data(df, selected_month) marketing_data = prepare_marketing_summary( df, @@ -619,7 +638,13 @@ def daily_sales(): 'month_kpi': month_kpi, 'is_month_view': is_month_view, 'chart_data': chart_data, - 'categories': category_list, + 'categories': visible_categories, + 'category_total_count': category_total_count, + 'category_visible_count': len(visible_categories), + 'category_table_limit': _CATEGORY_TABLE_DEFAULT_LIMIT, + 'category_show_all': show_all_categories, + 'category_show_all_url': _daily_sales_url_with_detail('all'), + 'category_limited_url': _daily_sales_url_with_detail(None), 'calendar_data': calendar_data, 'marketing_data': marketing_data, 'selected_month': selected_month.strftime('%Y-%m') if isinstance(selected_month, pd.Timestamp) else selected_month, diff --git a/templates/daily_sales.html b/templates/daily_sales.html index c52cc1b..800abc4 100644 --- a/templates/daily_sales.html +++ b/templates/daily_sales.html @@ -332,14 +332,26 @@ {% endif %} + {% set category_total = category_total_count|default(categories|length if categories else 0) %} + {% set category_visible = category_visible_count|default(categories|length if categories else 0) %}
- 分類業績明細 - + + 分類業績明細 + 顯示 {{ category_visible }} / {{ category_total }} 筆 + + + {% if category_show_all|default(false) and category_total > category_table_limit|default(120) %} + 收合前 {{ category_table_limit|default(120) }} 筆 + {% elif category_total > category_visible %} + 顯示全部 + {% endif %} + +
diff --git a/web/static/css/page-daily-sales.css b/web/static/css/page-daily-sales.css index 5883746..9b4e392 100644 --- a/web/static/css/page-daily-sales.css +++ b/web/static/css/page-daily-sales.css @@ -817,6 +817,39 @@ display: flex; justify-content: space-between; align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.daily-table-count { + display: inline-flex; + margin-left: 0.5rem; + color: var(--momo-text-tertiary); + font-size: 0.76rem; + font-weight: 700; +} + +.daily-table-actions { + display: inline-flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.btn-table-toggle { + background: var(--momo-bg-surface); + color: var(--momo-text-secondary); + border: 1px solid var(--momo-border-light); + border-radius: 6px; + padding: 0.35rem 0.85rem; + font-weight: 700; + font-size: 0.82rem; +} + +.btn-table-toggle:hover { + background: var(--momo-bg-paper); + color: var(--momo-text-primary); + border-color: var(--momo-page-accent-line); } .btn-export {