Some checks failed
CD Pipeline / deploy (push) Failing after 2m59s
P0-06: google_drive_service.py — pickle.load() 改 JSON token(消除 RCE 風險) P0-07: bot_api_routes.py:30 — BOT_API_TOKEN 移除硬編碼預設值 clawdbot_momo_2026 P0-08: auto_import_index.html — showAlert innerHTML 改 createTextNode(XSS 修復) P0-09: abc_analysis_detail.html + dashboard.html + daily_sales.html — Jinja2 | e 轉義 P0-10: openclaw_bot_routes.py:2634 — vendor PPT 補 return ppt_path(廠商報告恢復) P0-11: telegram_bot_service.py:177-214 — cmd_start/cmd_help 補 try/except P0-12: app.py:689-712 — 10 個 Blueprint 補齊 register(消滅 404 路由) P0-13: auto_heal_service.py — 實作 _write_heal_log(),AIOps 稽核閉環補完 P0-14: monitoring/prometheus.yml — 取消 alert_rules comment;新增 alert_rules.yml Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1906 lines
70 KiB
HTML
1906 lines
70 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-TW">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>當日業績看板 - WOOO TECH</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>
|
||
<link rel="stylesheet" href="https://cdn.datatables.net/1.11.5/css/dataTables.bootstrap5.min.css">
|
||
<style>
|
||
body {
|
||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
|
||
min-height: 100vh;
|
||
padding-top: 70px;
|
||
}
|
||
|
||
.navbar-dark.bg-primary {
|
||
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
|
||
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||
}
|
||
|
||
.navbar-dark .navbar-brand {
|
||
color: #ffffff !important;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.navbar-dark .navbar-nav .nav-link {
|
||
color: rgba(255, 255, 255, 0.9) !important;
|
||
font-weight: 500;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.navbar-dark .navbar-nav .nav-link:hover {
|
||
color: #ffffff !important;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.navbar-dark .navbar-nav .nav-link.active {
|
||
color: #ffffff !important;
|
||
background: rgba(255, 255, 255, 0.15);
|
||
border-radius: 6px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.navbar-dark .navbar-text {
|
||
color: rgba(255, 255, 255, 0.8) !important;
|
||
}
|
||
|
||
.navbar {
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%) !important;
|
||
}
|
||
|
||
.card {
|
||
border: none;
|
||
border-radius: 16px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||
margin-bottom: 1.5rem;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
background: #fff;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
.card-header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border-bottom: none;
|
||
font-weight: 700;
|
||
color: #fff !important;
|
||
padding: 1rem 1.5rem;
|
||
font-size: 1.05rem;
|
||
}
|
||
|
||
.card-header * {
|
||
color: #fff !important;
|
||
}
|
||
|
||
.card-header i {
|
||
color: rgba(255, 255, 255, 0.95) !important;
|
||
}
|
||
|
||
.kpi-card {
|
||
position: relative;
|
||
overflow: hidden;
|
||
border: none;
|
||
border-radius: 20px !important;
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important;
|
||
}
|
||
|
||
.kpi-card:hover {
|
||
transform: translateY(-6px) scale(1.02);
|
||
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.18) !important;
|
||
}
|
||
|
||
.kpi-card .icon-bg {
|
||
position: absolute;
|
||
right: -15px;
|
||
bottom: -15px;
|
||
font-size: 6rem;
|
||
opacity: 0.2;
|
||
transform: rotate(-15deg);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.kpi-value {
|
||
font-size: 2.2rem;
|
||
font-weight: 800;
|
||
letter-spacing: -0.5px;
|
||
margin-bottom: 0.3rem;
|
||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||
}
|
||
|
||
.kpi-label {
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
opacity: 0.95;
|
||
}
|
||
|
||
.kpi-percent {
|
||
color: white !important;
|
||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6), 0 0 4px rgba(0, 0, 0, 0.8);
|
||
font-weight: 700;
|
||
font-size: 0.95rem;
|
||
padding: 0.2rem 0.4rem;
|
||
background: rgba(0, 0, 0, 0.2);
|
||
border-radius: 4px;
|
||
display: inline-block;
|
||
}
|
||
|
||
.kpi-card .badge {
|
||
font-size: 0.75rem;
|
||
padding: 0.4rem 0.6rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.trend-up {
|
||
color: #2ecc71;
|
||
}
|
||
|
||
.trend-down {
|
||
color: #e74c3c;
|
||
}
|
||
|
||
.bg-purple {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
}
|
||
|
||
.chart-container {
|
||
position: relative;
|
||
height: 350px;
|
||
}
|
||
|
||
/* Top 10 圖表響應式滾動 */
|
||
.chart-responsive {
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
position: relative;
|
||
}
|
||
|
||
#top10ChartContainer {
|
||
min-width: 400px;
|
||
}
|
||
|
||
.error-message {
|
||
background: #fff3cd;
|
||
border: 1px solid #ffc107;
|
||
border-radius: 8px;
|
||
padding: 2rem;
|
||
text-align: center;
|
||
margin: 2rem 0;
|
||
}
|
||
|
||
/* 行事曆樣式 */
|
||
.calendar-container {
|
||
background: #fff;
|
||
border-radius: 16px;
|
||
padding: 2rem;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||
margin-bottom: 2rem;
|
||
border: 1px solid rgba(102, 126, 234, 0.1);
|
||
}
|
||
|
||
.calendar-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 1.5rem;
|
||
padding-bottom: 1.25rem;
|
||
border-bottom: 2px solid #f0f0f0;
|
||
}
|
||
|
||
.calendar-header h5 {
|
||
margin: 0;
|
||
font-weight: 700;
|
||
color: #2c3e50;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
font-size: 1.3rem;
|
||
}
|
||
|
||
.calendar-header h5:hover {
|
||
color: #667eea;
|
||
transform: translateX(-4px);
|
||
}
|
||
|
||
.calendar-header h5:hover i {
|
||
color: #667eea;
|
||
transform: rotate(360deg);
|
||
transition: transform 0.5s ease;
|
||
}
|
||
|
||
.calendar-nav {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.calendar-nav button {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border: none;
|
||
border-radius: 10px;
|
||
padding: 0.6rem 1.2rem;
|
||
color: #fff;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||
}
|
||
|
||
.calendar-nav button:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.calendar-nav button:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.calendar-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
gap: 8px;
|
||
}
|
||
|
||
.calendar-weekday {
|
||
text-align: center;
|
||
font-weight: 600;
|
||
font-size: 0.85rem;
|
||
color: #6c757d;
|
||
padding: 0.5rem;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.calendar-day {
|
||
background: #ffffff;
|
||
border: 2px solid #e8ecf1;
|
||
border-radius: 12px;
|
||
padding: 0.8rem;
|
||
min-height: 110px;
|
||
cursor: pointer;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
position: relative;
|
||
}
|
||
|
||
.calendar-day:hover {
|
||
transform: translateY(-3px) scale(1.02);
|
||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.15);
|
||
border-color: #667eea;
|
||
background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 100%);
|
||
}
|
||
|
||
.calendar-day.has-data {
|
||
background: linear-gradient(135deg, #ffffff 0%, #fafbff 100%);
|
||
border-color: #d0d5e0;
|
||
}
|
||
|
||
.calendar-day.has-data:hover {
|
||
border-color: #667eea;
|
||
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.2);
|
||
}
|
||
|
||
.calendar-day.other-month {
|
||
opacity: 0.25;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.calendar-day.selected {
|
||
border-color: #667eea !important;
|
||
border-width: 5px !important;
|
||
box-shadow: 0 0 0 6px rgba(102, 126, 234, 0.25), 0 12px 32px rgba(102, 126, 234, 0.4), inset 0 2px 10px rgba(102, 126, 234, 0.15) !important;
|
||
background: linear-gradient(135deg, #e8edff 0%, #d0d9ff 100%) !important;
|
||
transform: scale(1.08) !important;
|
||
position: relative;
|
||
animation: pulseGlow 2s ease-in-out infinite;
|
||
}
|
||
|
||
.calendar-day.selected::before {
|
||
content: '✓';
|
||
position: absolute;
|
||
top: -8px;
|
||
right: -8px;
|
||
width: 32px;
|
||
height: 32px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border-radius: 50%;
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.2rem;
|
||
font-weight: 900;
|
||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
|
||
z-index: 10;
|
||
}
|
||
|
||
.calendar-day.selected .calendar-day-number {
|
||
color: #4c3fb7 !important;
|
||
font-weight: 900 !important;
|
||
}
|
||
|
||
@keyframes pulseGlow {
|
||
|
||
0%,
|
||
100% {
|
||
box-shadow: 0 0 0 6px rgba(102, 126, 234, 0.25), 0 12px 32px rgba(102, 126, 234, 0.4), inset 0 2px 10px rgba(102, 126, 234, 0.15);
|
||
}
|
||
|
||
50% {
|
||
box-shadow: 0 0 0 8px rgba(102, 126, 234, 0.35), 0 16px 40px rgba(102, 126, 234, 0.5), inset 0 2px 10px rgba(102, 126, 234, 0.2);
|
||
}
|
||
}
|
||
|
||
.calendar-day-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
margin-bottom: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.calendar-day-number {
|
||
font-size: 1.5rem;
|
||
font-weight: 800;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.calendar-day-holiday {
|
||
font-size: 0.7rem;
|
||
color: #d63031;
|
||
font-weight: 700;
|
||
padding: 2px 6px;
|
||
background: rgba(214, 48, 49, 0.15);
|
||
border-radius: 3px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 文字顏色:黑色為主 */
|
||
.calendar-day-kpi {
|
||
font-size: 0.8rem;
|
||
line-height: 1.5;
|
||
font-weight: 600;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.calendar-day-kpi .label {
|
||
font-weight: 600;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.calendar-day-kpi .margin-rate {
|
||
color: #6c757d;
|
||
font-size: 0.7rem;
|
||
font-weight: 500;
|
||
margin-left: 3px;
|
||
}
|
||
|
||
/* DoD Badge 樣式:上漲紅色,下跌綠色 */
|
||
.calendar-day-badge {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
padding: 5px 10px;
|
||
border-radius: 8px;
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.calendar-day.has-data.dod-up .calendar-day-badge {
|
||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
||
color: #fff;
|
||
border: none;
|
||
}
|
||
|
||
.calendar-day.has-data.dod-down .calendar-day-badge {
|
||
background: linear-gradient(135deg, #51cf66 0%, #37b24d 100%);
|
||
color: #fff;
|
||
border: none;
|
||
}
|
||
|
||
/* 週末與假日背景 */
|
||
.calendar-day.is-weekend {
|
||
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
|
||
border-color: #d0d5ff;
|
||
}
|
||
|
||
.calendar-day.is-holiday {
|
||
background: linear-gradient(135deg, #fff5f7 0%, #ffe8ed 100%);
|
||
border-color: #ffc9d0;
|
||
}
|
||
|
||
.calendar-day.is-holiday .calendar-day-number {
|
||
color: #d63031;
|
||
}
|
||
|
||
.date-selector {
|
||
background: #fff;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 10px;
|
||
padding: 0.6rem 1.2rem;
|
||
font-size: 0.95rem;
|
||
min-width: 160px;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.date-selector:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||
}
|
||
|
||
/* 頁面標題樣式 */
|
||
.page-header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
padding: 2rem;
|
||
border-radius: 16px;
|
||
margin-bottom: 2rem;
|
||
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.25);
|
||
}
|
||
|
||
.page-header h4 {
|
||
color: #fff !important;
|
||
font-size: 1.8rem;
|
||
margin: 0;
|
||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.page-header .text-info {
|
||
color: rgba(255, 255, 255, 0.9) !important;
|
||
}
|
||
|
||
.page-header-controls {
|
||
display: flex;
|
||
gap: 1rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.page-header-label {
|
||
color: rgba(255, 255, 255, 0.9);
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 按鈕樣式優化 */
|
||
.btn-success {
|
||
background: linear-gradient(135deg, #51cf66 0%, #37b24d 100%);
|
||
border: none;
|
||
border-radius: 10px;
|
||
padding: 0.6rem 1.5rem;
|
||
font-weight: 600;
|
||
box-shadow: 0 4px 12px rgba(81, 207, 102, 0.3);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.btn-success:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 16px rgba(81, 207, 102, 0.4);
|
||
background: linear-gradient(135deg, #40c057 0%, #2f9e44 100%);
|
||
}
|
||
|
||
/* 表格樣式優化 */
|
||
.table {
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.table thead {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: #fff;
|
||
}
|
||
|
||
.table thead th {
|
||
border: none;
|
||
font-weight: 600;
|
||
padding: 1rem;
|
||
text-transform: uppercase;
|
||
font-size: 0.85rem;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.table tbody tr {
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.table tbody tr:hover {
|
||
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
|
||
transform: scale(1.01);
|
||
}
|
||
|
||
.table tbody td {
|
||
padding: 1rem;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
/* ==================== 響應式設計:平板與手機 ==================== */
|
||
|
||
/* 平板 (768px - 1024px) */
|
||
@media (max-width: 1024px) {
|
||
.calendar-day {
|
||
min-height: 100px;
|
||
padding: 0.6rem;
|
||
}
|
||
|
||
.calendar-day-number {
|
||
font-size: 1.3rem;
|
||
}
|
||
|
||
.calendar-day-kpi {
|
||
font-size: 0.7rem;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.calendar-day-badge {
|
||
font-size: 0.7rem;
|
||
padding: 3px 6px;
|
||
}
|
||
}
|
||
|
||
/* 手機橫向 (576px - 767px) */
|
||
@media (max-width: 767px) {
|
||
.calendar-container {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.calendar-header {
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.calendar-header h5 {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.calendar-day {
|
||
min-height: 90px;
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
.calendar-day-header {
|
||
margin-bottom: 0.3rem;
|
||
gap: 0.4rem;
|
||
}
|
||
|
||
.calendar-day-number {
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.calendar-day-holiday {
|
||
font-size: 0.65rem;
|
||
padding: 2px 4px;
|
||
}
|
||
|
||
.calendar-day-kpi {
|
||
font-size: 0.65rem;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.calendar-day-badge {
|
||
font-size: 0.65rem;
|
||
padding: 2px 5px;
|
||
top: 3px;
|
||
right: 3px;
|
||
}
|
||
|
||
.calendar-weekday {
|
||
font-size: 0.7rem;
|
||
padding: 0.3rem;
|
||
}
|
||
}
|
||
|
||
/* 手機直向 (< 576px) */
|
||
@media (max-width: 575px) {
|
||
|
||
/* 行事曆改為可左右滑動 */
|
||
.calendar-container {
|
||
padding: 0.75rem;
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.calendar-grid {
|
||
min-width: 700px;
|
||
/* 保證最小寬度,讓用戶可以橫向滾動 */
|
||
gap: 6px;
|
||
}
|
||
|
||
.calendar-header {
|
||
position: sticky;
|
||
left: 0;
|
||
background: #fff;
|
||
z-index: 10;
|
||
padding-bottom: 1rem;
|
||
}
|
||
|
||
.calendar-day {
|
||
min-height: 85px;
|
||
padding: 0.4rem;
|
||
}
|
||
|
||
.calendar-day-header {
|
||
margin-bottom: 0.25rem;
|
||
gap: 0.3rem;
|
||
}
|
||
|
||
.calendar-day-number {
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.calendar-day-holiday {
|
||
font-size: 0.6rem;
|
||
padding: 2px 3px;
|
||
}
|
||
|
||
/* 手機版簡化 KPI 顯示 */
|
||
.calendar-day-kpi {
|
||
font-size: 0.65rem;
|
||
line-height: 1.35;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.calendar-day-kpi .label {
|
||
font-weight: 700;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.calendar-day-kpi .margin-rate {
|
||
font-size: 0.6rem;
|
||
}
|
||
|
||
.calendar-day-badge {
|
||
font-size: 0.6rem;
|
||
padding: 2px 4px;
|
||
top: 2px;
|
||
right: 2px;
|
||
}
|
||
|
||
.calendar-weekday {
|
||
font-size: 0.65rem;
|
||
padding: 0.25rem;
|
||
}
|
||
|
||
/* 提示橫向滾動 */
|
||
.calendar-container::after {
|
||
content: '← 左右滑動查看完整行事曆 →';
|
||
display: block;
|
||
text-align: center;
|
||
font-size: 0.75rem;
|
||
color: #6c757d;
|
||
margin-top: 0.5rem;
|
||
padding: 0.5rem;
|
||
background: #f8f9fa;
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
|
||
/* 極小螢幕 (< 400px) */
|
||
@media (max-width: 399px) {
|
||
.calendar-grid {
|
||
min-width: 650px;
|
||
}
|
||
|
||
.calendar-day {
|
||
min-height: 80px;
|
||
padding: 0.3rem;
|
||
}
|
||
|
||
.calendar-day-number {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.calendar-day-kpi {
|
||
font-size: 0.6rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.calendar-day-kpi .label {
|
||
font-weight: 700;
|
||
}
|
||
|
||
.calendar-day-kpi .margin-rate {
|
||
font-size: 0.55rem;
|
||
}
|
||
}
|
||
|
||
/* 所有手機裝置:優化 KPI 卡片、圖表與列表 */
|
||
@media (max-width: 767px) {
|
||
|
||
/* KPI 卡片堆疊 */
|
||
.row>.col-md-4 {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
/* KPI 卡片字體縮小 */
|
||
.kpi-value {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.kpi-label {
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.kpi-percent {
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
/* 圖表高度調整 */
|
||
.chart-container {
|
||
height: 250px;
|
||
}
|
||
|
||
/* Top 10 圖表手機版優化 */
|
||
#top10ChartContainer {
|
||
min-width: 500px;
|
||
}
|
||
|
||
/* 日期選擇器 */
|
||
.date-selector {
|
||
min-width: 120px;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
/* 標題與按鈕 */
|
||
.d-flex.justify-content-between {
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
align-items: flex-start !important;
|
||
}
|
||
|
||
/* 表格響應式滾動 */
|
||
.table-responsive {
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
position: relative;
|
||
}
|
||
|
||
/* 確保表格有最小寬度 */
|
||
#categoryTable {
|
||
min-width: 800px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 表格欄位調整 */
|
||
#categoryTable th,
|
||
#categoryTable td {
|
||
font-size: 0.85rem;
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
/* 導覽列調整 */
|
||
.navbar-brand {
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.nav-link {
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
/* Card padding 縮小 */
|
||
.card-body {
|
||
padding: 1rem;
|
||
padding-top: 70px;
|
||
}
|
||
|
||
.card-header {
|
||
padding: 0.75rem 1rem;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
/* 容器 padding 縮小 */
|
||
.container-fluid {
|
||
padding-left: 0.75rem;
|
||
padding-right: 0.75rem;
|
||
}
|
||
}
|
||
|
||
/* 手機直向專屬優化 */
|
||
@media (max-width: 575px) {
|
||
|
||
/* 圖表更小 */
|
||
.chart-container {
|
||
height: 220px;
|
||
}
|
||
|
||
/* Top 10 圖表保持可讀性 */
|
||
#top10ChartContainer {
|
||
min-width: 450px;
|
||
}
|
||
|
||
/* KPI 卡片更緊湊 */
|
||
.kpi-card .card-body {
|
||
padding: 0.75rem;
|
||
padding-top: 70px;
|
||
}
|
||
|
||
.kpi-value {
|
||
font-size: 1.3rem;
|
||
}
|
||
|
||
.kpi-label {
|
||
font-size: 0.7rem;
|
||
}
|
||
|
||
/* 圖表區塊改為單列 */
|
||
.row>.col-lg-8,
|
||
.row>.col-lg-4 {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
/* 按鈕文字縮小 */
|
||
.btn {
|
||
font-size: 0.85rem;
|
||
padding: 0.4rem 0.8rem;
|
||
}
|
||
}
|
||
|
||
/* Custom Dark Gray Navbar */
|
||
.navbar.bg-custom-dark {
|
||
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.navbar.bg-custom-dark .navbar-brand {
|
||
color: #ffffff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.navbar.bg-custom-dark .navbar-nav .nav-link {
|
||
color: rgba(255, 255, 255, 0.85);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.navbar.bg-custom-dark .navbar-nav .nav-link:hover {
|
||
color: #ffffff;
|
||
}
|
||
|
||
.navbar.bg-custom-dark .navbar-nav .nav-link.active {
|
||
color: #ffffff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.navbar.bg-custom-dark .navbar-text {
|
||
color: rgba(255, 255, 255, 0.75);
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body class="bg-body-tertiary">
|
||
{% include 'components/_navbar.html' %}
|
||
|
||
<div class="container-fluid px-4">
|
||
{% if error %}
|
||
<div class="error-message">
|
||
<i class="fas fa-exclamation-triangle fa-3x text-warning mb-3"></i>
|
||
<h4>{{ error }}</h4>
|
||
<p class="text-muted">請前往 <a href="/system_settings">系統設定頁面</a> 匯入當日業績 Excel 檔案。</p>
|
||
</div>
|
||
{% else %}
|
||
<!-- Header with Date Selector -->
|
||
<div class="page-header d-flex justify-content-between align-items-center mt-4">
|
||
<h4 class="mb-0 fw-bold"><i class="fas fa-calendar-day me-2 text-info"></i>當日業績看板</h4>
|
||
<div class="page-header-controls">
|
||
<select id="dateSelector" class="date-selector" onchange="changeDate()">
|
||
{% for date in available_dates %}
|
||
<option value="{{ date }}" {% if date==selected_date %}selected{% endif %}>
|
||
{{ date }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
<span class="page-header-label">選擇日期查看詳細業績</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Calendar View -->
|
||
{% if calendar_data %}
|
||
<div class="calendar-container">
|
||
<div class="calendar-header">
|
||
<h5 onclick="backToMonthView()"><i class="fas fa-calendar-alt me-2 text-primary"></i>{{
|
||
calendar_data.month_name }} 業績行事曆</h5>
|
||
<div class="calendar-nav">
|
||
<button onclick="changeMonth('{{ calendar_data.prev_month }}')">
|
||
<i class="fas fa-chevron-left me-1"></i>上個月
|
||
</button>
|
||
<button onclick="changeMonth('{{ calendar_data.next_month }}')">
|
||
下個月<i class="fas fa-chevron-right ms-1"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Weekday Headers -->
|
||
<div class="calendar-grid">
|
||
<div class="calendar-weekday">週一</div>
|
||
<div class="calendar-weekday">週二</div>
|
||
<div class="calendar-weekday">週三</div>
|
||
<div class="calendar-weekday">週四</div>
|
||
<div class="calendar-weekday">週五</div>
|
||
<div class="calendar-weekday">週六</div>
|
||
<div class="calendar-weekday">週日</div>
|
||
|
||
<!-- Calendar Days -->
|
||
{% for week in calendar_data.weeks %}
|
||
{% for day in week %}
|
||
<div class="calendar-day
|
||
{% if day.has_data %}has-data dod-{{ day.dod_direction }}{% endif %}
|
||
{% if not day.is_current_month %}other-month{% endif %}
|
||
{% if day.is_weekend %}is-weekend{% endif %}
|
||
{% if day.is_holiday %}is-holiday{% endif %}
|
||
{% if day.date == selected_date and not is_month_view %}selected{% endif %}"
|
||
data-date="{{ day.date }}"
|
||
data-has-data="{{ 'true' if day.has_data and day.is_current_month else 'false' }}"
|
||
onclick="{% if day.has_data and day.is_current_month %}toggleDateSelection('{{ day.date }}', '{{ selected_date }}'){% endif %}"
|
||
title="{% if day.is_holiday %}🎊 {{ day.holiday_name | e }} | {% endif %}{{ day.weekday | e }}{% if day.has_data %} | 業績: ${{ '{:,.0f}'.format(day.revenue) }} | 毛利: ${{ '{:,.0f}'.format(day.profit) }} | SKU: {{ day.sku_count }} | 客單價: ${{ '{:,.0f}'.format(day.avg_price) }} | 銷量: {{ '{:,.0f}'.format(day.qty) }} | DoD: {{ day.dod_percent | e }}%{% else %} | 無資料{% endif %}">
|
||
|
||
<div class="calendar-day-header">
|
||
<div class="calendar-day-number">{{ day.day }}</div>
|
||
{% if day.is_holiday %}
|
||
<div class="calendar-day-holiday">🎊 {{ day.holiday_name }}</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if day.has_data %}
|
||
<div class="calendar-day-badge">
|
||
{% if day.dod_direction == 'up' %}<i class="fas fa-arrow-up"></i>
|
||
{% elif day.dod_direction == 'down' %}<i class="fas fa-arrow-down"></i>
|
||
{% endif %}
|
||
{{ day.dod_percent }}%
|
||
</div>
|
||
<div class="calendar-day-kpi">
|
||
<div data-label="💰"><span class="label">業績</span> ${{ '{:,.0f}'.format(day.revenue) }}</div>
|
||
<div data-label="📊"><span class="label">毛利</span> ${{ '{:,.0f}'.format(day.profit) }} <span
|
||
class="margin-rate">({{ '{:.1f}%'.format(day.margin_rate) }})</span></div>
|
||
<div data-label="📦"><span class="label">SKU</span> {{ '{:,.0f}'.format(day.sku_count) }}</div>
|
||
<div data-label="🛒"><span class="label">客單</span> ${{ '{:,.0f}'.format(day.avg_price) }}</div>
|
||
<div data-label="📈"><span class="label">銷量</span> {{ '{:,.0f}'.format(day.qty) }}</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- KPI Cards -->
|
||
<!-- V-New 2026-01-15: 顯示當前模式標籤 -->
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<div class="d-flex align-items-center gap-2">
|
||
{% if is_month_view %}
|
||
<span class="badge bg-primary fs-6"><i class="fas fa-calendar-alt me-1"></i>月度總計模式</span>
|
||
<small class="text-muted">顯示 {{ calendar_data.month_name }} 所有天數的加總</small>
|
||
{% else %}
|
||
<span class="badge bg-info fs-6"><i class="fas fa-calendar-day me-1"></i>單日模式</span>
|
||
<small class="text-muted">顯示 {{ selected_date }} 的業績</small>
|
||
<a href="javascript:backToMonthView();" class="btn btn-sm btn-outline-primary ms-2">
|
||
<i class="fas fa-chart-line me-1"></i>查看月度總計
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
{% if is_month_view and month_kpi %}
|
||
<span class="badge bg-secondary">累計 {{ month_kpi.days_with_data }} 天</span>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="row mb-4">
|
||
<div class="col-md-4 mb-3">
|
||
<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">總業績</div>
|
||
{% if is_month_view and month_kpi %}
|
||
<div class="kpi-value">${{ "{:,.0f}".format(month_kpi.total_revenue) }}</div>
|
||
<div class="mt-2">
|
||
<span class="badge bg-light text-primary">月度累計</span>
|
||
</div>
|
||
{% else %}
|
||
<div class="kpi-value">${{ "{:,.0f}".format(current.total_revenue) }}</div>
|
||
<div class="mt-2">
|
||
<span class="badge bg-dark text-white-primary me-2">DoD</span>
|
||
<span class="kpi-percent">
|
||
<i class="fas fa-{{ 'arrow-up' if dod.total_revenue >= 0 else 'arrow-down' }} me-1"></i>
|
||
{{ "{:+.1f}%".format(dod.total_revenue) }}
|
||
</span>
|
||
<span class="ms-2 badge bg-dark text-white">WoW</span>
|
||
<span class="kpi-percent">
|
||
<i class="fas fa-{{ 'arrow-up' if wow.total_revenue >= 0 else 'arrow-down' }} me-1"></i>
|
||
{{ "{:+.1f}%".format(wow.total_revenue) }}
|
||
</span>
|
||
</div>
|
||
{% endif %}
|
||
<i class="fas fa-chart-line icon-bg"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4 mb-3">
|
||
<div class="card kpi-card bg-warning text-white h-100 shadow-sm">
|
||
<div class="card-body p-4">
|
||
<div class="kpi-label text-white-50">總成本</div>
|
||
{% if is_month_view and month_kpi %}
|
||
<div class="kpi-value">${{ "{:,.0f}".format(month_kpi.total_cost) }}</div>
|
||
<div class="mt-2">
|
||
<span class="badge bg-light text-warning">月度累計</span>
|
||
</div>
|
||
{% else %}
|
||
<div class="kpi-value">${{ "{:,.0f}".format(current.total_cost) }}</div>
|
||
<div class="mt-2">
|
||
<span class="badge bg-dark text-white-warning me-2">DoD</span>
|
||
<span class="kpi-percent">
|
||
<i class="fas fa-{{ 'arrow-up' if dod.total_cost >= 0 else 'arrow-down' }} me-1"></i>
|
||
{{ "{:+.1f}%".format(dod.total_cost) }}
|
||
</span>
|
||
<span class="ms-2 badge bg-dark text-white">WoW</span>
|
||
<span class="kpi-percent">
|
||
<i class="fas fa-{{ 'arrow-up' if wow.total_cost >= 0 else 'arrow-down' }} me-1"></i>
|
||
{{ "{:+.1f}%".format(wow.total_cost) }}
|
||
</span>
|
||
</div>
|
||
{% endif %}
|
||
<i class="fas fa-dollar-sign icon-bg"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4 mb-3">
|
||
<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">毛利</div>
|
||
{% if is_month_view and month_kpi %}
|
||
<div class="kpi-value">${{ "{:,.0f}".format(month_kpi.gross_margin) }}</div>
|
||
<div class="mt-2">
|
||
<span class="badge bg-light text-success">月度累計</span>
|
||
<span class="badge bg-dark text-white ms-2">毛利率 {{ "{:.1f}%".format(month_kpi.margin_rate)
|
||
}}</span>
|
||
</div>
|
||
{% else %}
|
||
<div class="kpi-value">${{ "{:,.0f}".format(current.gross_margin) }}</div>
|
||
<div class="mt-2">
|
||
<span class="badge bg-dark text-white-success me-2">DoD</span>
|
||
<span class="kpi-percent">
|
||
<i class="fas fa-{{ 'arrow-up' if dod.gross_margin >= 0 else 'arrow-down' }} me-1"></i>
|
||
{{ "{:+.1f}%".format(dod.gross_margin) }}
|
||
</span>
|
||
<span class="ms-2 badge bg-dark text-white">WoW</span>
|
||
<span class="kpi-percent">
|
||
<i class="fas fa-{{ 'arrow-up' if wow.gross_margin >= 0 else 'arrow-down' }} me-1"></i>
|
||
{{ "{:+.1f}%".format(wow.gross_margin) }}
|
||
</span>
|
||
</div>
|
||
{% endif %}
|
||
<i class="fas fa-hand-holding-usd icon-bg"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row mb-4">
|
||
<div class="col-md-4 mb-3">
|
||
<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">SKU 數</div>
|
||
{% if is_month_view and month_kpi %}
|
||
<div class="kpi-value">{{ "{:,.0f}".format(month_kpi.sku_count) }}</div>
|
||
<div class="mt-2">
|
||
<span class="badge bg-light text-info">月度不重複商品</span>
|
||
</div>
|
||
{% else %}
|
||
<div class="kpi-value">{{ "{:,.0f}".format(current.sku_count) }}</div>
|
||
<div class="mt-2">
|
||
<span class="badge bg-dark text-white-info me-2">DoD</span>
|
||
<span class="kpi-percent">
|
||
<i class="fas fa-{{ 'arrow-up' if dod.sku_count >= 0 else 'arrow-down' }} me-1"></i>
|
||
{{ "{:+.1f}%".format(dod.sku_count) }}
|
||
</span>
|
||
<span class="ms-2 badge bg-dark text-white">WoW</span>
|
||
<span class="kpi-percent">
|
||
<i class="fas fa-{{ 'arrow-up' if wow.sku_count >= 0 else 'arrow-down' }} me-1"></i>
|
||
{{ "{:+.1f}%".format(wow.sku_count) }}
|
||
</span>
|
||
</div>
|
||
{% endif %}
|
||
<i class="fas fa-cubes icon-bg"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4 mb-3">
|
||
<div class="card kpi-card bg-purple text-white h-100 shadow-sm">
|
||
<div class="card-body p-4">
|
||
<div class="kpi-label text-white-50">客單價</div>
|
||
{% if is_month_view and month_kpi %}
|
||
<div class="kpi-value">${{ "{:,.0f}".format(month_kpi.avg_price) }}</div>
|
||
<div class="mt-2">
|
||
<span class="badge bg-light text-purple">月度平均</span>
|
||
</div>
|
||
{% else %}
|
||
<div class="kpi-value">${{ "{:,.0f}".format(current.avg_price) }}</div>
|
||
<div class="mt-2">
|
||
<span class="badge bg-dark text-white-purple me-2">DoD</span>
|
||
<span class="kpi-percent">
|
||
<i class="fas fa-{{ 'arrow-up' if dod.avg_price >= 0 else 'arrow-down' }} me-1"></i>
|
||
{{ "{:+.1f}%".format(dod.avg_price) }}
|
||
</span>
|
||
<span class="ms-2 badge bg-dark text-white">WoW</span>
|
||
<span class="kpi-percent">
|
||
<i class="fas fa-{{ 'arrow-up' if wow.avg_price >= 0 else 'arrow-down' }} me-1"></i>
|
||
{{ "{:+.1f}%".format(wow.avg_price) }}
|
||
</span>
|
||
</div>
|
||
{% endif %}
|
||
<i class="fas fa-shopping-cart icon-bg"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4 mb-3">
|
||
<div class="card kpi-card bg-secondary text-white h-100 shadow-sm">
|
||
<div class="card-body p-4">
|
||
<div class="kpi-label text-white-50">總銷量</div>
|
||
{% if is_month_view and month_kpi %}
|
||
<div class="kpi-value">{{ "{:,.0f}".format(month_kpi.total_qty) }}</div>
|
||
<div class="mt-2">
|
||
<span class="badge bg-light text-secondary">月度累計</span>
|
||
</div>
|
||
{% else %}
|
||
<div class="kpi-value">{{ "{:,.0f}".format(current.total_qty) }}</div>
|
||
<div class="mt-2">
|
||
<span class="badge bg-dark text-white-secondary me-2">DoD</span>
|
||
<span class="kpi-percent">
|
||
<i class="fas fa-{{ 'arrow-up' if dod.total_qty >= 0 else 'arrow-down' }} me-1"></i>
|
||
{{ "{:+.1f}%".format(dod.total_qty) }}
|
||
</span>
|
||
<span class="ms-2 badge bg-dark text-white">WoW</span>
|
||
<span class="kpi-percent">
|
||
<i class="fas fa-{{ 'arrow-up' if wow.total_qty >= 0 else 'arrow-down' }} me-1"></i>
|
||
{{ "{:+.1f}%".format(wow.total_qty) }}
|
||
</span>
|
||
</div>
|
||
{% endif %}
|
||
<i class="fas fa-boxes icon-bg"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Charts Row -->
|
||
<div class="row mb-4">
|
||
<div class="col-lg-8">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i class="fas fa-chart-area me-2"></i>每日業績趨勢(近 30 天)
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="chart-container">
|
||
<canvas id="trendChart"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i class="fas fa-percentage me-2"></i>日成長率 (DoD %)
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="chart-container">
|
||
<canvas id="dodChart"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row mb-4">
|
||
<div class="col-lg-8">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i class="fas fa-chart-bar me-2"></i>週成長對比 (WoW)
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="chart-container">
|
||
<canvas id="wowChart"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i class="fas fa-trophy me-2"></i>商品 Top 10
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="alert alert-info d-md-none py-2 mb-3" role="alert">
|
||
<i class="fas fa-hand-pointer me-1"></i>
|
||
<small>左右滑動查看完整圖表</small>
|
||
</div>
|
||
<div class="chart-responsive">
|
||
<div class="chart-container" id="top10ChartContainer">
|
||
<canvas id="top10Chart"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- V-New 2026-01-15: 行銷活動業績貢獻 -->
|
||
{% if marketing_data %}
|
||
<div class="row mb-4">
|
||
<div class="col-12">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span><i class="fas fa-bullhorn me-2"></i>行銷活動業績貢獻</span>
|
||
<button class="btn btn-sm btn-success" onclick="exportMarketingData()">
|
||
<i class="fas fa-file-excel me-1"></i>匯出 Excel
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row">
|
||
<!-- 折扣活動 -->
|
||
<div class="col-lg-6 mb-4">
|
||
<h6 class="mb-3"><i class="fas fa-tags text-primary me-2"></i>折扣活動 Top 10</h6>
|
||
{% if marketing_data.discount %}
|
||
<div style="height: 350px;">
|
||
<canvas id="discountChart"></canvas>
|
||
</div>
|
||
{% else %}
|
||
<div class="text-muted text-center py-5">
|
||
<i class="fas fa-info-circle fa-2x mb-2"></i>
|
||
<p>暫無折扣活動數據</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
<!-- 折價券活動 -->
|
||
<div class="col-lg-6 mb-4">
|
||
<h6 class="mb-3"><i class="fas fa-ticket-alt text-success me-2"></i>折價券活動 Top 10</h6>
|
||
{% if marketing_data.coupon %}
|
||
<div style="height: 350px;">
|
||
<canvas id="couponChart"></canvas>
|
||
</div>
|
||
{% else %}
|
||
<div class="text-muted text-center py-5">
|
||
<i class="fas fa-info-circle fa-2x mb-2"></i>
|
||
<p>暫無折價券活動數據</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Category Summary Table -->
|
||
<div class="row">
|
||
<div class="col-12">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span><i class="fas fa-table me-2"></i>分類業績明細</span>
|
||
<button class="btn btn-sm btn-success" onclick="exportCategoryTable()">
|
||
<i class="fas fa-file-excel me-1"></i>匯出 Excel
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="alert alert-info d-md-none py-2 mb-3" role="alert">
|
||
<i class="fas fa-hand-pointer me-1"></i>
|
||
<small>左右滑動查看完整列表</small>
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table id="categoryTable" class="table table-hover table-striped">
|
||
<thead>
|
||
<tr>
|
||
<th>分類</th>
|
||
<th>廠商</th>
|
||
<th>總業績</th>
|
||
<th>總成本</th>
|
||
<th>毛利</th>
|
||
<th>毛利率</th>
|
||
<th>總銷量</th>
|
||
<th>SKU 數</th>
|
||
<th>平均單價</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for cat in categories %}
|
||
<tr>
|
||
<td>{{ cat.category }}</td>
|
||
<td>{{ cat.vendor if cat.vendor else '-' }}</td>
|
||
<td>${{ "{:,.0f}".format(cat.revenue) }}</td>
|
||
<td>${{ "{:,.0f}".format(cat.cost if cat.cost else 0) }}</td>
|
||
<td>${{ "{:,.0f}".format(cat.profit if cat.profit else 0) }}</td>
|
||
<td>{{ "{:.1f}%".format(cat.margin_rate) }}</td>
|
||
<td>{{ "{:,.0f}".format(cat.qty if cat.qty else 0) }}</td>
|
||
<td>{{ cat.sku_count if cat.sku_count else 0 }}</td>
|
||
<td>${{ "{:,.0f}".format(cat.avg_price) }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
|
||
<script src="https://cdn.datatables.net/1.11.5/js/dataTables.bootstrap5.min.js"></script>
|
||
<script>
|
||
{% if not error %}
|
||
const chartData = {{ chart_data | tojson }};
|
||
|
||
// 調試:檢查數據
|
||
console.log('Chart Data:', chartData);
|
||
|
||
// 數據驗證和安全處理
|
||
const safeData = {
|
||
labels: chartData.labels || [],
|
||
revenue: chartData.revenue || [],
|
||
profit: chartData.profit || [],
|
||
avg_price: chartData.avg_price || [],
|
||
qty: chartData.qty || [],
|
||
dod_revenue: chartData.dod_revenue || [],
|
||
dod_profit: chartData.dod_profit || [],
|
||
dod_avg_price: chartData.dod_avg_price || [],
|
||
dod_qty: chartData.dod_qty || [],
|
||
wow_revenue: chartData.wow_revenue || [],
|
||
wow_profit: chartData.wow_profit || [],
|
||
wow_avg_price: chartData.wow_avg_price || [],
|
||
wow_qty: chartData.wow_qty || [],
|
||
top10_labels: chartData.top10_labels || [],
|
||
top10_values: chartData.top10_values || []
|
||
};
|
||
|
||
// 每日趨勢圖(多維度線圖)
|
||
if (safeData.labels && safeData.labels.length > 0) {
|
||
new Chart(document.getElementById('trendChart'), {
|
||
type: 'line',
|
||
data: {
|
||
labels: safeData.labels,
|
||
datasets: [
|
||
{
|
||
label: '業績',
|
||
data: safeData.revenue,
|
||
borderColor: 'rgba(54, 162, 235, 1)',
|
||
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||
borderWidth: 2,
|
||
yAxisID: 'y',
|
||
tension: 0.3,
|
||
fill: false
|
||
},
|
||
{
|
||
label: '毛利',
|
||
data: safeData.profit,
|
||
borderColor: 'rgba(46, 204, 113, 1)',
|
||
backgroundColor: 'rgba(46, 204, 113, 0.1)',
|
||
borderWidth: 2,
|
||
yAxisID: 'y',
|
||
tension: 0.3,
|
||
fill: false
|
||
},
|
||
{
|
||
label: '客單價',
|
||
data: safeData.avg_price,
|
||
borderColor: 'rgba(153, 102, 255, 1)',
|
||
backgroundColor: 'rgba(153, 102, 255, 0.1)',
|
||
borderWidth: 2,
|
||
yAxisID: 'y1',
|
||
tension: 0.3,
|
||
fill: false
|
||
},
|
||
{
|
||
label: '銷量',
|
||
data: safeData.qty,
|
||
borderColor: 'rgba(255, 159, 64, 1)',
|
||
backgroundColor: 'rgba(255, 159, 64, 0.1)',
|
||
borderWidth: 2,
|
||
yAxisID: 'y2',
|
||
tension: 0.3,
|
||
fill: false
|
||
}
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
interaction: {
|
||
mode: 'index',
|
||
intersect: false
|
||
},
|
||
plugins: {
|
||
legend: {
|
||
display: true,
|
||
position: 'top'
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
type: 'linear',
|
||
display: true,
|
||
position: 'left',
|
||
beginAtZero: true,
|
||
title: { display: true, text: '業績/毛利 ($)', color: '#54a0ff' }
|
||
},
|
||
y1: {
|
||
type: 'linear',
|
||
display: true,
|
||
position: 'right',
|
||
beginAtZero: true,
|
||
grid: { drawOnChartArea: false },
|
||
title: { display: true, text: '客單價 ($)', color: '#9966ff' }
|
||
},
|
||
y2: {
|
||
type: 'linear',
|
||
display: false,
|
||
position: 'right',
|
||
beginAtZero: true,
|
||
grid: { drawOnChartArea: false }
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// DoD 成長率圖(多維度線圖)
|
||
new Chart(document.getElementById('dodChart'), {
|
||
type: 'line',
|
||
data: {
|
||
labels: safeData.labels,
|
||
datasets: [
|
||
{
|
||
label: '業績 DoD%',
|
||
data: safeData.dod_revenue,
|
||
borderColor: 'rgba(54, 162, 235, 1)',
|
||
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||
borderWidth: 2,
|
||
tension: 0.3,
|
||
fill: false
|
||
},
|
||
{
|
||
label: '毛利 DoD%',
|
||
data: safeData.dod_profit,
|
||
borderColor: 'rgba(46, 204, 113, 1)',
|
||
backgroundColor: 'rgba(46, 204, 113, 0.1)',
|
||
borderWidth: 2,
|
||
tension: 0.3,
|
||
fill: false
|
||
},
|
||
{
|
||
label: '客單 DoD%',
|
||
data: safeData.dod_avg_price,
|
||
borderColor: 'rgba(153, 102, 255, 1)',
|
||
backgroundColor: 'rgba(153, 102, 255, 0.1)',
|
||
borderWidth: 2,
|
||
tension: 0.3,
|
||
fill: false
|
||
},
|
||
{
|
||
label: '銷量 DoD%',
|
||
data: safeData.dod_qty,
|
||
borderColor: 'rgba(255, 159, 64, 1)',
|
||
backgroundColor: 'rgba(255, 159, 64, 0.1)',
|
||
borderWidth: 2,
|
||
tension: 0.3,
|
||
fill: false
|
||
}
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
interaction: {
|
||
mode: 'index',
|
||
intersect: false
|
||
},
|
||
plugins: {
|
||
legend: {
|
||
display: true,
|
||
position: 'top'
|
||
},
|
||
tooltip: {
|
||
callbacks: {
|
||
label: function (context) {
|
||
return context.dataset.label + ': ' + context.parsed.y.toFixed(1) + '%';
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: false,
|
||
title: { display: true, text: 'DoD 成長率 (%)' }
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// WoW 對比圖(多維度線圖)
|
||
new Chart(document.getElementById('wowChart'), {
|
||
type: 'line',
|
||
data: {
|
||
labels: safeData.labels,
|
||
datasets: [
|
||
{
|
||
label: '業績 WoW%',
|
||
data: safeData.wow_revenue,
|
||
borderColor: 'rgba(54, 162, 235, 1)',
|
||
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||
borderWidth: 2,
|
||
tension: 0.3,
|
||
fill: false,
|
||
segment: {
|
||
borderColor: ctx => {
|
||
// 前 7 天顯示為淺灰色
|
||
return ctx.p0DataIndex < 7 ? 'rgba(200, 200, 200, 0.5)' : 'rgba(54, 162, 235, 1)';
|
||
}
|
||
}
|
||
},
|
||
{
|
||
label: '毛利 WoW%',
|
||
data: safeData.wow_profit,
|
||
borderColor: 'rgba(46, 204, 113, 1)',
|
||
backgroundColor: 'rgba(46, 204, 113, 0.1)',
|
||
borderWidth: 2,
|
||
tension: 0.3,
|
||
fill: false,
|
||
segment: {
|
||
borderColor: ctx => {
|
||
return ctx.p0DataIndex < 7 ? 'rgba(200, 200, 200, 0.5)' : 'rgba(46, 204, 113, 1)';
|
||
}
|
||
}
|
||
},
|
||
{
|
||
label: '客單 WoW%',
|
||
data: safeData.wow_avg_price,
|
||
borderColor: 'rgba(153, 102, 255, 1)',
|
||
backgroundColor: 'rgba(153, 102, 255, 0.1)',
|
||
borderWidth: 2,
|
||
tension: 0.3,
|
||
fill: false,
|
||
segment: {
|
||
borderColor: ctx => {
|
||
return ctx.p0DataIndex < 7 ? 'rgba(200, 200, 200, 0.5)' : 'rgba(153, 102, 255, 1)';
|
||
}
|
||
}
|
||
},
|
||
{
|
||
label: '銷量 WoW%',
|
||
data: safeData.wow_qty,
|
||
borderColor: 'rgba(255, 159, 64, 1)',
|
||
backgroundColor: 'rgba(255, 159, 64, 0.1)',
|
||
borderWidth: 2,
|
||
tension: 0.3,
|
||
fill: false,
|
||
segment: {
|
||
borderColor: ctx => {
|
||
return ctx.p0DataIndex < 7 ? 'rgba(200, 200, 200, 0.5)' : 'rgba(255, 159, 64, 1)';
|
||
}
|
||
}
|
||
}
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
interaction: {
|
||
mode: 'index',
|
||
intersect: false
|
||
},
|
||
plugins: {
|
||
legend: {
|
||
display: true,
|
||
position: 'top'
|
||
},
|
||
tooltip: {
|
||
callbacks: {
|
||
label: function (context) {
|
||
const val = context.parsed.y;
|
||
const index = context.dataIndex;
|
||
let label = context.dataset.label + ': ';
|
||
|
||
// 前 7 天或值為 0 顯示無對比資料
|
||
if (index < 7 || val === 0) {
|
||
label += '無對比資料(需上週同日數據)';
|
||
} else {
|
||
label += val.toFixed(1) + '%';
|
||
}
|
||
return label;
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: false,
|
||
title: { display: true, text: 'WoW 成長率 (%)' }
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// Top 10 商品(橫向柱狀圖)
|
||
if (safeData.top10_labels && safeData.top10_labels.length > 0) {
|
||
new Chart(document.getElementById('top10Chart'), {
|
||
type: 'bar',
|
||
data: {
|
||
labels: safeData.top10_labels,
|
||
datasets: [{
|
||
label: '銷售金額',
|
||
data: safeData.top10_values,
|
||
backgroundColor: 'rgba(255, 159, 64, 0.6)',
|
||
borderColor: 'rgba(255, 159, 64, 1)',
|
||
borderWidth: 1
|
||
}]
|
||
},
|
||
options: {
|
||
indexAxis: 'y',
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
scales: {
|
||
x: { beginAtZero: true }
|
||
}
|
||
}
|
||
});
|
||
}
|
||
} else {
|
||
console.warn('No chart data available');
|
||
}
|
||
|
||
// DataTables 初始化
|
||
$(document).ready(function () {
|
||
$('#categoryTable').DataTable({
|
||
order: [[2, 'desc']], // 按總業績排序(第3欄,索引2)
|
||
pageLength: 25,
|
||
language: {
|
||
url: '//cdn.datatables.net/plug-ins/1.11.5/i18n/zh-HANT.json'
|
||
}
|
||
});
|
||
});
|
||
|
||
// 匯出分類業績明細為 Excel
|
||
function exportCategoryTable() {
|
||
const selector = document.getElementById('dateSelector');
|
||
const selectedDate = selector ? selector.value : '';
|
||
|
||
if (!selectedDate) {
|
||
alert('請先選擇日期');
|
||
return;
|
||
}
|
||
|
||
// 直接跳轉到匯出 API
|
||
window.location.href = `/daily_sales/export?date=${selectedDate}`;
|
||
}
|
||
|
||
// V-New 2026-01-15: 匯出行銷活動業績為 Excel
|
||
function exportMarketingData() {
|
||
const selector = document.getElementById('dateSelector');
|
||
const selectedDate = selector ? selector.value : '';
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const isMonthView = !urlParams.has('date');
|
||
|
||
let url = '/daily_sales/export_marketing?type=all';
|
||
if (isMonthView) {
|
||
// 月度模式:匯出當月所有數據
|
||
const month = urlParams.get('month') || new Date().toISOString().slice(0, 7);
|
||
const [year, mon] = month.split('-');
|
||
const startDate = `${year}-${mon}-01`;
|
||
const endDate = new Date(year, mon, 0).toISOString().slice(0, 10);
|
||
url += `&start_date=${startDate}&end_date=${endDate}`;
|
||
} else {
|
||
// 單日模式
|
||
url += `&date=${selectedDate}`;
|
||
}
|
||
|
||
window.location.href = url;
|
||
}
|
||
|
||
// V-New 2026-01-15: 初始化行銷活動圖表
|
||
{% if marketing_data %}
|
||
// 折扣活動圖表
|
||
{% if marketing_data.discount %}
|
||
const discountCtx = document.getElementById('discountChart');
|
||
if (discountCtx) {
|
||
new Chart(discountCtx.getContext('2d'), {
|
||
type: 'bar',
|
||
data: {
|
||
labels: {{ marketing_data.discount | map(attribute = 'name') | list | tojson | safe }},
|
||
datasets: [{
|
||
label: '業績',
|
||
data: {{ marketing_data.discount | map(attribute = 'revenue') | list | tojson | safe }},
|
||
backgroundColor: [
|
||
'rgba(66, 135, 245, 0.8)',
|
||
'rgba(66, 135, 245, 0.7)',
|
||
'rgba(66, 135, 245, 0.6)',
|
||
'rgba(66, 135, 245, 0.55)',
|
||
'rgba(66, 135, 245, 0.5)',
|
||
'rgba(66, 135, 245, 0.45)',
|
||
'rgba(66, 135, 245, 0.4)',
|
||
'rgba(66, 135, 245, 0.35)',
|
||
'rgba(66, 135, 245, 0.3)',
|
||
'rgba(66, 135, 245, 0.25)'
|
||
],
|
||
borderColor: 'rgba(66, 135, 245, 1)',
|
||
borderWidth: 1
|
||
}]
|
||
},
|
||
options: {
|
||
indexAxis: 'y',
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { display: false },
|
||
tooltip: {
|
||
callbacks: {
|
||
label: function(context) {
|
||
return '$' + context.raw.toLocaleString();
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
x: {
|
||
beginAtZero: true,
|
||
ticks: {
|
||
callback: function(value) {
|
||
return '$' + value.toLocaleString();
|
||
}
|
||
}
|
||
},
|
||
y: {
|
||
ticks: {
|
||
autoSkip: false,
|
||
font: { size: 11 },
|
||
callback: function(value) {
|
||
const label = this.getLabelForValue(value);
|
||
return label.length > 25 ? label.substr(0, 25) + '...' : label;
|
||
}
|
||
}
|
||
}
|
||
},
|
||
onClick: function(evt) {
|
||
exportMarketingData();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
{% endif %}
|
||
|
||
// 折價券活動圖表
|
||
{% if marketing_data.coupon %}
|
||
const couponCtx = document.getElementById('couponChart');
|
||
if (couponCtx) {
|
||
new Chart(couponCtx.getContext('2d'), {
|
||
type: 'bar',
|
||
data: {
|
||
labels: {{ marketing_data.coupon | map(attribute = 'name') | list | tojson | safe }},
|
||
datasets: [{
|
||
label: '業績',
|
||
data: {{ marketing_data.coupon | map(attribute = 'revenue') | list | tojson | safe }},
|
||
backgroundColor: [
|
||
'rgba(40, 167, 69, 0.8)',
|
||
'rgba(40, 167, 69, 0.7)',
|
||
'rgba(40, 167, 69, 0.6)',
|
||
'rgba(40, 167, 69, 0.55)',
|
||
'rgba(40, 167, 69, 0.5)',
|
||
'rgba(40, 167, 69, 0.45)',
|
||
'rgba(40, 167, 69, 0.4)',
|
||
'rgba(40, 167, 69, 0.35)',
|
||
'rgba(40, 167, 69, 0.3)',
|
||
'rgba(40, 167, 69, 0.25)'
|
||
],
|
||
borderColor: 'rgba(40, 167, 69, 1)',
|
||
borderWidth: 1
|
||
}]
|
||
},
|
||
options: {
|
||
indexAxis: 'y',
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { display: false },
|
||
tooltip: {
|
||
callbacks: {
|
||
label: function(context) {
|
||
return '$' + context.raw.toLocaleString();
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
x: {
|
||
beginAtZero: true,
|
||
ticks: {
|
||
callback: function(value) {
|
||
return '$' + value.toLocaleString();
|
||
}
|
||
}
|
||
},
|
||
y: {
|
||
ticks: {
|
||
autoSkip: false,
|
||
font: { size: 11 },
|
||
callback: function(value) {
|
||
const label = this.getLabelForValue(value);
|
||
return label.length > 25 ? label.substr(0, 25) + '...' : label;
|
||
}
|
||
}
|
||
}
|
||
},
|
||
onClick: function(evt) {
|
||
exportMarketingData();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
{% endif %}
|
||
{% endif %}
|
||
|
||
// 日期選擇功能
|
||
function changeDate() {
|
||
const selector = document.getElementById('dateSelector');
|
||
const selectedDate = selector.value;
|
||
window.location.href = `/daily_sales?date=${selectedDate}`;
|
||
}
|
||
|
||
// 點擊行事曆日期(已廢棄,改用 toggleDateSelection)
|
||
function selectDate(date) {
|
||
window.location.href = `/daily_sales?date=${date}`;
|
||
}
|
||
|
||
// 切換日期選擇(支援取消選擇)
|
||
// 切換日期選擇(支援取消選擇)
|
||
function toggleDateSelection(clickedDate, currentSelectedDate) {
|
||
// V-Fix 2026-01-15: 引入 is_month_view 變數判斷
|
||
const isMonthView = {{ 'true' if is_month_view else 'false' }};
|
||
|
||
// 如果是月度模式,點擊任何日期都應該進入該日期的單日模式
|
||
if (isMonthView) {
|
||
window.location.href = `/daily_sales?date=${clickedDate}`;
|
||
return;
|
||
}
|
||
|
||
// 如果已經是單日模式,且點擊的是當前選中日期,則取消選擇回到月概覽
|
||
if (clickedDate === currentSelectedDate) {
|
||
backToMonthView();
|
||
} else {
|
||
// 選擇新日期
|
||
window.location.href = `/daily_sales?date=${clickedDate}`;
|
||
}
|
||
}
|
||
|
||
// 回到當月概覽(取消日期選擇)
|
||
function backToMonthView() {
|
||
// 取得當前月份參數
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const currentMonth = urlParams.get('month');
|
||
|
||
// 如果有月份參數,保留月份但移除日期
|
||
if (currentMonth) {
|
||
window.location.href = `/daily_sales?month=${currentMonth}`;
|
||
} else {
|
||
// 沒有月份參數,直接回到當前月份
|
||
window.location.href = '/daily_sales';
|
||
}
|
||
}
|
||
|
||
// 切換月份
|
||
function changeMonth(month) {
|
||
// 切換月份時不保留日期選擇,直接顯示月概覽
|
||
window.location.href = `/daily_sales?month=${month}`;
|
||
}
|
||
{% endif %}
|
||
</script>
|
||
</body>
|
||
|
||
</html> |