feat(reports): move monthly analysis to v2 shell
All checks were successful
CD Pipeline / deploy (push) Successful in 2m14s

This commit is contained in:
OoO
2026-05-01 21:13:18 +08:00
parent d6782ee710
commit 026d0e7539
5 changed files with 121 additions and 50 deletions

4
app.py
View File

@@ -95,8 +95,8 @@ except Exception as e:
sys_log.error(f"無法檢測磁碟空間: {e}")
# 🚩 系統版本定義 (備份與顯示用)
# 🚩 2026-05-01 V10.75: Move AI recommendation page onto V2 shell
SYSTEM_VERSION = "V10.75"
# 🚩 2026-05-01 V10.76: Move monthly analysis report onto V2 shell
SYSTEM_VERSION = "V10.76"
# ==========================================
# 🔒 SQL Injection 防護函數

View File

@@ -254,7 +254,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.75"
SYSTEM_VERSION = "V10.76"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -63,7 +63,8 @@ def monthly_summary_analysis_page():
"""月份總表數據分析展示頁 (Phase 9)"""
return render_template('monthly_summary_analysis.html',
datetime_now=datetime.now(TAIPEI_TZ).strftime('%Y-%m-%d %H:%M:%S'),
system_version=SYSTEM_VERSION)
system_version=SYSTEM_VERSION,
active_page='monthly')
# ==========================================

View File

@@ -1,48 +1,85 @@
<!-- cspell:ignore MOMO datatables -->
<!DOCTYPE html>
<html lang="zh-TW">
{% extends 'ewoooc_base.html' %}
{% block title %}月份總表數據分析 - WOOO TECH{% endblock %}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>月份總表數據分析 - WOOO TECH</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- DataTables CSS -->
{% block extra_css %}
<link rel="stylesheet" href="https://cdn.datatables.net/1.11.5/css/dataTables.bootstrap5.min.css">
<style>
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f4f6f9;
padding-top: 75px;
.monthly-analysis-page {
display: flex;
flex-direction: column;
gap: 18px;
}
.navbar.bg-custom-dark {
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
.monthly-analysis-hero {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 16px;
align-items: center;
padding: 22px;
border: 1px solid var(--momo-border-strong);
border-radius: 8px;
background:
radial-gradient(circle at 18px 18px, rgba(42, 37, 32, 0.12) 1px, transparent 1px),
linear-gradient(135deg, rgba(242, 178, 90, 0.22), rgba(255, 255, 255, 0.94) 46%, rgba(42, 37, 32, 0.06));
background-size: 18px 18px, auto;
box-shadow: var(--momo-shadow-soft);
}
.monthly-analysis-title {
display: flex;
align-items: center;
gap: 10px;
margin: 0;
color: var(--momo-text-strong);
font-family: var(--momo-font-display);
font-size: clamp(1.45rem, 2vw, 2.08rem);
font-weight: 800;
letter-spacing: 0;
}
.monthly-analysis-title i {
color: var(--momo-warm-caramel) !important;
}
.monthly-version-pill {
display: inline-flex;
align-items: center;
gap: 7px;
border: 1px solid rgba(42, 37, 32, 0.14);
border-radius: 999px;
background: rgba(255, 255, 255, 0.7);
color: var(--momo-text-muted);
font-family: var(--momo-font-mono);
font-size: 0.78rem;
font-weight: 800;
padding: 6px 10px;
}
.card {
border: none;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
border: 1px solid var(--momo-border-subtle) !important;
border-radius: 8px;
box-shadow: var(--momo-shadow-soft);
margin-bottom: 1.5rem;
background: #fff;
background: rgba(255, 255, 255, 0.84);
}
.card-header {
background-color: transparent;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background: rgba(250, 247, 240, 0.88) !important;
border-bottom: 1px solid var(--momo-border-subtle);
font-weight: 700;
padding: 1.25rem;
}
.filter-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
background:
radial-gradient(circle at 14px 14px, rgba(42, 37, 32, 0.1) 1px, transparent 1px),
linear-gradient(135deg, rgba(42, 37, 32, 0.94), rgba(68, 54, 40, 0.9));
background-size: 16px 16px, auto;
border-radius: 8px;
padding: 20px;
color: white;
margin-bottom: 2rem;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
box-shadow: var(--momo-shadow-medium);
}
.form-label {
@@ -76,7 +113,7 @@
}
.stat-card {
border-left: 4px solid #4F46E5;
border-left: 4px solid var(--momo-warm-caramel) !important;
transition: transform 0.3s;
}
@@ -87,17 +124,14 @@
.kpi-value {
font-size: 2.2rem;
font-weight: 800;
color: #1e293b;
color: var(--momo-text-strong);
font-family: var(--momo-font-mono);
}
.small {
font-size: 1rem !important;
}
.bg-custom-dark {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%) !important;
}
.loading-overlay {
position: fixed;
top: 0;
@@ -111,21 +145,38 @@
align-items: center;
backdrop-filter: blur(2px);
}
</style>
</head>
<body>
.monthly-analysis-page .table thead th,
.monthly-analysis-page .table-light th {
background: rgba(250, 247, 240, 0.96) !important;
color: var(--momo-text-muted);
font-weight: 800;
}
@media (max-width: 768px) {
.monthly-analysis-hero {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block ewooo_content %}
<div id="loadingOverlay" class="loading-overlay">
<div class="spinner-border text-primary" role="status"></div>
</div>
{% include 'components/_navbar.html' %}
<div class="container-fluid px-4 mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h3 class="mb-0 fw-bold text-dark"><i class="fas fa-chart-pie me-2 text-primary"></i>月份總表數據分析</h3>
<div class="text-muted small">系統版本: {{ system_version }}</div>
</div>
<div class="monthly-analysis-page">
<section class="monthly-analysis-hero">
<div>
<h1 class="monthly-analysis-title"><i class="fas fa-chart-pie me-2 text-primary"></i>月份總表數據分析</h1>
<p class="text-muted mb-0 mt-2">以資料庫 `monthly_summary_analysis` 的月結資料檢視業績、毛利、YoY 與品牌/區域/價格帶結構。</p>
</div>
<div class="monthly-version-pill">
<i class="fas fa-database"></i>
<span>{{ system_version }}</span>
</div>
</section>
<!-- ═══════ 進階篩選器 (對標業績分析) ═══════ -->
<div class="filter-section">
@@ -516,12 +567,13 @@
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/dataTables.bootstrap5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script>
let table;
let compareChart = echarts.init(document.getElementById('compareChart'));
@@ -1469,6 +1521,4 @@
$(`#${tableId}`).parent().html(`<table class="table table-bordered table-hover table-sm text-center mb-0 small">${tableHtml}</table>`);
}
</script>
</body>
</html>
{% endblock %}

View File

@@ -228,6 +228,26 @@ def test_ai_recommend_uses_v2_shell_and_runtime_category_data():
assert "@ai_bp.route('/api/ai/product_insights'" in route_source
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")
assert "{% extends 'ewoooc_base.html' %}" in template
assert "{% block ewooo_content %}" in template
assert "{% block extra_css %}" in template
assert "{% block extra_js %}" in template
assert "components/_navbar.html" not in template
assert "<!DOCTYPE html>" not in template
assert "monthly-analysis-hero" in template
assert "monthly-analysis-page" in template
assert "/api/monthly_summary_data" in template
assert "monthly_summary_analysis" in route_source
assert "active_page='monthly'" in route_source
assert "MonthlySummaryAnalysis" in route_source
assert "mock" not in template.lower()
assert "假商品" not in template
def test_dashboard_v2_restores_real_price_history_chart():
route_source = (ROOT / "routes/api_routes.py").read_text(encoding="utf-8")
dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8")