feat: add growth sales playbook board
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s
This commit is contained in:
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user