财务台账:金额按万元展示并增加余额筛选 #440

Merged
shanshi merged 1 commits from feature/shanshi-ledger-account-amounts-wan into main 2026-10-21 17:03:42 +08:00
3 changed files with 140 additions and 7 deletions

View File

@ -147,6 +147,15 @@ class FinanceLedgerPageData
});
}
$balanceRange = trim((string) request('accounts_balance', ''));
if ($balanceRange === '0-5') {
$query->where('current_balance', '>=', 0)->where('current_balance', '<', 50000);
} elseif ($balanceRange === '5-20') {
$query->where('current_balance', '>=', 50000)->where('current_balance', '<', 200000);
} elseif ($balanceRange === '20-plus') {
$query->where('current_balance', '>=', 200000);
}
$summaryQuery = clone $query;
$summary = [
'count' => (clone $query)->count(),

View File

@ -17,8 +17,40 @@
request('accounts_status'),
request('accounts_region'),
request('accounts_type'),
request('accounts_balance'),
], static fn ($value): bool => trim((string) $value) !== ''));
$money = static fn ($value): string => $value === null || $value === '' ? '' : number_format((float) $value, 2);
$moneyWan = static function ($value): string {
if ($value === null || trim((string) $value) === '') {
return '';
}
$text = trim((string) $value);
$parts = preg_split('/\s*\/\s*/', $text);
if (! is_array($parts) || count($parts) > 2) {
return $text;
}
$converted = [];
foreach ($parts as $part) {
$normalized = str_replace([',', ' '], '', $part);
if (! is_numeric($normalized)) {
return $text;
}
$converted[] = number_format((float) $normalized / 10000, 2);
}
return implode(' / ', $converted);
};
$moneyYuanTitle = static function ($value): string {
if ($value === null || trim((string) $value) === '') {
return '';
}
$normalized = str_replace([',', ' '], '', trim((string) $value));
return is_numeric($normalized) ? number_format((float) $normalized, 2) . ' 元' : trim((string) $value);
};
$date = static fn ($value): string => $value ? \Illuminate\Support\Carbon::parse($value)->format('Y-m-d') : '';
$ledgerPageUrl = route('business.pages.show', ['section' => 'financial-operations', 'page' => 'ledger']);
$tabUrl = static fn (string $tab): string => $ledgerPageUrl . '?' . http_build_query(['tab' => $tab]);
@ -208,6 +240,7 @@
<div class="ledger-field" style="min-width:8rem"><label for="accounts-status">账户状态</label><select id="accounts-status" name="accounts_status"><option value="">全部状态</option>@foreach ($accountStatusOptions as $statusCode => $statusLabel)<option value="{{ $statusCode }}" @selected(request('accounts_status') === $statusCode)>{{ $statusLabel }}</option>@endforeach</select></div>
<div class="ledger-field" style="min-width:8rem"><label for="accounts-region">地区</label><select id="accounts-region" name="accounts_region"><option value="">全部地区</option>@foreach (($accountFilterOptions['regions'] ?? []) as $region)<option value="{{ $region }}" @selected(request('accounts_region') === $region)>{{ $region }}</option>@endforeach<option value="__empty__" @selected(request('accounts_region') === '__empty__')>未填写</option></select></div>
<div class="ledger-field" style="min-width:8rem"><label for="accounts-type">账户类型</label><select id="accounts-type" name="accounts_type"><option value="">全部类型</option>@foreach (($accountFilterOptions['types'] ?? []) as $type)<option value="{{ $type }}" @selected(request('accounts_type') === $type)>{{ $type }}</option>@endforeach<option value="__empty__" @selected(request('accounts_type') === '__empty__')>未填写</option></select></div>
<div class="ledger-field" style="min-width:8rem"><label for="accounts-balance">当前余额</label><select id="accounts-balance" name="accounts_balance"><option value="">全部金额</option><option value="0-5" @selected(request('accounts_balance') === '0-5')>0-5万</option><option value="5-20" @selected(request('accounts_balance') === '5-20')>5-20万</option><option value="20-plus" @selected(request('accounts_balance') === '20-plus')>20万以上</option></select></div>
@if ($activeAccountFilterCount > 0)
<span class="ledger-filter-state" data-account-filter-state role="status" aria-live="polite" title="当前有 {{ $activeAccountFilterCount }} 个筛选条件生效"><span class="material-icons-outlined" aria-hidden="true">filter_alt</span><span>筛选中</span><b>{{ $activeAccountFilterCount }}</b></span>
@endif
@ -229,13 +262,13 @@
</form>
<div class="ledger-summary">
<div class="ledger-summary__item"><span>账户数</span><b>{{ number_format((int) ($ledgerAccountSummary['count'] ?? 0)) }}</b></div>
<div class="ledger-summary__item ledger-summary__item--available" data-ledger-summary-available><span>可用金额合计</span><b>{{ $money($ledgerAccountSummary['balance'] ?? 0) }}</b></div>
<a class="ledger-summary__item ledger-summary__item--blocked ledger-summary__item--link" data-ledger-summary-blocked href="{{ route('business.financial-ledger.unavailable-balances') }}" aria-label="查看账户异常待办"><span>不可用金额合计</span><b>{{ $money($ledgerAccountSummary['blocked'] ?? 0) }}</b></a>
<div class="ledger-summary__item ledger-summary__item--available" data-ledger-summary-available><span>可用金额合计(万元)</span><b title="{{ $moneyYuanTitle($ledgerAccountSummary['balance'] ?? 0) }}">{{ $moneyWan($ledgerAccountSummary['balance'] ?? 0) }}</b></div>
<a class="ledger-summary__item ledger-summary__item--blocked ledger-summary__item--link" data-ledger-summary-blocked href="{{ route('business.financial-ledger.unavailable-balances') }}" aria-label="查看账户异常待办"><span>不可用金额合计(万元)</span><b title="{{ $moneyYuanTitle($ledgerAccountSummary['blocked'] ?? 0) }}">{{ $moneyWan($ledgerAccountSummary['blocked'] ?? 0) }}</b></a>
<div class="ledger-summary__item"><span>金额口径</span><b>正常计可用,异常计不可用</b></div>
</div>
<div class="ledger-table-wrap ledger-accounts-table-wrap">
<table class="ledger-table ledger-accounts-table">
<thead><tr><th>公司名称</th><th>账户简称</th><th>账户类型</th><th class="ledger-status-column">账户状态</th><th>地区</th><th>账号</th><th>开户行</th><th>开户城市/网点</th><th class="ledger-plan-column">公司规划</th><th class="ledger-keep-column">账户是否保留</th><th>后4位</th><th>单笔限额</th><th>日限额</th><th>月限额</th><th>年限额</th><th class="num">当前余额</th><th class="num">可用金额</th><th class="num">不可用金额</th><th class="num">每日可出</th><th class="num">预计出清天数</th><th>提额目标</th><th>提额状态</th><th>提额动作</th><th>已做动作</th><th>下一步</th><th>备注</th><th>责任人</th></tr></thead>
<thead><tr><th>公司名称</th><th>账户简称</th><th>账户类型</th><th class="ledger-status-column">账户状态</th><th>地区</th><th>账号</th><th>开户行</th><th>开户城市/网点</th><th class="ledger-plan-column">公司规划</th><th class="ledger-keep-column">账户是否保留</th><th>后4位</th><th>单笔限额(万元)</th><th>日限额(万元)</th><th>月限额(万元)</th><th>年限额(万元)</th><th class="num">当前余额(万元)</th><th class="num">可用金额(万元)</th><th class="num">不可用金额(万元)</th><th class="num">每日可出(万元)</th><th class="num">预计出清天数</th><th>提额目标(万元)</th><th>提额状态</th><th>提额动作</th><th>已做动作</th><th>下一步</th><th>备注</th><th>责任人</th></tr></thead>
<tbody>
@forelse ($ledgerAccounts as $row)
<tr>
@ -289,7 +322,7 @@
</select>
@endif
</td>
<td>{{ $row->account_last4 }}</td><td>{{ $row->single_limit }}</td><td>{{ $row->daily_limit }}</td><td>{{ $row->monthly_limit }}</td><td>{{ $row->yearly_limit }}</td><td class="num" data-account-current-balance="{{ (float) $row->current_balance }}">{{ $money($row->current_balance) }}</td><td class="num" data-account-available-amount>{{ $money($row->daily_available) }}</td><td class="num" data-account-blocked-amount>{{ $money($row->blocked_amount) }}</td><td class="num" data-account-daily-available-amount>{{ $money($row->daily_available) }}</td><td class="num">{{ $row->clearing_days !== null ? number_format((float) $row->clearing_days, 2) : '' }}</td><td>{{ $row->raise_target }}</td><td>{{ $row->raise_status }}</td><td>{{ $row->raise_actions }}</td><td>{{ $row->done_actions }}</td><td>{{ $row->next_action }}</td><td>{{ $row->remark }}</td><td>{{ $row->owner }}</td>
<td>{{ $row->account_last4 }}</td><td title="{{ $moneyYuanTitle($row->single_limit) }}">{{ $moneyWan($row->single_limit) }}</td><td title="{{ $moneyYuanTitle($row->daily_limit) }}">{{ $moneyWan($row->daily_limit) }}</td><td title="{{ $moneyYuanTitle($row->monthly_limit) }}">{{ $moneyWan($row->monthly_limit) }}</td><td title="{{ $moneyYuanTitle($row->yearly_limit) }}">{{ $moneyWan($row->yearly_limit) }}</td><td class="num" data-account-current-balance="{{ (float) $row->current_balance }}" title="{{ $moneyYuanTitle($row->current_balance) }}">{{ $moneyWan($row->current_balance) }}</td><td class="num" data-account-available-amount title="{{ $moneyYuanTitle($row->daily_available) }}">{{ $moneyWan($row->daily_available) }}</td><td class="num" data-account-blocked-amount title="{{ $moneyYuanTitle($row->blocked_amount) }}">{{ $moneyWan($row->blocked_amount) }}</td><td class="num" data-account-daily-available-amount title="{{ $moneyYuanTitle($row->daily_available) }}">{{ $moneyWan($row->daily_available) }}</td><td class="num">{{ $row->clearing_days !== null ? number_format((float) $row->clearing_days, 2) : '' }}</td><td title="{{ $moneyYuanTitle($row->raise_target) }}">{{ $moneyWan($row->raise_target) }}</td><td>{{ $row->raise_status }}</td><td>{{ $row->raise_actions }}</td><td>{{ $row->done_actions }}</td><td>{{ $row->next_action }}</td><td>{{ $row->remark }}</td><td>{{ $row->owner }}</td>
</tr>
@empty
<tr><td colspan="27"><div class="ledger-empty">没有匹配的账户数据。</div></td></tr>
@ -972,6 +1005,11 @@
const currentCell = row?.querySelector('[data-account-current-balance]');
const currentAmount = Number(currentCell?.dataset.accountCurrentBalance || 0);
const currentText = currentCell?.textContent?.trim() || '0.00';
const currentAmountWan = currentAmount / 10000;
const currentYuanTitle = currentCell?.title || currentAmount.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}) + ' 元';
const nextNormal = value === 'normal';
const previousNormal = previous === 'normal';
const availableCell = row?.querySelector('[data-account-available-amount]');
@ -980,24 +1018,31 @@
if (availableCell) availableCell.textContent = nextNormal ? currentText : '0.00';
if (blockedCell) blockedCell.textContent = nextNormal ? '0.00' : currentText;
if (dailyAvailableCell) dailyAvailableCell.textContent = nextNormal ? currentText : '0.00';
if (availableCell) availableCell.title = nextNormal ? currentYuanTitle : '0.00 元';
if (blockedCell) blockedCell.title = nextNormal ? '0.00 元' : currentYuanTitle;
if (dailyAvailableCell) dailyAvailableCell.title = nextNormal ? currentYuanTitle : '0.00 元';
if (nextNormal !== previousNormal) {
const formatSummaryAmount = (amount) => amount.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const adjustSummary = (card, delta) => {
const adjustSummary = (card, displayDelta, yuanDelta) => {
const valueNode = card?.querySelector('b');
if (!valueNode) return;
const current = Number(valueNode.textContent.replace(/,/g, '')) || 0;
valueNode.textContent = formatSummaryAmount(Math.max(0, current + delta));
valueNode.textContent = formatSummaryAmount(Math.max(0, current + displayDelta));
const currentYuan = Number((valueNode.title || '').replace(/[^\d.-]/g, '')) || 0;
valueNode.title = formatSummaryAmount(Math.max(0, currentYuan + yuanDelta)) + ' 元';
};
adjustSummary(
page.querySelector('[data-ledger-summary-available]'),
nextNormal ? currentAmountWan : -currentAmountWan,
nextNormal ? currentAmount : -currentAmount,
);
adjustSummary(
page.querySelector('[data-ledger-summary-blocked]'),
nextNormal ? -currentAmountWan : currentAmountWan,
nextNormal ? -currentAmount : currentAmount,
);
}

View File

@ -94,6 +94,10 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
->assertSee('name="accounts_status"', false)
->assertSee('name="accounts_region"', false)
->assertSee('name="accounts_type"', false)
->assertSee('name="accounts_balance"', false)
->assertSee('0-5万')
->assertSee('5-20万')
->assertSee('20万以上')
->assertSee('data-account-type', false)
->assertSee('data-account-plan', false)
->assertSee('data-account-keep', false)
@ -124,10 +128,11 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
->assertSee('恢复默认')
->assertSee('可用金额合计')
->assertSee('不可用金额合计')
->assertSee('可用金额合计(万元)')
->assertSee('href="' . $this->unavailableBalancesUrl() . '"', false)
->assertSee('查看账户异常待办')
->assertSee('正常计可用,异常计不可用')
->assertSee('<th class="num">当前余额</th><th class="num">可用金额</th><th class="num">不可用金额</th>', false)
->assertSee('<th class="num">当前余额(万元)</th><th class="num">可用金额(万元)</th><th class="num">不可用金额(万元)</th>', false)
->assertSee('data-ledger-summary-available', false)
->assertSee('data-account-current-balance="321.45"', false)
->assertSee('data-account-available-amount', false)
@ -137,6 +142,80 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
->assertSee('value="normal" selected', false);
}
public function test_account_balance_filter_uses_non_overlapping_ranges(): void
{
DB::connection('business_mirror')->table('fin_accounts')->insert([
['company_name' => '余额筛选公司', 'account_short' => '余额-负数', 'current_balance' => -1],
['company_name' => '余额筛选公司', 'account_short' => '余额-0', 'current_balance' => 0],
['company_name' => '余额筛选公司', 'account_short' => '余额-49999', 'current_balance' => 49999.99],
['company_name' => '余额筛选公司', 'account_short' => '余额-50000', 'current_balance' => 50000],
['company_name' => '余额筛选公司', 'account_short' => '余额-199999', 'current_balance' => 199999.99],
['company_name' => '余额筛选公司', 'account_short' => '余额-200000', 'current_balance' => 200000],
]);
$this->loginAs()->get(route('business.pages.show', [
'company_id' => $this->company->id,
'section' => 'financial-operations',
'page' => 'ledger',
'accounts_balance' => '0-5',
]))
->assertOk()
->assertSee('data-account-short="余额-0"', false)
->assertSee('data-account-short="余额-49999"', false)
->assertDontSee('data-account-short="余额-负数"', false)
->assertDontSee('data-account-short="余额-50000"', false)
->assertSee('value="0-5" selected', false);
$this->loginAs()->get(route('business.pages.show', [
'company_id' => $this->company->id,
'section' => 'financial-operations',
'page' => 'ledger',
'accounts_balance' => '5-20',
]))
->assertOk()
->assertSee('data-account-short="余额-50000"', false)
->assertSee('data-account-short="余额-199999"', false)
->assertDontSee('data-account-short="余额-49999"', false)
->assertDontSee('data-account-short="余额-200000"', false);
$this->loginAs()->get(route('business.pages.show', [
'company_id' => $this->company->id,
'section' => 'financial-operations',
'page' => 'ledger',
'accounts_balance' => '20-plus',
]))
->assertOk()
->assertSee('data-account-short="余额-200000"', false)
->assertDontSee('data-account-short="余额-199999"', false);
}
public function test_account_table_monetary_columns_are_displayed_in_ten_thousands(): void
{
DB::connection('business_mirror')->table('fin_accounts')
->where('account_short', '测试账户')
->update([
'current_balance' => 123456.78,
'daily_available' => 120000,
'blocked_amount' => 3456.78,
'single_limit' => '200000/10000',
'daily_limit' => 300000,
'monthly_limit' => 400000,
'yearly_limit' => 500000,
'raise_target' => 50000,
]);
$this->loginAs()->get($this->pageUrl())
->assertOk()
->assertSee('当前余额(万元)')
->assertSee('单笔限额(万元)')
->assertSee('title="123,456.78 元"', false)
->assertSee('>12.35</td>', false)
->assertSee('>12.00</td>', false)
->assertSee('>0.35</td>', false)
->assertSee('>20.00 / 1.00</td>', false)
->assertSee('>5.00</td>', false);
}
public function test_unavailable_balance_tasks_page_is_separate_and_only_lists_blocked_accounts(): void
{
$connection = DB::connection('business_mirror');