Some checks failed
CD Pipeline / deploy (push) Failing after 59s
- 建立 Gitea Actions CD pipeline (.gitea/workflows/cd.yaml) - 部署模式: rsync Python 檔案至 188 → docker restart (volume mount) - Dockerfile/requirements 變動時自動重建 Docker image - 部署通知: Telegram (開始/成功/失敗) - 健康檢查: https://mo.wooo.work/health (最多 5 次重試) - 同步最新 CLAUDE.md / ADR-008 / memory (2026-04-19) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
250 lines
10 KiB
HTML
250 lines
10 KiB
HTML
<!-- cspell:ignore MOMO -->
|
|
<!DOCTYPE html>
|
|
<html lang="zh-TW">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>營運成長報表 - MOMO 監控系統</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
|
|
<style>
|
|
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background-color: #f4f6f9; }
|
|
.navbar { box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
|
|
.card { border: none; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.03); margin-bottom: 1.5rem; transition: all 0.3s ease; background: #fff; }
|
|
.card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.08); }
|
|
.card-header { background-color: transparent; border-bottom: 1px solid rgba(0,0,0,0.05); font-weight: 700; color: #2c3e50; padding: 1.25rem; }
|
|
|
|
.kpi-card { position: relative; overflow: hidden; border: none; }
|
|
.kpi-card .icon-bg { position: absolute; right: -15px; bottom: -15px; font-size: 6rem; opacity: 0.15; transform: rotate(-15deg); pointer-events: none; }
|
|
.kpi-value { font-size: 2rem; font-weight: 800; letter-spacing: -0.5px; margin-bottom: 0.2rem; }
|
|
.kpi-label { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.9; }
|
|
|
|
.trend-up { color: #2ecc71; }
|
|
.trend-down { color: #e74c3c; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-body-tertiary">
|
|
{% include 'components/_navbar.html' %}
|
|
|
|
<div class="container-fluid px-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4 mt-4">
|
|
<h4 class="mb-0 fw-bold text-dark"><i class="fas fa-rocket me-2 text-success"></i>營運成長策略報表</h4>
|
|
<span class="text-muted small">數據更新至: {{ chart_data.labels[-1] if chart_data.labels else '-' }}</span>
|
|
</div>
|
|
|
|
<!-- KPI Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-4">
|
|
<div class="card kpi-card bg-primary text-white h-100 shadow-sm">
|
|
<div class="card-body p-4">
|
|
<div class="kpi-label text-white-50">YTD 本年度累計業績 ({{ kpi.current_year }})</div>
|
|
<div class="kpi-value">${{ "{:,.0f}".format(kpi.ytd_revenue) }}</div>
|
|
<div class="mt-2">
|
|
<span class="badge bg-white text-primary me-2">YoY Growth</span>
|
|
<span class="fw-bold {{ 'text-white' if kpi.ytd_growth >= 0 else 'text-warning' }}">
|
|
<i class="fas fa-{{ 'arrow-up' if kpi.ytd_growth >= 0 else 'arrow-down' }} me-1"></i>
|
|
{{ "{:+.1f}%".format(kpi.ytd_growth) }}
|
|
</span>
|
|
<span class="small text-white-50 ms-1">vs 去年同期</span>
|
|
</div>
|
|
<i class="fas fa-chart-line icon-bg"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card kpi-card bg-success text-white h-100 shadow-sm">
|
|
<div class="card-body p-4">
|
|
<div class="kpi-label text-white-50">近30天平均客單價 (AOV)</div>
|
|
<div class="kpi-value">${{ "{:,.0f}".format(kpi.recent_aov) }}</div>
|
|
<div class="mt-2 small text-white-50">
|
|
真實訂單基礎 (Unique Order ID)
|
|
</div>
|
|
<i class="fas fa-shopping-cart icon-bg"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card kpi-card bg-info text-white h-100 shadow-sm">
|
|
<div class="card-body p-4">
|
|
<div class="kpi-label text-white-50">總訂單數 (Total Orders)</div>
|
|
<div class="kpi-value">{{ "{:,.0f}".format(kpi.total_orders) }}</div>
|
|
<div class="mt-2 small text-white-50">
|
|
全時期累計
|
|
</div>
|
|
<i class="fas fa-receipt icon-bg"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charts Row 1: Revenue & Growth -->
|
|
<div class="row mb-4">
|
|
<div class="col-lg-8">
|
|
<div class="card h-100">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<span><i class="fas fa-chart-bar me-2"></i>月營收與年增率 (Revenue & YoY)</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div style="height: 350px;">
|
|
<canvas id="revenueChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-4">
|
|
<div class="card h-100">
|
|
<div class="card-header">
|
|
<i class="fas fa-percentage me-2"></i>月增率分析 (MoM)
|
|
</div>
|
|
<div class="card-body">
|
|
<div style="height: 350px;">
|
|
<canvas id="momChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charts Row 2: AOV & Profit -->
|
|
<div class="row mb-4">
|
|
<div class="col-lg-6">
|
|
<div class="card h-100">
|
|
<div class="card-header">
|
|
<i class="fas fa-wallet me-2"></i>客單價趨勢 (AOV Trend)
|
|
</div>
|
|
<div class="card-body">
|
|
<div style="height: 300px;">
|
|
<canvas id="aovChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-6">
|
|
<div class="card h-100">
|
|
<div class="card-header">
|
|
<i class="fas fa-hand-holding-usd me-2"></i>獲利能力分析 (Gross Margin %)
|
|
</div>
|
|
<div class="card-body">
|
|
<div style="height: 300px;">
|
|
<canvas id="marginChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
|
|
<!-- Data Injection -->
|
|
<script id="chart-data" type="application/json">
|
|
{{ chart_data | tojson }}
|
|
</script>
|
|
|
|
<script>
|
|
const data = JSON.parse(document.getElementById('chart-data').textContent);
|
|
|
|
// 1. Revenue & YoY Chart (Mixed)
|
|
new Chart(document.getElementById('revenueChart'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.labels,
|
|
datasets: [
|
|
{
|
|
label: '月營收 ($)',
|
|
data: data.revenue,
|
|
backgroundColor: 'rgba(54, 162, 235, 0.6)',
|
|
order: 2
|
|
},
|
|
{
|
|
label: 'YoY 年增率 (%)',
|
|
data: data.yoy,
|
|
type: 'line',
|
|
borderColor: '#ff6384',
|
|
borderWidth: 2,
|
|
yAxisID: 'y1',
|
|
order: 1,
|
|
tension: 0.3
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: { beginAtZero: true, title: {display: true, text: '金額 ($)'} },
|
|
y1: {
|
|
position: 'right',
|
|
grid: { drawOnChartArea: false },
|
|
title: {display: true, text: '成長率 (%)'}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 2. MoM Chart
|
|
new Chart(document.getElementById('momChart'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.labels,
|
|
datasets: [{
|
|
label: 'MoM 月增率 (%)',
|
|
data: data.mom,
|
|
backgroundColor: (ctx) => {
|
|
const val = ctx.raw;
|
|
return val >= 0 ? 'rgba(75, 192, 192, 0.6)' : 'rgba(255, 99, 132, 0.6)';
|
|
}
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: { legend: { display: false } }
|
|
}
|
|
});
|
|
|
|
// 3. AOV Chart
|
|
new Chart(document.getElementById('aovChart'), {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.labels,
|
|
datasets: [{
|
|
label: '平均客單價 ($)',
|
|
data: data.aov,
|
|
borderColor: '#36a2eb',
|
|
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: { y: { beginAtZero: true } }
|
|
}
|
|
});
|
|
|
|
// 4. Margin Rate Chart
|
|
new Chart(document.getElementById('marginChart'), {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.labels,
|
|
datasets: [{
|
|
label: '毛利率 (%)',
|
|
data: data.margin_rate,
|
|
borderColor: '#2ecc71',
|
|
backgroundColor: 'rgba(46, 204, 113, 0.1)',
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: { y: { beginAtZero: true } }
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |