diff --git a/config.py b/config.py
index 8beafcf..ed19d6c 100644
--- a/config.py
+++ b/config.py
@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
-SYSTEM_VERSION = "V10.107"
+SYSTEM_VERSION = "V10.108"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示
diff --git a/routes/sales_routes.py b/routes/sales_routes.py
index 7cb771e..756f587 100644
--- a/routes/sales_routes.py
+++ b/routes/sales_routes.py
@@ -26,6 +26,8 @@ from services.logger_manager import SystemLogger
from services.daily_sales_service import prepare_marketing_summary
from services.cache_manager import (
_SALES_PROCESSED_CACHE,
+ _SALES_OPTIONS_CACHE,
+ _SALES_ANALYSIS_RESULT_CACHE,
set_sales_processed_cache,
)
from utils.text_helpers import get_color_for_string
@@ -41,6 +43,10 @@ sales_bp = Blueprint('sales', __name__)
_TABLE_DATA_CACHE = {}
_TABLE_DATA_CACHE_TTL = 60
+_SALES_PREVIEW_CACHE_TTL = 600
+_SALES_OPTIONS_CACHE_TTL = 1800
+_SALES_PAGE_CONTEXT_CACHE_TTL = 180
+_SALES_PAGE_CONTEXT_CACHE_MAX = 24
# ==========================================
@@ -52,6 +58,214 @@ from utils.df_helpers import find_col # noqa: E402, F401
from utils.security import validate_table_name, safe_read_sql # noqa: E402, F401
+def _get_timed_cache(store, key, ttl_seconds):
+ entry = store.get(key)
+ if not entry:
+ return None
+ if time.time() - entry.get('time', 0) >= ttl_seconds:
+ store.pop(key, None)
+ return None
+ return entry.get('data')
+
+
+def _set_timed_cache(store, key, data, max_entries=64):
+ if len(store) >= max_entries:
+ oldest_key = min(store, key=lambda k: store[k].get('time', 0))
+ store.pop(oldest_key, None)
+ store[key] = {'data': data, 'time': time.time()}
+
+
+def _sales_analysis_args_fingerprint(table_name, cache_key, processed_time=None):
+ args = tuple(
+ (key, tuple(values))
+ for key, values in sorted(request.args.lists())
+ )
+ raw = repr((table_name, cache_key, processed_time or 0, args)).encode('utf-8')
+ return hashlib.md5(raw).hexdigest()
+
+
+def _get_sales_page_context_cache(cache_key):
+ return _get_timed_cache(
+ _SALES_ANALYSIS_RESULT_CACHE,
+ cache_key,
+ _SALES_PAGE_CONTEXT_CACHE_TTL,
+ )
+
+
+def _set_sales_page_context_cache(cache_key, context):
+ _set_timed_cache(
+ _SALES_ANALYSIS_RESULT_CACHE,
+ cache_key,
+ context,
+ max_entries=_SALES_PAGE_CONTEXT_CACHE_MAX,
+ )
+
+
+def _format_sales_data_range(min_date, max_date):
+ if not min_date or not max_date:
+ return ''
+ try:
+ if isinstance(min_date, str):
+ min_date_obj = datetime.strptime(min_date.split()[0], '%Y-%m-%d')
+ max_date_obj = datetime.strptime(max_date.split()[0], '%Y-%m-%d')
+ else:
+ min_date_obj = min_date
+ max_date_obj = max_date
+ return f"{min_date_obj.year}年{min_date_obj.month}月 ~ {max_date_obj.year}年{max_date_obj.month}月"
+ except Exception:
+ return f"{min_date} ~ {max_date}"
+
+
+def _fetch_sales_data_range(engine, table_name):
+ validate_table_name(table_name)
+ cache_key = f"sales_analysis:data_range:{table_name}"
+ cached = _get_timed_cache(_SALES_OPTIONS_CACHE, cache_key, _SALES_PREVIEW_CACHE_TTL)
+ if cached is not None:
+ return cached
+
+ try:
+ date_query = text(f'SELECT MIN("日期") as min_date, MAX("日期") as max_date FROM "{table_name}"')
+ with engine.connect() as conn:
+ result = conn.execute(date_query).fetchone()
+ db_data_range = _format_sales_data_range(result[0], result[1]) if result else ''
+ except Exception as e:
+ sys_log.warning(f"[Sales Analysis] 無法取得資料期間範圍: {e}")
+ db_data_range = ''
+
+ _set_timed_cache(_SALES_OPTIONS_CACHE, cache_key, db_data_range)
+ return db_data_range
+
+
+def _preview_sales_filter_options(engine, table_name):
+ """首次進入業績分析時的輕量下拉資料,避免每次載入都掃預覽資料。"""
+ validate_table_name(table_name)
+ cache_key = f"sales_analysis:preview_options:{table_name}"
+ cached = _get_timed_cache(_SALES_OPTIONS_CACHE, cache_key, _SALES_PREVIEW_CACHE_TTL)
+ if cached:
+ return cached
+
+ options = {
+ 'categories': [],
+ 'brands': [],
+ 'vendors': [],
+ 'activities': [],
+ 'payments': [],
+ 'months': [],
+ }
+ preview_df = pd.read_sql(f'SELECT * FROM "{table_name}" LIMIT 1000', engine)
+ if preview_df.empty:
+ _set_timed_cache(_SALES_OPTIONS_CACHE, cache_key, options)
+ return options
+
+ cols = preview_df.columns.tolist()
+
+ def find_preview_col(keywords):
+ for k in keywords:
+ for col in cols:
+ if k in str(col):
+ return col
+ return None
+
+ col_category = find_preview_col(['館別', '商品館', '分類', 'Category'])
+ col_brand = find_preview_col(['品牌', 'Brand'])
+ col_vendor = find_preview_col(['廠商名稱', 'Vendor Name', '廠商', '供應商', 'Vendor', 'Supplier'])
+ col_activity = find_preview_col(['折扣活動名稱', '折價券活動名稱', '滿額再折扣活動名稱', '活動', 'Activity', 'Campaign'])
+ col_payment = find_preview_col(['付款', 'Payment', 'Pay'])
+ col_date_part = find_preview_col(['日期', '交易日期', 'Date', 'Day'])
+
+ def clean_values(col):
+ if not col:
+ return []
+ return sorted([
+ x for x in preview_df[col].dropna().astype(str).unique().tolist()
+ if x and x.strip()
+ ])
+
+ options['categories'] = clean_values(col_category)
+ options['brands'] = clean_values(col_brand)
+ options['vendors'] = clean_values(col_vendor)
+ options['activities'] = clean_values(col_activity)
+ options['payments'] = clean_values(col_payment)
+
+ if col_date_part:
+ try:
+ with engine.connect() as conn:
+ result = conn.execute(text(f"""
+ SELECT DISTINCT replace(substr("{col_date_part}", 1, 7), '/', '-') as month
+ FROM "{table_name}"
+ WHERE "{col_date_part}" IS NOT NULL AND "{col_date_part}" != ''
+ ORDER BY month
+ """)).fetchall()
+ options['months'] = [row[0] for row in result if row[0] and '-' in str(row[0])]
+ sys_log.info(f"[Sales Analysis] 預覽模式從「{col_date_part}」欄位提取到 {len(options['months'])} 個月份")
+ except Exception as e:
+ sys_log.warning(f"[Sales Analysis] 無法從「{col_date_part}」欄位提取月份: {e}")
+
+ _set_timed_cache(_SALES_OPTIONS_CACHE, cache_key, options)
+ return options
+
+
+def _get_sales_filter_options(engine, table_name, cols_map):
+ """完整篩選選項共用快取;資料匯入後 clear_sales_cache() 會一起清掉。"""
+ validate_table_name(table_name)
+ col_category = cols_map.get('category')
+ col_brand = cols_map.get('brand')
+ col_vendor = cols_map.get('vendor')
+ col_activity = cols_map.get('activity')
+ col_payment = cols_map.get('payment')
+ col_date = cols_map.get('date')
+ option_cols = (col_category, col_brand, col_vendor, col_activity, col_payment, col_date)
+ digest = hashlib.md5(repr(option_cols).encode('utf-8')).hexdigest()
+ cache_key = f"sales_analysis:filter_options:{table_name}:{digest}"
+ cached = _get_timed_cache(_SALES_OPTIONS_CACHE, cache_key, _SALES_OPTIONS_CACHE_TTL)
+ if cached:
+ return cached
+
+ options = {
+ 'categories': [],
+ 'brands': [],
+ 'vendors': [],
+ 'activities': [],
+ 'payments': [],
+ 'months': [],
+ }
+
+ def read_distinct(conn, col):
+ if not col:
+ return []
+ sql = f'SELECT DISTINCT "{col}" FROM "{table_name}" WHERE "{col}" IS NOT NULL AND "{col}" <> \'\' ORDER BY "{col}"'
+ result = conn.execute(text(sql)).fetchall()
+ return [str(row[0]) for row in result if row[0]]
+
+ with engine.connect() as conn:
+ options['categories'] = read_distinct(conn, col_category)
+ options['brands'] = read_distinct(conn, col_brand)
+ options['vendors'] = read_distinct(conn, col_vendor)
+ options['activities'] = read_distinct(conn, col_activity)
+ options['payments'] = read_distinct(conn, col_payment)
+
+ if col_date:
+ date_fields = ['日期', '訂單日期', '時間']
+ for field in date_fields:
+ try:
+ result = conn.execute(text(f"""
+ SELECT DISTINCT replace(substr("{field}", 1, 7), '/', '-') as month
+ FROM "{table_name}"
+ WHERE "{field}" IS NOT NULL AND "{field}" != ''
+ ORDER BY month
+ """)).fetchall()
+ months = [row[0] for row in result if row[0] and '-' in str(row[0])]
+ if months:
+ options['months'] = months
+ sys_log.info(f"[Sales Analysis] 從欄位 {field} 提取到 {len(months)} 個月份: {months}")
+ break
+ except Exception as ex:
+ sys_log.warning(f"[Sales Analysis] 從欄位 {field} 提取月份失敗: {ex}")
+
+ _set_timed_cache(_SALES_OPTIONS_CACHE, cache_key, options)
+ return options
+
+
def _growth_empty_payload(now_taipei=None):
now_taipei = now_taipei or datetime.now(TAIPEI_TZ)
return (
@@ -558,7 +772,7 @@ def sales_analysis():
# 1. 檢查資料表是否存在
inspector = inspect(db.engine)
- if table_name not in inspector.get_table_names():
+ if not inspector.has_table(table_name):
return render_template('sales_analysis.html',
error="尚未匯入「即時業績(全月)」資料,請先至設定頁面匯入 Excel。",
table_name=table_name,
@@ -571,31 +785,8 @@ def sales_analysis():
active_page='sales',
db_data_range='')
- # V-New: 查詢資料庫的資料期間範圍
- db_data_range = ''
- try:
- # 取得日期欄位的最小值和最大值
- from sqlalchemy import text
- date_query = text(f"SELECT MIN(日期) as min_date, MAX(日期) as max_date FROM {table_name}")
- # V-Fix: SQLAlchemy 2.0 需要使用 connection 對象
- with db.engine.connect() as conn:
- result = conn.execute(date_query).fetchone()
- if result and result[0] and result[1]:
- min_date = result[0]
- max_date = result[1]
- # 格式化為 YYYY年MM月 格式
- if isinstance(min_date, str):
- from datetime import datetime
- try:
- min_date_obj = datetime.strptime(min_date.split()[0], '%Y-%m-%d')
- max_date_obj = datetime.strptime(max_date.split()[0], '%Y-%m-%d')
- db_data_range = f"{min_date_obj.year}年{min_date_obj.month}月 ~ {max_date_obj.year}年{max_date_obj.month}月"
- except:
- db_data_range = f"{min_date} ~ {max_date}"
- else:
- db_data_range = f"{min_date.year}年{min_date.month}月 ~ {max_date.year}年{max_date.month}月"
- except Exception as e:
- sys_log.warning(f"[Sales Analysis] 無法取得資料期間範圍: {e}")
+ # V-Opt: 資料期間在同一批匯入後穩定,避免每次首屏都查 MIN/MAX。
+ db_data_range = _fetch_sales_data_range(db.engine, table_name)
# V-New: 取得篩選參數
data_range_param = request.args.get('data_range', '') # 不再設預設值
@@ -605,63 +796,13 @@ def sales_analysis():
# V-New: 按需載入 - 如果沒有任何篩選條件,顯示引導頁面
if not data_range_param and not start_date and not end_date:
sys_log.info("[Sales Analysis] 👋 首次進入頁面,等待用戶選擇篩選條件")
-
- # V-Fix: 即使在引導頁面,也要提供下拉選單選項
- # V-Opt: 讀取較多筆數(1000筆)以獲得更完整的月份資訊
- preview_df = pd.read_sql(f"SELECT * FROM {table_name} LIMIT 1000", db.engine)
- preview_categories = []
- preview_brands = []
- preview_vendors = []
- preview_activities = []
- preview_payments = []
- preview_months = [] # V-New: 新增月份列表
-
- if not preview_df.empty:
- cols = preview_df.columns.tolist()
- def find_col(keywords):
- for k in keywords:
- for col in cols:
- if k in str(col): return col
- return None
-
- col_category = find_col(['館別', '商品館', '分類', 'Category'])
- col_brand = find_col(['品牌', 'Brand'])
- col_vendor = find_col(['廠商名稱', 'Vendor Name', '廠商', '供應商', 'Vendor', 'Supplier'])
- # V-Fix: 優先匹配具體的活動欄位名稱
- col_activity = find_col(['折扣活動名稱', '折價券活動名稱', '滿額再折扣活動名稱', '活動', 'Activity', 'Campaign'])
- col_payment = find_col(['付款', 'Payment', 'Pay'])
- # V-Fix: 優先匹配「日期」欄位(「訂單日期」是固定文字,不是實際日期)
- col_date_part = find_col(['日期', '交易日期', 'Date', 'Day'])
- col_time_part = find_col(['訂單時間', '成立時間', '下單時間', '購買時間', '時間', 'Time', 'Created'])
-
- # V-Fix: 篩選掉空字串,只保留有效數據
- if col_category:
- preview_categories = sorted([x for x in preview_df[col_category].dropna().astype(str).unique().tolist() if x and x.strip()])
- if col_brand:
- preview_brands = sorted([x for x in preview_df[col_brand].dropna().astype(str).unique().tolist() if x and x.strip()])
- if col_vendor:
- preview_vendors = sorted([x for x in preview_df[col_vendor].dropna().astype(str).unique().tolist() if x and x.strip()])
- if col_activity:
- preview_activities = sorted([x for x in preview_df[col_activity].dropna().astype(str).unique().tolist() if x and x.strip()])
- if col_payment:
- preview_payments = sorted([x for x in preview_df[col_payment].dropna().astype(str).unique().tolist() if x and x.strip()])
-
- # V-Fix: 從數據庫直接查詢所有月份(而不是從預覽數據提取,避免只獲得部分月份)
- if col_date_part:
- try:
- from sqlalchemy import text
- with db.engine.connect() as conn:
- result = conn.execute(text(f"""
- SELECT DISTINCT replace(substr("{col_date_part}", 1, 7), '/', '-') as month
- FROM {table_name}
- WHERE "{col_date_part}" IS NOT NULL AND "{col_date_part}" != ''
- ORDER BY month
- """)).fetchall()
- preview_months = [row[0] for row in result if row[0] and '-' in str(row[0])]
- sys_log.info(f"[Sales Analysis] 預覽模式從「{col_date_part}」欄位提取到 {len(preview_months)} 個月份")
- except Exception as e:
- sys_log.warning(f"[Sales Analysis] 無法從「{col_date_part}」欄位提取月份: {e}")
- pass
+ preview_options = _preview_sales_filter_options(db.engine, table_name)
+ preview_categories = preview_options['categories']
+ preview_brands = preview_options['brands']
+ preview_vendors = preview_options['vendors']
+ preview_activities = preview_options['activities']
+ preview_payments = preview_options['payments']
+ preview_months = preview_options['months']
# 傳遞必要的變數以避免模板錯誤
selected_metric = request.args.get('metric', 'amount')
@@ -1020,6 +1161,16 @@ def sales_analysis():
}
set_sales_processed_cache(cache_key, cache_entry, aliases=(table_name,))
+ processed_entry = _SALES_PROCESSED_CACHE.get(cache_key, {})
+ page_cache_key = "sales_analysis:page_context:" + _sales_analysis_args_fingerprint(
+ table_name,
+ cache_key,
+ processed_entry.get('time'),
+ )
+ cached_context = _get_sales_page_context_cache(page_cache_key)
+ if cached_context:
+ return render_template('sales_analysis.html', **cached_context)
+
# 🚩 V-Opt: 使用共用篩選函式
target_df, cols_map, err = _get_filtered_sales_data(cache_key)
if err:
@@ -1043,9 +1194,6 @@ def sales_analysis():
col_date = cols_map.get('date')
col_pid = cols_map.get('pid')
- # V-Fix: 準備前端需要的下拉選單資料
- # V-Opt: 從數據庫直接查詢所有可用選項,而不是只從篩選後的快取中讀取
- # 這樣可以確保下拉選單顯示完整的可選項,即使當前篩選範圍很小
all_categories = []
all_brands = []
all_vendors = []
@@ -1054,58 +1202,13 @@ def sales_analysis():
all_months = []
try:
- from sqlalchemy import text
- # V-Fix: SQLAlchemy 2.0 需要使用 connection 對象
- with db.engine.connect() as conn:
- # 讀取完整資料表的所有可用選項(使用 DISTINCT 以提升效能)
- # V-Fix: 使用單引號空字串,兼容 PostgreSQL
- if col_category:
- sql = f"SELECT DISTINCT \"{col_category}\" FROM {table_name} WHERE \"{col_category}\" IS NOT NULL AND \"{col_category}\" <> ''"
- result = conn.execute(text(sql)).fetchall()
- all_categories = sorted([str(row[0]) for row in result if row[0]])
-
- if col_brand:
- sql = f"SELECT DISTINCT \"{col_brand}\" FROM {table_name} WHERE \"{col_brand}\" IS NOT NULL AND \"{col_brand}\" <> ''"
- result = conn.execute(text(sql)).fetchall()
- all_brands = sorted([str(row[0]) for row in result if row[0]])
-
- if col_vendor:
- sql = f"SELECT DISTINCT \"{col_vendor}\" FROM {table_name} WHERE \"{col_vendor}\" IS NOT NULL AND \"{col_vendor}\" <> ''"
- result = conn.execute(text(sql)).fetchall()
- all_vendors = sorted([str(row[0]) for row in result if row[0]])
-
- if col_activity:
- sql = f"SELECT DISTINCT \"{col_activity}\" FROM {table_name} WHERE \"{col_activity}\" IS NOT NULL AND \"{col_activity}\" <> ''"
- result = conn.execute(text(sql)).fetchall()
- all_activities = sorted([str(row[0]) for row in result if row[0]])
-
- if col_payment:
- sql = f"SELECT DISTINCT \"{col_payment}\" FROM {table_name} WHERE \"{col_payment}\" IS NOT NULL AND \"{col_payment}\" <> ''"
- result = conn.execute(text(sql)).fetchall()
- all_payments = sorted([str(row[0]) for row in result if row[0]])
-
- # V-Fix: 從數據庫提取所有月份(格式:YYYY-MM)
- if col_date:
- # 從日期欄位提取月份(支援多種日期欄位名稱)
- date_fields = ['日期', '訂單日期', '時間']
- for field in date_fields:
- try:
- # V-Fix: 使用 substr 提取年月部分,並將斜線替換為橫線
- # 數據庫格式: "2025/07/01" -> 提取前7個字符 "2025/07" -> 替換斜線 "2025-07"
- result = conn.execute(text(f"""
- SELECT DISTINCT replace(substr(\"{field}\", 1, 7), '/', '-') as month
- FROM {table_name}
- WHERE \"{field}\" IS NOT NULL AND \"{field}\" != ''
- ORDER BY month
- """)).fetchall()
- if result and len(result) > 0:
- all_months = [row[0] for row in result if row[0] and '-' in str(row[0])]
- if all_months: # 如果成功提取到月份,就使用這個欄位
- sys_log.info(f"[Sales Analysis] 從欄位 {field} 提取到 {len(all_months)} 個月份: {all_months}")
- break
- except Exception as ex:
- sys_log.warning(f"[Sales Analysis] 從欄位 {field} 提取月份失敗: {ex}")
- continue
+ filter_options = _get_sales_filter_options(db.engine, table_name, cols_map)
+ all_categories = filter_options['categories']
+ all_brands = filter_options['brands']
+ all_vendors = filter_options['vendors']
+ all_activities = filter_options['activities']
+ all_payments = filter_options['payments']
+ all_months = filter_options['months']
except Exception as e:
sys_log.warning(f"[Sales Analysis] 從數據庫查詢下拉選項失敗: {e}")
# 如果查詢失敗,回退到從快取讀取
@@ -1531,54 +1634,78 @@ def sales_analysis():
if not target_df.empty:
marketing_data = prepare_marketing_summary(target_df, sort_by=selected_metric)
- return render_template('sales_analysis.html',
- marketing_data=marketing_data, # V-New: 傳遞行銷活動數據
- items=table_items,
- kpi={
- 'revenue': total_revenue,
- 'qty': total_qty,
- 'count': total_count,
- 'sku_count': sku_count, # V-Fix 2026-01-15: 唯一商品數
- 'cost': total_cost,
- 'gross_margin': gross_margin,
- 'gross_margin_rate': gross_margin_rate,
- 'avg_price': avg_price
- },
- insights=insights,
- abc_stats=abc_stats, # V-New: 傳遞 ABC 分析數據
- vendor_stats=vendor_stats, # V-New: 傳遞廠商排行數據
- seasonality_data=seasonality_data, # V-New: 傳遞淡旺季數據
- bar_data=bar_data,
- cat_data=cat_data,
- category_data=cat_data,
- price_dist_data=price_dist_data,
- scatter_data=scatter_data,
- bcg_data=bcg_data, # V-New: 傳遞 BCG 數據
- dow_data=dow_data,
- hourly_data=hourly_data,
- monthly_data=monthly_data,
- weekly_data=weekly_data,
- heatmap_data=heatmap_data,
- treemap_data=treemap_data,
- all_categories=all_categories,
- all_brands=all_brands, all_vendors=all_vendors, all_activities=all_activities, all_payments=all_payments,
- all_months=all_months, # V-New: 傳遞月份列表
- selected_category=selected_category,
- selected_brand=selected_brand, selected_vendor=selected_vendor,
- selected_activity=selected_activity, selected_payment=selected_payment,
- selected_dow=selected_dow, selected_hour=selected_hour,
- selected_month=selected_month,
- selected_metric=selected_metric,
- keyword=keyword, min_price=min_price, max_price=max_price,
- min_margin=min_margin, max_margin=max_margin,
- cols={'name': col_name, 'amount': col_amount, 'qty': col_qty, 'cat': col_category, 'date': col_date, 'cost': col_cost, 'profit': col_profit, 'vendor': col_vendor, 'brand': col_brand, 'return_qty': col_return_qty, 'pid': col_pid},
- table_name=table_name,
- data_range_months=data_range_months,
- start_date=start_date, # V-New: 傳遞自訂開始日期
- end_date=end_date, # V-New: 傳遞自訂結束日期
- total_records=len(df),
- active_page='sales',
- db_data_range=db_data_range) # V-New: 傳遞資料庫資料期間
+ context = {
+ 'marketing_data': marketing_data,
+ 'items': table_items,
+ 'kpi': {
+ 'revenue': total_revenue,
+ 'qty': total_qty,
+ 'count': total_count,
+ 'sku_count': sku_count,
+ 'cost': total_cost,
+ 'gross_margin': gross_margin,
+ 'gross_margin_rate': gross_margin_rate,
+ 'avg_price': avg_price,
+ },
+ 'insights': insights,
+ 'abc_stats': abc_stats,
+ 'vendor_stats': vendor_stats,
+ 'seasonality_data': seasonality_data,
+ 'bar_data': bar_data,
+ 'cat_data': cat_data,
+ 'category_data': cat_data,
+ 'price_dist_data': price_dist_data,
+ 'scatter_data': scatter_data,
+ 'bcg_data': bcg_data,
+ 'dow_data': dow_data,
+ 'hourly_data': hourly_data,
+ 'monthly_data': monthly_data,
+ 'weekly_data': weekly_data,
+ 'heatmap_data': heatmap_data,
+ 'treemap_data': treemap_data,
+ 'all_categories': all_categories,
+ 'all_brands': all_brands,
+ 'all_vendors': all_vendors,
+ 'all_activities': all_activities,
+ 'all_payments': all_payments,
+ 'all_months': all_months,
+ 'selected_category': selected_category,
+ 'selected_brand': selected_brand,
+ 'selected_vendor': selected_vendor,
+ 'selected_activity': selected_activity,
+ 'selected_payment': selected_payment,
+ 'selected_dow': selected_dow,
+ 'selected_hour': selected_hour,
+ 'selected_month': selected_month,
+ 'selected_metric': selected_metric,
+ 'keyword': keyword,
+ 'min_price': min_price,
+ 'max_price': max_price,
+ 'min_margin': min_margin,
+ 'max_margin': max_margin,
+ 'cols': {
+ 'name': col_name,
+ 'amount': col_amount,
+ 'qty': col_qty,
+ 'cat': col_category,
+ 'date': col_date,
+ 'cost': col_cost,
+ 'profit': col_profit,
+ 'vendor': col_vendor,
+ 'brand': col_brand,
+ 'return_qty': col_return_qty,
+ 'pid': col_pid,
+ },
+ 'table_name': table_name,
+ 'data_range_months': data_range_months,
+ 'start_date': start_date,
+ 'end_date': end_date,
+ 'total_records': len(df),
+ 'active_page': 'sales',
+ 'db_data_range': db_data_range,
+ }
+ _set_sales_page_context_cache(page_cache_key, context)
+ return render_template('sales_analysis.html', **context)
except Exception as e:
sys_log.error(f"Sales Analysis Error: {e}")
diff --git a/templates/sales_analysis.html b/templates/sales_analysis.html
index c7a2c5a..6589ce7 100644
--- a/templates/sales_analysis.html
+++ b/templates/sales_analysis.html
@@ -847,11 +847,13 @@
{% endblock %}
{% block extra_js %}
+ {% if not no_filter %}
+ {% endif %}