From 9610b4da189d61b3e0541bffd678364e1031fef2 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 24 Jun 2026 20:51:11 +0800 Subject: [PATCH] feat: add growth sales playbook board --- config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 1 + templates/ai_intelligence.html | 243 ++++++++++++++++++++ tests/test_pchome_revenue_growth_service.py | 4 + 4 files changed, 249 insertions(+), 1 deletion(-) diff --git a/config.py b/config.py index 3ae8011..443ead2 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.648" +SYSTEM_VERSION = "V10.649" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index 417fcc5..dea4c8e 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -84,6 +84,7 @@ - V10.646 起 `/ai_intelligence` 的商品明細必須提供搜尋與排序;搜尋至少涵蓋商品、分類、商品編號與 MOMO 候選資訊,排序至少支援優先級、近 7 天業績、價差、下滑幅度與可信度。搜尋/排序後的行動摘要與明細列表必須使用同一批結果。 - V10.647 起 `/ai_intelligence` 的商品明細每一筆都必須能打開單品作戰詳情,詳情需顯示商品、建議動作、近 7 天業績、業績變化、PChome/MOMO 價格證據、價差、可信度、判斷原因與下一步操作;不得只讓使用者看一排文字後自行猜測。 - V10.648 起 `/ai_intelligence` 的商品明細上方必須提供分類策略看板,把商品依分類彙總成可點擊的數據條列;每列至少顯示分類、近 7 天業績、商品數、價格壓力、價格優勢、缺比價、待確認與建議下一步。點擊分類後必須切到該分類商品明細。 +- V10.649 起 `/ai_intelligence` 必須提供銷售策略建議看板,把商品分成價格防守、主推曝光、組合/單位價、資料補齊等營運路徑;每張策略卡需顯示件數、近 7 天業績、代表商品與可點擊下一步,點擊後必須切到對應商品明細。 ## 零之一、12 Agent 決策信封(2026-05-24) diff --git a/templates/ai_intelligence.html b/templates/ai_intelligence.html index 8a80ba7..2e17cf6 100644 --- a/templates/ai_intelligence.html +++ b/templates/ai_intelligence.html @@ -907,6 +907,122 @@ text-align: right; } + .growth-playbook-board { + border: 1px solid rgba(42, 37, 32, 0.1); + border-radius: 8px; + background: rgba(255, 255, 255, 0.7); + margin-bottom: 10px; + padding: 10px; + } + + .growth-playbook-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 8px; + } + + .growth-playbook-title { + color: var(--momo-text-strong); + font-size: 0.84rem; + font-weight: 950; + margin: 0; + } + + .growth-playbook-note { + color: var(--momo-text-muted); + font-size: 0.68rem; + font-weight: 820; + text-align: right; + } + + .growth-playbook-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; + } + + .growth-playbook-card { + border: 1px solid rgba(42, 37, 32, 0.09); + border-radius: 8px; + background: rgba(250, 247, 240, 0.58); + color: var(--momo-text-strong); + display: grid; + gap: 7px; + min-height: 142px; + padding: 10px; + text-align: left; + width: 100%; + } + + .growth-playbook-card:hover, + .growth-playbook-card:focus, + .growth-playbook-card.is-active { + border-color: rgba(172, 92, 58, 0.3); + background: rgba(255, 248, 232, 0.82); + outline: 0; + } + + .growth-playbook-card.is-defense { + border-color: rgba(185, 79, 58, 0.2); + } + + .growth-playbook-card.is-boost { + border-color: rgba(47, 143, 102, 0.2); + } + + .growth-playbook-card.is-bundle { + border-color: rgba(216, 161, 58, 0.22); + } + + .growth-playbook-card.is-data { + border-color: rgba(92, 111, 135, 0.2); + } + + .growth-playbook-label { + color: var(--momo-text-muted); + font-size: 0.68rem; + font-weight: 950; + line-height: 1.2; + } + + .growth-playbook-value { + color: var(--momo-text-strong); + font-family: var(--momo-font-mono); + font-size: 1.22rem; + font-weight: 950; + line-height: 1; + } + + .growth-playbook-sales, + .growth-playbook-product, + .growth-playbook-action { + color: var(--momo-text-muted); + font-size: 0.68rem; + font-weight: 820; + line-height: 1.35; + } + + .growth-playbook-action { + color: #8f4d33; + font-weight: 950; + } + + .growth-playbook-track { + background: rgba(42, 37, 32, 0.08); + border-radius: 999px; + height: 7px; + overflow: hidden; + } + + .growth-playbook-bar { + background: #ac5c3a; + border-radius: inherit; + display: block; + height: 100%; + } + .growth-decision-panel { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -1768,6 +1884,23 @@ text-align: left; } + .growth-playbook-head { + align-items: flex-start; + flex-direction: column; + } + + .growth-playbook-note { + text-align: left; + } + + .growth-playbook-grid { + grid-template-columns: 1fr; + } + + .growth-playbook-card { + min-height: 0; + } + .growth-detail-price-grid { grid-template-columns: 1fr; } @@ -1972,6 +2105,15 @@
整理分類中...
+
+
+

銷售策略建議

+ 把比價結果轉成可執行動作 +
+
+
整理策略中...
+
+

正在整理處理建議

@@ -2335,6 +2477,10 @@ function bindActionDelegation() { showGrowthCategoryDetail(growthButton.dataset.categoryName || ''); return; } + if (growthButton.dataset.growthAction === 'show-playbook-detail') { + showGrowthPlaybookDetail(growthButton.dataset.playbookKind || ''); + return; + } if (growthButton.dataset.growthAction === 'review-candidate') { focusReviewCandidate(growthButton.dataset.productKey || ''); return; @@ -2864,6 +3010,7 @@ function growthDetailConfig(kind) { review: ['MOMO 候選待確認', '候選已找到,確認同款後才會進入價格判斷。'], risk: ['MOMO 更便宜商品', 'MOMO 參考價較低,優先檢查 PChome 售價、券或組合。'], advantage: ['PChome 價格優勢商品', 'PChome 目前較有價格優勢,適合檢查曝光與主推位置。'], + bundle: ['組合 / 單位價商品', '需要檢查組合包、入數、容量或單位價,避免只看總價誤判。'], source: ['外部價格來源明細', '只列出已接到 MOMO 外部參考價的商品。'], decline: ['業績下滑商品', '近 7 天比前 7 天下滑的商品。'], category: [selectedCategory ? `${selectedCategory} 商品明細` : '分類商品明細', '這個分類內的商品與價格狀態。'], @@ -2883,6 +3030,7 @@ function growthDetailRows(kind) { 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 === 'bundle') return isBundleOrUnitRow(row); if (kind === 'source') return Boolean(price); if (kind === 'decline') return Number(row.sales_delta_pct || 0) < 0; if (kind === 'category') return selectedCategory && row.category === selectedCategory; @@ -3182,6 +3330,95 @@ function showGrowthCategoryDetail(category) { showGrowthDetail('category'); } +function isBundleOrUnitRow(row) { + const price = row?.external_price || null; + const name = String(row?.product_name || ''); + return Boolean(price) && ( + price.price_basis === 'unit_price' + || /組|入|套|盒|包|ml|g|kg|公升|毫升/.test(name) + ); +} + +function growthPlaybookRows(kind) { + const rows = Array.isArray(latestGrowthRows) ? latestGrowthRows : []; + if (kind === 'defense') return rows.filter((row) => classifyGrowthRow(row).risk); + if (kind === 'boost') return rows.filter((row) => classifyGrowthRow(row).advantage); + if (kind === 'bundle') return rows.filter(isBundleOrUnitRow); + if (kind === 'data') return rows.filter((row) => { + const flags = classifyGrowthRow(row); + return flags.needs || flags.review; + }); + return []; +} + +function playbookTargetKind(kind) { + if (kind === 'defense') return 'risk'; + if (kind === 'boost') return 'advantage'; + if (kind === 'bundle') return 'bundle'; + if (kind === 'data') return 'needs'; + return 'all'; +} + +function renderGrowthPlaybookBoard() { + const board = document.getElementById('growthPlaybookBoard'); + if (!board) return; + const grid = board.querySelector('.growth-playbook-grid'); + if (!grid) return; + + const playbooks = [ + { + kind: 'defense', + className: 'is-defense', + label: '價格防守', + action: '檢查售價 / 券 / 活動', + }, + { + kind: 'boost', + className: 'is-boost', + label: '主推曝光', + action: '加強主推位置與文案', + }, + { + kind: 'bundle', + className: 'is-bundle', + label: '組合 / 單位價', + action: '檢查組合包與單位價', + }, + { + kind: 'data', + className: 'is-data', + label: '資料補齊', + action: '補候選或確認同款', + }, + ]; + + const summaries = playbooks.map((item) => { + const rows = growthPlaybookRows(item.kind).sort((a, b) => Number(b.sales_7d || 0) - Number(a.sales_7d || 0)); + const sales = rows.reduce((sum, row) => sum + Number(row.sales_7d || 0), 0); + return { ...item, rows, sales, topProduct: rows[0]?.product_name || '目前沒有商品' }; + }); + const maxSales = Math.max(1, ...summaries.map((item) => item.sales)); + + grid.innerHTML = summaries.map((item) => { + const pct = clampPercent((item.sales / maxSales) * 100); + const targetKind = playbookTargetKind(item.kind); + const activeClass = activeGrowthDetailKind === targetKind ? ' is-active' : ''; + return ``; + }).join(''); +} + +function showGrowthPlaybookDetail(kind) { + const target = playbookTargetKind(kind); + showGrowthDetail(target); +} + function renderGrowthStrategySummary() { const box = document.getElementById('growthStrategyGrid'); if (!box) return; @@ -3250,6 +3487,11 @@ function growthDecisionConfig(kind) { copy: 'PChome 有價格優勢,適合排主推、加強文案與提高曝光位置。', actionLabel: '看主推清單', }, + bundle: { + title: '檢查組合包與單位價', + copy: '先確認容量、入數與單位價,再決定要做組合、加券或調整主圖文案。', + actionLabel: '看組合商品', + }, review: { title: '先確認候選是否同款', copy: '確認同款後才會進入價格判斷;色號、容量或組合不一致就排除。', @@ -3359,6 +3601,7 @@ function renderGrowthDetail(kind = activeGrowthDetailKind) { }); renderGrowthStrategySummary(); renderGrowthCategoryBoard(); + renderGrowthPlaybookBoard(); const [title, subtitle] = growthDetailConfig(activeGrowthDetailKind); const rows = growthDetailRows(activeGrowthDetailKind); diff --git a/tests/test_pchome_revenue_growth_service.py b/tests/test_pchome_revenue_growth_service.py index d594a34..bafb187 100644 --- a/tests/test_pchome_revenue_growth_service.py +++ b/tests/test_pchome_revenue_growth_service.py @@ -479,6 +479,10 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint(): assert "分類策略看板" in template assert "showGrowthCategoryDetail" in template assert "data-growth-action=\"show-category-detail\"" in template + assert "growthPlaybookBoard" in template + assert "銷售策略建議" in template + assert "showGrowthPlaybookDetail" in template + assert "組合 / 單位價" in template assert "scrollToPanel('externalPricePanel')" in template assert "備援資料檢查" in template assert "外部報價預檢" not in template