feat(governance): automate identity and comparison coverage
This commit is contained in:
@@ -88,6 +88,12 @@
|
||||
padding: 7px 12px;
|
||||
}
|
||||
|
||||
.growth-command-status-pill.is-degraded {
|
||||
border-color: rgba(176, 93, 39, 0.28);
|
||||
background: rgba(255, 241, 230, 0.96);
|
||||
color: #8b421f;
|
||||
}
|
||||
|
||||
.growth-command-action-group {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -150,6 +156,11 @@
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
.growth-command-card.is-degraded {
|
||||
border-color: rgba(176, 93, 39, 0.34);
|
||||
background: rgba(255, 249, 244, 0.96);
|
||||
}
|
||||
|
||||
.growth-command-card.is-clickable {
|
||||
cursor: pointer;
|
||||
transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
|
||||
@@ -195,6 +206,16 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.growth-command-kpi-note.is-essential {
|
||||
display: block;
|
||||
min-height: 2.4em;
|
||||
margin-top: 9px;
|
||||
color: var(--momo-text-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.growth-command-spark {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -2864,7 +2885,7 @@
|
||||
</div>
|
||||
|
||||
<div class="growth-command-kpi-grid" aria-label="業績重點數字">
|
||||
<article class="growth-command-card is-sales is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('all')" onkeydown="handleGrowthDetailKey(event, 'all')">
|
||||
<article class="growth-command-card is-sales is-clickable" id="commandSalesCard" role="button" tabindex="0" onclick="showGrowthDetail('all')" onkeydown="handleGrowthDetailKey(event, 'all')">
|
||||
<div class="growth-command-kpi-label">
|
||||
<span>PChome 近 7 天業績</span>
|
||||
<i class="fas fa-chart-line"></i>
|
||||
@@ -2873,14 +2894,14 @@
|
||||
<span class="growth-command-kpi-note" id="commandSalesFreshness">確認資料新鮮度</span>
|
||||
<span class="growth-command-kpi-note" id="commandSalesDelta">等待資料</span>
|
||||
</article>
|
||||
<article class="growth-command-card is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('ready')" onkeydown="handleGrowthDetailKey(event, 'ready')">
|
||||
<article class="growth-command-card is-clickable" id="commandCoverageCard" role="button" tabindex="0" onclick="showGrowthDetail('ready')" onkeydown="handleGrowthDetailKey(event, 'ready')">
|
||||
<div class="growth-command-kpi-label">
|
||||
<span>比價可用率</span>
|
||||
<span>同款比價證據 · TOP 50</span>
|
||||
<i class="fas fa-link"></i>
|
||||
</div>
|
||||
<strong class="growth-command-kpi-value" id="commandMatchRate">—%</strong>
|
||||
<strong class="growth-command-kpi-value" id="commandMatchRate">— / —</strong>
|
||||
<div class="growth-command-progress"><span id="commandMatchRateBar"></span></div>
|
||||
<span class="growth-command-kpi-note" id="commandMappedRatio">等待外部平台對應</span>
|
||||
<span class="growth-command-kpi-note is-essential" id="commandMappedRatio">等待分子與分母</span>
|
||||
</article>
|
||||
<article class="growth-command-card is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('all')" onkeydown="handleGrowthDetailKey(event, 'all')">
|
||||
<div class="growth-command-kpi-label">
|
||||
@@ -2892,19 +2913,19 @@
|
||||
</article>
|
||||
<article class="growth-command-card is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('needs')" onkeydown="handleGrowthDetailKey(event, 'needs')">
|
||||
<div class="growth-command-kpi-label">
|
||||
<span>待補比價</span>
|
||||
<span>尚未找到同款</span>
|
||||
<i class="fas fa-magnifying-glass-chart"></i>
|
||||
</div>
|
||||
<strong class="growth-command-kpi-value" id="commandNeedMapping">—</strong>
|
||||
<span class="growth-command-kpi-note"><span id="commandReviewCount">—</span> 筆候選待確認</span>
|
||||
<span class="growth-command-kpi-note is-essential"><span id="commandReviewCount">—</span> 筆候選 / 來源待驗證</span>
|
||||
</article>
|
||||
<article class="growth-command-card is-clickable" role="button" tabindex="0" onclick="showGrowthCategoryDetail('')" onkeydown="handleGrowthDetailKey(event, 'all')">
|
||||
<article class="growth-command-card is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('source')" onkeydown="handleGrowthDetailKey(event, 'source')">
|
||||
<div class="growth-command-kpi-label">
|
||||
<span>最大業績分類</span>
|
||||
<span>正式資料平台</span>
|
||||
<i class="fas fa-layer-group"></i>
|
||||
</div>
|
||||
<strong class="growth-command-kpi-value" id="commandTopCategory">—</strong>
|
||||
<span class="growth-command-kpi-note" id="commandTopCategorySales">等待分類業績</span>
|
||||
<strong class="growth-command-kpi-value" id="commandPlatformCoverage">— / —</strong>
|
||||
<span class="growth-command-kpi-note is-essential" id="commandPlatformCoverageNote">僅計正式 runtime 資料</span>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -2953,9 +2974,9 @@
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="growth-command-panel" aria-label="PChome 與外部平台價格狀態">
|
||||
<section class="growth-command-panel" aria-label="TOP 50 同款比價覆蓋狀態">
|
||||
<div class="growth-command-panel-head">
|
||||
<h2 class="growth-command-panel-title">PChome 與外部平台</h2>
|
||||
<h2 class="growth-command-panel-title">比價覆蓋 · TOP 50</h2>
|
||||
<button class="growth-command-panel-link" type="button" onclick="showGrowthDetail('risk')">看價格壓力</button>
|
||||
</div>
|
||||
<div class="growth-command-donut-wrap">
|
||||
@@ -2965,18 +2986,18 @@
|
||||
<div class="growth-command-legend">
|
||||
<div class="growth-command-legend-row">
|
||||
<span class="growth-command-dot is-ready"></span>
|
||||
<span>PChome 價格優勢</span>
|
||||
<strong id="commandPriceAdvantage">—</strong>
|
||||
<span>具正式比價證據</span>
|
||||
<strong id="commandComparisonReady">—</strong>
|
||||
</div>
|
||||
<div class="growth-command-legend-row">
|
||||
<span class="growth-command-dot is-review"></span>
|
||||
<span>外部低價壓力</span>
|
||||
<strong id="commandPricePressure">—</strong>
|
||||
<span>候選 / 來源驗證</span>
|
||||
<strong id="commandCandidateValidation">—</strong>
|
||||
</div>
|
||||
<div class="growth-command-legend-row">
|
||||
<span class="growth-command-dot"></span>
|
||||
<span>待確認 / 待補</span>
|
||||
<strong id="commandPricePending">—</strong>
|
||||
<span>尚未找到同款</span>
|
||||
<strong id="commandUnmatched">—</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2994,12 +3015,12 @@
|
||||
<em>已有外部價格可判斷</em>
|
||||
</div>
|
||||
<div class="growth-command-mini-card">
|
||||
<span>待確認</span>
|
||||
<span>候選 / 來源驗證</span>
|
||||
<strong id="commandStatusReview">—</strong>
|
||||
<em>先確認同款</em>
|
||||
</div>
|
||||
<div class="growth-command-mini-card">
|
||||
<span>待補比價</span>
|
||||
<span>尚未找到同款</span>
|
||||
<strong id="commandStatusNeeds">—</strong>
|
||||
<em>需要補外部平台同款</em>
|
||||
</div>
|
||||
@@ -3896,26 +3917,52 @@ async function readJsonResponse(response) {
|
||||
throw new Error(`伺服器回傳非預期內容,狀態碼 ${response.status || '未知'}。`);
|
||||
}
|
||||
|
||||
function growthComparisonState(stats = {}) {
|
||||
const candidateCount = Number(stats.candidate_count || 0);
|
||||
const comparisonReadyCount = Number(
|
||||
stats.comparison_ready_count ?? stats.mapped_count ?? 0
|
||||
);
|
||||
const candidateValidationCount = Number(
|
||||
stats.candidate_validation_count
|
||||
?? stats.current_review_candidate_count
|
||||
?? stats.review_candidate_count
|
||||
?? 0
|
||||
);
|
||||
const unmatchedCount = Number(
|
||||
stats.unmatched_count
|
||||
?? Math.max(0, candidateCount - comparisonReadyCount - candidateValidationCount)
|
||||
);
|
||||
return {
|
||||
candidateCount,
|
||||
comparisonReadyCount,
|
||||
candidateValidationCount,
|
||||
unmatchedCount,
|
||||
unresolvedCount: Math.max(0, candidateCount - comparisonReadyCount),
|
||||
};
|
||||
}
|
||||
|
||||
function renderOpsCommandDashboard(stats, scope = {}) {
|
||||
latestGrowthStats = 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 readyRate = candidateCount ? Math.round((mappedCount / candidateCount) * 100) : 0;
|
||||
const state = growthComparisonState(stats);
|
||||
const readyRate = state.candidateCount
|
||||
? Math.round((state.comparisonReadyCount / state.candidateCount) * 100)
|
||||
: 0;
|
||||
const freshness = stats.sales_freshness || {};
|
||||
|
||||
document.getElementById('opsReadyRate').textContent = `${readyRate}%`;
|
||||
document.getElementById('opsFunnelCandidateText').textContent = `${formatCount(candidateCount)} 件`;
|
||||
document.getElementById('opsFunnelMappedText').textContent = `${formatCount(mappedCount)} 件`;
|
||||
document.getElementById('opsFunnelNeedsText').textContent = `${formatCount(needsMapping)} 件`;
|
||||
document.getElementById('opsDashboardStatus').textContent = candidateCount
|
||||
? `資料就緒率 ${readyRate}%`
|
||||
document.getElementById('opsFunnelCandidateText').textContent = `${formatCount(state.candidateCount)} 件`;
|
||||
document.getElementById('opsFunnelMappedText').textContent = `${formatCount(state.comparisonReadyCount)} 件`;
|
||||
document.getElementById('opsFunnelNeedsText').textContent = `${formatCount(state.unresolvedCount)} 件`;
|
||||
document.getElementById('opsDashboardStatus').textContent = freshness.decision_ready === false
|
||||
? '決策暫停 · 業績資料過期'
|
||||
: state.candidateCount
|
||||
? `比價證據 ${readyRate}%`
|
||||
: '等待 PChome 業績資料';
|
||||
renderNextAction(stats);
|
||||
|
||||
setWidth('opsFunnelCandidateBar', candidateCount ? 100 : 0);
|
||||
setWidth('opsFunnelMappedBar', candidateCount ? (mappedCount / candidateCount) * 100 : 0);
|
||||
setWidth('opsFunnelNeedsBar', candidateCount ? (needsMapping / candidateCount) * 100 : 0);
|
||||
setWidth('opsFunnelCandidateBar', state.candidateCount ? 100 : 0);
|
||||
setWidth('opsFunnelMappedBar', state.candidateCount ? (state.comparisonReadyCount / state.candidateCount) * 100 : 0);
|
||||
setWidth('opsFunnelNeedsBar', state.candidateCount ? (state.unresolvedCount / state.candidateCount) * 100 : 0);
|
||||
renderOpsSourceBars(stats.external_platform_counts || {}, scope);
|
||||
renderGrowthExecutiveSummary(stats);
|
||||
renderGrowthCommandCenter(stats, latestGrowthRows);
|
||||
@@ -3937,10 +3984,14 @@ function setCommandTask(title, copy, label, action) {
|
||||
|
||||
function renderGrowthCommandCenter(stats = {}, rows = []) {
|
||||
const safeRows = Array.isArray(rows) ? rows : [];
|
||||
const candidateCount = Number(stats.candidate_count || safeRows.length || 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 state = growthComparisonState({
|
||||
...stats,
|
||||
candidate_count: Number(stats.candidate_count || safeRows.length || 0),
|
||||
});
|
||||
const candidateCount = state.candidateCount;
|
||||
const mappedCount = state.comparisonReadyCount;
|
||||
const candidateValidationCount = state.candidateValidationCount;
|
||||
const unmatchedCount = state.unmatchedCount;
|
||||
const decliningCount = Number(stats.declining_product_count || 0);
|
||||
const activeCount = Number(stats.active_product_count || 0);
|
||||
const sales7d = Number(stats.overall_sales_7d || stats.total_sales_7d || 0);
|
||||
@@ -3948,13 +3999,14 @@ function renderGrowthCommandCenter(stats = {}, rows = []) {
|
||||
const salesDeltaPct = Number(stats.overall_sales_delta_pct || 0);
|
||||
const matchRate = Number(stats.mapping_rate || (candidateCount ? (mappedCount / candidateCount) * 100 : 0));
|
||||
const latestSalesDate = String(stats.overall_latest_sales_date || stats.latest_sales_date || '').slice(0, 10);
|
||||
const actionCounts = stats.action_code_counts || {};
|
||||
const pressureCount = Number(actionCounts.review_price_or_promo || 0);
|
||||
const advantageCount = Number(actionCounts.amplify_price_advantage || 0);
|
||||
const pendingCount = Math.max(0, needsMapping + reviewCandidateCount);
|
||||
const readyShare = candidateCount ? (advantageCount / candidateCount) * 100 : 0;
|
||||
const pressureShare = candidateCount ? (pressureCount / candidateCount) * 100 : 0;
|
||||
const readyShare = candidateCount ? (mappedCount / candidateCount) * 100 : 0;
|
||||
const validationShare = candidateCount ? (candidateValidationCount / candidateCount) * 100 : 0;
|
||||
const freshness = stats.sales_freshness || {};
|
||||
const activeCatalogCount = Number(stats.active_catalog_product_count || stats.active_product_count || 0);
|
||||
const activeMappedCount = Number(stats.active_mapped_product_count || 0);
|
||||
const platformCount = Number(stats.registered_external_source_count || 0);
|
||||
const runtimePlatformCount = Number(stats.platforms_with_runtime_data || 0);
|
||||
const platformRate = Number(stats.platform_source_coverage_rate || 0);
|
||||
|
||||
setText('commandSales7d', sales7d ? formatMoney(sales7d) : '—');
|
||||
setText('commandSalesDelta', prevSales7d
|
||||
@@ -3963,21 +4015,26 @@ function renderGrowthCommandCenter(stats = {}, rows = []) {
|
||||
setText('commandSalesFreshness', freshness.age_days === null || freshness.age_days === undefined
|
||||
? (freshness.label || '尚無資料')
|
||||
: `${freshness.label || '資料狀態'} · ${formatCount(freshness.age_days)} 天`);
|
||||
setText('commandMatchRate', `${Math.round(matchRate * 10) / 10}%`);
|
||||
setText('commandMappedRatio', `${formatCount(mappedCount)} / ${formatCount(candidateCount)}`);
|
||||
setText('commandMatchRate', `${formatCount(mappedCount)} / ${formatCount(candidateCount)}`);
|
||||
setText(
|
||||
'commandMappedRatio',
|
||||
`${Math.round(matchRate * 10) / 10}% · 活躍商品 ${formatCount(activeMappedCount)} / ${formatCount(activeCatalogCount)}`
|
||||
);
|
||||
setText('commandDecliningProducts', formatCount(decliningCount));
|
||||
setText('commandActiveProducts', `${formatCount(activeCount)} 件`);
|
||||
setText('commandNeedMapping', formatCount(needsMapping));
|
||||
setText('commandReviewCount', formatCount(reviewCandidateCount));
|
||||
setText('commandNeedMapping', formatCount(unmatchedCount));
|
||||
setText('commandReviewCount', formatCount(candidateValidationCount));
|
||||
setText('commandTopCategory', stats.top_category || '—');
|
||||
setText('commandTopCategorySales', stats.top_category_sales_7d ? formatMoney(stats.top_category_sales_7d) : '等待分類業績');
|
||||
setText('commandPlatformCoverage', `${formatCount(runtimePlatformCount)} / ${formatCount(platformCount)}`);
|
||||
setText('commandPlatformCoverageNote', `${platformRate.toFixed(1)}% · 僅計正式 runtime 資料`);
|
||||
setText('commandStatusReady', formatCount(mappedCount));
|
||||
setText('commandStatusReview', formatCount(reviewCandidateCount));
|
||||
setText('commandStatusNeeds', formatCount(needsMapping));
|
||||
setText('commandStatusReview', formatCount(candidateValidationCount));
|
||||
setText('commandStatusNeeds', formatCount(unmatchedCount));
|
||||
setText('commandLatestSalesDate', latestSalesDate || '—');
|
||||
setText('commandPriceAdvantage', `${formatCount(advantageCount)} 件`);
|
||||
setText('commandPricePressure', `${formatCount(pressureCount)} 件`);
|
||||
setText('commandPricePending', `${formatCount(pendingCount)} 件`);
|
||||
setText('commandComparisonReady', `${formatCount(mappedCount)} 件`);
|
||||
setText('commandCandidateValidation', `${formatCount(candidateValidationCount)} 件`);
|
||||
setText('commandUnmatched', `${formatCount(unmatchedCount)} 件`);
|
||||
setText('commandDonutText', `${Math.round(matchRate)}%`);
|
||||
|
||||
const status = document.getElementById('growthCommandStatus');
|
||||
@@ -3987,13 +4044,16 @@ function renderGrowthCommandCenter(stats = {}, rows = []) {
|
||||
: `${freshness.label || '待資料'}${latestSalesDate ? ` · ${latestSalesDate}` : ''}`;
|
||||
const icon = freshness.decision_ready ? 'fa-circle-check' : 'fa-triangle-exclamation';
|
||||
status.innerHTML = `<i class="fas ${icon}"></i><span>${escapeHtml(text)}</span>`;
|
||||
status.classList.toggle('is-degraded', freshness.decision_ready === false);
|
||||
}
|
||||
document.getElementById('commandSalesCard')?.classList.toggle('is-degraded', freshness.decision_ready === false);
|
||||
document.getElementById('commandCoverageCard')?.classList.toggle('is-degraded', freshness.decision_ready === false);
|
||||
const rateBar = document.getElementById('commandMatchRateBar');
|
||||
if (rateBar) rateBar.style.width = `${clampPercent(matchRate)}%`;
|
||||
const donut = document.getElementById('commandPriceDonut');
|
||||
if (donut) {
|
||||
donut.style.setProperty('--ready', clampPercent(readyShare));
|
||||
donut.style.setProperty('--review', clampPercent(pressureShare));
|
||||
donut.style.setProperty('--review', clampPercent(validationShare));
|
||||
}
|
||||
|
||||
if (freshness.decision_ready === false) {
|
||||
@@ -4012,16 +4072,16 @@ function renderGrowthCommandCenter(stats = {}, rows = []) {
|
||||
'業績',
|
||||
() => { window.location.href = '/daily_sales'; }
|
||||
);
|
||||
} else if (reviewCandidateCount > 0) {
|
||||
} else if (candidateValidationCount > 0) {
|
||||
setCommandTask(
|
||||
`確認候選 ${formatCount(reviewCandidateCount)}`,
|
||||
`驗證候選 ${formatCount(candidateValidationCount)}`,
|
||||
'待確認',
|
||||
'確認候選',
|
||||
() => scrollToPanel('growthReviewPanel')
|
||||
);
|
||||
} else if (needsMapping > mappedCount) {
|
||||
} else if (unmatchedCount > mappedCount) {
|
||||
setCommandTask(
|
||||
`補比價 ${formatCount(needsMapping)}`,
|
||||
`補比價 ${formatCount(unmatchedCount)}`,
|
||||
'待補',
|
||||
'補比價',
|
||||
() => backfillPchomeMatches()
|
||||
@@ -4078,10 +4138,11 @@ function renderNextAction(stats = {}) {
|
||||
const reason = document.getElementById('nextActionReason');
|
||||
const button = document.getElementById('nextActionButton');
|
||||
if (!title || !reason || !button) return;
|
||||
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 state = growthComparisonState(stats);
|
||||
const candidateCount = state.candidateCount;
|
||||
const mappedCount = state.comparisonReadyCount;
|
||||
const needsMapping = state.unmatchedCount;
|
||||
const reviewCandidateCount = state.candidateValidationCount;
|
||||
const freshness = stats.sales_freshness || {};
|
||||
|
||||
if (freshness.decision_ready === false) {
|
||||
@@ -4147,10 +4208,11 @@ function renderNextAction(stats = {}) {
|
||||
}
|
||||
|
||||
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 state = growthComparisonState(stats);
|
||||
const candidateCount = state.candidateCount;
|
||||
const mappedCount = state.comparisonReadyCount;
|
||||
const needsMapping = state.unmatchedCount;
|
||||
const reviewCandidateCount = state.candidateValidationCount;
|
||||
const latestSalesDate = String(stats.latest_sales_date || '').slice(0, 10);
|
||||
const freshness = stats.sales_freshness || {};
|
||||
|
||||
@@ -4311,10 +4373,11 @@ function renderGrowthActionHint(stats) {
|
||||
const hint = document.getElementById('growthActionHint');
|
||||
if (!hint) return;
|
||||
|
||||
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 state = growthComparisonState(stats);
|
||||
const candidateCount = state.candidateCount;
|
||||
const mappedCount = state.comparisonReadyCount;
|
||||
const needsMapping = state.unmatchedCount;
|
||||
const reviewCandidateCount = state.candidateValidationCount;
|
||||
const freshness = stats.sales_freshness || {};
|
||||
|
||||
if (freshness.decision_ready === false) {
|
||||
@@ -4410,9 +4473,10 @@ function renderGrowthSourceReadiness(sources) {
|
||||
|
||||
function resolveGrowthDetailKind(kind) {
|
||||
if (kind !== 'task') return kind || 'all';
|
||||
const reviewCandidateCount = Number(latestGrowthStats.review_candidate_count || 0);
|
||||
const needsMapping = Number(latestGrowthStats.needs_mapping_count || 0);
|
||||
const mappedCount = Number(latestGrowthStats.mapped_count || 0);
|
||||
const state = growthComparisonState(latestGrowthStats);
|
||||
const reviewCandidateCount = state.candidateValidationCount;
|
||||
const needsMapping = state.unmatchedCount;
|
||||
const mappedCount = state.comparisonReadyCount;
|
||||
if (reviewCandidateCount > 0) return 'review';
|
||||
if (needsMapping > mappedCount) return 'needs';
|
||||
return 'risk';
|
||||
|
||||
Reference in New Issue
Block a user