Fix PPT auto generation and analytics fallbacks
Some checks failed
CD Pipeline / deploy (push) Failing after 26s

This commit is contained in:
OoO
2026-05-18 11:52:31 +08:00
parent 3284b1cf82
commit c420d48263
26 changed files with 1559 additions and 258 deletions

View File

@@ -593,6 +593,83 @@
}
};
function initPptAutoGeneration() {
const panel = document.querySelector('[data-ppt-auto-generation]');
document.querySelectorAll('[data-ppt-aider-heal]').forEach(button => {
if (button.dataset.bound === '1') return;
button.dataset.bound = '1';
button.addEventListener('click', () => {
window.triggerAiderHeal(button.dataset.pptFilename || '', button.dataset.pptError || '');
});
});
if (!panel) return;
const button = panel.querySelector('[data-ppt-generate-missing]');
const status = panel.querySelector('[data-ppt-auto-status]');
const reportTypes = (panel.dataset.reportTypes || '')
.split(',')
.map(value => value.trim())
.filter(Boolean);
async function triggerGeneration(isAuto) {
if (button) {
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>補齊中';
}
if (status) {
status.classList.add('is-working');
status.textContent = isAuto
? '偵測到本月定義簡報缺漏,已排入背景補齊。'
: '已排入背景補齊,產出完成後重新整理即可看到最新檔案。';
}
try {
const response = await postJson('/observability/ppt_audit/generate_missing', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ report_types: reportTypes })
});
const data = await response.json();
if (status) {
status.textContent = data.message || (data.status === 'queued'
? '已排入背景補齊,請稍後重新整理。'
: `狀態:${data.status || '已送出'}`);
}
} catch (error) {
console.warn('ppt_auto_generation_failed', error);
if (status) {
status.classList.remove('is-working');
status.textContent = '補齊任務送出失敗,請稍後再試或查看系統日誌。';
}
if (button) button.disabled = false;
}
}
if (button) {
button.addEventListener('click', () => triggerGeneration(false));
}
if (panel.dataset.autoStart === 'true') {
const key = `ppt-auto-generation:${new Date().toISOString().slice(0, 10)}`;
let last = 0;
const now = Date.now();
try {
last = Number(window.localStorage.getItem(key) || 0);
} catch (_error) {
last = 0;
}
if (reportTypes.length && (!last || now - last > 6 * 60 * 60 * 1000)) {
try {
window.localStorage.setItem(key, String(now));
} catch (_error) {
// Ignore storage failures; the server-side generation lock still protects the job.
}
triggerGeneration(true);
}
}
}
function escapeHtml(value) {
if (!value) return '';
return String(value).replace(/[&<>"']/g, char => ({
@@ -661,6 +738,7 @@
renderPromotionReview();
renderQualityTrend();
renderPptAudit();
initPptAutoGeneration();
}
if (document.readyState === 'loading') {

View File

@@ -82,6 +82,88 @@
top10_labels: cd.top10_labels || [],
top10_values: cd.top10_values || []
};
const chartInstances = [];
function rememberChart(chart) {
if (chart) chartInstances.push(chart);
return chart;
}
function stabilizeCharts() {
window.requestAnimationFrame(() => {
chartInstances.forEach(chart => {
if (!chart) return;
if (typeof chart.resize === 'function') chart.resize();
if (typeof chart.update === 'function') chart.update('none');
});
window.dispatchEvent(new Event('resize'));
});
}
function formatShort(value, mode) {
const n = Number(value || 0);
if (mode === 'pct') return `${n.toFixed(1)}%`;
if (mode === 'currency') return `$${Math.round(n).toLocaleString()}`;
return Math.round(n).toLocaleString();
}
function renderHtmlBars(canvasId, labels, values, options = {}) {
const canvas = document.getElementById(canvasId);
const wrap = canvas ? canvas.closest('.chart-container') : null;
if (!wrap || wrap.querySelector('.chart-fallback-bars')) return;
const pairs = (labels || []).map((label, index) => ({
label: String(label || ''),
value: Number((values || [])[index] || 0)
})).filter(item => Number.isFinite(item.value));
const data = options.limit ? pairs.slice(-options.limit) : pairs;
if (!data.length) return;
wrap.classList.add('has-html-chart');
canvas.setAttribute('aria-hidden', 'true');
const max = Math.max(...data.map(item => Math.abs(item.value)), 1);
const chart = document.createElement('div');
chart.className = `chart-fallback-bars ${options.horizontal ? 'is-horizontal' : 'is-vertical'}`;
data.forEach(item => {
const bar = document.createElement('div');
bar.className = `chart-fallback-bar ${item.value < 0 ? 'is-negative' : ''}`;
const pct = Math.max(4, Math.round(Math.abs(item.value) / max * 100));
if (options.horizontal) {
bar.style.setProperty('--bar-w', `${pct}%`);
} else {
bar.style.setProperty('--bar-h', `${pct}%`);
}
const label = document.createElement('span');
label.className = 'chart-fallback-label';
label.textContent = item.label.length > 10 ? `${item.label.slice(0, 10)}...` : item.label;
const value = document.createElement('strong');
value.className = 'chart-fallback-value';
value.textContent = formatShort(item.value, options.mode);
bar.append(label, value);
chart.appendChild(bar);
});
wrap.appendChild(chart);
}
function renderHtmlChartFallbacks() {
renderHtmlBars('trendChart', safe.labels, safe.revenue, { mode: 'currency', limit: 14 });
renderHtmlBars('dodChart', safe.labels, safe.dod_revenue, { mode: 'pct', limit: 14 });
renderHtmlBars('wowChart', safe.labels, safe.wow_revenue, { mode: 'pct', limit: 14 });
renderHtmlBars('top10Chart', safe.top10_labels, safe.top10_values, {
mode: 'currency',
horizontal: true
});
const mk = dailySalesData.marketing || {};
if (mk.discount) renderHtmlBars('discountChart', mk.discount.labels, mk.discount.values, {
mode: 'currency',
horizontal: true
});
if (mk.coupon) renderHtmlBars('couponChart', mk.coupon.labels, mk.coupon.values, {
mode: 'currency',
horizontal: true
});
}
// -- Helpers ----------------------------------------------------------
function makeLineDataset(label, data, color, yAxisID) {
@@ -113,7 +195,7 @@
const el = document.getElementById('trendChart');
if (!el || !safe.labels.length) return;
new Chart(el, {
rememberChart(new Chart(el, {
type: 'line',
data: {
labels: safe.labels,
@@ -145,7 +227,7 @@
}
}
}
});
}));
}
// -- Chart 2: DoD (multi-line %) --------------------------------------
@@ -153,7 +235,7 @@
const el = document.getElementById('dodChart');
if (!el || !safe.labels.length) return;
new Chart(el, {
rememberChart(new Chart(el, {
type: 'line',
data: {
labels: safe.labels,
@@ -180,7 +262,7 @@
y: { beginAtZero: false, title: { display: true, text: 'DoD 成長率 (%)' } }
}
}
});
}));
}
// -- Chart 3: WoW (multi-line %, 前 7 天淡灰) -------------------------
@@ -188,7 +270,7 @@
const el = document.getElementById('wowChart');
if (!el || !safe.labels.length) return;
new Chart(el, {
rememberChart(new Chart(el, {
type: 'line',
data: {
labels: safe.labels,
@@ -222,7 +304,7 @@
y: { beginAtZero: false, title: { display: true, text: 'WoW 成長率 (%)' } }
}
}
});
}));
}
// -- Chart 4: Top 10 (橫向 bar) ---------------------------------------
@@ -230,7 +312,7 @@
const el = document.getElementById('top10Chart');
if (!el || !safe.top10_labels.length) return;
new Chart(el, {
rememberChart(new Chart(el, {
type: 'bar',
data: {
labels: safe.top10_labels,
@@ -249,7 +331,7 @@
plugins: { legend: { display: false } },
scales: { x: { beginAtZero: true } }
}
});
}));
}
// -- Marketing charts ----------------------------------------------
@@ -261,7 +343,7 @@
return rgba(color, Math.max(a, 0.25));
});
new Chart(el.getContext('2d'), {
rememberChart(new Chart(el.getContext('2d'), {
type: 'bar',
data: {
labels: marketing.labels,
@@ -301,7 +383,7 @@
},
onClick: () => exportMarketingData()
}
});
}));
}
// -- DataTables init ------------------------------------------------
@@ -430,12 +512,26 @@
const mk = dailySalesData.marketing || {};
if (mk.discount) renderMarketingBar('discountChart', mk.discount, palette.caramel);
if (mk.coupon) renderMarketingBar('couponChart', mk.coupon, palette.olive);
stabilizeCharts();
renderHtmlChartFallbacks();
}
function bootCharts() {
document.documentElement.dataset.dailyCharts = 'loading';
loadChartJs()
.then(renderAllCharts)
.catch(error => console.error('[daily_sales] Chart.js 載入失敗:', error));
.then(() => {
renderAllCharts();
document.documentElement.dataset.dailyCharts = 'ready';
})
.catch(error => {
document.documentElement.dataset.dailyCharts = 'error';
document.documentElement.dataset.dailyChartsError = error && error.message ? error.message : String(error);
console.error('[daily_sales] Chart.js 載入失敗:', error);
});
}
function scheduleChartBoot() {
window.setTimeout(bootCharts, 0);
}
function observeCharts() {
@@ -458,6 +554,6 @@
document.addEventListener('DOMContentLoaded', function () {
initDailySalesActions();
initDataTable();
observeCharts();
scheduleChartBoot();
});
})();

View File

@@ -33,6 +33,66 @@
rust: token('--momo-danger-text', '#7a3210'),
rustSoft: token('--momo-danger-bg', '#efd3c4')
};
const chartInstances = [];
function rememberChart(chart) {
if (chart) chartInstances.push(chart);
return chart;
}
function stabilizeCharts() {
window.requestAnimationFrame(() => {
chartInstances.forEach(chart => {
if (!chart) return;
if (typeof chart.resize === 'function') chart.resize();
if (typeof chart.update === 'function') chart.update('none');
});
window.dispatchEvent(new Event('resize'));
});
}
function formatShort(value, mode) {
const n = Number(value || 0);
if (mode === 'pct') return `${n.toFixed(1)}%`;
if (mode === 'currency') return `$${Math.round(n).toLocaleString()}`;
return Math.round(n).toLocaleString();
}
function renderHtmlBars(canvasId, labels, values, options = {}) {
const canvas = document.getElementById(canvasId);
const wrap = canvas ? canvas.closest('.ga-chart-card__body') : null;
if (!wrap || wrap.querySelector('.ga-chart-fallback')) return;
const pairs = (labels || []).map((label, index) => ({
label: String(label || ''),
value: Number((values || [])[index] || 0)
})).filter(item => Number.isFinite(item.value));
if (!pairs.length) return;
const max = Math.max(...pairs.map(item => Math.abs(item.value)), 1);
wrap.classList.add('has-html-chart');
canvas.setAttribute('aria-hidden', 'true');
const chart = document.createElement('div');
chart.className = 'ga-chart-fallback';
pairs.forEach(item => {
const bar = document.createElement('div');
bar.className = `ga-chart-fallback__bar ${item.value < 0 ? 'is-negative' : ''}`;
bar.style.setProperty('--bar-h', `${Math.max(4, Math.round(Math.abs(item.value) / max * 100))}%`);
const label = document.createElement('span');
label.textContent = item.label;
const value = document.createElement('strong');
value.textContent = formatShort(item.value, options.mode);
bar.append(label, value);
chart.appendChild(bar);
});
wrap.appendChild(chart);
}
function renderHtmlChartFallbacks() {
renderHtmlBars('revenueChart', data.labels, data.revenue, { mode: 'currency' });
renderHtmlBars('momChart', data.labels, data.mom, { mode: 'pct' });
renderHtmlBars('aovChart', data.labels, data.aov, { mode: 'currency' });
renderHtmlBars('marginChart', data.labels, data.margin_rate, { mode: 'pct' });
}
function loadChartJs() {
if (window.EwoooCChartTheme && window.EwoooCChartTheme.loadChartJs) {
@@ -56,7 +116,7 @@
const marginEl = document.getElementById('marginChart');
if (!revenueEl || !momEl || !aovEl || !marginEl) return;
new Chart(revenueEl, {
rememberChart(new Chart(revenueEl, {
type: 'bar',
data: {
labels: data.labels,
@@ -91,9 +151,9 @@
}
}
}
});
}));
new Chart(momEl, {
rememberChart(new Chart(momEl, {
type: 'bar',
data: {
labels: data.labels,
@@ -108,9 +168,9 @@
maintainAspectRatio: false,
plugins: { legend: { display: false } }
}
});
}));
new Chart(aovEl, {
rememberChart(new Chart(aovEl, {
type: 'line',
data: {
labels: data.labels,
@@ -128,9 +188,9 @@
maintainAspectRatio: false,
scales: { y: { beginAtZero: true } }
}
});
}));
new Chart(marginEl, {
rememberChart(new Chart(marginEl, {
type: 'line',
data: {
labels: data.labels,
@@ -148,13 +208,27 @@
maintainAspectRatio: false,
scales: { y: { beginAtZero: true } }
}
});
}));
stabilizeCharts();
renderHtmlChartFallbacks();
}
function bootCharts() {
document.documentElement.dataset.growthCharts = 'loading';
loadChartJs()
.then(renderCharts)
.catch(error => console.error('[growth_analysis] Chart.js 載入失敗:', error));
.then(() => {
renderCharts();
document.documentElement.dataset.growthCharts = 'ready';
})
.catch(error => {
document.documentElement.dataset.growthCharts = 'error';
document.documentElement.dataset.growthChartsError = error && error.message ? error.message : String(error);
console.error('[growth_analysis] Chart.js 載入失敗:', error);
});
}
function scheduleChartBoot() {
window.setTimeout(bootCharts, 0);
}
function observeCharts() {
@@ -175,8 +249,8 @@
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', observeCharts, { once: true });
document.addEventListener('DOMContentLoaded', scheduleChartBoot, { once: true });
} else {
observeCharts();
scheduleChartBoot();
}
})();