Merge remote-tracking branch 'origin/main' into codex/growth-p0-sales-source-reconciliation-dev

This commit is contained in:
ogt
2026-07-15 16:41:29 +08:00
6 changed files with 110 additions and 11 deletions

View File

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

View File

@@ -153,6 +153,14 @@
</div>
</section>
<section class="ms-period-empty" id="monthlyPeriodEmptyState" role="status" hidden>
<i class="fas fa-calendar-xmark" aria-hidden="true"></i>
<div>
<strong><span id="monthlyPeriodEmptyLabel">所選期間</span></strong>
<p>頁面不會混入其他月份仍保留去年同期資料供趨勢比較</p>
</div>
</section>
<section class="row g-3 mb-4 ms-highlight-grid" id="highlightsRow" style="display: none;" aria-label="Top 3 品牌洞察">
<div class="col-md-4">
<article class="card h-100 border-0 shadow-sm overflow-hidden ms-highlight ms-highlight--accent">

View File

@@ -63,7 +63,7 @@
{% if db_data_range %}
<span class="sa-tag sa-tag--neutral">
<i class="fas fa-calendar-alt" aria-hidden="true"></i>
資料期間{{ db_data_range }}
資料庫涵蓋{{ db_data_range }}
</span>
{% endif %}
</div>

View File

@@ -278,3 +278,5 @@ def test_monthly_frontend_has_replayable_period_and_no_fixed_years():
assert "2024" not in route
assert "2025" not in route
assert "data-analysis-target" in tabs
assert "renderChartEmpty" in script
assert "未以其他月份回填" in script

View File

@@ -156,6 +156,33 @@
border: 1px solid var(--momo-border-subtle);
border-radius: 4px;
}
.ms-period-empty {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 16px;
border-left: 4px solid var(--momo-page-accent);
padding: 14px 16px;
background: var(--momo-surface-muted, rgba(248, 250, 252, 0.88));
}
.ms-period-empty[hidden] {
display: none;
}
.ms-period-empty i {
margin-top: 2px;
color: var(--momo-page-accent);
font-size: 1.15rem;
}
.ms-period-empty strong {
color: var(--momo-text-primary);
font-size: var(--momo-text-body, 14px);
}
.ms-period-empty p {
margin: 4px 0 0;
color: var(--momo-text-secondary);
font-size: var(--momo-text-body-sm, 13px);
line-height: 1.5;
}
.ms-filter-group__label {
display: flex; align-items: center; gap: 6px;
font-family: var(--momo-font-display);

View File

@@ -219,6 +219,13 @@
function updateUI(data) {
const period = data.period || {};
$('#activeReportPeriod').text(period.label || '全部資料');
const periodEmpty = Number(data.total_rows || 0) === 0;
const periodEmptyState = document.getElementById('monthlyPeriodEmptyState');
if (periodEmptyState) {
periodEmptyState.hidden = !periodEmpty;
const periodEmptyLabel = document.getElementById('monthlyPeriodEmptyLabel');
if (periodEmptyLabel) periodEmptyLabel.textContent = period.label || '所選期間';
}
if (!currentFilters.year && !currentFilters.month && !currentFilters.start_month && period.start_month) {
currentFilters.start_month = period.start_month;
currentFilters.end_month = period.end_month;
@@ -273,6 +280,25 @@
renderAreaRankingChart(areaRankingChart, 'areaRankingChartTableBody', data.area_ranking, period);
}
function renderChartEmpty(chart, tableBodyId, message) {
if (!chart) return;
chart.clear();
chart.setOption({
title: {
text: '尚無可繪製資料',
subtext: message,
left: 'center',
top: 'middle',
textStyle: { color: '#3f3a34', fontSize: 15, fontWeight: 700 },
subtextStyle: { color: '#746b61', fontSize: 12, lineHeight: 18 }
},
series: []
});
if (tableBodyId) {
$(`#${tableBodyId}`).html(`<tr><td class="text-muted py-3">${message}</td></tr>`);
}
}
// ── Highlights (Top 3) ────────────────────────────────
function updateHighlights(h) {
if (!h || (!h.rev_top.length && !h.profit_top.length)) { $('#highlightsRow').hide(); return; }
@@ -288,7 +314,11 @@
// ── Excel-style 主對比圖 ─────────────────────────────
function renderExcelChart(chart, tableBodyId, trend, period = {}) {
if (!trend) return;
if (!trend || !trend.length) {
renderChartEmpty(chart, tableBodyId, '所選期間與去年同期皆無月結資料。');
return;
}
chart.clear();
const compact = isCompactViewport();
const years = [...new Set(trend.map(t => t.date.split('/')[0]))].sort().reverse();
const currYear = String(period.current_year || years[0] || new Date().getFullYear());
@@ -362,7 +392,11 @@
// ── YoY Trend ─────────────────────────────────────────
function renderYoYTrendChart(chart, tableId, data) {
if (!data || !data.length) return;
if (!data || !data.length) {
renderChartEmpty(chart, tableId, '所選期間尚無本期業績,未以其他月份回填。');
return;
}
chart.clear();
const compact = isCompactViewport();
const dates = data.map(d => d.date.split('/')[1] + '月');
const currData = data.map(d => d.curr);
@@ -391,7 +425,11 @@
// ── 類別分佈 (雙圓餅) ─────────────────────────────────
function renderDivisionDistChart(chart, _tableId, data, period = {}) {
if (!data || !data.length) return;
if (!data || !data.length) {
renderChartEmpty(chart, _tableId, '所選期間與去年同期皆無類別業績。');
return;
}
chart.clear();
const compact = isCompactViewport();
const previousLabel = period.previous_label || `${period.previous_year || ''}`;
const currentLabel = period.label || `${period.current_year || ''}`;
@@ -414,7 +452,11 @@
// ── 價格帶 ────────────────────────────────────────────
function renderPriceRangeChart(chart, tableId, data, period = {}) {
if (!data || !data.length) return;
if (!data || !data.length) {
renderChartEmpty(chart, tableId, '所選期間與去年同期皆無價格帶資料。');
return;
}
chart.clear();
const compact = isCompactViewport();
const order = ['0-499','500-999','1,000-1,999','2,000-4,999','5,000-9,999','10,000+'];
data.sort((a,b) => order.indexOf(a.range) - order.indexOf(b.range));
@@ -458,7 +500,11 @@
// ── BCG 矩陣4 象限顯式配色) ───────────────────────
function renderBCGMatrixChart(chart, data) {
if (!data || !data.length) return;
if (!data || !data.length) {
renderChartEmpty(chart, null, '所選期間尚無可建立品牌策略矩陣的本期資料。');
return;
}
chart.clear();
const compact = isCompactViewport();
const C_STAR = 'var(--momo-warm-olive, #6f7a4a)';
const C_COW = 'var(--momo-warm-honey, #c89043)';
@@ -492,7 +538,11 @@
// ── 散佈圖(價/量) ──────────────────────────────────
function renderScatterChart(chart, data) {
if (!data || !data.length) return;
if (!data || !data.length) {
renderChartEmpty(chart, null, '所選期間尚無可比較價格與銷量的本期資料。');
return;
}
chart.clear();
const compact = isCompactViewport();
const series = data.map(d => {
const avg = d.qty > 0 ? Math.round(d.sales / d.qty) : 0;
@@ -509,7 +559,11 @@
// ── 廠商排行 ──────────────────────────────────────────
function renderVendorRankingChart(chart, tableId, data, period = {}) {
if (!data || !data.length) return;
if (!data || !data.length) {
renderChartEmpty(chart, tableId, '所選期間與去年同期皆無廠商業績。');
return;
}
chart.clear();
const compact = isCompactViewport();
const cd = data.slice().reverse();
const previousLabel = period.previous_label || '去年同期';
@@ -560,7 +614,11 @@
// ── 季節熱力圖 ────────────────────────────────────────
function renderSeasonalityHeatmap(chart, data, _period = {}) {
if (!data || !data.length) return;
if (!data || !data.length) {
renderChartEmpty(chart, null, '所選期間與去年同期皆無季節分佈資料。');
return;
}
chart.clear();
const compact = isCompactViewport();
const months = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
const areaSales = {};
@@ -591,7 +649,11 @@
// ── 區域排行 ──────────────────────────────────────────
function renderAreaRankingChart(chart, tableId, data, period = {}) {
if (!data || !data.length) return;
if (!data || !data.length) {
renderChartEmpty(chart, tableId, '所選期間與去年同期皆無區域業績。');
return;
}
chart.clear();
const compact = isCompactViewport();
const previousLabel = period.previous_label || '去年同期';
const currentLabel = period.label || '本期';