Fix PPT auto generation and analytics fallbacks
Some checks failed
CD Pipeline / deploy (push) Failing after 26s
Some checks failed
CD Pipeline / deploy (push) Failing after 26s
This commit is contained in:
@@ -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') {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user