413 lines
18 KiB
HTML
413 lines
18 KiB
HTML
{% extends 'ewoooc_base.html' %}
|
||
{% block title %}當日業績看板 - EwoooC{% endblock %}
|
||
|
||
{#
|
||
v3 改寫重點:
|
||
- 移除 622 行 inline <style>,全部抽到 page-daily-sales.css
|
||
- KPI 卡片從 Bootstrap bg-primary/warning/success/info/secondary/purple 漸層
|
||
→ 改用語意化 .kpi-card.kpi--{revenue|cost|profit|sku|aov|qty}
|
||
- 行事曆 selected/dod-up/dod-down 配色改用 token,不再寫死紫紅綠
|
||
- 漲跌色:上漲 → tag-rust(暖紅),下跌 → tag-olive(暖綠橄欖),與 dashboard 一致
|
||
- 圖表配色完全由 page-daily-sales.js 從 CSS var 讀取
|
||
- extends ewoooc_base.html(自動載入 sidebar、tokens、dotmatrix)
|
||
- 用 Jinja macro 收斂 6 張 KPI 卡片重複結構
|
||
#}
|
||
|
||
{% block extra_css %}
|
||
<link rel="stylesheet" href="https://cdn.datatables.net/1.11.5/css/dataTables.bootstrap5.min.css">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-daily-sales.css') }}">
|
||
{% endblock %}
|
||
|
||
{# ---- KPI 卡片 macro ---- #}
|
||
{% macro kpi_card(variant, label, icon, current_val, dod_val, wow_val, month_val, month_extra, is_month_view, value_format='${:,.0f}') %}
|
||
<div class="daily-kpi-slot">
|
||
<article class="kpi-card kpi--{{ variant }}">
|
||
<div class="kpi-card__body">
|
||
<div class="kpi-card__label">{{ label }}</div>
|
||
{% if is_month_view %}
|
||
<div class="kpi-card__value">{{ value_format.format(month_val) }}</div>
|
||
<div class="kpi-card__meta">
|
||
<span class="kpi-tag">月度累計</span>
|
||
{% if month_extra %}<span class="kpi-tag kpi-tag--ghost">{{ month_extra }}</span>{% endif %}
|
||
</div>
|
||
{% else %}
|
||
<div class="kpi-card__value">{{ value_format.format(current_val) }}</div>
|
||
<div class="kpi-card__meta">
|
||
<span class="kpi-delta {{ 'is-up' if dod_val >= 0 else 'is-down' }}">
|
||
<span class="kpi-delta__label">DoD</span>
|
||
<i class="fas fa-{{ 'arrow-up' if dod_val >= 0 else 'arrow-down' }}"></i>
|
||
{{ "{:+.1f}%".format(dod_val) }}
|
||
</span>
|
||
<span class="kpi-delta {{ 'is-up' if wow_val >= 0 else 'is-down' }}">
|
||
<span class="kpi-delta__label">WoW</span>
|
||
<i class="fas fa-{{ 'arrow-up' if wow_val >= 0 else 'arrow-down' }}"></i>
|
||
{{ "{:+.1f}%".format(wow_val) }}
|
||
</span>
|
||
</div>
|
||
{% endif %}
|
||
<i class="fas fa-{{ icon }} kpi-card__icon-bg" aria-hidden="true"></i>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
{% endmacro %}
|
||
|
||
{% block ewooo_content %}
|
||
<div class="daily-sales-page" data-screen-label="daily_sales">
|
||
{% include 'components/_analysis_report_tabs.html' ignore missing %}
|
||
|
||
{% if error %}
|
||
<div class="empty-state">
|
||
<i class="fas fa-exclamation-triangle empty-state__icon" aria-hidden="true"></i>
|
||
<h4 class="empty-state__title">{{ error }}</h4>
|
||
<p class="empty-state__hint">請前往 <a href="/system_settings">系統設定頁面</a> 匯入當日業績 Excel 檔案。</p>
|
||
</div>
|
||
{% else %}
|
||
|
||
<!-- ============ Hero / Date Selector ============ -->
|
||
<section class="daily-hero">
|
||
<div class="daily-hero__lead">
|
||
<h1 class="daily-hero__title">
|
||
<i class="fas fa-calendar-day" aria-hidden="true"></i>
|
||
當日業績看板
|
||
</h1>
|
||
<p class="daily-hero__subtitle">
|
||
{% if is_month_view %}{{ calendar_data.month_name }} 月度總計{% else %}{{ selected_date }} 單日檢視{% endif %}
|
||
</p>
|
||
</div>
|
||
<div class="daily-hero__controls">
|
||
<label for="dateSelector" class="daily-hero__label">日期</label>
|
||
<select id="dateSelector" class="form-select form-select-sm" onchange="changeDate()">
|
||
{% for date in available_dates %}
|
||
<option value="{{ date }}" {% if date == selected_date %}selected{% endif %}>{{ date }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ Mode / KPI Summary ============ -->
|
||
<section class="daily-mode-banner">
|
||
{% if is_month_view %}
|
||
<span class="badge-mode badge-mode--month"><i class="fas fa-calendar-alt"></i> 月度總計</span>
|
||
<span class="daily-mode-banner__hint">{{ calendar_data.month_name }}</span>
|
||
{% if month_kpi %}<span class="badge-mode badge-mode--ghost">累計 {{ month_kpi.days_with_data }} 天</span>{% endif %}
|
||
{% else %}
|
||
<span class="badge-mode badge-mode--single"><i class="fas fa-calendar-day"></i> 單日</span>
|
||
<span class="daily-mode-banner__hint">{{ selected_date }}</span>
|
||
<a href="javascript:backToMonthView();" class="daily-mode-banner__link">
|
||
<i class="fas fa-chart-line"></i> 月度總計
|
||
</a>
|
||
{% endif %}
|
||
</section>
|
||
|
||
<section class="daily-kpi-grid" aria-label="業績摘要">
|
||
{{ kpi_card(
|
||
'revenue', '總業績', 'chart-line',
|
||
current.total_revenue, dod.total_revenue, wow.total_revenue,
|
||
month_kpi.total_revenue if month_kpi else 0,
|
||
None, is_month_view
|
||
) }}
|
||
{{ kpi_card(
|
||
'cost', '總成本', 'dollar-sign',
|
||
current.total_cost, dod.total_cost, wow.total_cost,
|
||
month_kpi.total_cost if month_kpi else 0,
|
||
None, is_month_view
|
||
) }}
|
||
{{ kpi_card(
|
||
'profit', '毛利', 'hand-holding-usd',
|
||
current.gross_margin, dod.gross_margin, wow.gross_margin,
|
||
month_kpi.gross_margin if month_kpi else 0,
|
||
('毛利率 ' ~ "{:.1f}%".format(month_kpi.margin_rate)) if month_kpi else None,
|
||
is_month_view
|
||
) }}
|
||
{{ kpi_card(
|
||
'sku', 'SKU 數', 'cubes',
|
||
current.sku_count, dod.sku_count, wow.sku_count,
|
||
month_kpi.sku_count if month_kpi else 0,
|
||
'不重複商品' if is_month_view else None,
|
||
is_month_view, value_format='{:,.0f}'
|
||
) }}
|
||
{{ kpi_card(
|
||
'aov', '客單價', 'shopping-cart',
|
||
current.avg_price, dod.avg_price, wow.avg_price,
|
||
month_kpi.avg_price if month_kpi else 0,
|
||
None, is_month_view
|
||
) }}
|
||
{{ kpi_card(
|
||
'qty', '總銷量', 'boxes',
|
||
current.total_qty, dod.total_qty, wow.total_qty,
|
||
month_kpi.total_qty if month_kpi else 0,
|
||
None, is_month_view, value_format='{:,.0f}'
|
||
) }}
|
||
</section>
|
||
|
||
<!-- ============ Calendar ============ -->
|
||
{% if calendar_data %}
|
||
<section class="daily-calendar">
|
||
<header class="daily-calendar__header">
|
||
<h2 class="daily-calendar__title" onclick="backToMonthView()">
|
||
<i class="fas fa-calendar-alt" aria-hidden="true"></i>
|
||
{{ calendar_data.month_name }} 業績行事曆
|
||
</h2>
|
||
<nav class="daily-calendar__nav" aria-label="月份切換">
|
||
<button type="button" class="btn-month" onclick="changeMonth('{{ calendar_data.prev_month }}')">
|
||
<i class="fas fa-chevron-left" aria-hidden="true"></i>上個月
|
||
</button>
|
||
<button type="button" class="btn-month" onclick="changeMonth('{{ calendar_data.next_month }}')">
|
||
下個月<i class="fas fa-chevron-right" aria-hidden="true"></i>
|
||
</button>
|
||
</nav>
|
||
</header>
|
||
|
||
<div class="daily-calendar__grid">
|
||
{% for w in ['週一','週二','週三','週四','週五','週六','週日'] %}
|
||
<div class="daily-calendar__weekday">{{ w }}</div>
|
||
{% endfor %}
|
||
|
||
{% for week in calendar_data.weeks %}
|
||
{% for day in week %}
|
||
<div
|
||
class="cal-day
|
||
{% if day.has_data %}has-data dod-{{ day.dod_direction }}{% endif %}
|
||
{% if not day.is_current_month %}is-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 %}is-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) }} | DoD {{ day.dod_percent | e }}%{% endif %}">
|
||
|
||
<div class="cal-day__head">
|
||
<span class="cal-day__num">{{ day.day }}</span>
|
||
{% if day.is_holiday %}<span class="cal-day__holiday">{{ day.holiday_name }}</span>{% endif %}
|
||
</div>
|
||
|
||
{% if day.has_data %}
|
||
<span class="cal-day__badge">
|
||
{% if day.dod_direction == 'up' %}<i class="fas fa-arrow-up" aria-hidden="true"></i>
|
||
{% elif day.dod_direction == 'down' %}<i class="fas fa-arrow-down" aria-hidden="true"></i>
|
||
{% endif %}
|
||
{{ day.dod_percent }}%
|
||
</span>
|
||
<div class="cal-day__metric">
|
||
<span class="cal-day__metric-label">業績</span>
|
||
<strong>${{ '{:,.0f}'.format(day.revenue) }}</strong>
|
||
</div>
|
||
<div class="cal-day__metric cal-day__metric--sub">
|
||
<span class="cal-day__metric-label">毛利</span>
|
||
<strong>${{ '{:,.0f}'.format(day.profit) }}</strong>
|
||
<span>{{ '{:.1f}%'.format(day.margin_rate) }}</span>
|
||
</div>
|
||
<div class="cal-day__chips">
|
||
<span>SKU {{ '{:,.0f}'.format(day.sku_count) }}</span>
|
||
<span>客單 ${{ '{:,.0f}'.format(day.avg_price) }}</span>
|
||
<span>銷量 {{ '{:,.0f}'.format(day.qty) }}</span>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<div class="daily-calendar-mobile-list" aria-label="手機版業績日期清單">
|
||
{% set mobile_days = namespace(count=0) %}
|
||
{% for week in calendar_data.weeks %}
|
||
{% for day in week %}
|
||
{% if day.has_data and day.is_current_month %}
|
||
{% set mobile_days.count = mobile_days.count + 1 %}
|
||
<a
|
||
class="daily-mobile-day {% if day.date == selected_date and not is_month_view %}is-selected{% endif %}"
|
||
href="/daily_sales?date={{ day.date }}"
|
||
aria-label="{{ day.date }} 業績 {{ '{:,.0f}'.format(day.revenue) }}">
|
||
<span class="daily-mobile-day__date">
|
||
<strong>{{ day.day }}</strong>
|
||
<span>{{ day.weekday }}{% if day.is_holiday %} · {{ day.holiday_name }}{% endif %}</span>
|
||
</span>
|
||
<span class="daily-mobile-day__trend dod-{{ day.dod_direction }}">
|
||
{% if day.dod_direction == 'up' %}<i class="fas fa-arrow-up" aria-hidden="true"></i>
|
||
{% elif day.dod_direction == 'down' %}<i class="fas fa-arrow-down" aria-hidden="true"></i>
|
||
{% endif %}
|
||
{{ day.dod_percent }}%
|
||
</span>
|
||
<span class="daily-mobile-day__metric">
|
||
<span>業績</span>
|
||
<strong>${{ '{:,.0f}'.format(day.revenue) }}</strong>
|
||
</span>
|
||
<span class="daily-mobile-day__metric">
|
||
<span>毛利</span>
|
||
<strong>${{ '{:,.0f}'.format(day.profit) }}</strong>
|
||
<em>{{ '{:.1f}%'.format(day.margin_rate) }}</em>
|
||
</span>
|
||
<span class="daily-mobile-day__chips">
|
||
<span>SKU {{ '{:,.0f}'.format(day.sku_count) }}</span>
|
||
<span>客單 ${{ '{:,.0f}'.format(day.avg_price) }}</span>
|
||
<span>銷量 {{ '{:,.0f}'.format(day.qty) }}</span>
|
||
</span>
|
||
</a>
|
||
{% endif %}
|
||
{% endfor %}
|
||
{% endfor %}
|
||
{% if mobile_days.count == 0 %}
|
||
<div class="daily-mobile-empty">本月尚無可檢視的業績日期。</div>
|
||
{% endif %}
|
||
</div>
|
||
</section>
|
||
{% endif %}
|
||
|
||
<!-- ============ Charts Row 1 ============ -->
|
||
<div class="row mb-4">
|
||
<div class="col-lg-8">
|
||
<div class="card chart-card">
|
||
<div class="card-header"><i class="fas fa-chart-area"></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 chart-card">
|
||
<div class="card-header"><i class="fas fa-percentage"></i> 日成長率 (DoD %)</div>
|
||
<div class="card-body"><div class="chart-container"><canvas id="dodChart"></canvas></div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ============ Charts Row 2 ============ -->
|
||
<div class="row mb-4">
|
||
<div class="col-lg-8">
|
||
<div class="card chart-card">
|
||
<div class="card-header"><i class="fas fa-chart-bar"></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 chart-card">
|
||
<div class="card-header"><i class="fas fa-trophy"></i> 商品 Top 10</div>
|
||
<div class="card-body">
|
||
<div class="chart-mobile-hint d-md-none">
|
||
<i class="fas fa-hand-pointer"></i> 左右滑動查看完整圖表
|
||
</div>
|
||
<div class="chart-responsive">
|
||
<div class="chart-container" id="top10ChartContainer">
|
||
<canvas id="top10Chart"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ============ Marketing ============ -->
|
||
{% if marketing_data %}
|
||
<div class="row mb-4">
|
||
<div class="col-12">
|
||
<div class="card chart-card">
|
||
<header class="card-header card-header--split">
|
||
<span><i class="fas fa-bullhorn"></i> 行銷活動業績貢獻</span>
|
||
<button type="button" class="btn btn-sm btn-export" onclick="exportMarketingData()">
|
||
<i class="fas fa-file-excel"></i> 匯出 Excel
|
||
</button>
|
||
</header>
|
||
<div class="card-body">
|
||
<div class="row">
|
||
<div class="col-lg-6 mb-4">
|
||
<h6 class="marketing-subhead"><i class="fas fa-tags"></i> 折扣活動 Top 10</h6>
|
||
{% if marketing_data.discount %}
|
||
<div class="chart-container chart-container--md"><canvas id="discountChart"></canvas></div>
|
||
{% else %}
|
||
<div class="chart-empty"><i class="fas fa-info-circle"></i><p>暫無折扣活動數據</p></div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="col-lg-6 mb-4">
|
||
<h6 class="marketing-subhead"><i class="fas fa-ticket-alt"></i> 折價券活動 Top 10</h6>
|
||
{% if marketing_data.coupon %}
|
||
<div class="chart-container chart-container--md"><canvas id="couponChart"></canvas></div>
|
||
{% else %}
|
||
<div class="chart-empty"><i class="fas fa-info-circle"></i><p>暫無折價券活動數據</p></div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- ============ Category Table ============ -->
|
||
<div class="row">
|
||
<div class="col-12">
|
||
<div class="card chart-card">
|
||
<header class="card-header card-header--split">
|
||
<span><i class="fas fa-table"></i> 分類業績明細</span>
|
||
<button type="button" class="btn btn-sm btn-export" onclick="exportCategoryTable()">
|
||
<i class="fas fa-file-excel"></i> 匯出 Excel
|
||
</button>
|
||
</header>
|
||
<div class="card-body">
|
||
<div class="chart-mobile-hint d-md-none">
|
||
<i class="fas fa-hand-pointer"></i> 左右滑動查看完整列表
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table id="categoryTable" class="table table-hover">
|
||
<thead>
|
||
<tr>
|
||
{% for col in ['分類','廠商','總業績','總成本','毛利','毛利率','總銷量','SKU 數','平均單價'] %}
|
||
<th>{{ col }}</th>
|
||
{% endfor %}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for cat in categories %}
|
||
<tr>
|
||
<td>{{ cat.category }}</td>
|
||
<td>{{ cat.vendor or '-' }}</td>
|
||
<td class="num">${{ "{:,.0f}".format(cat.revenue) }}</td>
|
||
<td class="num">${{ "{:,.0f}".format(cat.cost or 0) }}</td>
|
||
<td class="num">${{ "{:,.0f}".format(cat.profit or 0) }}</td>
|
||
<td class="num">{{ "{:.1f}%".format(cat.margin_rate) }}</td>
|
||
<td class="num">{{ "{:,.0f}".format(cat.qty or 0) }}</td>
|
||
<td class="num">{{ cat.sku_count or 0 }}</td>
|
||
<td class="num">${{ "{:,.0f}".format(cat.avg_price) }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% endif %}
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.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 src="{{ url_for('static', filename='js/analysis-chart-theme.js') }}"></script>
|
||
{% if not error %}
|
||
<script>
|
||
window.__DAILY_SALES__ = {
|
||
chartData: {{ chart_data | tojson | safe }},
|
||
marketing: {
|
||
{% if marketing_data and marketing_data.discount %}
|
||
discount: {
|
||
labels: {{ marketing_data.discount | map(attribute='name') | list | tojson | safe }},
|
||
values: {{ marketing_data.discount | map(attribute='revenue') | list | tojson | safe }}
|
||
},
|
||
{% endif %}
|
||
{% if marketing_data and marketing_data.coupon %}
|
||
coupon: {
|
||
labels: {{ marketing_data.coupon | map(attribute='name') | list | tojson | safe }},
|
||
values: {{ marketing_data.coupon | map(attribute='revenue') | list | tojson | safe }}
|
||
}
|
||
{% endif %}
|
||
},
|
||
isMonthView: {{ 'true' if is_month_view else 'false' }}
|
||
};
|
||
</script>
|
||
<script src="{{ url_for('static', filename='js/page-daily-sales.js') }}"></script>
|
||
{% endif %}
|
||
{% endblock %}
|