feat: link growth dashboard metrics to details
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
This commit is contained in:
@@ -298,6 +298,25 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.growth-exec-card.is-clickable,
|
||||
.ops-dashboard-tile.is-clickable,
|
||||
.growth-metric.is-clickable {
|
||||
cursor: pointer;
|
||||
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
|
||||
}
|
||||
|
||||
.growth-exec-card.is-clickable:hover,
|
||||
.growth-exec-card.is-clickable:focus,
|
||||
.ops-dashboard-tile.is-clickable:hover,
|
||||
.ops-dashboard-tile.is-clickable:focus,
|
||||
.growth-metric.is-clickable:hover,
|
||||
.growth-metric.is-clickable:focus {
|
||||
border-color: rgba(172, 92, 58, 0.34);
|
||||
box-shadow: 0 0 0 3px rgba(172, 92, 58, 0.1), var(--momo-shadow-soft);
|
||||
outline: none;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.growth-exec-detail {
|
||||
margin-top: 7px;
|
||||
color: var(--momo-text-muted);
|
||||
@@ -306,6 +325,17 @@
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.drilldown-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 6px;
|
||||
color: var(--momo-warm-rust);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 900;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ops-flow {
|
||||
border: 1px solid var(--momo-border-subtle);
|
||||
border-radius: 8px;
|
||||
@@ -878,6 +908,13 @@
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.review-candidate-row.is-highlight {
|
||||
border-radius: 8px;
|
||||
background: rgba(242, 178, 90, 0.16);
|
||||
box-shadow: inset 3px 0 0 var(--momo-warm-caramel);
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.review-candidate-title {
|
||||
margin: 0;
|
||||
color: var(--momo-text-strong);
|
||||
@@ -1114,37 +1151,37 @@
|
||||
</section>
|
||||
|
||||
<section class="growth-executive-strip" aria-label="今日任務摘要">
|
||||
<article class="growth-exec-card is-primary" id="growthExecTaskCard">
|
||||
<article class="growth-exec-card is-primary is-clickable" id="growthExecTaskCard" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<div class="growth-exec-label">
|
||||
<span>今日任務</span>
|
||||
<i class="fas fa-location-arrow"></i>
|
||||
</div>
|
||||
<div class="growth-exec-value" id="growthExecTask">整理中</div>
|
||||
<div class="growth-exec-detail" id="growthExecTaskDetail">正在讀取 PChome 業績與 MOMO 外部價格。</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">
|
||||
<article class="growth-exec-card is-ready is-clickable" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<div class="growth-exec-label">
|
||||
<span>可立即處理</span>
|
||||
<i class="fas fa-circle-check"></i>
|
||||
</div>
|
||||
<div class="growth-exec-value" id="growthExecReady">—</div>
|
||||
<div class="growth-exec-detail">已有可用比價資料</div>
|
||||
<div class="growth-exec-detail">已有可用比價資料<span class="drilldown-hint">看清單</span></div>
|
||||
</article>
|
||||
<article class="growth-exec-card is-gap" id="growthExecGapCard">
|
||||
<article class="growth-exec-card is-gap is-clickable" id="growthExecGapCard" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<div class="growth-exec-label">
|
||||
<span>待補比價</span>
|
||||
<i class="fas fa-link-slash"></i>
|
||||
</div>
|
||||
<div class="growth-exec-value" id="growthExecGap">—</div>
|
||||
<div class="growth-exec-detail">有業績但缺外部參考</div>
|
||||
<div class="growth-exec-detail">有業績但缺外部參考<span class="drilldown-hint">看商品</span></div>
|
||||
</article>
|
||||
<article class="growth-exec-card">
|
||||
<article class="growth-exec-card is-clickable" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<div class="growth-exec-label">
|
||||
<span>最新業績日</span>
|
||||
<i class="fas fa-calendar-day"></i>
|
||||
</div>
|
||||
<div class="growth-exec-value" id="growthExecLatestDate">—</div>
|
||||
<div class="growth-exec-detail" id="growthExecLatestDetail">等待資料</div>
|
||||
<div class="growth-exec-detail"><span id="growthExecLatestDetail">等待資料</span><span class="drilldown-hint">看清單</span></div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -1167,7 +1204,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="ops-flow-grid">
|
||||
<div class="ops-dashboard-tile">
|
||||
<div class="ops-dashboard-tile is-clickable" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<div class="ops-dashboard-title">
|
||||
<span>商品處理進度</span>
|
||||
<strong class="ops-dashboard-value" id="opsReadyRate">—%</strong>
|
||||
@@ -1187,7 +1224,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ops-dashboard-tile">
|
||||
<div class="ops-dashboard-tile is-clickable" role="button" tabindex="0" onclick="scrollToPanel('externalPricePanel')" onkeydown="handleDrilldownKey(event, 'externalPricePanel')">
|
||||
<div class="ops-dashboard-title">
|
||||
<span>外部價格來源</span>
|
||||
<strong class="ops-dashboard-value" id="opsSourceTotal">—</strong>
|
||||
@@ -1241,19 +1278,19 @@
|
||||
<div class="growth-ops-grid">
|
||||
<div>
|
||||
<div class="growth-metric-row">
|
||||
<div class="growth-metric">
|
||||
<div class="growth-metric is-clickable" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<strong id="growthCandidateCount">—</strong>
|
||||
<span>追蹤商品</span>
|
||||
</div>
|
||||
<div class="growth-metric">
|
||||
<div class="growth-metric is-clickable" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<strong id="growthMappedCount">—</strong>
|
||||
<span>可立即處理</span>
|
||||
</div>
|
||||
<div class="growth-metric">
|
||||
<div class="growth-metric is-clickable" role="button" tabindex="0" onclick="scrollToPanel('growthOpsPanel')" onkeydown="handleDrilldownKey(event, 'growthOpsPanel')">
|
||||
<strong id="growthNeedsMapping">—</strong>
|
||||
<span>無法比價</span>
|
||||
</div>
|
||||
<div class="growth-metric">
|
||||
<div class="growth-metric is-clickable" role="button" tabindex="0" onclick="scrollToPanel('growthReviewPanel')" onkeydown="handleDrilldownKey(event, 'growthReviewPanel')">
|
||||
<strong id="growthReviewCandidateCount">—</strong>
|
||||
<span>待確認</span>
|
||||
</div>
|
||||
@@ -1542,6 +1579,10 @@ function bindActionDelegation() {
|
||||
backfillPchomeMatches();
|
||||
return;
|
||||
}
|
||||
if (growthButton.dataset.growthAction === 'review-candidate') {
|
||||
focusReviewCandidate(growthButton.dataset.productKey || '');
|
||||
return;
|
||||
}
|
||||
focusPriceTable(growthButton.dataset.productKey || '');
|
||||
});
|
||||
}
|
||||
@@ -1555,6 +1596,22 @@ function focusPriceTable(keyword) {
|
||||
scrollToPanel('externalPricePanel');
|
||||
}
|
||||
|
||||
function focusReviewCandidate(productKey) {
|
||||
scrollToPanel('growthReviewPanel');
|
||||
const key = String(productKey || '').trim();
|
||||
if (!key) return;
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('.review-candidate-row.is-highlight')
|
||||
.forEach((row) => row.classList.remove('is-highlight'));
|
||||
const target = Array.from(document.querySelectorAll('.review-candidate-row'))
|
||||
.find((row) => row.dataset.pchomeId === key);
|
||||
if (target) {
|
||||
target.classList.add('is-highlight');
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}, 220);
|
||||
}
|
||||
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
const res = await fetch('/api/ai/icaim/dashboard');
|
||||
@@ -1627,6 +1684,12 @@ function scrollToPanel(panelId) {
|
||||
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
function handleDrilldownKey(event, panelId) {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return;
|
||||
event.preventDefault();
|
||||
scrollToPanel(panelId);
|
||||
}
|
||||
|
||||
function clampPercent(value) {
|
||||
return Math.max(0, Math.min(100, Number(value || 0)));
|
||||
}
|
||||
@@ -1680,6 +1743,7 @@ function renderOpsCommandDashboard(stats, scope = {}) {
|
||||
const candidateCount = Number(stats.candidate_count || 0);
|
||||
const mappedCount = Number(stats.mapped_count || 0);
|
||||
const needsMapping = Number(stats.needs_mapping_count || 0);
|
||||
const reviewCandidateCount = Number(stats.review_candidate_count || 0);
|
||||
const readyRate = candidateCount ? Math.round((mappedCount / candidateCount) * 100) : 0;
|
||||
|
||||
document.getElementById('opsReadyRate').textContent = `${readyRate}%`;
|
||||
@@ -1689,7 +1753,7 @@ function renderOpsCommandDashboard(stats, scope = {}) {
|
||||
document.getElementById('opsDashboardStatus').textContent = candidateCount
|
||||
? `資料就緒率 ${readyRate}%`
|
||||
: '等待 PChome 業績資料';
|
||||
renderNextAction(candidateCount, mappedCount, needsMapping);
|
||||
renderNextAction(candidateCount, mappedCount, needsMapping, reviewCandidateCount);
|
||||
|
||||
setWidth('opsFunnelCandidateBar', candidateCount ? 100 : 0);
|
||||
setWidth('opsFunnelMappedBar', candidateCount ? (mappedCount / candidateCount) * 100 : 0);
|
||||
@@ -1698,7 +1762,7 @@ function renderOpsCommandDashboard(stats, scope = {}) {
|
||||
renderGrowthExecutiveSummary(stats);
|
||||
}
|
||||
|
||||
function renderNextAction(candidateCount, mappedCount, needsMapping) {
|
||||
function renderNextAction(candidateCount, mappedCount, needsMapping, reviewCandidateCount = 0) {
|
||||
const title = document.getElementById('nextActionTitle');
|
||||
const reason = document.getElementById('nextActionReason');
|
||||
const button = document.getElementById('nextActionButton');
|
||||
@@ -1714,6 +1778,14 @@ function renderNextAction(candidateCount, mappedCount, needsMapping) {
|
||||
}
|
||||
|
||||
if (needsMapping > 0 && mappedCount === 0) {
|
||||
if (reviewCandidateCount > 0) {
|
||||
title.textContent = `今天先做:確認 ${formatCount(reviewCandidateCount)} 筆 MOMO 候選`;
|
||||
reason.textContent = '候選已找到,先確認同款或排除,確認後才會進入價格判斷。';
|
||||
button.textContent = '確認候選';
|
||||
delete button.dataset.action;
|
||||
button.onclick = () => scrollToPanel('growthReviewPanel');
|
||||
return;
|
||||
}
|
||||
title.textContent = `今天先做:補齊 ${formatCount(needsMapping)} 件比價資料`;
|
||||
reason.textContent = '這些商品有業績,但目前還看不到 MOMO 參考價,請先補齊比價資料。';
|
||||
button.textContent = '補齊比價資料';
|
||||
@@ -1723,6 +1795,14 @@ function renderNextAction(candidateCount, mappedCount, needsMapping) {
|
||||
}
|
||||
|
||||
if (needsMapping > mappedCount) {
|
||||
if (reviewCandidateCount > 0) {
|
||||
title.textContent = `今天先做:確認 ${formatCount(reviewCandidateCount)} 筆 MOMO 候選`;
|
||||
reason.textContent = '先把已找到的候選變成可用資料,再補剩下找不到同款的商品。';
|
||||
button.textContent = '確認候選';
|
||||
delete button.dataset.action;
|
||||
button.onclick = () => scrollToPanel('growthReviewPanel');
|
||||
return;
|
||||
}
|
||||
title.textContent = `今天先做:補齊 ${formatCount(needsMapping)} 件比價資料`;
|
||||
reason.textContent = '目前無法比價的商品比可處理商品多,請先補齊商品對應。';
|
||||
button.textContent = '補齊比價資料';
|
||||
@@ -1742,6 +1822,7 @@ function renderGrowthExecutiveSummary(stats = {}) {
|
||||
const candidateCount = Number(stats.candidate_count || 0);
|
||||
const mappedCount = Number(stats.mapped_count || 0);
|
||||
const needsMapping = Number(stats.needs_mapping_count || 0);
|
||||
const reviewCandidateCount = Number(stats.review_candidate_count || 0);
|
||||
const latestSalesDate = String(stats.latest_sales_date || '').slice(0, 10);
|
||||
|
||||
document.getElementById('growthExecReady').textContent = formatCount(mappedCount);
|
||||
@@ -1762,6 +1843,12 @@ function renderGrowthExecutiveSummary(stats = {}) {
|
||||
}
|
||||
|
||||
if (needsMapping > 0 && mappedCount === 0) {
|
||||
if (reviewCandidateCount > 0) {
|
||||
task.textContent = `先確認 ${formatCount(reviewCandidateCount)} 筆候選`;
|
||||
detail.textContent = '候選商品已找到,確認同款後才能進入價格判斷。';
|
||||
gapCard?.classList.add('is-gap');
|
||||
return;
|
||||
}
|
||||
task.textContent = `先補 ${formatCount(needsMapping)} 件 MOMO 參考`;
|
||||
detail.textContent = '高業績商品還不能比價,先補對應資料才會有可行動建議。';
|
||||
gapCard?.classList.add('is-gap');
|
||||
@@ -1769,6 +1856,12 @@ function renderGrowthExecutiveSummary(stats = {}) {
|
||||
}
|
||||
|
||||
if (needsMapping > mappedCount) {
|
||||
if (reviewCandidateCount > 0) {
|
||||
task.textContent = `先確認 ${formatCount(reviewCandidateCount)} 筆候選`;
|
||||
detail.textContent = '先處理候選清單,再補剩下找不到同款的商品。';
|
||||
gapCard?.classList.add('is-gap');
|
||||
return;
|
||||
}
|
||||
task.textContent = `先補比價,再處理 ${formatCount(mappedCount)} 件`;
|
||||
detail.textContent = '待補比價比可處理商品多,先擴大 MOMO 對應覆蓋率。';
|
||||
gapCard?.classList.add('is-gap');
|
||||
@@ -1962,9 +2055,12 @@ function renderGrowthOps(rows) {
|
||||
const action = row.recommended_action || {};
|
||||
const reason = (row.reason_lines || []).slice(0, 2).join(' ');
|
||||
const price = row.external_price;
|
||||
const reviewCandidate = row.review_candidate || null;
|
||||
const gap = price && price.gap_pct !== null && price.gap_pct !== undefined ? Number(price.gap_pct) : null;
|
||||
const basisLabel = price?.price_basis_label || '商品總價';
|
||||
const priceText = gap === null
|
||||
const priceText = reviewCandidate
|
||||
? '候選待確認'
|
||||
: gap === null
|
||||
? '資料不足,先補比價'
|
||||
: gap < 0
|
||||
? `${basisLabel} PChome 貴 ${Math.abs(gap).toFixed(1)}%`
|
||||
@@ -1980,8 +2076,16 @@ function renderGrowthOps(rows) {
|
||||
const qualityLabel = quality.label || (price ? '可直接參考' : '資料不足');
|
||||
const qualityIssues = Array.isArray(quality.issues) ? quality.issues.join('、') : '';
|
||||
const productKey = escapeHtml(row.pchome_product_id || row.product_name || '');
|
||||
const nextLabel = action.code === 'map_external_product' ? '補齊比價' : '檢查價格';
|
||||
const nextAction = action.code === 'map_external_product' ? 'backfill' : 'focus-price';
|
||||
const nextLabel = action.code === 'review_external_candidate'
|
||||
? '確認候選'
|
||||
: action.code === 'map_external_product'
|
||||
? '補齊比價'
|
||||
: '檢查價格';
|
||||
const nextAction = action.code === 'review_external_candidate'
|
||||
? 'review-candidate'
|
||||
: action.code === 'map_external_product'
|
||||
? 'backfill'
|
||||
: 'focus-price';
|
||||
return `<tr>
|
||||
<td data-label="優先級">
|
||||
<span class="badge ${priorityClass}">${priority}</span>
|
||||
@@ -2073,7 +2177,7 @@ function renderGrowthReviewCandidates(rows) {
|
||||
const pchomePrice = row.pchome_price ? formatMoney(row.pchome_price) : '未取得 PChome 價格';
|
||||
const safeUrl = safeHttpUrl(row.product_url);
|
||||
const url = safeUrl ? `<a href="${escapeHtml(safeUrl)}" target="_blank" rel="noopener">看 MOMO</a>` : '';
|
||||
return `<article class="review-candidate-row">
|
||||
return `<article class="review-candidate-row" data-pchome-id="${escapeHtml(row.pchome_product_id || '')}">
|
||||
<div>
|
||||
<h3 class="review-candidate-title">${escapeHtml(row.pchome_product_name || row.pchome_product_id || 'PChome 商品')}</h3>
|
||||
<p class="review-candidate-meta">
|
||||
|
||||
Reference in New Issue
Block a user