feat: add growth sales playbook board
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s

This commit is contained in:
ogt
2026-06-24 20:51:11 +08:00
parent 2aa1ae04ed
commit 9610b4da18
4 changed files with 249 additions and 1 deletions

View File

@@ -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 # 用於模板顯示

View File

@@ -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

View File

@@ -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 @@
<div class="text-center py-3 text-muted">整理分類中...</div>
</div>
</div>
<div class="growth-playbook-board" id="growthPlaybookBoard" aria-label="銷售策略建議">
<div class="growth-playbook-head">
<h3 class="growth-playbook-title">銷售策略建議</h3>
<span class="growth-playbook-note">把比價結果轉成可執行動作</span>
</div>
<div class="growth-playbook-grid">
<div class="text-center py-3 text-muted">整理策略中...</div>
</div>
</div>
<div class="growth-decision-panel" id="growthDecisionSummary">
<div>
<h3 class="growth-decision-title">正在整理處理建議</h3>
@@ -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 `<button type="button" class="growth-playbook-card ${item.className}${activeClass}" data-growth-action="show-playbook-detail" data-playbook-kind="${escapeHtml(item.kind)}">
<span class="growth-playbook-label">${escapeHtml(item.label)}</span>
<span class="growth-playbook-value">${formatCount(item.rows.length)}</span>
<span class="growth-playbook-sales">近 7 天 ${escapeHtml(formatMoney(item.sales))}</span>
<span class="growth-playbook-track"><span class="growth-playbook-bar" style="width:${pct}%"></span></span>
<span class="growth-playbook-product">${escapeHtml(item.topProduct)}</span>
<span class="growth-playbook-action">${escapeHtml(item.action)}</span>
</button>`;
}).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);

View File

@@ -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