Merge remote-tracking branch 'origin/main' into codex/growth-p0-sales-source-reconciliation-dev
This commit is contained in:
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 || '本期';
|
||||
|
||||
Reference in New Issue
Block a user