933 lines
32 KiB
Vue
933 lines
32 KiB
Vue
<template>
|
||
<div class="default-main bookkeeping-dashboard">
|
||
<section class="dashboard-grid">
|
||
<div class="dashboard-panel bank-panel">
|
||
<div class="panel-title">Available Bank Balance</div>
|
||
<div class="table-scroll">
|
||
<table class="bank-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Bank Name</th>
|
||
<th>Current Balance</th>
|
||
<th>Transaction Breakdown</th>
|
||
<th>Safe Alert</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="bank in visibleBanks" :key="bank.id" :class="bank.rowClass" :style="{ backgroundColor: bank.labelColor }">
|
||
<td>
|
||
<strong>{{ bank.name }}</strong>
|
||
<small>{{ bank.account }}</small>
|
||
</td>
|
||
<td class="balance">AUD {{ money(bank.balance) }}</td>
|
||
<td>
|
||
<div class="bank-operate">
|
||
<el-button size="small" :icon="Coin" circle />
|
||
<el-button size="small" :icon="Switch" @click="openTransfer(bank)">Transfer</el-button>
|
||
<div class="breakdown">
|
||
<span><i class="dot income">↓</i> ({{ bank.depositCount }}) {{ money(bank.deposit) }}</span>
|
||
<span><i class="dot outcome">↑</i> ({{ bank.withdrawCount }}) {{ money(bank.withdraw) }}</span>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<el-tag size="small" effect="plain" :type="bank.alert ? 'danger' : 'info'">{{ bank.alert || 'No Alert' }}</el-tag>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="!visibleBanks.length">
|
||
<td class="empty-banks" colspan="4">No bank data</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<button v-if="banks.length > 4" class="more-info" type="button" @click="showAllBanks = !showAllBanks">
|
||
{{ showAllBanks ? 'show less' : 'more info' }}
|
||
<Icon :name="showAllBanks ? 'fa fa-caret-up' : 'fa fa-caret-down'" />
|
||
</button>
|
||
</div>
|
||
|
||
<aside class="summary-side">
|
||
<div class="dashboard-panel summary-panel">
|
||
<div class="panel-title">Customer Summary</div>
|
||
<dl>
|
||
<template v-for="item in summary" :key="item.label">
|
||
<dt>{{ item.label }}:</dt>
|
||
<dd>{{ item.value }}</dd>
|
||
</template>
|
||
</dl>
|
||
</div>
|
||
<el-button class="create-button" type="success" @click="openCreate">CREATE NEW TRANSACTION</el-button>
|
||
</aside>
|
||
</section>
|
||
|
||
<el-alert class="webhook-alert" type="warning" :closable="true" show-icon>
|
||
<template #title>
|
||
<strong>The Transaction Mode you currently set has the Webhook (JDK) feature enabled</strong>
|
||
</template>
|
||
<p>
|
||
Hint: If a transaction approved in the JK backend is not automatically recorded here within 10 minutes, click "Create New Transaction"
|
||
to record it manually.
|
||
</p>
|
||
<p>提示:如果 JK 后台 Approved 的 Transaction 在这里 10 分钟内没有自动记录,请点击 "Create New Transaction" 手动记录。</p>
|
||
</el-alert>
|
||
|
||
<section class="transaction-section">
|
||
<div class="filter-row">
|
||
<div class="date-filter">
|
||
<label>Start Date:</label>
|
||
<el-date-picker v-model="filters.startDate" type="date" value-format="YYYY-MM-DD" />
|
||
<label>End Date:</label>
|
||
<el-date-picker v-model="filters.endDate" type="date" value-format="YYYY-MM-DD" />
|
||
<el-button @click="search">Search</el-button>
|
||
<el-button @click="setToday">Today</el-button>
|
||
</div>
|
||
<div class="totals">
|
||
<span>Date of data: {{ dateRangeText }}</span>
|
||
<b
|
||
>Total Deposit / IN: <em>AUD {{ money(totalDeposit) }}</em></b
|
||
>
|
||
<b
|
||
>Total Withdraw / OUT: <em>AUD {{ money(totalWithdraw) }}</em></b
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<el-table :data="transactions" border size="small" class="transaction-table" :row-class-name="transactionRowClass">
|
||
<el-table-column prop="createdBy" label="Created by" width="110" />
|
||
<el-table-column prop="createdTime" label="Created Time" width="170" />
|
||
<el-table-column prop="category" label="Category" width="100" />
|
||
<el-table-column prop="username" label="Username" width="110" />
|
||
<el-table-column prop="remark" label="Remark" min-width="205" />
|
||
<el-table-column prop="bank" label="Bank" min-width="165" />
|
||
<el-table-column prop="type" label="Type" width="95" />
|
||
<el-table-column label="Amount (AUD)" width="130" align="right">
|
||
<template #default="{ row }">
|
||
<strong :class="row.flow === 'in' ? 'amount-in' : 'amount-out'">{{ money(row.amount) }}</strong>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="label" label="Label" width="100" />
|
||
<el-table-column label="Game Ticket" min-width="140">
|
||
<template #default="{ row }">
|
||
<div v-for="(ticket, index) in row.ticket" :key="`${ticket}-${index}`">{{ ticket }}</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="Action" fixed="right" width="180">
|
||
<template #default="{ row }">
|
||
<el-button link type="primary" size="small" @click="openHistory(row)">history</el-button>
|
||
<el-button link type="primary" size="small" @click="openEdit(row)">edit</el-button>
|
||
<el-button link type="danger" size="small" @click="removeTransaction(row)">delete</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<div class="pagination">
|
||
<el-pagination
|
||
v-model:current-page="transactionPage.currentPage"
|
||
background
|
||
layout="prev, pager, next"
|
||
:total="transactionPage.count"
|
||
:page-size="transactionPage.pageSize"
|
||
@current-change="onTransactionPageChange"
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
<el-dialog v-model="transactionDialog.visible" :title="transactionDialog.title" width="680px">
|
||
<el-form :model="transactionForm" label-width="145px">
|
||
<el-form-item label="Date & Time">
|
||
<el-date-picker v-model="transactionForm.time" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" />
|
||
<el-radio-group v-model="transactionForm.timeMode" class="inline-mode">
|
||
<el-radio value="Auto">Auto</el-radio>
|
||
<el-radio value="Manual">Manual</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="Category">
|
||
<el-select v-model="transactionForm.category">
|
||
<el-option label="Customer" :value="1" />
|
||
<el-option label="Other Adjust" :value="2" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="Type">
|
||
<el-radio-group v-model="transactionForm.type">
|
||
<el-radio-button :value="1">Deposit</el-radio-button>
|
||
<el-radio-button :value="2">Withdraw</el-radio-button>
|
||
<el-radio-button :value="3">IN</el-radio-button>
|
||
<el-radio-button :value="4">OUT</el-radio-button>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="Username">
|
||
<el-input v-model="transactionForm.username" :disabled="transactionDialog.mode === 'edit'" placeholder="e.g. PLAYER001" />
|
||
</el-form-item>
|
||
<el-form-item label="Remark">
|
||
<el-input v-model="transactionForm.remark" placeholder="(optional)" />
|
||
</el-form-item>
|
||
<el-form-item label="Amount (AUD)">
|
||
<el-input-number v-model="transactionForm.amount" :min="0" :precision="2" />
|
||
</el-form-item>
|
||
<el-form-item label="Bank">
|
||
<el-select v-model="transactionForm.bank" placeholder="- (optional) -">
|
||
<el-option v-for="bank in banks" :key="bank.id" :label="bank.name" :value="bank.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="Label">
|
||
<el-select v-model="transactionForm.label" placeholder="- (optional) -">
|
||
<el-option label="First Deposit" :value="1" />
|
||
<el-option label="Unclaim" :value="2" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="Game Ticket Auto">
|
||
<el-checkbox v-model="transactionForm.ticketAuto">Auto generate ticket</el-checkbox>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button type="primary" :loading="transactionDialog.loading" @click="submitTransaction">CONFIRM</el-button>
|
||
<el-button @click="transactionDialog.visible = false">CANCEL</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="historyDialog.visible" title="Transaction Edit History" width="720px">
|
||
<p class="dialog-note">Transaction edit history is only kept for the most recent 12 months.</p>
|
||
<p class="dialog-note">Transaction 的编辑历史记录仅保存最近 12 个月。</p>
|
||
<el-table v-loading="historyDialog.loading" :data="historyDialog.rows" border size="small" empty-text="No Record">
|
||
<el-table-column prop="id" label="Tx ID" width="100" />
|
||
<el-table-column prop="editedBy" label="Edit By" width="120" />
|
||
<el-table-column prop="editedTime" label="Edit Time" width="170" />
|
||
<el-table-column prop="changes" label="Changes (Old → New)" />
|
||
</el-table>
|
||
<template #footer>
|
||
<el-button @click="historyDialog.visible = false">CANCEL</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="transferDialog.visible" title="Bank Transfer" width="560px">
|
||
<el-form :model="transferForm" label-width="125px">
|
||
<el-form-item label="Transfer (From)">
|
||
<el-input v-model="transferForm.fromName" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="Transfer (To)">
|
||
<el-select v-model="transferForm.bankTo" placeholder="- Select Bank -">
|
||
<el-option
|
||
v-for="bank in banks"
|
||
:key="bank.id"
|
||
:label="bank.name"
|
||
:value="bank.id"
|
||
:disabled="bank.id === transferForm.bankFrom"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="Amount">
|
||
<el-input-number v-model="transferForm.money" :min="0" :precision="2" />
|
||
</el-form-item>
|
||
<el-form-item label="Remark">
|
||
<el-input v-model="transferForm.remark" placeholder="(optional)" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button type="primary" :loading="transferDialog.loading" @click="submitBankTransfer">CONFIRM</el-button>
|
||
<el-button @click="transferDialog.visible = false">CANCEL</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { Coin, Switch } from '@element-plus/icons-vue'
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { bankTransact, delTransact, editTransact, index as getDashboard, logHistory, newTransact } from '/@/api/backend/dashboard'
|
||
import type { DashboardTransactPayload } from '/@/api/backend/dashboard'
|
||
|
||
defineOptions({
|
||
name: 'dashboard',
|
||
})
|
||
|
||
interface Bank {
|
||
id: number | string
|
||
name: string
|
||
account: string
|
||
balance: number
|
||
deposit: number
|
||
withdraw: number
|
||
depositCount: number
|
||
withdrawCount: number
|
||
alert?: string
|
||
rowClass?: string
|
||
labelColor?: string
|
||
}
|
||
|
||
interface DashboardBank {
|
||
id: number | string
|
||
bank_name?: string
|
||
bank_account?: string
|
||
balance?: number | string
|
||
current_balance?: number | string
|
||
tx_in?: number | string
|
||
tx_out?: number | string
|
||
fund_in?: number | string
|
||
fund_out?: number | string
|
||
total_fund_in?: number | string
|
||
total_fund_out?: number | string
|
||
count_fund_in?: number | string
|
||
count_fund_out?: number | string
|
||
deposit_count?: number | string
|
||
withdraw_count?: number | string
|
||
safe_alert?: number | string
|
||
status?: number | string
|
||
label_color?: string
|
||
}
|
||
|
||
interface Transaction {
|
||
id: number
|
||
createdBy: string
|
||
createdTime: string
|
||
category: string
|
||
username: string
|
||
remark: string
|
||
bank: string
|
||
type: string
|
||
flow: 'in' | 'out'
|
||
amount: number
|
||
label: string
|
||
ticket: string[]
|
||
}
|
||
|
||
interface DashboardScoreLog {
|
||
game_type_text?: string
|
||
money_log_id?: number | string
|
||
game_type?: number | string
|
||
score?: number | string
|
||
}
|
||
|
||
interface DashboardTransaction {
|
||
id: number
|
||
user_id?: number | string
|
||
money?: number | string
|
||
before?: number | string
|
||
after?: number | string
|
||
type?: number | string
|
||
transaction_id?: string
|
||
created_by?: string
|
||
memo?: string
|
||
create_time?: number | string
|
||
bank_id?: number | string
|
||
category?: number | string
|
||
user_name?: string
|
||
bank_name?: string
|
||
label?: number | string
|
||
scoreLog?: DashboardScoreLog[]
|
||
}
|
||
|
||
interface DashboardTransactionPage {
|
||
count?: number | string
|
||
current_page?: number | string
|
||
last_page?: number | string
|
||
list?: DashboardTransaction[]
|
||
total_deposit?: number | string
|
||
total_withdraw?: number | string
|
||
}
|
||
|
||
interface DashboardCustomerSummary {
|
||
total_deposit?: number | string
|
||
total_withdraw?: number | string
|
||
count_deposit?: number | string
|
||
count_withdraw?: number | string
|
||
active_player?: number | string
|
||
first_deposit?: number | string
|
||
unclaim_amount?: number | string
|
||
unclaim_receipt?: number | string
|
||
}
|
||
|
||
interface HistoryRow {
|
||
id: number | string
|
||
editedBy: string
|
||
editedTime: string
|
||
changes: string
|
||
}
|
||
|
||
const today = () => {
|
||
const date = new Date()
|
||
const pad = (value: number) => value.toString().padStart(2, '0')
|
||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||
}
|
||
|
||
const money = (value: number) =>
|
||
Number(value).toLocaleString('en-AU', {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
})
|
||
|
||
const safeAlertLabels: Record<string, string> = {
|
||
'1': 'Hourly Alert',
|
||
'2': 'Daily Alert',
|
||
'3': 'Weekly Alert',
|
||
'4': 'Monthly Alert',
|
||
'5': 'Yearly Alert',
|
||
'6': 'Lifetime Alert',
|
||
}
|
||
|
||
const toNumber = (value: unknown) => {
|
||
const number = Number(value)
|
||
return Number.isFinite(number) ? number : 0
|
||
}
|
||
|
||
const formatDateTime = (value: unknown) => {
|
||
if (typeof value === 'string' && value.trim() && !Number.isFinite(Number(value))) {
|
||
return value
|
||
}
|
||
|
||
const timestamp = toNumber(value)
|
||
if (!timestamp) return ''
|
||
|
||
const date = new Date(timestamp * 1000)
|
||
const pad = (number: number) => number.toString().padStart(2, '0')
|
||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||
}
|
||
|
||
const mapBank = (bank: DashboardBank): Bank => {
|
||
const fundIn = toNumber(bank.total_fund_in ?? bank.fund_in)
|
||
const fundOut = toNumber(bank.total_fund_out ?? bank.fund_out)
|
||
const safeAlert = String(bank.safe_alert ?? '0')
|
||
|
||
return {
|
||
id: bank.id,
|
||
name: bank.bank_name || '-',
|
||
account: bank.bank_account || '',
|
||
balance: toNumber(bank.current_balance ?? bank.balance ?? fundIn - fundOut),
|
||
deposit: fundIn,
|
||
withdraw: fundOut,
|
||
depositCount: toNumber(bank.count_fund_in ?? bank.deposit_count ?? bank.tx_in),
|
||
withdrawCount: toNumber(bank.count_fund_out ?? bank.withdraw_count ?? bank.tx_out),
|
||
alert: safeAlertLabels[safeAlert],
|
||
rowClass: String(bank.status ?? '1') === '0' ? 'bank-muted' : '',
|
||
labelColor: bank.label_color || '',
|
||
}
|
||
}
|
||
|
||
const banks = ref<Bank[]>([])
|
||
const transactions = ref<Transaction[]>([])
|
||
const customerSummary = reactive({
|
||
totalDeposit: 0,
|
||
totalWithdraw: 0,
|
||
countDeposit: 0,
|
||
countWithdraw: 0,
|
||
activePlayer: 0,
|
||
firstDeposit: 0,
|
||
unclaimAmount: 0,
|
||
unclaimReceipt: 0,
|
||
})
|
||
|
||
const filters = reactive({
|
||
startDate: today(),
|
||
endDate: today(),
|
||
})
|
||
const transactionPage = reactive({
|
||
count: 0,
|
||
currentPage: 1,
|
||
lastPage: 1,
|
||
pageSize: 10,
|
||
})
|
||
const showAllBanks = ref(false)
|
||
const visibleBanks = computed(() => (showAllBanks.value ? banks.value : banks.value.slice(0, 4)))
|
||
const totalDeposit = computed(() => customerSummary.totalDeposit)
|
||
const totalWithdraw = computed(() => customerSummary.totalWithdraw)
|
||
const dateRangeText = computed(() => {
|
||
const start = new Date(`${filters.startDate}T00:00:00`)
|
||
const end = new Date(`${filters.endDate}T00:00:00`)
|
||
const diffDays = Math.max(1, Math.floor((end.getTime() - start.getTime()) / 86400000) + 1)
|
||
return `${filters.startDate} to ${filters.endDate} (${diffDays} ${diffDays === 1 ? 'Day' : 'Days'})`
|
||
})
|
||
const summary = computed(() => [
|
||
{ label: 'Total Deposit / IN', value: `AUD ${money(totalDeposit.value)}` },
|
||
{ label: 'Total Withdraw / OUT', value: `AUD ${money(totalWithdraw.value)}` },
|
||
{ label: 'Deposit Count', value: customerSummary.countDeposit },
|
||
{ label: 'Withdraw Count', value: customerSummary.countWithdraw },
|
||
{ label: 'Active Player', value: customerSummary.activePlayer },
|
||
{ label: 'First Deposit Player', value: customerSummary.firstDeposit },
|
||
{ label: 'Unclaimed Amount', value: `AUD ${money(customerSummary.unclaimAmount)}` },
|
||
{ label: 'Unclaimed Receipt', value: customerSummary.unclaimReceipt },
|
||
])
|
||
|
||
const transactionDialog = reactive<{ visible: boolean; title: string; loading: boolean; mode: 'create' | 'edit'; editId: number | string | '' }>({
|
||
visible: false,
|
||
title: 'Create New Transaction',
|
||
loading: false,
|
||
mode: 'create',
|
||
editId: '',
|
||
})
|
||
const transactionForm = reactive({
|
||
time: '',
|
||
timeMode: 'Auto',
|
||
category: 1,
|
||
type: 1,
|
||
username: '',
|
||
remark: '',
|
||
amount: 0,
|
||
bank: '',
|
||
label: '',
|
||
ticketAuto: true,
|
||
})
|
||
const historyDialog = reactive({
|
||
visible: false,
|
||
loading: false,
|
||
rows: [] as HistoryRow[],
|
||
})
|
||
const transferDialog = reactive({ visible: false, loading: false })
|
||
const transferForm = reactive<{ bankFrom: Bank['id'] | ''; fromName: string; bankTo: Bank['id'] | ''; money: number; remark: string }>({
|
||
bankFrom: '',
|
||
fromName: '',
|
||
bankTo: '',
|
||
money: 0,
|
||
remark: '',
|
||
})
|
||
|
||
const typeLabels: Record<string, string> = {
|
||
'1': 'Deposit',
|
||
'2': 'Withdraw',
|
||
'3': 'IN',
|
||
'4': 'OUT',
|
||
}
|
||
|
||
const categoryLabels: Record<string, string> = {
|
||
'1': 'Customer',
|
||
'2': 'Other Adjust',
|
||
}
|
||
|
||
const transactionLabelTexts: Record<string, string> = {
|
||
'1': 'First Deposit',
|
||
'2': 'Unclaim',
|
||
}
|
||
|
||
const categoryValues: Record<string, number> = {
|
||
Customer: 1,
|
||
'Other Adjust': 2,
|
||
}
|
||
|
||
const typeValues: Record<string, number> = {
|
||
Deposit: 1,
|
||
Withdraw: 2,
|
||
IN: 3,
|
||
OUT: 4,
|
||
}
|
||
|
||
const labelValues: Record<string, number> = {
|
||
'First Deposit': 1,
|
||
Unclaim: 2,
|
||
}
|
||
|
||
const mapTransaction = (transaction: DashboardTransaction): Transaction => {
|
||
const type = String(transaction.type ?? '')
|
||
return {
|
||
id: transaction.id,
|
||
createdBy: transaction.created_by || '',
|
||
createdTime: formatDateTime(transaction.create_time),
|
||
category: categoryLabels[String(transaction.category ?? '')] || '',
|
||
username: transaction.user_name || '',
|
||
remark: transaction.memo || '',
|
||
bank: transaction.bank_name || '',
|
||
type: typeLabels[type] || type,
|
||
flow: ['1', '3'].includes(type) ? 'in' : 'out',
|
||
amount: toNumber(transaction.money),
|
||
label: transactionLabelTexts[String(transaction.label ?? '')] || '',
|
||
ticket: Array.isArray(transaction.scoreLog)
|
||
? transaction.scoreLog.map((score) => `${score.game_type_text || ''} : ${score.score ?? ''}`).filter((score) => score.trim() !== ':')
|
||
: [],
|
||
}
|
||
}
|
||
|
||
const loadDashboard = (page = transactionPage.currentPage) => {
|
||
return getDashboard({
|
||
start: filters.startDate,
|
||
end: filters.endDate,
|
||
page,
|
||
}).then((res) => {
|
||
const bankData = Array.isArray(res.data.bank) ? res.data.bank : res.data.bank?.list
|
||
banks.value = Array.isArray(bankData) ? bankData.map(mapBank) : []
|
||
|
||
const transactionData = res.data.transaction as DashboardTransactionPage | undefined
|
||
const transactionList = Array.isArray(transactionData?.list) ? transactionData.list : []
|
||
transactions.value = transactionList.map(mapTransaction)
|
||
transactionPage.count = toNumber(transactionData?.count)
|
||
transactionPage.currentPage = toNumber(transactionData?.current_page) || page
|
||
transactionPage.lastPage = toNumber(transactionData?.last_page) || 1
|
||
transactionPage.pageSize = transactionPage.lastPage > 0 ? Math.max(1, Math.ceil(transactionPage.count / transactionPage.lastPage)) : 10
|
||
|
||
const customerData = res.data.customer as DashboardCustomerSummary | undefined
|
||
customerSummary.totalDeposit = toNumber(customerData?.total_deposit)
|
||
customerSummary.totalWithdraw = toNumber(customerData?.total_withdraw)
|
||
customerSummary.countDeposit = toNumber(customerData?.count_deposit)
|
||
customerSummary.countWithdraw = toNumber(customerData?.count_withdraw)
|
||
customerSummary.activePlayer = toNumber(customerData?.active_player)
|
||
customerSummary.firstDeposit = toNumber(customerData?.first_deposit)
|
||
customerSummary.unclaimAmount = toNumber(customerData?.unclaim_amount)
|
||
customerSummary.unclaimReceipt = toNumber(customerData?.unclaim_receipt)
|
||
})
|
||
}
|
||
|
||
const resetTransactionForm = () => {
|
||
Object.assign(transactionForm, {
|
||
time: '',
|
||
timeMode: 'Auto',
|
||
category: 1,
|
||
type: 1,
|
||
username: '',
|
||
remark: '',
|
||
amount: 0,
|
||
bank: '',
|
||
label: '',
|
||
ticketAuto: true,
|
||
})
|
||
}
|
||
|
||
const openCreate = () => {
|
||
resetTransactionForm()
|
||
transactionDialog.title = 'Create New Transaction'
|
||
transactionDialog.mode = 'create'
|
||
transactionDialog.editId = ''
|
||
transactionDialog.visible = true
|
||
}
|
||
|
||
const openEdit = (row: Transaction) => {
|
||
Object.assign(transactionForm, {
|
||
time: row.createdTime,
|
||
timeMode: 'Manual',
|
||
category: categoryValues[row.category] || 1,
|
||
type: typeValues[row.type] || 1,
|
||
username: row.username,
|
||
remark: row.remark,
|
||
amount: row.amount,
|
||
bank: banks.value.find((bank) => bank.name === row.bank)?.id || '',
|
||
label: labelValues[row.label] || '',
|
||
ticketAuto: row.ticket.length > 0,
|
||
})
|
||
transactionDialog.title = `Edit Transaction #${row.id}`
|
||
transactionDialog.mode = 'edit'
|
||
transactionDialog.editId = row.id
|
||
transactionDialog.visible = true
|
||
}
|
||
|
||
const transactionTimestamp = () => {
|
||
if (transactionForm.timeMode === 'Auto' || !transactionForm.time) {
|
||
return Math.floor(Date.now() / 1000)
|
||
}
|
||
|
||
const date = new Date(transactionForm.time.replace(' ', 'T'))
|
||
const timestamp = Math.floor(date.getTime() / 1000)
|
||
return Number.isFinite(timestamp) ? timestamp : Math.floor(Date.now() / 1000)
|
||
}
|
||
|
||
const buildTransactPayload = (): DashboardTransactPayload => ({
|
||
create_time: transactionTimestamp(),
|
||
category: transactionForm.category,
|
||
type: transactionForm.type,
|
||
user_name: transactionForm.username,
|
||
memo: transactionForm.remark,
|
||
money: transactionForm.amount,
|
||
bank_id: transactionForm.bank,
|
||
label: transactionForm.label,
|
||
game_ticket: transactionForm.ticketAuto ? 1 : 0,
|
||
})
|
||
|
||
const submitTransaction = () => {
|
||
transactionDialog.loading = true
|
||
const request =
|
||
transactionDialog.mode === 'edit' && transactionDialog.editId !== ''
|
||
? editTransact({ ...buildTransactPayload(), id: transactionDialog.editId })
|
||
: newTransact(buildTransactPayload())
|
||
|
||
request
|
||
.then(() => {
|
||
transactionDialog.visible = false
|
||
transactionPage.currentPage = 1
|
||
return loadDashboard(1)
|
||
})
|
||
.finally(() => {
|
||
transactionDialog.loading = false
|
||
})
|
||
}
|
||
|
||
const stringifyChange = (history: Record<string, unknown>) => {
|
||
if (history.bank_befter !== undefined || history.bank_after !== undefined) {
|
||
return `Bank:${history.bank_befter ?? ''}→${history.bank_after ?? ''}`
|
||
}
|
||
|
||
return JSON.stringify(history)
|
||
}
|
||
|
||
const mapHistory = (history: Record<string, unknown>): HistoryRow => ({
|
||
id: (history.id || history.money_log_id || '') as number | string,
|
||
editedBy: String(history.admin_name || ''),
|
||
editedTime: formatDateTime(history.create_time),
|
||
changes: stringifyChange(history),
|
||
})
|
||
|
||
const openHistory = (row: Transaction) => {
|
||
historyDialog.visible = true
|
||
historyDialog.loading = true
|
||
historyDialog.rows = []
|
||
|
||
logHistory({ id: row.id })
|
||
.then((res) => {
|
||
const data = Array.isArray(res.data) ? res.data : res.data?.list
|
||
historyDialog.rows = Array.isArray(data) ? data.map((item) => mapHistory(item as Record<string, unknown>)) : []
|
||
})
|
||
.finally(() => {
|
||
historyDialog.loading = false
|
||
})
|
||
}
|
||
|
||
const openTransfer = (bank: Bank) => {
|
||
Object.assign(transferForm, { bankFrom: bank.id, fromName: bank.name, bankTo: '', money: 0, remark: '' })
|
||
transferDialog.visible = true
|
||
}
|
||
|
||
const submitBankTransfer = () => {
|
||
transferDialog.loading = true
|
||
bankTransact({
|
||
money: transferForm.money,
|
||
bank_from: transferForm.bankFrom,
|
||
bank_to: transferForm.bankTo,
|
||
remark: transferForm.remark,
|
||
})
|
||
.then(() => {
|
||
transferDialog.visible = false
|
||
return loadDashboard()
|
||
})
|
||
.finally(() => {
|
||
transferDialog.loading = false
|
||
})
|
||
}
|
||
|
||
const removeTransaction = (row: Transaction) => {
|
||
delTransact({ id: row.id }).then(() => {
|
||
return loadDashboard(transactionPage.currentPage)
|
||
})
|
||
}
|
||
|
||
const setToday = () => {
|
||
filters.startDate = today()
|
||
filters.endDate = today()
|
||
transactionPage.currentPage = 1
|
||
loadDashboard(1).catch(() => {
|
||
// Request errors are displayed by the shared Axios interceptor.
|
||
})
|
||
}
|
||
|
||
const search = () => {
|
||
transactionPage.currentPage = 1
|
||
loadDashboard(1).catch(() => {
|
||
// Request errors are displayed by the shared Axios interceptor.
|
||
})
|
||
}
|
||
|
||
const onTransactionPageChange = (page: number) => {
|
||
loadDashboard(page).catch(() => {
|
||
// Request errors are displayed by the shared Axios interceptor.
|
||
})
|
||
}
|
||
|
||
const transactionRowClass = ({ row }: { row: Transaction }) => (row.flow === 'in' ? 'transaction-in' : 'transaction-out')
|
||
|
||
onMounted(() => {
|
||
loadDashboard().catch(() => {
|
||
// Request errors are displayed by the shared Axios interceptor.
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.bookkeeping-dashboard {
|
||
color: var(--el-text-color-primary);
|
||
font-size: 13px;
|
||
}
|
||
.dashboard-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 310px;
|
||
gap: 16px;
|
||
}
|
||
.dashboard-panel,
|
||
.transaction-section {
|
||
border: 1px solid var(--el-border-color);
|
||
border-radius: 4px;
|
||
background: var(--el-bg-color);
|
||
}
|
||
.panel-title {
|
||
padding: 11px 14px;
|
||
border-bottom: 1px solid var(--el-border-color);
|
||
background: var(--el-fill-color-light);
|
||
color: var(--el-text-color-primary);
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
}
|
||
.table-scroll {
|
||
overflow-x: auto;
|
||
}
|
||
.bank-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
th,
|
||
td {
|
||
padding: 9px 12px;
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
text-align: left;
|
||
vertical-align: middle;
|
||
}
|
||
th {
|
||
color: var(--el-text-color-secondary);
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
}
|
||
strong,
|
||
small {
|
||
display: block;
|
||
}
|
||
small {
|
||
margin-top: 3px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
.balance {
|
||
color: var(--el-color-primary);
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
}
|
||
.empty-banks {
|
||
padding: 24px;
|
||
color: var(--el-text-color-secondary);
|
||
text-align: center;
|
||
}
|
||
}
|
||
.bank-muted {
|
||
background: var(--el-fill-color-lighter);
|
||
}
|
||
.bank-operate {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
}
|
||
.breakdown {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
color: var(--el-text-color-regular);
|
||
white-space: nowrap;
|
||
}
|
||
.dot {
|
||
font-style: normal;
|
||
font-weight: 700;
|
||
}
|
||
.income,
|
||
.amount-in {
|
||
color: var(--el-color-success);
|
||
}
|
||
.outcome,
|
||
.amount-out {
|
||
color: var(--el-color-danger);
|
||
}
|
||
.more-info {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 9px;
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--el-color-primary);
|
||
cursor: pointer;
|
||
}
|
||
.summary-side {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
}
|
||
.summary-panel dl {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto;
|
||
gap: 0;
|
||
margin: 0;
|
||
}
|
||
.summary-panel dt,
|
||
.summary-panel dd {
|
||
margin: 0;
|
||
padding: 8px 12px;
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
}
|
||
.summary-panel dd {
|
||
color: var(--el-color-primary);
|
||
font-weight: 700;
|
||
text-align: right;
|
||
}
|
||
.create-button {
|
||
width: 100%;
|
||
min-height: 44px;
|
||
font-weight: 700;
|
||
}
|
||
.webhook-alert {
|
||
margin: 16px 0;
|
||
p {
|
||
margin: 6px 0 0;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
.filter-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
padding: 12px;
|
||
border-bottom: 1px solid var(--el-border-color);
|
||
}
|
||
.date-filter,
|
||
.totals {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
.date-filter :deep(.el-date-editor) {
|
||
width: 145px;
|
||
}
|
||
.totals {
|
||
justify-content: flex-end;
|
||
em {
|
||
color: var(--el-color-primary);
|
||
font-style: normal;
|
||
}
|
||
}
|
||
.transaction-table :deep(.transaction-in) {
|
||
--el-table-tr-bg-color: var(--el-color-success-light-9);
|
||
}
|
||
.transaction-table :deep(.transaction-out) {
|
||
--el-table-tr-bg-color: var(--el-color-danger-light-9);
|
||
}
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 12px;
|
||
}
|
||
.inline-mode {
|
||
margin-left: 12px;
|
||
}
|
||
.dialog-note {
|
||
margin: 0 0 8px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
:deep(.el-dialog__body) {
|
||
padding-top: 12px;
|
||
}
|
||
:deep(.el-form-item .el-select),
|
||
:deep(.el-form-item .el-input) {
|
||
width: 100%;
|
||
}
|
||
@media screen and (max-width: 1100px) {
|
||
.dashboard-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
@media screen and (max-width: 720px) {
|
||
.filter-row {
|
||
flex-direction: column;
|
||
}
|
||
.totals {
|
||
justify-content: flex-start;
|
||
}
|
||
.bank-table {
|
||
min-width: 760px;
|
||
}
|
||
}
|
||
</style>
|