Files
ewoooc/daily_sales.html
ogt 1b4f3a7bbe
Some checks failed
CD Pipeline / deploy (push) Failing after 59s
feat: EwoooC 初始化 — 完整專案推版至 Gitea
- 建立 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>
2026-04-19 01:21:13 +08:00

1906 lines
70 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 }} | {% endif %}{{ day.weekday }}{% 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 }}%{% 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>