Files
17168ERP/web/admin/transfer/group_reconcile.aspx
2025-08-29 01:27:25 +08:00

682 lines
26 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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="group_reconcile.aspx.cs" Inherits="admin_transfer_group_reconcile" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" runat="Server">
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="page_nav" runat="Server">
<h5 class="mb-0">共同 - 沖帳流程</h5>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
<div id="group-reconcile-app">
<v-app>
<v-container>
<v-card>
<v-card-title class="bg-primary white--text text-center">
<h5 class="mb-0">共同 - 沖帳流程</h5>
</v-card-title>
<v-card-text>
<v-data-table
:headers="headers"
:items="items"
:loading="loading"
loading-text="載入中..."
class="elevation-1"
item-key="id"
>
<template v-slot:item.follower="{ item }">
<span :title="item.f_num">{{ item.follower }}</span>
</template>
<template v-slot:item.activity_info="{ item }">
<div>
<div class="font-weight-bold">{{ item.activity_name }}</div>
<div class="caption text--secondary">
<span v-if="hasFollowerList(item.draft)" style="margin-right: 4px;">👥</span>
{{ item.acc_name }}
</div>
</div>
</template>
<template v-slot:item.check_date="{ item }">
<span>{{ item.check_date | date }}</span>
</template>
<template v-slot:item.check_memo="{ item }">
<span>{{ item.check_memo }}</span>
</template>
<template v-slot:item.verify_note="{ item }">
<span>{{ item.verify_note }}</span>
</template>
<template v-slot:item.follower_selection="{ item }">
<div>
<v-btn
small
color="primary"
outlined
@click="showFollowerSelection(item)"
class="mb-1"
>
<v-icon left small>mdi-account-multiple</v-icon>
選擇支付人
</v-btn>
<div v-if="hasFollowerList(item.draft)" class="caption text-success mt-1">
<v-icon small color="success">mdi-check-circle</v-icon>
已選擇 {{ getFollowerCount(item.draft) }} 位
</div>
</div>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
small
color="success"
@click="showGroupReconcileDialog(item)"
:disabled="!hasFollowerList(item.draft)"
>
<v-icon left small>mdi-receipt</v-icon>
{{ hasFollowerList(item.draft) ? '共同沖帳' : '請先選擇支付人' }}
</v-btn>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-container>
<!-- 選擇共同支付人對話框 -->
<v-dialog v-model="followerDialog.show" max-width="800px">
<v-card>
<v-card-title class="grey lighten-2">
<v-icon color="primary" class="mr-2">mdi-account-multiple</v-icon>
選擇共同支付人
<v-spacer></v-spacer>
<v-btn icon @click="followerDialog.show = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="mt-4">
<div v-if="followerDialog.selected">
<h6 class="mb-3">法會:{{ followerDialog.selected.activity_name }}</h6>
<v-text-field
v-model="followerDialog.search"
label="搜尋信眾"
dense
outlined
prepend-inner-icon="mdi-magnify"
@input="searchActivityFollowers"
></v-text-field>
<v-data-table
:headers="followerDialog.headers"
:items="followerDialog.items"
:loading="followerDialog.loading"
item-key="f_num"
class="elevation-1 mt-2"
show-select
v-model="followerDialog.selectedFollowers"
hide-default-footer
:disable-pagination="true"
>
<template v-slot:item.follower_name="{ item }">
<div>
<div class="font-weight-bold">{{ item.follower_name }}</div>
<div class="caption text--secondary">編號: {{ item.f_num }}</div>
</div>
</template>
<template v-slot:item.total_due="{ item }">
<span class="text-danger">{{ item.total_due | currency }}</span>
</template>
<template v-slot:item.item_count="{ item }">
<span class="text-info">{{ item.item_count }} 項</span>
</template>
</v-data-table>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn color="grey" @click="followerDialog.show = false">取消</v-btn>
<v-btn color="primary" @click="confirmFollowerSelection" :disabled="followerDialog.selectedFollowers.length === 0">
確認選擇 ({{ followerDialog.selectedFollowers.length }})
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 共同沖帳明細對話框 -->
<v-dialog v-model="reconcileDialog.show" max-width="1200px">
<v-card>
<v-card-title class="grey lighten-2">
<v-icon color="success" class="mr-2">mdi-receipt</v-icon>
共同沖帳明細
<v-spacer></v-spacer>
<v-btn icon @click="reconcileDialog.show = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="mb-0 pb-0">
<div v-if="reconcileDialog.selected">
<div class="row my-2">
<h6 class="col">共同支付人:{{ getSelectedFollowersText() }}</h6>
<div class="col">
<span class="font-weight-bold">入帳金額:</span>
<span class="text-primary">{{ reconcileDialog.selected.check_amount | currency }}</span>
</div>
<div class="col">
<span class="font-weight-bold">已沖金額:</span>
<span :class="{'text-danger': isOverPaid, 'text-dark': !isOverPaid}">{{ sumReconcile | currency }}</span>
</div>
<div class="col">
<span class="font-weight-bold">未繳餘款:</span>
<span class="text-danger">{{ remainDue | currency }}</span>
</div>
<div class="col">
<span class="font-weight-bold">入帳後餘額:</span>
<span class="text-success">{{ overPaid | currency }}</span>
</div>
</div>
<v-data-table
:headers="reconcileDialog.headers"
:items="reconcileDialog.items"
class="elevation-1 mt-3"
hide-default-footer
:disable-pagination="true"
>
<template v-slot:item.follower_info="{ item }">
<div>
<div class="font-weight-bold text-primary">{{ item.follower_name }}</div>
<div class="text-muted" style="font-size:12px;">{{ item.order_no }}</div>
</div>
</template>
<template v-slot:item.activity_name="{ item }">
<div>
<div>{{ item.activity_name }}</div>
<div class="text-muted" style="font-size:12px;">{{ item.reg_time | date }}</div>
</div>
</template>
<template v-slot:item.paid="{ item }">
<span>{{ item.paid | currency }}</span>
</template>
<template v-slot:item.due="{ item }">
<span>{{ item.due | currency }}</span>
</template>
<template v-slot:item.reconcile="{ item, index }">
<v-text-field
v-model="item.reconcile"
type="number"
dense
outlined
hide-details="auto"
:rules="[
v => !isNaN(Number(v)) || '請輸入數字',
v => Number(v) >= 0 || '不可小於 0',
v => Number(v) <= Number(item.due) || `不可大於待繳金額 ${item.due}`
]"
@blur="validateAndUpdateReconcile($event.target.value, item, index)"
></v-text-field>
</template>
</v-data-table>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<div>
<div v-if="hasUnallocated && remainDue > 0" class="text-dark mb-2">
⚠️ 尚有未分配的入帳金額,請確認是否全部分配
</div>
<div v-if="hasUnallocated && remainDue === 0" class="text-info mb-2">
已無未繳項目,剩餘入帳金額將成為餘額
</div>
<div v-if="!canConfirm && buttonErrorMessage" class="text-danger">
❌ {{ buttonErrorMessage }}
</div>
</div>
<v-spacer></v-spacer>
<v-btn color="info" @click="redistributeReconcile" class="mr-2">重新分配</v-btn>
<v-btn color="orange" @click="saveDraft" class="mr-2">暫存</v-btn>
<v-btn color="primary" @click="confirmGroupReconcile" :disabled="!canConfirm">確認沖帳</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-app>
</div>
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" runat="Server">
<script src="draft-utils.js"></script>
<script>
// 確保 DraftUtils 已載入
if (typeof window.DraftUtils === 'undefined') {
console.warn('DraftUtils 未載入,使用備用方案');
window.DraftUtils = {
hasFollowerList: function(draft) { return false; },
getFollowerList: function(draft) { return []; },
updateFollowerList: function(draft, list) {
return { follower_list: list };
},
getDraftField: function(draft, field) { return null; },
updateDraftField: function(draft, field, value) {
return { [field]: value };
}
};
}
new Vue({
el: '#group-reconcile-app',
vuetify: new Vuetify(),
data() {
return {
loading: false,
headers: [
{ text: '匯款人', value: 'follower' },
{ text: '法會/入帳帳戶', value: 'activity_info' },
{ text: '入帳日期', value: 'check_date' },
{ text: '帳簿備註', value: 'check_memo' },
{ text: '入帳金額', value: 'check_amount' },
{ text: '核對記錄', value: 'verify_note' },
{ text: '選擇共同支付人', value: 'follower_selection', sortable: false },
{ text: '共同沖帳', value: 'actions', sortable: false }
],
items: [],
followerDialog: {
show: false,
selected: null,
search: '',
loading: false,
items: [],
selectedFollowers: [],
headers: [
{ text: '信眾', value: 'follower_name' },
{ text: '待繳金額', value: 'total_due' },
{ text: '項目數', value: 'item_count' }
]
},
reconcileDialog: {
show: false,
selected: null,
headers: [
{ text: '信眾/報名單號', value: 'follower_info' },
{ text: '法會/報名日期', value: 'activity_name' },
{ text: '項目', value: 'actitem_name' },
{ text: '應繳金額', value: 'price', sortable: false },
{ text: '已繳金額', value: 'paid', sortable: false },
{ text: '待繳金額', value: 'due', sortable: false },
{ text: '沖帳金額', value: 'reconcile', sortable: false }
],
items: [],
loading: false,
errorMessage: ''
},
hasUnallocated: false,
draftDataChanged: false
}
},
computed: {
sumReconcile() {
return this.reconcileDialog.items.reduce((sum, item) => sum + (Number(item.reconcile) || 0), 0);
},
remainDue() {
const totalDue = this.reconcileDialog.items.reduce((sum, item) => sum + (Number(item.due) || 0), 0);
return totalDue - this.sumReconcile;
},
overPaid() {
if (!this.reconcileDialog.selected) return 0;
return this.reconcileDialog.selected.check_amount - this.sumReconcile;
},
isOverPaid() {
if (!this.reconcileDialog.selected) return false;
return this.sumReconcile > this.reconcileDialog.selected.check_amount;
},
canConfirm() {
if (!this.reconcileDialog.selected) return false;
const hasReconcile = this.reconcileDialog.items.some(item => Number(item.reconcile) > 0);
if (!hasReconcile) return false;
const validAmounts = this.reconcileDialog.items.every(item => {
const amount = Number(item.reconcile) || 0;
return amount >= 0 && amount <= Number(item.due);
});
if (!validAmounts) return false;
if (this.sumReconcile > this.reconcileDialog.selected.check_amount) return false;
if (this.hasUnallocated && this.remainDue > 0) return false;
return true;
},
buttonErrorMessage() {
if (!this.reconcileDialog.selected) return '';
const hasReconcile = this.reconcileDialog.items.some(item => Number(item.reconcile) > 0);
if (!hasReconcile) return '請至少輸入一筆沖帳金額';
const invalidItem = this.reconcileDialog.items.find(item => {
const amount = Number(item.reconcile) || 0;
return amount < 0 || amount > Number(item.due);
});
if (invalidItem) return '沖帳金額超出可沖帳範圍';
if (this.sumReconcile > this.reconcileDialog.selected.check_amount) {
return '已沖金額不可大於入帳金額';
}
if (this.hasUnallocated && this.remainDue > 0) {
return '尚有未繳項目,請將入帳金額完全分配';
}
return '';
}
},
mounted() {
this.loadTableData();
},
filters: {
currency(val) {
if (!val) return '0';
return Number(val).toLocaleString();
},
date(val) {
if (!val) return '';
const date = new Date(val);
return date.toLocaleDateString();
}
},
methods: {
// 檢查是否有 follower_list
hasFollowerList(draft) {
return window.DraftUtils && window.DraftUtils.hasFollowerList && window.DraftUtils.hasFollowerList(draft);
},
// 取得已選擇的支付人數量
getFollowerCount(draft) {
if (!window.DraftUtils || !window.DraftUtils.getFollowerList) return 0;
const followerList = window.DraftUtils.getFollowerList(draft);
return followerList ? followerList.length : 0;
},
loadTableData() {
this.loading = true;
axios.get('../../api/transfer_register/group_reconcile_list')
.then(res => {
this.items = res.data;
})
.catch(() => {
this.items = [];
})
.finally(() => {
this.loading = false;
});
},
showGroupReconcileDialog(item) {
// 檢查是否已有選擇的共同支付人
const followerList = window.DraftUtils.getFollowerList(item.draft);
if (followerList && followerList.length > 0) {
// 直接顯示沖帳明細
this.showReconcileDetail(item, followerList);
} else {
// 先選擇共同支付人
this.showFollowerSelection(item);
}
},
showFollowerSelection(item) {
this.followerDialog.selected = item;
this.followerDialog.selectedFollowers = [];
this.followerDialog.items = [];
this.followerDialog.show = true;
this.searchActivityFollowers();
},
searchActivityFollowers() {
if (!this.followerDialog.selected || !this.followerDialog.selected.activity_num) return;
this.followerDialog.loading = true;
axios.get('../../api/transfer_register/activity_followers', {
params: { activity_num: this.followerDialog.selected.activity_num }
})
.then(res => {
let items = res.data;
// 如果有搜尋關鍵字,過濾結果
if (this.followerDialog.search && this.followerDialog.search.trim()) {
const keyword = this.followerDialog.search.trim().toLowerCase();
items = items.filter(item =>
item.follower_name.toLowerCase().includes(keyword)
);
}
this.followerDialog.items = items;
})
.catch(() => {
this.followerDialog.items = [];
})
.finally(() => {
this.followerDialog.loading = false;
});
},
confirmFollowerSelection() {
if (this.followerDialog.selectedFollowers.length === 0) {
alert('請選擇至少一位共同支付人');
return;
}
if (!this.followerDialog.selected) {
alert('系統錯誤:未選擇記錄');
return;
}
// 儲存選擇的共同支付人到 draft
const followerList = this.followerDialog.selectedFollowers.map(f => ({
f_num: f.f_num,
f_name: f.follower_name,
activity_num: this.followerDialog.selected.activity_num
}));
const draftData = window.DraftUtils.updateFollowerList(this.followerDialog.selected.draft, followerList);
this.updateFollowerDraftToDB(this.followerDialog.selected.id, draftData)
.then(() => {
this.followerDialog.selected.draft = JSON.stringify(draftData);
this.followerDialog.show = false;
// 重新載入資料以更新顯示
this.loadTableData();
// 顯示沖帳明細
this.showReconcileDetail(this.followerDialog.selected, followerList);
})
.catch(err => {
alert('儲存失敗,請重試');
console.error(err);
});
},
showReconcileDetail(item, followerList) {
this.reconcileDialog.selected = item;
this.reconcileDialog.items = [];
this.reconcileDialog.show = true;
// 取得所選信眾的訂單明細
const fNums = followerList.map(f => f.f_num).join(',');
axios.get('../../api/transfer_register/group_follower_orders', {
params: {
activity_num: item.activity_num,
f_nums: fNums
}
})
.then(res => {
this.reconcileDialog.items = res.data.map(item => ({
...item,
reconcile: 0 // 初始化沖帳金額
}));
this.autoDistributeReconcile();
})
.catch(() => {
this.reconcileDialog.items = [];
this.updateSumReconcile();
});
},
getSelectedFollowersText() {
if (!this.reconcileDialog.selected) return '';
const followerList = window.DraftUtils.getFollowerList(this.reconcileDialog.selected.draft);
return followerList.map(f => f.f_name).join('、');
},
autoDistributeReconcile() {
// 先進先出分配沖帳金額
let remainAmount = this.reconcileDialog.selected ? this.reconcileDialog.selected.check_amount : 0;
// 先將所有項目的 reconcile 清為 0
this.reconcileDialog.items.forEach(item => {
this.$set(item, 'reconcile', 0);
});
// 依報名日期排序(先進先出)
const sortedItems = [...this.reconcileDialog.items].sort((a, b) =>
new Date(a.reg_time) - new Date(b.reg_time)
);
// 逐項分配
sortedItems.forEach(item => {
if (remainAmount > 0) {
const canPay = Math.min(item.due, remainAmount);
const originalItem = this.reconcileDialog.items.find(i => i === item);
if (originalItem) {
this.$set(originalItem, 'reconcile', canPay);
}
remainAmount -= canPay;
}
});
this.updateSumReconcile();
},
redistributeReconcile() {
this.autoDistributeReconcile();
},
updateSumReconcile() {
const maxTotal = this.reconcileDialog.selected ? (this.reconcileDialog.selected.check_amount || 0) : 0;
this.hasUnallocated = maxTotal > this.sumReconcile;
},
saveDraft() {
try {
// 組成新的資料
const followerList = window.DraftUtils.getFollowerList(this.reconcileDialog.selected.draft);
const reconcileData = this.reconcileDialog.items
.filter(item => Number(item.reconcile) > 0)
.map(item => ({
f_num: item.f_num,
follower_name: item.follower_name,
pro_order_detail_num: item.num,
reconcile: Number(item.reconcile)
}));
const draftData = {
transfer_draft: [],
pro_order_detail_items: reconcileData,
follower_list: followerList
};
this.updateDraftToDB(this.reconcileDialog.selected.id, draftData)
.then(() => {
alert('暫存成功!');
this.reconcileDialog.selected.draft = JSON.stringify(draftData);
});
} catch (e) {
alert('暫存失敗:資料格式錯誤');
}
},
updateFollowerDraftToDB(id, draftData) {
const selectedItem = this.followerDialog.selected;
const updateData = {
id: id,
activity_num: selectedItem.activity_num,
name: selectedItem.name,
phone: selectedItem.phone,
pay_type: selectedItem.pay_type,
account_last5: selectedItem.account_last5,
amount: selectedItem.amount,
pay_mode: selectedItem.pay_mode,
note: selectedItem.note,
proof_img: selectedItem.proof_img,
status: selectedItem.status,
f_num_match: selectedItem.f_num_match,
f_num: selectedItem.f_num,
acc_num: selectedItem.acc_num,
check_date: selectedItem.check_date,
check_amount: selectedItem.check_amount,
check_memo: selectedItem.check_memo,
check_status: selectedItem.check_status,
acc_kind: selectedItem.acc_kind,
member_num: selectedItem.member_num,
verify_time: selectedItem.verify_time,
verify_note: selectedItem.verify_note,
draft: JSON.stringify(draftData)
};
return axios.put(`../../api/transfer_register/${id}`, updateData);
},
updateDraftToDB(id, draftData) {
const updateData = {
id: id,
activity_num: this.reconcileDialog.selected.activity_num,
name: this.reconcileDialog.selected.name,
phone: this.reconcileDialog.selected.phone,
pay_type: this.reconcileDialog.selected.pay_type,
account_last5: this.reconcileDialog.selected.account_last5,
amount: this.reconcileDialog.selected.amount,
pay_mode: this.reconcileDialog.selected.pay_mode,
note: this.reconcileDialog.selected.note,
proof_img: this.reconcileDialog.selected.proof_img,
status: this.reconcileDialog.selected.status,
f_num_match: this.reconcileDialog.selected.f_num_match,
f_num: this.reconcileDialog.selected.f_num,
acc_num: this.reconcileDialog.selected.acc_num,
check_date: this.reconcileDialog.selected.check_date,
check_amount: this.reconcileDialog.selected.check_amount,
check_memo: this.reconcileDialog.selected.check_memo,
check_status: this.reconcileDialog.selected.check_status,
acc_kind: this.reconcileDialog.selected.acc_kind,
member_num: this.reconcileDialog.selected.member_num,
verify_time: this.reconcileDialog.selected.verify_time,
verify_note: this.reconcileDialog.selected.verify_note,
draft: JSON.stringify(draftData)
};
return axios.put(`../../api/transfer_register/${id}`, updateData);
},
confirmGroupReconcile() {
if (!this.canConfirm) return;
this.reconcileDialog.loading = true;
const overPayment = this.reconcileDialog.selected.check_amount - this.sumReconcile;
const postData = {
transfer_register_id: this.reconcileDialog.selected.id,
details: this.reconcileDialog.items
.filter(item => item.reconcile > 0)
.map(item => ({
f_num: item.f_num,
pro_order_detail_num: item.num,
amount: Number(item.reconcile),
reg_time: new Date().toISOString()
})),
over_payment: overPayment
};
axios.post('../../api/transfer_register/group_reconcile', postData)
.then(res => {
const message = res.data && res.data.message ? res.data.message : '共同沖帳完成';
alert(message);
this.reconcileDialog.show = false;
this.loadTableData(); // 重新載入清單
})
.catch(err => {
let errorMessage = '沖帳失敗,請聯繫系統管理員';
if (err.response && err.response.data) {
if (typeof err.response.data === 'string') {
errorMessage = err.response.data;
} else if (err.response.data.message) {
errorMessage = err.response.data.message;
}
}
alert(errorMessage);
})
.finally(() => {
this.reconcileDialog.loading = false;
});
},
validateAndUpdateReconcile(value, item, index) {
let numValue = Number(value);
if (isNaN(numValue)) {
numValue = 0;
}
numValue = Math.round(numValue);
this.$set(item, 'reconcile', numValue);
this.updateSumReconcile();
}
}
});
</script>
</asp:Content>