@@ -3003,7 +3001,7 @@
待補比價
—
- 需要補抓 MOMO
+ 需要補外部平台同款
最新業績日
@@ -3913,12 +3911,12 @@ function renderOpsCommandDashboard(stats, scope = {}) {
document.getElementById('opsDashboardStatus').textContent = candidateCount
? `資料就緒率 ${readyRate}%`
: '等待 PChome 業績資料';
- renderNextAction(candidateCount, mappedCount, needsMapping, reviewCandidateCount);
+ renderNextAction(stats);
setWidth('opsFunnelCandidateBar', candidateCount ? 100 : 0);
setWidth('opsFunnelMappedBar', candidateCount ? (mappedCount / candidateCount) * 100 : 0);
setWidth('opsFunnelNeedsBar', candidateCount ? (needsMapping / candidateCount) * 100 : 0);
- renderOpsSourceBars(stats.external_data_source_counts || {}, scope);
+ renderOpsSourceBars(stats.external_platform_counts || {}, scope);
renderGrowthExecutiveSummary(stats);
renderGrowthCommandCenter(stats, latestGrowthRows);
}
@@ -3956,11 +3954,15 @@ function renderGrowthCommandCenter(stats = {}, rows = []) {
const pendingCount = Math.max(0, needsMapping + reviewCandidateCount);
const readyShare = candidateCount ? (advantageCount / candidateCount) * 100 : 0;
const pressureShare = candidateCount ? (pressureCount / candidateCount) * 100 : 0;
+ const freshness = stats.sales_freshness || {};
setText('commandSales7d', sales7d ? formatMoney(sales7d) : '—');
setText('commandSalesDelta', prevSales7d
? `${salesDeltaPct >= 0 ? '+' : ''}${salesDeltaPct.toFixed(1)}%`
: '待比');
+ setText('commandSalesFreshness', freshness.age_days === null || freshness.age_days === undefined
+ ? (freshness.label || '尚無資料')
+ : `${freshness.label || '資料狀態'} · ${formatCount(freshness.age_days)} 天`);
setText('commandMatchRate', `${Math.round(matchRate * 10) / 10}%`);
setText('commandMappedRatio', `${formatCount(mappedCount)} / ${formatCount(candidateCount)}`);
setText('commandDecliningProducts', formatCount(decliningCount));
@@ -3980,8 +3982,11 @@ function renderGrowthCommandCenter(stats = {}, rows = []) {
const status = document.getElementById('growthCommandStatus');
if (status) {
- const text = latestSalesDate ? `更新 ${latestSalesDate}` : '待資料';
- status.innerHTML = `${escapeHtml(text)}`;
+ const text = freshness.decision_ready
+ ? `資料可用 · ${latestSalesDate}`
+ : `${freshness.label || '待資料'}${latestSalesDate ? ` · ${latestSalesDate}` : ''}`;
+ const icon = freshness.decision_ready ? 'fa-circle-check' : 'fa-triangle-exclamation';
+ status.innerHTML = `${escapeHtml(text)}`;
}
const rateBar = document.getElementById('commandMatchRateBar');
if (rateBar) rateBar.style.width = `${clampPercent(matchRate)}%`;
@@ -3991,7 +3996,16 @@ function renderGrowthCommandCenter(stats = {}, rows = []) {
donut.style.setProperty('--review', clampPercent(pressureShare));
}
- if (!candidateCount) {
+ if (freshness.decision_ready === false) {
+ setCommandTask(
+ '先更新業績資料',
+ freshness.age_days === null || freshness.age_days === undefined
+ ? '尚未取得可用業績'
+ : `資料落後 ${formatCount(freshness.age_days)} 天`,
+ '查看匯入狀態',
+ () => { window.location.href = '/daily_sales'; }
+ );
+ } else if (!candidateCount) {
setCommandTask(
'更新業績',
'待業績',
@@ -4059,11 +4073,28 @@ function renderGrowthCommandTopDecliners(rows) {
}).join('');
}
-function renderNextAction(candidateCount, mappedCount, needsMapping, reviewCandidateCount = 0) {
+function renderNextAction(stats = {}) {
const title = document.getElementById('nextActionTitle');
const reason = document.getElementById('nextActionReason');
const button = document.getElementById('nextActionButton');
if (!title || !reason || !button) return;
+ const candidateCount = Number(stats.candidate_count || 0);
+ const mappedCount = Number(stats.mapped_count || 0);
+ const needsMapping = Number(stats.needs_mapping_count || 0);
+ const reviewCandidateCount = Number(stats.review_candidate_count || 0);
+ const freshness = stats.sales_freshness || {};
+
+ if (freshness.decision_ready === false) {
+ const age = freshness.age_days;
+ title.textContent = '今天先做:更新 PChome 業績';
+ reason.textContent = age === null || age === undefined
+ ? '尚未取得可用業績,價格與曝光決策暫不執行。'
+ : `最新業績已落後 ${formatCount(age)} 天,系統先恢復資料來源,再重新計算作戰清單。`;
+ button.textContent = '查看匯入狀態';
+ delete button.dataset.action;
+ button.onclick = () => { window.location.href = '/daily_sales'; };
+ return;
+ }
if (!candidateCount) {
title.textContent = '今天先做:更新 PChome 業績';
@@ -4076,7 +4107,7 @@ function renderNextAction(candidateCount, mappedCount, needsMapping, reviewCandi
if (needsMapping > 0 && mappedCount === 0) {
if (reviewCandidateCount > 0) {
- title.textContent = `今天先做:確認 ${formatCount(reviewCandidateCount)} 筆 MOMO 候選`;
+ title.textContent = `今天先做:AI 驗證 ${formatCount(reviewCandidateCount)} 筆候選`;
reason.textContent = '候選已找到,先確認同款或排除,確認後才會進入價格判斷。';
button.textContent = '確認候選';
delete button.dataset.action;
@@ -4084,7 +4115,7 @@ function renderNextAction(candidateCount, mappedCount, needsMapping, reviewCandi
return;
}
title.textContent = `今天先做:補齊 ${formatCount(needsMapping)} 件比價資料`;
- reason.textContent = '這些商品有業績,但目前還看不到 MOMO 參考價,請先補齊比價資料。';
+ reason.textContent = '這些商品有業績,但目前還沒有可決策的外部平台同款,系統先補齊商品對應。';
button.textContent = '補齊比價資料';
button.dataset.action = 'backfill';
button.onclick = () => backfillPchomeMatches();
@@ -4093,7 +4124,7 @@ function renderNextAction(candidateCount, mappedCount, needsMapping, reviewCandi
if (needsMapping > mappedCount) {
if (reviewCandidateCount > 0) {
- title.textContent = `今天先做:確認 ${formatCount(reviewCandidateCount)} 筆 MOMO 候選`;
+ title.textContent = `今天先做:AI 驗證 ${formatCount(reviewCandidateCount)} 筆候選`;
reason.textContent = '先把已找到的候選變成可用資料,再補剩下找不到同款的商品。';
button.textContent = '確認候選';
delete button.dataset.action;
@@ -4121,17 +4152,27 @@ function renderGrowthExecutiveSummary(stats = {}) {
const needsMapping = Number(stats.needs_mapping_count || 0);
const reviewCandidateCount = Number(stats.review_candidate_count || 0);
const latestSalesDate = String(stats.latest_sales_date || '').slice(0, 10);
+ const freshness = stats.sales_freshness || {};
document.getElementById('growthExecReady').textContent = formatCount(mappedCount);
document.getElementById('growthExecGap').textContent = formatCount(needsMapping);
document.getElementById('growthExecLatestDate').textContent = latestSalesDate || '—';
- document.getElementById('growthExecLatestDetail').textContent = latestSalesDate ? '已接到 PChome 業績' : '尚未確認業績日期';
+ document.getElementById('growthExecLatestDetail').textContent = freshness.label || (latestSalesDate ? '已接到 PChome 業績' : '尚未確認業績日期');
const task = document.getElementById('growthExecTask');
const detail = document.getElementById('growthExecTaskDetail');
const gapCard = document.getElementById('growthExecGapCard');
if (!task || !detail) return;
+ if (freshness.decision_ready === false) {
+ task.textContent = '先恢復最新業績';
+ detail.textContent = freshness.age_days === null || freshness.age_days === undefined
+ ? '尚未取得可用業績,營收決策暫停。'
+ : `業績落後 ${formatCount(freshness.age_days)} 天,系統先完成自動匯入。`;
+ gapCard?.classList.add('is-gap');
+ return;
+ }
+
if (!candidateCount) {
task.textContent = '先更新 PChome 業績';
detail.textContent = '目前還沒有足夠資料,請確認最新業績檔是否已匯入。';
@@ -4225,9 +4266,10 @@ async function loadGrowthOps(forceRefresh = false) {
renderGrowthDataSourceSummary(stats);
const active = (scope.active_external_sources || []).join('、') || '尚未接入';
- const paused = (scope.paused_external_sources || []).join('、') || '無';
+ const observed = (scope.observed_external_sources || []).length;
+ const paused = (scope.paused_external_sources || []).length;
document.getElementById('growthSourceNote').textContent =
- `業績:${scope.primary_sales_source || 'PChome 後台業績'} · 外部:${active} · 暫停:${paused}`;
+ `業績:${scope.primary_sales_source || 'PChome 後台業績'} · 決策來源:${active} · 已有證據 ${observed} 個 · 待接入 ${paused} 個`;
renderGrowthSourceReadiness((scope.source_readiness || {}).sources || []);
latestGrowthRows = data.opportunities || [];
@@ -4273,6 +4315,14 @@ function renderGrowthActionHint(stats) {
const mappedCount = Number(stats.mapped_count || 0);
const needsMapping = Number(stats.needs_mapping_count || 0);
const reviewCandidateCount = Number(stats.review_candidate_count || 0);
+ const freshness = stats.sales_freshness || {};
+
+ if (freshness.decision_ready === false) {
+ hint.textContent = freshness.age_days === null || freshness.age_days === undefined
+ ? '尚未取得可用業績,系統先恢復上游資料。'
+ : `最新業績落後 ${formatCount(freshness.age_days)} 天,先完成自動匯入再執行營收動作。`;
+ return;
+ }
if (!candidateCount) {
hint.textContent = '還不能產生今日清單,原因是缺少最新 PChome 業績。';
@@ -4301,10 +4351,10 @@ function renderGrowthDataSourceSummary(stats) {
const summary = document.getElementById('growthDataSourceSummary');
if (!summary) return;
- const counts = stats.external_data_source_counts || {};
+ const counts = stats.external_platform_counts || {};
const entries = Object.entries(counts)
.filter(([, count]) => Number(count || 0) > 0)
- .map(([label, count]) => `${formatDataSourceLabel(label)} ${Number(count).toLocaleString()} 筆`);
+ .map(([label, count]) => `${label} ${Number(count).toLocaleString()} 筆`);
if (!entries.length) {
summary.textContent = '來源:尚未接到外部參考價';
@@ -4328,7 +4378,11 @@ function renderGrowthSourceReadiness(sources) {
return;
}
- box.innerHTML = sources.map((source) => {
+ const visibleSources = sources.filter((source) =>
+ source.status_code === 'active' || source.has_runtime_data
+ );
+ const pendingCount = Math.max(0, sources.length - visibleSources.length);
+ const cards = visibleSources.map((source) => {
const usable = Number(source.usable_offer_count || 0);
const detail = usable > 0
? `${source.data_quality_label || '資料可用'} · ${usable.toLocaleString()} 筆`
@@ -4341,7 +4395,17 @@ function renderGrowthSourceReadiness(sources) {
${escapeHtml(detail)}
`;
- }).join('');
+ });
+ if (pendingCount > 0) {
+ cards.push(`