feat: add growth dashboard drilldown details
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s
This commit is contained in:
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.641"
|
||||
SYSTEM_VERSION = "V10.642"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
- V10.639 起待確認候選排序必須容忍缺少單位數量;沒有 `momo_total_quantity` / `competitor_total_quantity` 時仍可保存為 `needs_review`,不得中斷 PChome 導向 MOMO 回填。
|
||||
- V10.640 起 `/ai_intelligence` 必須提供 MOMO 待確認候選操作佇列;使用者可直接確認同款或排除候選。確認後 `external_offers` 會轉為 `verified/verified` 並進入作戰清單,排除後轉為 `rejected/rejected`,兩者都必須清掉 PChome 成長作戰清單快取。
|
||||
- V10.641 起 `/ai_intelligence` 的摘要數字不可只是靜態文字;第一屏 KPI、商品處理進度、待確認數字都必須可點擊並導向對應明細。今日清單若已有 MOMO 待確認候選,下一步必須顯示「確認候選」並跳到候選面板,不得再只顯示「補齊比價」。
|
||||
- V10.642 起 `/ai_intelligence` 的摘要卡與商品處理數字不可只跳到大區塊;點擊後必須開啟商品明細面板,列出商品名稱、分類、近 7 天業績、業績變化、MOMO 比價狀態與下一步按鈕。明細需至少支援全部、價格壓力、價格優勢、待確認、缺比價與有外部價切換;外部價格風險分佈也必須能一鍵篩選下方表格。
|
||||
|
||||
## 零之一、12 Agent 決策信封(2026-05-24)
|
||||
|
||||
|
||||
@@ -473,6 +473,20 @@
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.price-risk-row[role="button"] {
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
transition: background-color 0.16s ease, transform 0.16s ease;
|
||||
}
|
||||
|
||||
.price-risk-row[role="button"]:hover,
|
||||
.price-risk-row[role="button"]:focus {
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
outline: 0;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ops-funnel-meta,
|
||||
.ops-source-meta,
|
||||
.price-risk-meta {
|
||||
@@ -624,6 +638,98 @@
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.growth-detail-panel {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.growth-detail-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.growth-detail-title {
|
||||
margin: 0;
|
||||
color: var(--momo-text-strong);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 900;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.growth-detail-meta {
|
||||
color: var(--momo-text-muted);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.growth-detail-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.growth-detail-tab {
|
||||
border: 1px solid rgba(42, 37, 32, 0.12);
|
||||
border-radius: 999px;
|
||||
background: rgba(250, 247, 240, 0.66);
|
||||
color: var(--momo-text-muted);
|
||||
font-size: 0.74rem;
|
||||
font-weight: 900;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.growth-detail-tab.is-active {
|
||||
border-color: rgba(172, 92, 58, 0.34);
|
||||
background: rgba(242, 178, 90, 0.18);
|
||||
color: var(--momo-text-strong);
|
||||
}
|
||||
|
||||
.growth-detail-result {
|
||||
border: 1px solid rgba(42, 37, 32, 0.1);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.76);
|
||||
min-height: 190px;
|
||||
max-height: 390px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.growth-detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(118px, auto);
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid rgba(42, 37, 32, 0.08);
|
||||
padding: 11px 12px;
|
||||
}
|
||||
|
||||
.growth-detail-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.growth-detail-name {
|
||||
margin: 0;
|
||||
color: var(--momo-text-strong);
|
||||
font-size: 0.86rem;
|
||||
font-weight: 900;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.growth-detail-line {
|
||||
margin: 4px 0 0;
|
||||
color: var(--momo-text-muted);
|
||||
font-size: 0.74rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.growth-detail-action {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
align-content: start;
|
||||
justify-items: end;
|
||||
}
|
||||
|
||||
.growth-source-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -1111,9 +1217,14 @@
|
||||
}
|
||||
|
||||
.growth-item,
|
||||
.growth-detail-row,
|
||||
.offer-dryrun-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.growth-detail-action {
|
||||
justify-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -1151,7 +1262,7 @@
|
||||
</section>
|
||||
|
||||
<section class="growth-executive-strip" aria-label="今日任務摘要">
|
||||
<article class="growth-exec-card is-primary is-clickable" id="growthExecTaskCard" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<article class="growth-exec-card is-primary is-clickable" id="growthExecTaskCard" role="button" tabindex="0" onclick="showGrowthDetail('task')" onkeydown="handleGrowthDetailKey(event, 'task')">
|
||||
<div class="growth-exec-label">
|
||||
<span>今日任務</span>
|
||||
<i class="fas fa-location-arrow"></i>
|
||||
@@ -1159,7 +1270,7 @@
|
||||
<div class="growth-exec-value" id="growthExecTask">整理中</div>
|
||||
<div class="growth-exec-detail"><span id="growthExecTaskDetail">正在讀取 PChome 業績與 MOMO 外部價格。</span><span class="drilldown-hint">看明細</span></div>
|
||||
</article>
|
||||
<article class="growth-exec-card is-ready is-clickable" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<article class="growth-exec-card is-ready is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('ready')" onkeydown="handleGrowthDetailKey(event, 'ready')">
|
||||
<div class="growth-exec-label">
|
||||
<span>可立即處理</span>
|
||||
<i class="fas fa-circle-check"></i>
|
||||
@@ -1167,7 +1278,7 @@
|
||||
<div class="growth-exec-value" id="growthExecReady">—</div>
|
||||
<div class="growth-exec-detail">已有可用比價資料<span class="drilldown-hint">看清單</span></div>
|
||||
</article>
|
||||
<article class="growth-exec-card is-gap is-clickable" id="growthExecGapCard" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<article class="growth-exec-card is-gap is-clickable" id="growthExecGapCard" role="button" tabindex="0" onclick="showGrowthDetail('needs')" onkeydown="handleGrowthDetailKey(event, 'needs')">
|
||||
<div class="growth-exec-label">
|
||||
<span>待補比價</span>
|
||||
<i class="fas fa-link-slash"></i>
|
||||
@@ -1175,7 +1286,7 @@
|
||||
<div class="growth-exec-value" id="growthExecGap">—</div>
|
||||
<div class="growth-exec-detail">有業績但缺外部參考<span class="drilldown-hint">看商品</span></div>
|
||||
</article>
|
||||
<article class="growth-exec-card is-clickable" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<article class="growth-exec-card is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('all')" onkeydown="handleGrowthDetailKey(event, 'all')">
|
||||
<div class="growth-exec-label">
|
||||
<span>最新業績日</span>
|
||||
<i class="fas fa-calendar-day"></i>
|
||||
@@ -1204,7 +1315,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="ops-flow-grid">
|
||||
<div class="ops-dashboard-tile is-clickable" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<div class="ops-dashboard-tile is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('all')" onkeydown="handleGrowthDetailKey(event, 'all')">
|
||||
<div class="ops-dashboard-title">
|
||||
<span>商品處理進度</span>
|
||||
<strong class="ops-dashboard-value" id="opsReadyRate">—%</strong>
|
||||
@@ -1224,7 +1335,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ops-dashboard-tile is-clickable" role="button" tabindex="0" onclick="scrollToPanel('externalPricePanel')" onkeydown="handleDrilldownKey(event, 'externalPricePanel')">
|
||||
<div class="ops-dashboard-tile is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('source')" onkeydown="handleGrowthDetailKey(event, 'source')">
|
||||
<div class="ops-dashboard-title">
|
||||
<span>外部價格來源</span>
|
||||
<strong class="ops-dashboard-value" id="opsSourceTotal">—</strong>
|
||||
@@ -1263,6 +1374,31 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── 點擊摘要後的商品明細 ── -->
|
||||
<section class="card shadow-sm ai-panel growth-detail-panel" id="growthDrilldownPanel">
|
||||
<div class="card-body">
|
||||
<div class="growth-detail-toolbar">
|
||||
<div>
|
||||
<h2 class="growth-detail-title" id="growthDrilldownTitle">今日商品明細</h2>
|
||||
<div class="growth-detail-meta" id="growthDrilldownMeta">整理中</div>
|
||||
</div>
|
||||
<div class="growth-detail-tabs" aria-label="明細切換">
|
||||
<button type="button" class="growth-detail-tab is-active" data-detail-kind="all" onclick="showGrowthDetail('all')">全部</button>
|
||||
<button type="button" class="growth-detail-tab" data-detail-kind="risk" onclick="showGrowthDetail('risk')">價格壓力</button>
|
||||
<button type="button" class="growth-detail-tab" data-detail-kind="advantage" onclick="showGrowthDetail('advantage')">價格優勢</button>
|
||||
<button type="button" class="growth-detail-tab" data-detail-kind="review" onclick="showGrowthDetail('review')">待確認</button>
|
||||
<button type="button" class="growth-detail-tab" data-detail-kind="needs" onclick="showGrowthDetail('needs')">缺比價</button>
|
||||
<button type="button" class="growth-detail-tab" data-detail-kind="source" onclick="showGrowthDetail('source')">有外部價</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="growth-detail-result" id="growthDrilldownResult">
|
||||
<div class="text-center py-4 text-muted">
|
||||
<div class="spinner-border spinner-border-sm me-2"></div>整理商品明細中...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── 今日處理清單 ── -->
|
||||
<section class="card shadow-sm ai-panel" id="growthOpsPanel">
|
||||
<div class="card-header d-flex justify-content-between align-items-center py-2 bg-white border-bottom">
|
||||
@@ -1278,19 +1414,19 @@
|
||||
<div class="growth-ops-grid">
|
||||
<div>
|
||||
<div class="growth-metric-row">
|
||||
<div class="growth-metric is-clickable" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<div class="growth-metric is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('all')" onkeydown="handleGrowthDetailKey(event, 'all')">
|
||||
<strong id="growthCandidateCount">—</strong>
|
||||
<span>追蹤商品</span>
|
||||
</div>
|
||||
<div class="growth-metric is-clickable" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<div class="growth-metric is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('ready')" onkeydown="handleGrowthDetailKey(event, 'ready')">
|
||||
<strong id="growthMappedCount">—</strong>
|
||||
<span>可立即處理</span>
|
||||
</div>
|
||||
<div class="growth-metric is-clickable" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<div class="growth-metric is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('needs')" onkeydown="handleGrowthDetailKey(event, 'needs')">
|
||||
<strong id="growthNeedsMapping">—</strong>
|
||||
<span>無法比價</span>
|
||||
</div>
|
||||
<div class="growth-metric is-clickable" role="button" tabindex="0" onclick="scrollToPanel('growthReviewPanel')" onkeydown="handleDrilldownKey(event, 'growthReviewPanel')">
|
||||
<div class="growth-metric is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('review')" onkeydown="handleGrowthDetailKey(event, 'review')">
|
||||
<strong id="growthReviewCandidateCount">—</strong>
|
||||
<span>待確認</span>
|
||||
</div>
|
||||
@@ -1418,15 +1554,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="price-risk-board" id="priceRiskBoard" aria-label="價格風險分佈">
|
||||
<div class="price-risk-row">
|
||||
<div class="price-risk-row" role="button" tabindex="0" onclick="showPriceRiskDetail('HIGH')" onkeydown="handlePriceRiskKey(event, 'HIGH')">
|
||||
<div class="price-risk-meta"><span>需檢查價格</span><strong id="priceRiskHighText">—</strong></div>
|
||||
<div class="price-risk-track"><span class="price-risk-bar is-high" id="priceRiskHighBar"></span></div>
|
||||
</div>
|
||||
<div class="price-risk-row">
|
||||
<div class="price-risk-row" role="button" tabindex="0" onclick="showPriceRiskDetail('MED')" onkeydown="handlePriceRiskKey(event, 'MED')">
|
||||
<div class="price-risk-meta"><span>留意價差</span><strong id="priceRiskMediumText">—</strong></div>
|
||||
<div class="price-risk-track"><span class="price-risk-bar is-medium" id="priceRiskMediumBar"></span></div>
|
||||
</div>
|
||||
<div class="price-risk-row">
|
||||
<div class="price-risk-row" role="button" tabindex="0" onclick="showPriceRiskDetail('LOW')" onkeydown="handlePriceRiskKey(event, 'LOW')">
|
||||
<div class="price-risk-meta"><span>價格有利</span><strong id="priceRiskLowText">—</strong></div>
|
||||
<div class="price-risk-track"><span class="price-risk-bar is-low" id="priceRiskLowBar"></span></div>
|
||||
</div>
|
||||
@@ -1557,6 +1693,8 @@
|
||||
// ── 全域資料 ────────────────────────────────────────
|
||||
let allCompetitors = [];
|
||||
let latestGrowthStats = {};
|
||||
let latestGrowthRows = [];
|
||||
let activeGrowthDetailKind = 'all';
|
||||
|
||||
// ── 頁面載入 ────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -1690,6 +1828,18 @@ function handleDrilldownKey(event, panelId) {
|
||||
scrollToPanel(panelId);
|
||||
}
|
||||
|
||||
function handleGrowthDetailKey(event, kind) {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return;
|
||||
event.preventDefault();
|
||||
showGrowthDetail(kind);
|
||||
}
|
||||
|
||||
function handlePriceRiskKey(event, risk) {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return;
|
||||
event.preventDefault();
|
||||
showPriceRiskDetail(risk);
|
||||
}
|
||||
|
||||
function clampPercent(value) {
|
||||
return Math.max(0, Math.min(100, Number(value || 0)));
|
||||
}
|
||||
@@ -1912,7 +2062,7 @@ async function loadGrowthOps(forceRefresh = false) {
|
||||
}
|
||||
|
||||
try {
|
||||
const url = '/api/ai/pchome-growth/opportunities?limit=8' + (forceRefresh ? '&refresh=1' : '');
|
||||
const url = '/api/ai/pchome-growth/opportunities?limit=50' + (forceRefresh ? '&refresh=1' : '');
|
||||
const res = await fetch(url);
|
||||
const data = await readJsonResponse(res);
|
||||
if (!data.success) throw new Error(data.error || '讀取失敗');
|
||||
@@ -1933,13 +2083,17 @@ async function loadGrowthOps(forceRefresh = false) {
|
||||
`業績:${scope.primary_sales_source || 'PChome 後台業績'} · 外部:${active} · 暫停:${paused}`;
|
||||
|
||||
renderGrowthSourceReadiness((scope.source_readiness || {}).sources || []);
|
||||
renderGrowthOps(data.opportunities || []);
|
||||
latestGrowthRows = data.opportunities || [];
|
||||
renderGrowthOps(latestGrowthRows);
|
||||
renderGrowthDetail(activeGrowthDetailKind);
|
||||
loadGrowthReviewCandidates();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
latestGrowthRows = [];
|
||||
renderOpsCommandDashboard({}, {});
|
||||
renderGrowthActionHint({ candidate_count: 0, mapped_count: 0, needs_mapping_count: 0 });
|
||||
renderGrowthDataSourceSummary({});
|
||||
renderGrowthDetail('all');
|
||||
renderGrowthReviewCandidates([]);
|
||||
list.innerHTML = `<div class="text-center py-4 text-muted">
|
||||
<i class="fas fa-circle-exclamation d-block mb-2"></i>
|
||||
@@ -2041,6 +2195,131 @@ function renderGrowthSourceReadiness(sources) {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function resolveGrowthDetailKind(kind) {
|
||||
if (kind !== 'task') return kind || 'all';
|
||||
const reviewCandidateCount = Number(latestGrowthStats.review_candidate_count || 0);
|
||||
const needsMapping = Number(latestGrowthStats.needs_mapping_count || 0);
|
||||
const mappedCount = Number(latestGrowthStats.mapped_count || 0);
|
||||
if (reviewCandidateCount > 0) return 'review';
|
||||
if (needsMapping > mappedCount) return 'needs';
|
||||
return 'risk';
|
||||
}
|
||||
|
||||
function growthDetailConfig(kind) {
|
||||
const topCategory = latestGrowthStats.top_category || '';
|
||||
const configs = {
|
||||
all: ['今日商品明細', '依優先級排序,先看高業績、下滑或價格有壓力的商品。'],
|
||||
ready: ['可立即處理商品', '已有 MOMO 參考價,可以直接檢查售價、活動或曝光。'],
|
||||
needs: ['缺比價商品', '有 PChome 業績,但還沒有可用 MOMO 參考。'],
|
||||
review: ['MOMO 候選待確認', '候選已找到,確認同款後才會進入價格判斷。'],
|
||||
risk: ['MOMO 更便宜商品', 'MOMO 參考價較低,優先檢查 PChome 售價、券或組合。'],
|
||||
advantage: ['PChome 價格優勢商品', 'PChome 目前較有價格優勢,適合檢查曝光與主推位置。'],
|
||||
source: ['外部價格來源明細', '只列出已接到 MOMO 外部參考價的商品。'],
|
||||
decline: ['業績下滑商品', '近 7 天比前 7 天下滑的商品。'],
|
||||
category: [topCategory ? `${topCategory} 商品明細` : '最大業績分類明細', '目前最大業績分類內的商品。'],
|
||||
};
|
||||
return configs[kind] || configs.all;
|
||||
}
|
||||
|
||||
function growthDetailRows(kind) {
|
||||
const rows = Array.isArray(latestGrowthRows) ? [...latestGrowthRows] : [];
|
||||
const topCategory = latestGrowthStats.top_category || '';
|
||||
const filtered = rows.filter((row) => {
|
||||
const actionCode = row.recommended_action?.code || '';
|
||||
const price = row.external_price || null;
|
||||
const gap = price && price.gap_pct !== null && price.gap_pct !== undefined ? Number(price.gap_pct) : null;
|
||||
if (kind === 'ready') return Boolean(price);
|
||||
if (kind === 'needs') return !price && !row.review_candidate;
|
||||
if (kind === 'review') return Boolean(row.review_candidate) || actionCode === 'review_external_candidate';
|
||||
if (kind === 'risk') return Boolean(price) && (actionCode === 'review_price_or_promo' || (gap !== null && gap < -5));
|
||||
if (kind === 'advantage') return Boolean(price) && (actionCode === 'amplify_price_advantage' || (gap !== null && gap > 5));
|
||||
if (kind === 'source') return Boolean(price);
|
||||
if (kind === 'decline') return Number(row.sales_delta_pct || 0) < 0;
|
||||
if (kind === 'category') return topCategory && row.category === topCategory;
|
||||
return true;
|
||||
});
|
||||
|
||||
return filtered.sort((a, b) => Number(b.priority_score || 0) - Number(a.priority_score || 0));
|
||||
}
|
||||
|
||||
function showGrowthDetail(kind, shouldScroll = true) {
|
||||
activeGrowthDetailKind = resolveGrowthDetailKind(kind);
|
||||
renderGrowthDetail(activeGrowthDetailKind);
|
||||
if (shouldScroll) scrollToPanel('growthDrilldownPanel');
|
||||
}
|
||||
|
||||
function renderGrowthDetail(kind = activeGrowthDetailKind) {
|
||||
const titleBox = document.getElementById('growthDrilldownTitle');
|
||||
const metaBox = document.getElementById('growthDrilldownMeta');
|
||||
const resultBox = document.getElementById('growthDrilldownResult');
|
||||
if (!titleBox || !metaBox || !resultBox) return;
|
||||
|
||||
activeGrowthDetailKind = resolveGrowthDetailKind(kind);
|
||||
document.querySelectorAll('.growth-detail-tab').forEach((tab) => {
|
||||
tab.classList.toggle('is-active', tab.dataset.detailKind === activeGrowthDetailKind);
|
||||
});
|
||||
|
||||
const [title, subtitle] = growthDetailConfig(activeGrowthDetailKind);
|
||||
const rows = growthDetailRows(activeGrowthDetailKind);
|
||||
const visibleRows = rows.slice(0, 50);
|
||||
const salesTotal = rows.reduce((sum, row) => sum + Number(row.sales_7d || 0), 0);
|
||||
titleBox.textContent = title;
|
||||
metaBox.textContent = `${rows.length.toLocaleString()} 件 · 近 7 天業績 ${formatMoney(salesTotal)} · ${subtitle} · 先列 ${visibleRows.length.toLocaleString()} 件`;
|
||||
|
||||
if (!rows.length) {
|
||||
resultBox.innerHTML = `<div class="text-center py-4 text-muted">
|
||||
<i class="fas fa-circle-info d-block mb-2"></i>
|
||||
目前沒有符合這個條件的商品。
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
resultBox.innerHTML = visibleRows.map((row) => {
|
||||
const action = row.recommended_action || {};
|
||||
const price = row.external_price || null;
|
||||
const reviewCandidate = row.review_candidate || null;
|
||||
const gap = price && price.gap_pct !== null && price.gap_pct !== undefined ? Number(price.gap_pct) : null;
|
||||
const gapText = reviewCandidate
|
||||
? '候選待確認'
|
||||
: gap === null
|
||||
? '缺 MOMO 參考'
|
||||
: gap < 0
|
||||
? `PChome 貴 ${Math.abs(gap).toFixed(1)}%`
|
||||
: gap > 0
|
||||
? `PChome 便宜 ${gap.toFixed(1)}%`
|
||||
: '價格差不多';
|
||||
const delta = row.sales_delta_pct === null || row.sales_delta_pct === undefined
|
||||
? '前期不足'
|
||||
: `${Number(row.sales_delta_pct).toFixed(1)}%`;
|
||||
const productKey = escapeHtml(row.pchome_product_id || row.product_name || '');
|
||||
const nextAction = action.code === 'review_external_candidate'
|
||||
? 'review-candidate'
|
||||
: action.code === 'map_external_product'
|
||||
? 'backfill'
|
||||
: 'focus-price';
|
||||
const nextLabel = action.code === 'review_external_candidate'
|
||||
? '確認候選'
|
||||
: action.code === 'map_external_product'
|
||||
? '補齊比價'
|
||||
: '看價格';
|
||||
return `<article class="growth-detail-row">
|
||||
<div>
|
||||
<h3 class="growth-detail-name">${escapeHtml(row.product_name || '未命名商品')}</h3>
|
||||
<p class="growth-detail-line">
|
||||
${escapeHtml(row.category || '未分類')} · 近 7 天 ${escapeHtml(formatMoney(row.sales_7d))} · 變化 ${escapeHtml(delta)}
|
||||
</p>
|
||||
<p class="growth-detail-line">
|
||||
${escapeHtml(action.label || '待判斷')} · ${escapeHtml(gapText)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="growth-detail-action">
|
||||
<span class="growth-action-pill">${escapeHtml(action.label || '待判斷')}</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary table-row-action" data-growth-action="${nextAction}" data-product-key="${productKey}">${nextLabel}</button>
|
||||
</div>
|
||||
</article>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderGrowthOps(rows) {
|
||||
const list = document.getElementById('growthOpsList');
|
||||
if (!rows.length) {
|
||||
@@ -2051,7 +2330,8 @@ function renderGrowthOps(rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = rows.map((row, index) => {
|
||||
const visibleRows = rows.slice(0, 12);
|
||||
const body = visibleRows.map((row, index) => {
|
||||
const action = row.recommended_action || {};
|
||||
const reason = (row.reason_lines || []).slice(0, 2).join(' ');
|
||||
const price = row.external_price;
|
||||
@@ -2351,6 +2631,15 @@ function renderPriceRiskBoard(rows) {
|
||||
setWidth('priceRiskLowBar', (counts.low / total) * 100);
|
||||
}
|
||||
|
||||
function showPriceRiskDetail(risk) {
|
||||
const select = document.getElementById('riskFilter');
|
||||
if (select) {
|
||||
select.value = risk || 'all';
|
||||
filterTable();
|
||||
}
|
||||
scrollToPanel('externalPricePanel');
|
||||
}
|
||||
|
||||
function renderCompetitorTable(rows) {
|
||||
rows = Array.isArray(rows) ? rows : [];
|
||||
const tbody = document.getElementById('competitorTbody');
|
||||
|
||||
@@ -441,6 +441,17 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint():
|
||||
assert "growthDataSourceSummary" in template
|
||||
assert "external_data_source_counts" in template
|
||||
assert "compSourceSummary" in template
|
||||
assert "growthDrilldownPanel" in template
|
||||
assert "showGrowthDetail" in template
|
||||
assert "growthDetailRows" in template
|
||||
assert "growth-detail-tab" in template
|
||||
assert "今日商品明細" in template
|
||||
assert "價格壓力" in template
|
||||
assert "價格優勢" in template
|
||||
assert "有外部價" in template
|
||||
assert "showPriceRiskDetail" in template
|
||||
assert "handlePriceRiskKey" in template
|
||||
assert "opportunities?limit=50" in template
|
||||
assert "scrollToPanel('externalPricePanel')" in template
|
||||
assert "備援資料檢查" in template
|
||||
assert "外部報價預檢" not in template
|
||||
|
||||
Reference in New Issue
Block a user