migrate to new git

This commit is contained in:
2025-08-29 01:27:25 +08:00
parent 946eb9961e
commit af2c152ef6
8623 changed files with 1000453 additions and 1 deletions

View File

@@ -0,0 +1,529 @@
<%@ Page Title="個人-沖帳流程" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" EnableEventValidation="false" CodeFile="personal_reconcile.aspx.cs" Inherits="admin_transfer_personal_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="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.acc_name="{ item }">
<span>
<span v-if="hasTransferDraft(item.draft)" style="margin-right: 4px;">📝</span>{{ item.acc_name }}
</span>
</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.check_status="{ item }">
<span>{{ item.check_status }}</span>
</template>
<template v-slot:item.verify_note="{ item }">
<span>{{ item.verify_note }}</span>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
small
color="success"
@click="showReconcileDialog(item)"
>
沖帳
</v-btn>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-container>
<!-- 沖帳明細對話框 -->
<v-dialog v-model="dialog.show" max-width="960px">
<v-card>
<v-card-title class="grey lighten-2">
<span v-if="dialog.selected && hasTransferDraft(dialog.selected.draft)" style="margin-right: 8px;">📝</span>沖帳明細
<v-spacer></v-spacer>
<v-btn icon @click="dialog.show = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="mb-0 pb-0">
<div v-if="dialog.selected">
<div class="row my-2">
<h6 class="col">信眾:{{ dialog.selected.follower }}</h6>
<div class="col">
<span class="font-weight-bold">入帳金額:</span>
<span class="text-primary">{{ dialog.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="dialog.headers"
:items="dialog.items"
class="elevation-1 mt-3"
hide-default-footer
:disable-pagination="true"
>
<template v-slot:item.activity_name="{ item }">
<div>
<div>{{ item.activity_name }}</div>
<div class="text-muted" style="font-size:12px;">{{ item.order_no }}</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.reg_time="{ item }">
<span>{{ item.reg_time | date }}</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="confirmReconcile" :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 = {
hasTransferDraft: function(draft) { return false; },
getDraftField: function(draft, field) { return null; },
updateDraftField: function(draft, field, value) {
return { [field]: value };
}
};
}
new Vue({
el: '#reconcile-app',
vuetify: new Vuetify(),
data() {
return {
loading: false,
headers: [
//{ text: '信眾#', value: 'f_num' },
{ text: '信眾', value: 'follower' },
{ text: '入帳銀行/帳戶', value: 'acc_name' },
{ text: '入帳日期', value: 'check_date' },
{ text: '帳簿備註', value: 'check_memo' },
{ text: '入帳金額', value: 'check_amount' },
//{ text: '狀態', value: 'check_status' },
{ text: '核對記錄', value: 'verify_note' },
{ text: '沖帳', value: 'actions', sortable: false }
],
items: [],
dialog: {
show: false,
selected: null,
headers: [
{ text: '法會/報名單號', value: 'activity_name' },
{ text: '報名日期', value: 'reg_time' },
{ 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.dialog.items.reduce((sum, item) => sum + (Number(item.reconcile) || 0), 0);
},
remainDue() {
const totalDue = this.dialog.items.reduce((sum, item) => sum + (Number(item.due) || 0), 0);
return totalDue - this.sumReconcile;
},
overPaid() {
if (!this.dialog.selected) return 0;
return this.dialog.selected.check_amount - this.sumReconcile;
},
isOverPaid() {
if (!this.dialog.selected) return false;
return this.sumReconcile > this.dialog.selected.check_amount;
},
canConfirm() {
if (!this.dialog.selected) return false;
const hasReconcile = this.dialog.items.some(item => Number(item.reconcile) > 0);
if (!hasReconcile) return false;
const validAmounts = this.dialog.items.every(item => {
const amount = Number(item.reconcile) || 0;
return amount >= 0 && amount <= Number(item.due);
});
if (!validAmounts) return false;
if (this.sumReconcile > this.dialog.selected.check_amount) return false;
// 有未分配金額時,只有在還有未繳餘款的情況下才禁用按鈕
// 如果未繳餘款為 0表示沒有更多項目可沖帳應允許確認
if (this.hasUnallocated && this.remainDue > 0) return false;
return true;
},
buttonErrorMessage() {
if (!this.dialog.selected) return '';
const hasReconcile = this.dialog.items.some(item => Number(item.reconcile) > 0);
if (!hasReconcile) return '請至少輸入一筆沖帳金額';
const invalidItem = this.dialog.items.find(item => {
const amount = Number(item.reconcile) || 0;
return amount < 0 || amount > Number(item.due);
});
if (invalidItem) return '沖帳金額超出可沖帳範圍';
if (this.sumReconcile > this.dialog.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: {
// 檢查是否有 DraftUtils 可用
hasTransferDraft(draft) {
return window.DraftUtils && window.DraftUtils.hasTransferDraft && window.DraftUtils.hasTransferDraft(draft);
},
// 使用全域 DraftUtils 工具函數
showError(message) {
this.dialog.errorMessage = message;
},
clearError() {
this.dialog.errorMessage = '';
},
loadTableData() {
this.loading = true;
axios.get('../../api/transfer_register/personal_reconcile_list')
.then(res => {
this.items = res.data;
})
.catch(() => {
this.items = [];
})
.finally(() => {
this.loading = false;
});
},
showReconcileDialog(item) {
this.dialog.selected = item;
this.dialog.items = [];
this.dialog.show = true;
// 依 f_num 呼叫 API 取得訂單明細
if (item.f_num) {
axios.get('../../api/transfer_register/follower_orders', { params: { f_num: item.f_num } })
.then(res => {
this.dialog.items = res.data;
this.loadFromDraftOrAutoDistribute();
})
.catch(() => {
this.dialog.items = [];
this.updateSumReconcile();
});
} else {
this.dialog.items = [];
this.updateSumReconcile();
}
},
autoDistributeReconcile() {
// 先進先出分配沖帳金額
let remainAmount = this.dialog.selected ? this.dialog.selected.check_amount : 0;
// 先將所有項目的 reconcile 清為 0
this.dialog.items.forEach(item => {
this.$set(item, 'reconcile', 0);
});
// 依報名日期排序(先進先出)
const sortedItems = [...this.dialog.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.dialog.items.find(i => i === item);
if (originalItem) {
this.$set(originalItem, 'reconcile', canPay);
}
remainAmount -= canPay;
}
});
this.updateSumReconcile();
},
redistributeReconcile() {
// 重新依先進先出原則分配沖帳金額
this.autoDistributeReconcile();
},
updateSumReconcile() {
// 只更新 hasUnallocated 狀態
const maxTotal = this.dialog.selected ? (this.dialog.selected.check_amount || 0) : 0;
this.hasUnallocated = maxTotal > this.sumReconcile;
},
loadFromDraftOrAutoDistribute() {
const draft = this.dialog.selected ? this.dialog.selected.draft : null;
if (draft && draft.trim() && window.DraftUtils && window.DraftUtils.getDraftField) {
const transferDraft = window.DraftUtils.getDraftField(draft, 'transfer_draft');
if (transferDraft && Array.isArray(transferDraft)) {
this.loadFromDraft(transferDraft);
} else {
this.autoDistributeReconcile();
}
} else {
this.autoDistributeReconcile();
}
},
loadFromDraft(draftData) {
this.draftDataChanged = false;
// 先將所有項目的 reconcile 設為 0
this.dialog.items.forEach(item => {
this.$set(item, 'reconcile', 0);
});
// 從 draft 資料填入沖帳金額
draftData.forEach(draftItem => {
const matchedItem = this.dialog.items.find(item =>
item.num === draftItem.pro_order_detail_num
);
if (matchedItem) {
this.$set(matchedItem, 'reconcile', draftItem.reconcile || 0);
} else {
// 找不到對應的項目,標記資料已改動
this.draftDataChanged = true;
}
});
this.updateSumReconcile();
// 如果有資料改動,提醒用戶
if (this.draftDataChanged) {
setTimeout(() => {
alert('資料已改動,請注意金額一致性');
}, 100);
}
},
saveDraft() {
try {
// 組成新的 transfer_draft 資料
const transferDraftData = this.dialog.items
.filter(item => Number(item.reconcile) > 0)
.map(item => ({
pro_order_detail_num: item.num,
reconcile: Number(item.reconcile)
}));
// 使用工具函數安全地更新 draft 物件
const currentDraft = this.dialog.selected.draft || '';
let draftJson = '';
if (window.DraftUtils && window.DraftUtils.updateDraftField) {
const newDraftObj = window.DraftUtils.updateDraftField(currentDraft, 'transfer_draft', transferDraftData);
draftJson = JSON.stringify(newDraftObj);
} else {
// 如果 DraftUtils 不可用,使用簡單的格式
draftJson = JSON.stringify({ transfer_draft: transferDraftData });
}
// 更新 draft 欄位
const updateData = {
id: this.dialog.selected.id,
activity_num: this.dialog.selected.activity_num,
name: this.dialog.selected.name,
phone: this.dialog.selected.phone,
pay_type: this.dialog.selected.pay_type,
account_last5: this.dialog.selected.account_last5,
amount: this.dialog.selected.amount,
pay_mode: this.dialog.selected.pay_mode,
note: this.dialog.selected.note,
proof_img: this.dialog.selected.proof_img,
status: this.dialog.selected.status,
f_num_match: this.dialog.selected.f_num_match,
f_num: this.dialog.selected.f_num,
acc_num: this.dialog.selected.acc_num,
check_date: this.dialog.selected.check_date,
check_amount: this.dialog.selected.check_amount,
check_memo: this.dialog.selected.check_memo,
check_status: this.dialog.selected.check_status,
acc_kind: this.dialog.selected.acc_kind,
member_num: this.dialog.selected.member_num,
verify_time: this.dialog.selected.verify_time,
verify_note: this.dialog.selected.verify_note,
draft: draftJson // 更新 draft 欄位
};
axios.put(`../../api/transfer_register/${this.dialog.selected.id}`, updateData)
.then(() => {
alert('暫存成功!');
// 更新本地資料
this.dialog.selected.draft = draftJson;
})
.catch(err => {
alert('暫存失敗:' + (err.response?.data?.message || err.message));
});
} catch (e) {
alert('暫存失敗:資料格式錯誤');
}
},
confirmReconcile() {
if (!this.canConfirm) {
this.showError(this.buttonErrorMessage);
return;
}
this.dialog.loading = true;
const over_payment = this.dialog.selected.check_amount - this.sumReconcile;
const postData = {
transfer_register_id: this.dialog.selected.id,
details: this.dialog.items
.filter(item => item.reconcile > 0)
.map(item => ({
pro_order_detail_num: item.num,
amount: Number(item.reconcile),
reg_time: new Date().toISOString(),
demo: `${this.dialog.selected.check_memo || ''}`
})),
over_payment: over_payment
};
axios.post('../../api/transfer_register/reconcile', postData)
.then(res => {
// 顯示成功訊息
const message = res.data && res.data.message ? res.data.message : '沖帳完成';
msgtop(message, 'success');
this.dialog.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;
if (err.response.data.exceptionMessage) {
errorMessage += `\\n${err.response.data.exceptionMessage}`;
}
}
}
alert(errorMessage);
})
.finally(() => {
this.dialog.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>