Files
17168ERP/web/admin/pivot/pivot-01.aspx
2025-10-20 11:54:40 +08:00

1375 lines
64 KiB
Plaintext
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.
<%@ Page Title="法會報名統計分析" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" EnableEventValidation="false" CodeFile="pivot-01.aspx.cs" Inherits="admin_pivot_pivot01" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" runat="Server">
<link rel="stylesheet" href="../../js/_bootstrap-icons-1.8.1/bootstrap-icons.css">
<style>
/* 限定範圍:使用 #pivot01-app 選擇器 */
/* 欄位色彩標記 */
#pivot01-app .field-activity { border-left: 4px solid #ff9800; } /* 橘色 - 法會 */
#pivot01-app .field-follower { border-left: 4px solid #2196f3; } /* 藍色 - 信眾 */
#pivot01-app .field-merit { border-left: 4px solid #4caf50; } /* 綠色 - 功德 */
#pivot01-app .field-calculated { border-left: 4px solid #9c27b0; } /* 紫色 - 計算 */
/* 表格樣式 */
#pivot01-app .v-data-table thead th {
background-color: #f5f5f5;
font-weight: bold;
position: sticky;
top: 0;
z-index: 2;
}
#pivot01-app .v-data-table tbody tr:hover {
background-color: #e3f2fd;
}
/* 頁籤樣式 */
#pivot01-app .v-tabs {
margin-bottom: 20px;
}
/* 查詢條件區域 */
#pivot01-app .query-section {
background-color: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-top: 10px;
margin-bottom: 10px;
}
/* 資訊列樣式 - 預設使用灰色背景Tab3/Tab4*/
#pivot01-app .info-bar {
background-color: #f5f5f5;
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
}
/* 資訊列樣式 - Tab2/Tab5 使用漸層背景 */
#pivot01-app .info-bar.gradient {
background-color: #667eea;
color: white;
padding: 15px 20px;
margin: 10px 0;
}
#pivot01-app .info-bar .text-h6 {
margin-top: 4px;
}
/* 統計卡片 */
#pivot01-app .stat-card {
padding: 15px;
border-radius: 8px;
text-align: center;
}
#pivot01-app .stat-card .stat-value {
font-size: 1.8em;
font-weight: bold;
margin-bottom: 5px;
}
#pivot01-app .stat-card .stat-label {
font-size: 0.9em;
opacity: 0.8;
}
/* 載入動畫 */
#pivot01-app .loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
/* Tab2 樞紐分析表樣式 */
#pivot01-app .pivot-table .v-data-table__wrapper tbody tr:has(.font-weight-bold.primary--text) {
background-color: #f8f9fa;
}
#pivot01-app .pivot-detail-row {
background-color: #ffffff;
}
#pivot01-app .pivot-detail-row td {
border-left: 3px solid #4caf50;
padding-left: 12px;
}
/* 收合按鈕樣式 */
#pivot01-app .pivot-table .v-btn--icon.v-size--small {
width: 24px;
height: 24px;
}
#pivot01-app .pivot-table .v-btn--icon .v-icon {
font-size: 16px;
}
/* 總計行樣式 */
#pivot01-app .pivot-table tfoot tr {
background-color: #e3f2fd;
}
#pivot01-app .pivot-table tfoot td {
border-top: 2px solid #2196f3;
font-weight: bold;
}
/* 確保欄位對齊 */
#pivot01-app .pivot-table .v-data-table__wrapper table {
table-layout: fixed;
}
#pivot01-app .pivot-table .v-data-table__wrapper th:nth-child(1),
#pivot01-app .pivot-table .v-data-table__wrapper td:nth-child(1) {
width: 300px;
}
#pivot01-app .pivot-table .v-data-table__wrapper th:nth-child(2),
#pivot01-app .pivot-table .v-data-table__wrapper td:nth-child(2) {
width: 120px;
text-align: right;
}
#pivot01-app .pivot-table .v-data-table__wrapper th:nth-child(3),
#pivot01-app .pivot-table .v-data-table__wrapper td:nth-child(3) {
width: 150px;
text-align: right;
}
/* Tab4 橫向樞紐表樣式 */
#pivot01-app .pivot-table-horizontal {
overflow-x: auto;
}
/* 覆蓋全域的表頭固定設定 */
#pivot01-app .pivot-table-horizontal thead th {
position: relative;
top: auto;
}
#pivot01-app .pivot-table-horizontal .v-data-table__wrapper {
overflow-x: auto;
overflow-y: visible;
}
/* 固定第一欄(信眾姓名)*/
#pivot01-app .pivot-table-horizontal thead th:first-child {
position: sticky;
left: 0;
z-index: 5;
background-color: #f5f5f5;
box-shadow: 2px 0 4px rgba(0,0,0,0.1);
}
#pivot01-app .pivot-table-horizontal tbody td:first-child {
position: sticky;
left: 0;
z-index: 3;
background-color: #fff;
box-shadow: 2px 0 4px rgba(0,0,0,0.1);
font-weight: 500;
}
/* 固定第二欄(總計)*/
#pivot01-app .pivot-table-horizontal thead th:nth-child(2) {
position: sticky;
left: 150px;
z-index: 5;
background-color: #e8eaf6;
box-shadow: 2px 0 4px rgba(0,0,0,0.1);
}
#pivot01-app .pivot-table-horizontal tbody td:nth-child(2) {
position: sticky;
left: 150px;
z-index: 3;
background-color: #f5f5f5;
font-weight: bold;
box-shadow: 2px 0 4px rgba(0,0,0,0.1);
}
/* 零值淡化 */
#pivot01-app .grey--text {
color: #9e9e9e;
}
/* 表格包裝容器 */
#pivot01-app .table-wrapper {
overflow-x: auto;
position: relative;
}
</style>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="page_nav" runat="Server">
<h5 class="mb-0">
<i class="bi bi-pie-chart-fill"></i> 法會報名統計分析
</h5>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
<div id="pivot01-app"><v-app>
<!-- 載入遮罩 -->
<div v-if="loading" class="loading-overlay">
<v-progress-circular
:size="70"
:width="7"
color="primary"
indeterminate
></v-progress-circular>
</div>
<v-container fluid>
<!-- 頁籤導航 -->
<v-tabs v-model="activeTab" background-color="white" color="primary" grow>
<v-tab>
<v-icon left>mdi-calendar-search</v-icon>
法會選擇
</v-tab>
<v-tab :disabled="!hasData">
<v-icon left>mdi-tablet-dashboard</v-icon>
牌位分析
</v-tab>
<v-tab :disabled="!hasData">
<v-icon left>mdi-account-group</v-icon>
信眾報名分析
</v-tab>
<v-tab :disabled="!hasData">
<v-icon left>mdi-account-star</v-icon>
功德主查詢分析
</v-tab>
<v-tab :disabled="!hasData">
<v-icon left>mdi-table-large</v-icon>
報名明細資料
<v-chip v-if="rawData.length > 0" small class="ml-2">{{ rawData.length }}</v-chip>
</v-tab>
</v-tabs>
<!-- 頁籤內容 -->
<v-tabs-items v-model="activeTab">
<!-- Tab1: 法會選擇 -->
<v-tab-item>
<v-card>
<v-card-title class="primary white--text">
<v-icon left color="white">mdi-filter</v-icon>
查詢條件
</v-card-title>
<v-card-text>
<div class="query-section">
<v-row align="center">
<v-col cols="12" md="3">
<v-select
v-model="tab1.year"
:items="tab1.yearOptions"
label="查詢年份"
dense
outlined
prepend-icon="mdi-calendar"
hide-details
></v-select>
</v-col>
<v-col cols="12" md="2">
<v-btn
color="primary"
large
block
@click="loadActivities"
:loading="tab1.loading"
>
<v-icon left>mdi-magnify</v-icon>
查看
</v-btn>
</v-col>
<v-col cols="12" md="7">
<v-alert
v-if="tab1.activities.length > 0"
dense
text
type="info"
class="mb-0"
>
共查詢到 <strong>{{ tab1.activities.length }}</strong> 場法會活動
</v-alert>
</v-col>
</v-row>
</div>
<!-- 法會清單 -->
<v-data-table
:headers="tab1Headers"
:items="tab1.activities"
:loading="tab1.loading"
loading-text="載入中..."
class="elevation-1"
item-key="num"
:items-per-page="15"
:footer-props="{
'items-per-page-options': [10, 15, 20, 50]
}"
>
<template v-slot:item.開始日期="{ item }">
{{ formatDate(item.開始日期) }}
</template>
<template v-slot:item.結束日期="{ item }">
{{ formatDate(item.結束日期) }}
</template>
<template v-slot:item.報名截止日="{ item }">
{{ formatDate(item.報名截止日) }}
</template>
<template v-slot:item.功德數量="{ item }">
<v-chip small color="info" text-color="white">{{ item.功德數量 || 0 }}</v-chip>
</template>
<template v-slot:item.合計="{ item }">
<strong>{{ formatCurrency(item.合計) }}</strong>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
small
color="success"
@click="selectActivity(item)"
:loading="item.loading"
>
<v-icon left small>mdi-check-circle</v-icon>
選擇
</v-btn>
</template>
<template v-slot:no-data>
<v-alert type="info" class="ma-4">
請選擇年份後點擊「查看」按鈕查詢法會資料
</v-alert>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-tab-item>
<!-- Tab2: 牌位分析 -->
<v-tab-item>
<v-card>
<v-card-title class="info white--text">
<v-icon left color="white">mdi-tablet-dashboard</v-icon>
牌位分析
<v-spacer></v-spacer>
<v-chip v-if="tab1.selectedActivity" color="white" text-color="info">
{{ tab1.selectedActivity.活動名稱 }}
</v-chip>
</v-card-title>
<v-card-text>
<!-- 資訊列 -->
<div v-if="tab1.selectedActivity" class="info-bar gradient">
<v-row>
<v-col cols="12" md="6">
<div>
<v-icon color="white" left>mdi-chart-pie</v-icon>
<strong>樞紐分析表</strong>
</div>
<div class="mt-1">
<small>按功德類型和功德名稱分組統計</small>
</div>
</v-col>
<v-col cols="12" md="6" class="text-right">
<div>
項目總數: <strong>{{ tab2PivotData.filter(item => !item.isGroup).length }}</strong> 項
</div>
<div class="mt-1">
類型總數: <strong>{{ tab2PivotData.filter(item => item.isGroup).length }}</strong> 類
</div>
</v-col>
</v-row>
</div>
<!-- 樞紐分析表格 -->
<v-data-table
:headers="tab2.headers"
:items="tab2PivotData"
class="elevation-1 pivot-table"
hide-default-footer
disable-pagination
>
<!-- 功德項目欄位 -->
<template v-slot:item.name="{ item }">
<div :class="{'font-weight-bold primary--text': item.isGroup, 'ml-6': !item.isGroup}">
<!-- 群組項目:功德類型 -->
<template v-if="item.isGroup">
<v-btn @click="toggleGroup(item.id)" icon small class="mr-2">
<v-icon small>{{ tab2.collapsed.includes(item.id) ? 'mdi-chevron-right' : 'mdi-chevron-down' }}</v-icon>
</v-btn>
<v-icon left color="warning">mdi-tag</v-icon>
{{ item.name }}
</template>
<!-- 子項目:功德名稱 -->
<template v-else>
<v-icon left color="success" small>mdi-circle-small</v-icon>
{{ item.name }}
</template>
</div>
</template>
<!-- 數量總計欄位 -->
<template v-slot:item.totalQty="{ item }">
<span :class="{'font-weight-bold': item.isGroup}">
{{ item.totalQty.toLocaleString() }}
</span>
</template>
<!-- 金額總計欄位 -->
<template v-slot:item.totalAmount="{ item }">
<span :class="{'font-weight-bold': item.isGroup}">
{{ formatCurrency(item.totalAmount) }}
</span>
</template>
<!-- 總計 Footer -->
<template v-slot:foot>
<tfoot>
<tr>
<td class="font-weight-bold" style="width: 300px;">
<v-icon left color="primary">mdi-sigma</v-icon>
總計
</td>
<td class="text-right font-weight-bold" style="width: 120px;">{{ tab2GrandTotalQty.toLocaleString() }}</td>
<td class="text-right font-weight-bold" style="width: 150px;">{{ formatCurrency(tab2GrandTotalAmount) }}</td>
<td style="width: 48px;"></td>
</tr>
</tfoot>
</template>
<!-- 無資料提示 -->
<template v-slot:no-data>
<v-alert type="info" class="ma-4">
請先選擇法會以載入分析資料
</v-alert>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-tab-item>
<!-- Tab3: 信眾報名分析 -->
<v-tab-item>
<v-card>
<v-card-title class="success white--text mb-4">
<v-icon left color="white">mdi-account-multiple</v-icon>
信眾報名分析
<v-spacer></v-spacer>
<v-chip v-if="tab1.selectedActivity" color="white" text-color="success">
{{ tab1.selectedActivity.活動名稱 }}
</v-chip>
</v-card-title>
<v-card-text>
<!-- 搜尋列 -->
<v-row class="mb-4">
<v-col cols="12">
<v-text-field
v-model="tab3.search"
label="搜尋信眾(編號或姓名)"
prepend-icon="mdi-magnify"
clearable
outlined
dense
></v-text-field>
</v-col>
</v-row>
<!-- 統計資訊列 -->
<div v-if="hasData" class="info-bar mb-4">
<v-row>
<v-col cols="6" md="3">
<div><strong>總人數</strong></div>
<div class="text-h6 success--text">{{ tab3Summary.總人數 }} 人</div>
</v-col>
<v-col cols="6" md="3">
<div><strong>功德主數量</strong></div>
<div class="text-h6">{{ tab3Summary.功德主數量合計.toLocaleString() }}</div>
</v-col>
<v-col cols="6" md="3">
<div><strong>功德主金額</strong></div>
<div class="text-h6">{{ formatCurrency(tab3Summary.功德主金額合計) }}</div>
</v-col>
<v-col cols="6" md="3">
<div><strong>總金額</strong></div>
<div class="text-h6 primary--text">
{{ formatCurrency(tab3Summary.功德主金額合計 + tab3Summary.功德金額合計) }}
</div>
</v-col>
</v-row>
</div>
<!-- 信眾統計表格 -->
<v-data-table
:headers="tab3.headers"
:items="tab3FilteredData"
:items-per-page="20"
class="elevation-1"
>
<!-- 功德主金額欄位 -->
<template v-slot:item.功德主金額="{ item }">
<span :class="{'font-weight-bold success--text': item.功德主金額 > 0}">
{{ formatCurrency(item.功德主金額) }}
</span>
</template>
<!-- 功德金額欄位 -->
<template v-slot:item.功德金額="{ item }">
{{ formatCurrency(item.功德金額) }}
</template>
<!-- 無資料提示 -->
<template v-slot:no-data>
<v-alert type="info" class="ma-4">
{{ tab3.search ? '查無符合條件的信眾' : '請先選擇法會以載入分析資料' }}
</v-alert>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-tab-item>
<!-- Tab4: 功德主查詢分析 -->
<v-tab-item>
<v-card>
<v-card-title class="warning white--text mb-4">
<v-icon left color="white">mdi-table-pivot</v-icon>
功德主查詢分析
<v-spacer></v-spacer>
<v-chip v-if="tab1.selectedActivity" color="white" text-color="warning">
{{ tab1.selectedActivity.活動名稱 }}
</v-chip>
</v-card-title>
<v-card-text>
<!-- 搜尋與過濾列 -->
<v-row class="mt-2 mb-4 pt-4">
<!-- 姓名搜尋 -->
<v-col cols="12" md="3">
<v-text-field
v-model="tab4.search"
label="搜尋姓名"
prepend-icon="mdi-magnify"
hide-details
clearable
dense
></v-text-field>
</v-col>
<!-- 功德類型下拉 -->
<v-col cols="12" md="3">
<v-select
v-model="tab4.meritType"
:items="tab4MeritTypeOptions"
label="功德類型"
prepend-icon="mdi-tag"
hide-details
dense
></v-select>
</v-col>
<!-- 值類型切換 -->
<v-col cols="12" md="3" class="d-flex align-center">
<v-radio-group v-model="tab4.valueType" row dense hide-details class="mt-0">
<v-radio label="金額" value="amount"></v-radio>
<v-radio label="數量" value="quantity"></v-radio>
</v-radio-group>
</v-col>
<!-- 功德主過濾 -->
<v-col cols="12" md="3" class="d-flex align-center">
<v-checkbox
v-model="tab4.showMasterOnly"
label="只顯示功德主"
dense
hide-details
class="mt-0"
></v-checkbox>
</v-col>
</v-row>
<!-- 統計資訊 -->
<div v-if="hasData" class="info-bar mb-4">
<v-row>
<v-col cols="6">
<strong>信眾數:</strong> {{ tab4PivotData.length }} 人
</v-col>
<v-col cols="6">
<strong>功德項目:</strong> {{ tab4MeritItems.length }} 項
</v-col>
</v-row>
</div>
<!-- 樞紐分析表格 -->
<div class="table-wrapper">
<v-data-table
:headers="tab4Headers"
:items="tab4PivotData"
:items-per-page="15"
class="elevation-1 pivot-table-horizontal"
dense
>
<!-- 總計欄位強調 -->
<template v-slot:item.總計="{ item }">
<span class="font-weight-bold primary--text">
{{ tab4.valueType === 'amount' ? formatCurrency(item.總計) : item.總計.toLocaleString() }}
</span>
</template>
<!-- 功德項目欄位格式化 -->
<template v-for="meritItem in tab4MeritItems" v-slot:[`item.${meritItem}`]="{ item }">
<span :class="{'grey--text': item[meritItem] === 0}">
{{ tab4.valueType === 'amount' ? formatCurrency(item[meritItem]) : (item[meritItem] || '-') }}
</span>
</template>
<!-- 無資料提示 -->
<template v-slot:no-data>
<v-alert type="info" class="ma-4">
{{ tab4.search ? '查無符合條件的信眾' : '請先選擇法會以載入分析資料' }}
</v-alert>
</template>
</v-data-table>
</div>
</v-card-text>
</v-card>
</v-tab-item>
<!-- Tab5: 報名明細資料 -->
<v-tab-item>
<v-card>
<v-card-title class="purple white--text">
<v-icon left color="white">mdi-table-large</v-icon>
報名明細資料
<v-spacer></v-spacer>
<v-chip v-if="tab1.selectedActivity" color="white" text-color="purple">
{{ tab1.selectedActivity.活動名稱 }}
</v-chip>
</v-card-title>
<v-card-text>
<!-- 資訊列 -->
<div v-if="tab1.selectedActivity" class="info-bar gradient">
<v-row>
<v-col cols="12" md="6">
<div>
<v-icon color="white" left>mdi-calendar-range</v-icon>
<strong>{{ tab1.selectedActivity.活動名稱 }}</strong>
</div>
<div class="mt-1">
<small>
{{ formatDate(tab1.selectedActivity.開始日期) }} ~
{{ formatDate(tab1.selectedActivity.結束日期) }}
</small>
</div>
</v-col>
<v-col cols="12" md="6" class="text-right">
<div>
總筆數: <strong>{{ filteredData.length }}</strong> 筆
</div>
<div class="mt-1">
總金額: <strong>{{ formatCurrency(totalAmount) }}</strong>
</div>
</v-col>
</v-row>
</div>
<!-- 搜尋和匯出 -->
<v-row class="my-3">
<v-col cols="12" md="8">
<v-text-field
v-model="tab5.search"
label="搜尋(姓名、功德類型、功德名稱)"
prepend-icon="mdi-magnify"
clearable
dense
outlined
hide-details
></v-text-field>
</v-col>
<v-col cols="12" md="4">
<v-btn
color="success"
large
block
@click="exportToExcel"
:disabled="filteredData.length === 0"
>
<v-icon left>mdi-file-excel</v-icon>
匯出 Excel
</v-btn>
</v-col>
</v-row>
<!-- 報名明細表格 -->
<v-data-table
:headers="tab5Headers"
:items="filteredData"
:items-per-page="50"
class="elevation-1"
:footer-props="{
'items-per-page-options': [20, 50, 100, 200]
}"
>
<template v-slot:item.報名日期="{ item }">
{{ formatDate(item.報名日期) }}
</template>
<template v-slot:item.功德主="{ item }">
<v-chip :color="item.功德主 === '是' ? 'success' : 'default'" x-small>
{{ item.功德主 }}
</v-chip>
</template>
<template v-slot:item.金額="{ item }">
{{ formatCurrency(item.金額) }}
</template>
<template v-slot:item.已收="{ item }">
{{ formatCurrency(item.已收) }}
</template>
<template v-slot:item.未收="{ item }">
<span :class="item.未收 > 0 ? 'error--text font-weight-bold' : ''">
{{ formatCurrency(item.未收) }}
</span>
</template>
<template v-slot:no-data>
<v-alert type="info" class="ma-4">
無報名明細資料
</v-alert>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-tab-item>
</v-tabs-items>
</v-container>
</v-app>
</div>
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" runat="Server">
<!-- Master Page 已載入 Vue/Vuetify/Axios/Sweetalert2不需重複載入 -->
<script>
new Vue({
el: '#pivot01-app',
vuetify: new Vuetify(),
data() {
return {
// 當前頁籤
activeTab: 0,
// 全域載入狀態
loading: false,
// Tab1: 法會選擇
tab1: {
year: new Date().getFullYear(),
yearOptions: [],
loading: false,
activities: [],
selectedActivity: null
},
// Tab1 表頭
tab1Headers: [
{ text: '法會名稱', value: '活動名稱', width: 250 },
{ text: '活動類型', value: '活動詳細分類', width: 150 },
{ text: '開始日期', value: '開始日期', width: 120 },
{ text: '結束日期', value: '結束日期', width: 120 },
{ text: '報名截止', value: '報名截止日', width: 120 },
{ text: '報名數', value: '功德數量', width: 100, align: 'center' },
{ text: '總金額', value: '合計', width: 120, align: 'end' },
{ text: '操作', value: 'actions', width: 120, sortable: false, align: 'center' }
],
// 全域資料(所有 Tab 共用)
rawData: [],
// Tab2: 牌位分析
tab2: {
collapsed: [], // 收合的功德類型 ID 陣列
headers: [
{ text: '功德項目', value: 'name', width: 300 },
{ text: '數量總計', value: 'totalQty', width: 120, align: 'end' },
{ text: '金額總計', value: 'totalAmount', width: 150, align: 'end' }
]
},
// Tab3: 信眾報名分析
tab3: {
search: '',
headers: [
{ text: '信眾編號', value: '信眾編號', width: 120, class: 'field-follower' },
{ text: '信眾姓名', value: '信眾姓名', width: 120, class: 'field-follower' },
{ text: '功德主數量', value: '功德主數量', width: 120, align: 'end', class: 'field-merit' },
{ text: '功德主金額', value: '功德主金額', width: 150, align: 'end', class: 'field-merit' },
{ text: '功德數量', value: '功德數量', width: 120, align: 'end', class: 'field-calculated' },
{ text: '功德金額', value: '功德金額', width: 150, align: 'end', class: 'field-calculated' }
]
},
// Tab4: 功德主查詢分析
tab4: {
search: '',
meritType: '全部',
valueType: 'amount', // 'amount' 或 'quantity'
showMasterOnly: false
},
// Tab5: 報名明細
tab5: {
search: '',
headers: [
{ text: '信眾姓名', value: '信眾姓名', width: 100, class: 'field-follower' },
{ text: '報名日期', value: '報名日期', width: 120, class: 'field-follower' },
{ text: '功德類型', value: '功德類型', width: 120, class: 'field-merit' },
{ text: '功德名稱', value: '功德名稱', width: 180, class: 'field-merit' },
{ text: '功德主', value: '功德主', width: 80, align: 'center', class: 'field-merit' },
{ text: '數量', value: '數量', width: 80, align: 'end', class: 'field-calculated' },
{ text: '金額', value: '金額', width: 100, align: 'end', class: 'field-calculated' },
{ text: '已收', value: '已收', width: 100, align: 'end', class: 'field-calculated' },
{ text: '未收', value: '未收', width: 100, align: 'end', class: 'field-calculated' }
]
}
};
},
computed: {
// Tab5 表頭
tab5Headers() {
return this.tab5.headers;
},
// 是否有資料Tab2-5 可用)
hasData() {
return this.rawData && this.rawData.length > 0;
},
// Tab2: 樞紐分析表資料
tab2PivotData() {
if (!this.rawData || this.rawData.length === 0) return [];
// Step 1: 按功德類型分組
const typeGroups = {};
this.rawData.forEach(row => {
const type = row.功德類型 || '未分類';
const name = row.功德名稱 || '未命名';
const qty = parseInt(row.數量) || 0;
const amount = parseFloat(row.金額) || 0;
if (!typeGroups[type]) {
typeGroups[type] = {
name: type,
isGroup: true,
totalQty: 0,
totalAmount: 0,
children: {},
id: type // 用於展開/收合的唯一標識
};
}
// Step 2: 按功德名稱分組
if (!typeGroups[type].children[name]) {
typeGroups[type].children[name] = {
name: name,
isGroup: false,
totalQty: 0,
totalAmount: 0,
parentType: type,
id: type + '_' + name
};
}
// Step 3: 累計統計
typeGroups[type].children[name].totalQty += qty;
typeGroups[type].children[name].totalAmount += amount;
typeGroups[type].totalQty += qty;
typeGroups[type].totalAmount += amount;
});
// Step 4: 轉換為表格資料(根據收合狀態過濾)
const result = [];
Object.values(typeGroups).forEach(group => {
result.push(group);
// 只有當群組未收合時才顯示子項目
if (!this.tab2.collapsed.includes(group.id)) {
Object.values(group.children).forEach(child => {
result.push(child);
});
}
});
return result;
},
// Tab2: 總計數量
tab2GrandTotalQty() {
return this.rawData.reduce((sum, row) => {
return sum + (parseInt(row.數量) || 0);
}, 0);
},
// Tab2: 總計金額
tab2GrandTotalAmount() {
return this.rawData.reduce((sum, row) => {
return sum + (parseFloat(row.金額) || 0);
}, 0);
},
// Tab3: 樞紐分析表資料
tab3PivotData() {
if (!this.rawData || this.rawData.length === 0) return [];
// Step 1: 按信眾分組
const followerGroups = {};
this.rawData.forEach(row => {
const fNumber = row.信眾編號 || '';
const fName = row.信眾姓名 || '';
const key = fNumber + '_' + fName;
const qty = parseInt(row.數量) || 0;
const amount = parseFloat(row.金額) || 0;
const isMaster = row.功德主 === '是';
if (!followerGroups[key]) {
followerGroups[key] = {
信眾編號: fNumber,
信眾姓名: fName,
功德主數量: 0,
功德主金額: 0,
功德數量: 0,
功德金額: 0
};
}
// Step 2: 累計統計
if (isMaster) {
followerGroups[key].功德主數量 += qty;
followerGroups[key].功德主金額 += amount;
} else {
followerGroups[key].功德數量 += qty;
followerGroups[key].功德金額 += amount;
}
});
// Step 3: 轉換為陣列並排序
return Object.values(followerGroups)
.sort((a, b) => {
// 依總金額降序,再依姓名升序
const amountDiff = (b.功德主金額 + b.功德金額) - (a.功德主金額 + a.功德金額);
return amountDiff !== 0 ? amountDiff : a.信眾姓名.localeCompare(b.信眾姓名, 'zh-TW');
});
},
// Tab3: 搜尋過濾
tab3FilteredData() {
if (!this.tab3.search) return this.tab3PivotData;
const keyword = this.tab3.search.toLowerCase();
return this.tab3PivotData.filter(row => {
return row.信眾編號.toLowerCase().includes(keyword) ||
row.信眾姓名.toLowerCase().includes(keyword);
});
},
// Tab3: 統計摘要
tab3Summary() {
const data = this.tab3FilteredData;
return {
總人數: data.length,
功德主數量合計: data.reduce((sum, row) => sum + row.功德主數量, 0),
功德主金額合計: data.reduce((sum, row) => sum + row.功德主金額, 0),
功德數量合計: data.reduce((sum, row) => sum + row.功德數量, 0),
功德金額合計: data.reduce((sum, row) => sum + row.功德金額, 0)
};
},
// Tab4: 功德類型選項
tab4MeritTypeOptions() {
if (!this.rawData || this.rawData.length === 0) return ['全部'];
const types = new Set(this.rawData.map(row => row.功德類型).filter(Boolean));
return ['全部', ...Array.from(types).sort()];
},
// Tab4: 過濾後的原始資料
tab4FilteredRawData() {
let data = this.rawData;
// 功德主過濾
if (this.tab4.showMasterOnly) {
data = data.filter(row => row.功德主 === '是');
}
// 功德類型過濾
if (this.tab4.meritType !== '全部') {
data = data.filter(row => row.功德類型 === this.tab4.meritType);
}
// 姓名過濾
if (this.tab4.search) {
const keyword = this.tab4.search.toLowerCase();
data = data.filter(row =>
row.信眾姓名 && row.信眾姓名.toLowerCase().includes(keyword)
);
}
return data;
},
// Tab4: 取得功德項目清單(橫向欄位)
tab4MeritItems() {
const items = new Set(
this.tab4FilteredRawData.map(row => row.功德名稱).filter(Boolean)
);
return Array.from(items).sort();
},
// Tab4: 動態表頭
tab4Headers() {
const headers = [
{ text: '信眾姓名', value: '信眾姓名', width: 150, fixed: true, class: 'field-follower sticky-col' }
];
// 總計欄
const valueLabel = this.tab4.valueType === 'amount' ? '總金額' : '總數量';
headers.push({
text: valueLabel,
value: '總計',
width: 120,
align: 'end',
class: 'field-calculated font-weight-bold'
});
// 功德項目欄(動態)
this.tab4MeritItems.forEach(item => {
headers.push({
text: item,
value: item,
width: 100,
align: 'end',
class: 'field-merit'
});
});
return headers;
},
// Tab4: 樞紐分析資料
tab4PivotData() {
if (!this.tab4FilteredRawData || this.tab4FilteredRawData.length === 0) return [];
// Step 1: 按信眾和功德項目分組
const followerMeritMap = {};
this.tab4FilteredRawData.forEach(row => {
const fName = row.信眾姓名;
const mName = row.功德名稱;
const qty = parseInt(row.數量) || 0;
const amount = parseFloat(row.金額) || 0;
if (!followerMeritMap[fName]) {
followerMeritMap[fName] = { 信眾姓名: fName };
}
if (!followerMeritMap[fName][mName]) {
followerMeritMap[fName][mName] = { qty: 0, amount: 0 };
}
followerMeritMap[fName][mName].qty += qty;
followerMeritMap[fName][mName].amount += amount;
});
// Step 2: 轉換為表格格式
const result = Object.values(followerMeritMap).map(follower => {
const row = { 信眾姓名: follower.信眾姓名 };
let total = 0;
// 計算每個功德項目的值
this.tab4MeritItems.forEach(item => {
if (follower[item]) {
const value = this.tab4.valueType === 'amount'
? follower[item].amount
: follower[item].qty;
row[item] = value;
total += value;
} else {
row[item] = 0;
}
});
row.總計 = total;
return row;
});
// Step 3: 依總計降序排列
return result.sort((a, b) => b.總計 - a.總計);
},
// Tab5: 過濾後的資料
filteredData() {
if (!this.tab5.search) return this.rawData;
const keyword = this.tab5.search.toLowerCase();
return this.rawData.filter(row => {
return (
(row.信眾姓名 && row.信眾姓名.toLowerCase().includes(keyword)) ||
(row.功德類型 && row.功德類型.toLowerCase().includes(keyword)) ||
(row.功德名稱 && row.功德名稱.toLowerCase().includes(keyword))
);
});
},
// Tab5: 總金額
totalAmount() {
return this.filteredData.reduce((sum, row) => {
return sum + (row.金額 * row.數量);
}, 0);
}
},
methods: {
/**
* 初始化年份選項
*/
initYearOptions() {
const currentYear = new Date().getFullYear();
this.tab1.yearOptions = [];
for (let i = -5; i <= 5; i++) {
this.tab1.yearOptions.push(currentYear + i);
}
},
/**
* 切換群組的收合/展開狀態
*/
toggleGroup(groupId) {
const index = this.tab2.collapsed.indexOf(groupId);
if (index > -1) {
// 已收合,展開它
this.tab2.collapsed.splice(index, 1);
} else {
// 未收合,收合它
this.tab2.collapsed.push(groupId);
}
},
/**
* 查詢法會清單
*/
async loadActivities() {
this.tab1.loading = true;
this.tab1.activities = [];
try {
const response = await axios.get(HTTP_HOST + 'api/pivot01/activity_stats', {
params: { year: this.tab1.year }
});
if (response.data.success) {
this.tab1.activities = response.data.data;
if (this.tab1.activities.length === 0) {
Swal.fire({
icon: 'info',
title: '查無資料',
text: `${this.tab1.year} 年度查無法會資料`,
timer: 2000
});
}
} else {
throw new Error(response.data.message || '查詢失敗');
}
} catch (error) {
console.error('載入法會清單失敗:', error);
Swal.fire({
icon: 'error',
title: '查詢失敗',
text: error.response?.data?.Message || error.message || '查詢失敗,請稍後再試'
});
} finally {
this.tab1.loading = false;
}
},
/**
* 選擇法會並載入完整資料
*/
async selectActivity(activity) {
this.tab1.selectedActivity = activity;
this.loading = true;
try {
const response = await axios.get(HTTP_HOST + 'api/pivot01/registration_details', {
params: {
activityNum: activity.num
}
});
if (response.data.success) {
this.rawData = response.data.data;
// 自動切換到 Tab5報名明細
this.activeTab = 4;
Swal.fire({
icon: 'success',
title: '載入成功',
text: `已載入 ${this.rawData.length} 筆報名資料`,
timer: 2000,
showConfirmButton: false
});
} else {
throw new Error(response.data.message || '載入失敗');
}
} catch (error) {
console.error('載入報名資料失敗:', error);
Swal.fire({
icon: 'error',
title: '載入失敗',
text: error.response?.data?.Message || error.message || '載入失敗,請稍後再試'
});
} finally {
this.loading = false;
}
},
/**
* 匯出 ExcelCSV 格式)
*/
exportToExcel() {
if (!this.filteredData || this.filteredData.length === 0) {
Swal.fire('提示', '沒有資料可匯出', 'warning');
return;
}
try {
// 顯示載入訊息
Swal.fire({
title: '匯出中...',
text: '請稍候',
icon: 'info',
showConfirmButton: false,
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
}
});
// 準備匯出資料
const exportData = this.filteredData.map(item => ({
'法會名稱': item.法會名稱,
'信眾編號': item.信眾編號,
'信眾姓名': item.信眾姓名,
'報名日期': this.formatDateTime(item.報名日期),
'功德類型': item.功德類型,
'功德名稱': item.功德名稱,
'功德主': item.功德主,
'數量': item.數量,
'金額': item.金額,
'已收': item.已收,
'未收': item.未收
}));
// 轉換為 CSV
const headers = Object.keys(exportData[0]);
const csvRows = [
headers.join(','),
...exportData.map(row => {
return headers.map(h => {
let value = row[h];
// 信眾編號強制為文字(使用 ="值" 格式)
if (h === '信眾編號') {
return `"=""${value}"""`;
}
// 一般文字欄位處理
if (typeof value === 'string') {
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}
return value !== null && value !== undefined ? value : '';
}).join(',');
})
];
const csv = csvRows.join('\n');
// 下載檔案
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
const fileName = `報名明細_${this.tab1.selectedActivity.活動名稱}_${new Date().getTime()}.csv`;
link.setAttribute('href', url);
link.setAttribute('download', fileName);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 直接關閉載入對話框,不顯示完成提示
Swal.close();
} catch (error) {
console.error('匯出失敗:', error);
Swal.fire('錯誤', `匯出失敗: ${error.message}`, 'error');
}
},
/**
* 格式化金額
*/
formatCurrency(amount) {
if (amount === null || amount === undefined) return '-';
return new Intl.NumberFormat('zh-TW', {
style: 'currency',
currency: 'TWD',
minimumFractionDigits: 0
}).format(amount);
},
/**
* 格式化日期
*/
formatDate(date) {
if (!date) return '-';
const d = new Date(date);
if (isNaN(d.getTime())) return '-';
return d.toLocaleDateString('zh-TW', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
},
/**
* 格式化日期時間
*/
formatDateTime(datetime) {
if (!datetime) return '-';
const d = new Date(datetime);
if (isNaN(d.getTime())) return '-';
return d.toLocaleString('zh-TW', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
},
mounted() {
console.log('Pivot-01 App 已載入');
console.log('toggleGroup method exists:', typeof this.toggleGroup === 'function');
// 初始化年份選項
this.initYearOptions();
// 頁面載入時自動查詢當年度法會
this.loadActivities();
}
});
</script>
</asp:Content>