feat: add growth daily action plan
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.649"
|
||||
SYSTEM_VERSION = "V10.650"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
- V10.647 起 `/ai_intelligence` 的商品明細每一筆都必須能打開單品作戰詳情,詳情需顯示商品、建議動作、近 7 天業績、業績變化、PChome/MOMO 價格證據、價差、可信度、判斷原因與下一步操作;不得只讓使用者看一排文字後自行猜測。
|
||||
- V10.648 起 `/ai_intelligence` 的商品明細上方必須提供分類策略看板,把商品依分類彙總成可點擊的數據條列;每列至少顯示分類、近 7 天業績、商品數、價格壓力、價格優勢、缺比價、待確認與建議下一步。點擊分類後必須切到該分類商品明細。
|
||||
- V10.649 起 `/ai_intelligence` 必須提供銷售策略建議看板,把商品分成價格防守、主推曝光、組合/單位價、資料補齊等營運路徑;每張策略卡需顯示件數、近 7 天業績、代表商品與可點擊下一步,點擊後必須切到對應商品明細。
|
||||
- V10.650 起 `/ai_intelligence` 必須提供「今日策略動作」清單,從作戰商品中挑出前 5 件具體行動;每列需顯示處理順序、動作、商品、近 7 天業績、原因與可點擊的詳情/處理入口,避免使用者只看到分類與策略後仍不知道下一步要做哪一件商品。
|
||||
|
||||
## 零之一、12 Agent 決策信封(2026-05-24)
|
||||
|
||||
|
||||
@@ -1023,6 +1023,102 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.growth-action-board {
|
||||
border: 1px solid rgba(42, 37, 32, 0.1);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.growth-action-board-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.growth-action-board-title {
|
||||
color: var(--momo-text-strong);
|
||||
font-size: 0.84rem;
|
||||
font-weight: 950;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.growth-action-board-note {
|
||||
color: var(--momo-text-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 820;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.growth-action-list {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.growth-action-row {
|
||||
border: 1px solid rgba(42, 37, 32, 0.08);
|
||||
border-radius: 8px;
|
||||
background: rgba(250, 247, 240, 0.58);
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1.1fr) minmax(170px, 0.55fr) auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.growth-action-rank {
|
||||
align-items: center;
|
||||
background: rgba(172, 92, 58, 0.12);
|
||||
border-radius: 999px;
|
||||
color: #8f4d33;
|
||||
display: inline-flex;
|
||||
font-family: var(--momo-font-mono);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 950;
|
||||
height: 34px;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
}
|
||||
|
||||
.growth-action-title {
|
||||
color: var(--momo-text-strong);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 950;
|
||||
line-height: 1.3;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.growth-action-meta,
|
||||
.growth-action-reason {
|
||||
color: var(--momo-text-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 820;
|
||||
line-height: 1.35;
|
||||
margin: 3px 0 0;
|
||||
}
|
||||
|
||||
.growth-action-pill {
|
||||
border: 1px solid rgba(172, 92, 58, 0.18);
|
||||
border-radius: 999px;
|
||||
color: #8f4d33;
|
||||
display: inline-flex;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 950;
|
||||
justify-content: center;
|
||||
padding: 4px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.growth-action-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.growth-decision-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
@@ -1901,6 +1997,27 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.growth-action-board-head {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.growth-action-board-note {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.growth-action-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.growth-action-buttons {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.growth-action-buttons .table-row-action {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.growth-detail-price-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -2114,6 +2231,15 @@
|
||||
<div class="text-center py-3 text-muted">整理策略中...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="growth-action-board" id="growthActionBoard" aria-label="今日策略動作">
|
||||
<div class="growth-action-board-head">
|
||||
<h3 class="growth-action-board-title">今日策略動作</h3>
|
||||
<span class="growth-action-board-note">照順序處理最可能影響業績的商品</span>
|
||||
</div>
|
||||
<div class="growth-action-list">
|
||||
<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>
|
||||
@@ -3419,6 +3545,109 @@ function showGrowthPlaybookDetail(kind) {
|
||||
showGrowthDetail(target);
|
||||
}
|
||||
|
||||
function growthActionPlanForRow(row) {
|
||||
const flags = classifyGrowthRow(row);
|
||||
const next = growthRowNextAction(row);
|
||||
const price = row?.external_price || null;
|
||||
const gap = price && price.gap_pct !== null && price.gap_pct !== undefined ? Number(price.gap_pct) : null;
|
||||
if (flags.risk) {
|
||||
return {
|
||||
label: '價格防守',
|
||||
title: '檢查售價、券或活動',
|
||||
reason: gap === null ? 'MOMO 參考價較低,先檢查價格壓力。' : formatGapDisplay(gap),
|
||||
next,
|
||||
};
|
||||
}
|
||||
if (flags.advantage) {
|
||||
return {
|
||||
label: '主推曝光',
|
||||
title: '放大價格優勢',
|
||||
reason: gap === null ? 'PChome 有外部價格優勢,適合提高曝光。' : formatGapDisplay(gap),
|
||||
next,
|
||||
};
|
||||
}
|
||||
if (isBundleOrUnitRow(row)) {
|
||||
return {
|
||||
label: '組合 / 單位價',
|
||||
title: '檢查組合包與單位價',
|
||||
reason: '先確認容量、入數與單位價,再決定組合或券。',
|
||||
next,
|
||||
};
|
||||
}
|
||||
if (flags.review) {
|
||||
return {
|
||||
label: '候選確認',
|
||||
title: '確認 MOMO 候選是否同款',
|
||||
reason: '同款確認後才會進入價格判斷。',
|
||||
next,
|
||||
};
|
||||
}
|
||||
if (flags.needs) {
|
||||
return {
|
||||
label: '補資料',
|
||||
title: '補齊 MOMO 參考商品',
|
||||
reason: '目前缺外部參考價,先補資料才有可行動建議。',
|
||||
next,
|
||||
};
|
||||
}
|
||||
if (Number(row?.sales_delta_pct || 0) < 0) {
|
||||
return {
|
||||
label: '找回動能',
|
||||
title: '檢查下滑商品',
|
||||
reason: `近 7 天業績變化 ${Number(row.sales_delta_pct || 0).toFixed(1)}%。`,
|
||||
next,
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: '追蹤',
|
||||
title: '維持追蹤',
|
||||
reason: '目前沒有明確價格或資料風險。',
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
function renderGrowthActionBoard() {
|
||||
const board = document.getElementById('growthActionBoard');
|
||||
if (!board) return;
|
||||
const list = board.querySelector('.growth-action-list');
|
||||
if (!list) return;
|
||||
const rows = (Array.isArray(latestGrowthRows) ? latestGrowthRows : [])
|
||||
.map((row) => ({ row, plan: growthActionPlanForRow(row) }))
|
||||
.filter((item) => item.plan.label !== '追蹤')
|
||||
.sort((a, b) => Number(b.row.priority_score || 0) - Number(a.row.priority_score || 0))
|
||||
.slice(0, 5);
|
||||
|
||||
if (!rows.length) {
|
||||
list.innerHTML = `<div class="text-center py-3 text-muted">目前沒有需要立即處理的商品。</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = rows.map((item, index) => {
|
||||
const row = item.row;
|
||||
const plan = item.plan;
|
||||
const key = escapeHtml(growthRowKey(row));
|
||||
const nextAttrs = plan.next.action === 'backfill'
|
||||
? 'data-growth-action="backfill"'
|
||||
: `data-growth-action="${plan.next.action}" data-product-key="${key}"`;
|
||||
return `<article class="growth-action-row">
|
||||
<span class="growth-action-rank">${String(index + 1).padStart(2, '0')}</span>
|
||||
<div>
|
||||
<p class="growth-action-title">${escapeHtml(plan.title)}</p>
|
||||
<p class="growth-action-meta">${escapeHtml(row.product_name || '未命名商品')}</p>
|
||||
<p class="growth-action-reason">${escapeHtml(plan.reason)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="growth-action-pill">${escapeHtml(plan.label)}</span>
|
||||
<p class="growth-action-meta">近 7 天 ${escapeHtml(formatMoney(row.sales_7d))}</p>
|
||||
</div>
|
||||
<div class="growth-action-buttons">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary table-row-action" data-growth-action="show-product-detail" data-product-key="${key}">詳情</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary table-row-action" ${nextAttrs}>${escapeHtml(plan.next.label)}</button>
|
||||
</div>
|
||||
</article>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderGrowthStrategySummary() {
|
||||
const box = document.getElementById('growthStrategyGrid');
|
||||
if (!box) return;
|
||||
@@ -3602,6 +3831,7 @@ function renderGrowthDetail(kind = activeGrowthDetailKind) {
|
||||
renderGrowthStrategySummary();
|
||||
renderGrowthCategoryBoard();
|
||||
renderGrowthPlaybookBoard();
|
||||
renderGrowthActionBoard();
|
||||
|
||||
const [title, subtitle] = growthDetailConfig(activeGrowthDetailKind);
|
||||
const rows = growthDetailRows(activeGrowthDetailKind);
|
||||
|
||||
@@ -483,6 +483,10 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint():
|
||||
assert "銷售策略建議" in template
|
||||
assert "showGrowthPlaybookDetail" in template
|
||||
assert "組合 / 單位價" in template
|
||||
assert "growthActionBoard" in template
|
||||
assert "今日策略動作" in template
|
||||
assert "renderGrowthActionBoard" in template
|
||||
assert "growthActionPlanForRow" in template
|
||||
assert "scrollToPanel('externalPricePanel')" in template
|
||||
assert "備援資料檢查" in template
|
||||
assert "外部報價預檢" not in template
|
||||
|
||||
Reference in New Issue
Block a user