315 lines
33 KiB
PHP
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.

@php
$meta = $ledgerMeta ?? [];
$tabs = $ledgerTabs ?? [];
$activeTab = $ledgerTab ?? 'accounts';
$canEdit = (bool) ($ledgerCanEditMapping ?? false);
$money = static fn ($value): string => $value === null || $value === '' ? '' : number_format((float) $value, 2);
$date = static fn ($value): string => $value ? \Illuminate\Support\Carbon::parse($value)->format('Y-m-d') : '';
$tabUrl = static fn (string $tab): string => route('business.pages.show', ['section' => 'financial-ledger', 'page' => 'ledger', 'tab' => $tab]);
$sourceLabels = ['dingtalk' => '钉钉审批', 'shop' => '店铺提现', 'payable' => '供应商应付', 'store' => '店铺开店', 'workorder' => '聚工单'];
@endphp
<meta name="csrf-token" content="{{ csrf_token() }}">
<div class="ledger-page" data-ledger-page>
<style>
.ledger-page { color: var(--vip-content-ink, #171717); font-size: .8125rem; }
.ledger-page * { box-sizing: border-box; }
.ledger-hero { display:flex; flex-wrap:wrap; align-items:flex-end; justify-content:space-between; gap:1rem; padding:1rem 1.15rem; margin-bottom:1rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.75rem; background:#fff; }
.ledger-hero h2 { margin:0 0 .25rem; color:var(--vip-content-ink, #171717); font-size:1rem; font-weight:700; }
.ledger-hero p { margin:0; color:var(--vip-content-muted, #595959); font-size:.75rem; line-height:1.6; }
.ledger-hero__meta { display:flex; flex-wrap:wrap; gap:.75rem 1.25rem; color:var(--vip-content-muted, #595959); font-size:.72rem; }
.ledger-hero__meta b { color:var(--vip-content-ink, #171717); font-variant-numeric:tabular-nums; }
.ledger-tabbar { display:flex; gap:.25rem; padding:.3rem; margin-bottom:1rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.75rem; background:#fff; overflow-x:auto; }
.ledger-tab { display:inline-flex; flex:0 0 auto; align-items:center; gap:.4rem; min-height:2.35rem; padding:0 .85rem; border:0; border-radius:.5rem; background:transparent; color:var(--vip-content-muted, #595959); font:inherit; font-weight:600; cursor:pointer; white-space:nowrap; }
.ledger-tab:hover { color:var(--vip-content-accent-dark, #856632); background:var(--vip-content-accent-soft, #F7F0DE); }
.ledger-tab.is-active { color:#fff; background:var(--vip-content-accent-dark, #856632); }
.ledger-tab:focus-visible { outline:2px solid var(--vip-content-accent-dark, #856632); outline-offset:2px; }
.ledger-tab .material-icons-outlined { font-size:1.05rem; }
.ledger-panel[hidden] { display:none !important; }
.ledger-toolbar { display:flex; flex-wrap:wrap; align-items:flex-end; gap:.65rem; padding:.85rem 1rem; margin-bottom:.85rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.65rem; background:#fff; }
.ledger-field { display:flex; flex-direction:column; gap:.3rem; min-width:10rem; }
.ledger-field label { color:var(--vip-content-muted, #595959); font-size:.7rem; font-weight:600; }
.ledger-field input, .ledger-field select { height:2.15rem; padding:0 .6rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.45rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; }
.ledger-field input:focus, .ledger-field select:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); }
.ledger-actions { display:flex; gap:.45rem; padding-bottom:.05rem; }
.ledger-btn { display:inline-flex; align-items:center; justify-content:center; gap:.35rem; height:2.15rem; padding:0 .8rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.45rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; font-weight:600; text-decoration:none; cursor:pointer; white-space:nowrap; }
.ledger-btn:hover { border-color:var(--vip-content-accent-dark, #856632); color:var(--vip-content-accent-dark, #856632); }
.ledger-btn.primary { border-color:var(--vip-content-accent-dark, #856632); background:var(--vip-content-accent-dark, #856632); color:#fff; }
.ledger-btn.primary:hover { background:#6A5129; color:#fff; }
.ledger-summary { display:flex; flex-wrap:wrap; gap:.6rem 1.5rem; padding:.7rem 1rem; margin-bottom:.85rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.65rem; background:var(--vip-content-accent-soft, #F7F0DE); }
.ledger-summary__item { display:flex; flex-direction:column; gap:.12rem; min-width:7rem; }
.ledger-summary__item span { color:var(--vip-content-muted, #595959); font-size:.68rem; }
.ledger-summary__item b { color:var(--vip-content-ink, #171717); font-size:.92rem; font-variant-numeric:tabular-nums; }
.ledger-note { padding:.65rem .9rem; margin-bottom:.85rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.55rem; background:#fff; color:var(--vip-content-muted, #595959); font-size:.72rem; line-height:1.6; }
.ledger-table-wrap { overflow:auto; border:1px solid var(--vip-border, #D8D2C7); border-radius:.65rem; background:#fff; }
.ledger-table { width:100%; min-width:82rem; border-collapse:separate; border-spacing:0; font-size:.72rem; }
.ledger-table th, .ledger-table td { padding:.52rem .6rem; border-right:1px solid #e8e3da; border-bottom:1px solid #e8e3da; vertical-align:top; white-space:nowrap; }
.ledger-table th { position:sticky; top:0; z-index:1; background:var(--vip-content-accent-soft, #F7F0DE); color:var(--vip-content-ink, #171717); font-weight:700; text-align:left; }
.ledger-table tr:hover td { background:#fcfaf4; }
.ledger-table th:first-child, .ledger-table td:first-child { position:sticky; left:0; z-index:2; background:#fff; }
.ledger-table th:first-child { z-index:3; background:var(--vip-content-accent-soft, #F7F0DE); }
.ledger-table tr:hover td:first-child { background:#fcfaf4; }
.ledger-table td.num, .ledger-table th.num { text-align:right; font-variant-numeric:tabular-nums; }
.ledger-table td.wrap { white-space:normal; min-width:14rem; max-width:22rem; line-height:1.5; }
.ledger-table .muted { color:var(--vip-content-muted, #595959); }
.ledger-table .danger { color:var(--vip-danger, #B80000); }
.ledger-table .success { color:var(--vip-success, #53793E); }
.ledger-pagination { display:flex; align-items:center; justify-content:space-between; gap:.75rem; padding:.65rem .9rem; color:var(--vip-content-muted, #595959); font-size:.72rem; }
.ledger-pagination a { color:var(--vip-content-accent-dark, #856632); text-decoration:none; margin-left:.75rem; font-weight:600; }
.ledger-empty { padding:3.5rem 1rem; text-align:center; color:var(--vip-content-muted, #595959); }
.ledger-progress { display:flex; align-items:center; gap:.65rem; min-width:15rem; }
.ledger-progress__track { flex:1; height:.45rem; overflow:hidden; border-radius:999px; background:#e8e3da; }
.ledger-progress__bar { height:100%; border-radius:999px; background:var(--vip-content-accent, #B9954F); transition:width .2s ease; }
.ledger-map-input { width:13rem; height:2rem; padding:0 .5rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.4rem; background:#fff; font:inherit; }
.ledger-map-input:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); }
.ledger-map-input.is-saving { border-color:var(--vip-content-accent, #B9954F); background:#fffdf6; }
.ledger-map-input.is-error { border-color:var(--vip-danger, #B80000); background:#fff5f5; }
.ledger-toast { position:fixed; right:1.5rem; bottom:1.5rem; z-index:80; max-width:24rem; padding:.7rem 1rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.5rem; background:#fff; box-shadow:0 4px 12px rgba(23,23,23,.15); font-size:.78rem; display:none; }
.ledger-toast.is-visible { display:block; }
.ledger-toast.is-error { border-color:var(--vip-danger, #B80000); color:var(--vip-danger, #B80000); }
.ledger-source-tag { display:inline-flex; align-items:center; padding:.1rem .4rem; border-radius:.25rem; background:#f4f1eb; color:var(--vip-content-muted, #595959); font-size:.68rem; }
.ledger-mono { font-variant-numeric:tabular-nums; }
@media (max-width: 900px) { .ledger-hero { align-items:flex-start; flex-direction:column; } .ledger-field { min-width:calc(50% - .5rem); } }
</style>
<div class="ledger-hero">
<div>
<h2>财务台账</h2>
<p>账户、店铺、期初与动账明细统一归集;期初日 {{ $ledgerOpeningDate }},流水口径:期末 = 期初 + 入账 − 出账。</p>
</div>
<div class="ledger-hero__meta">
<span>账户 <b>{{ number_format((int) ($meta['accounts'] ?? 0)) }}</b></span>
<span>店铺 <b>{{ number_format((int) ($meta['shops'] ?? 0)) }}</b></span>
<span>公司 <b>{{ number_format((int) ($meta['companies'] ?? 0)) }}</b></span>
<span>动账 <b>{{ number_format((int) ($meta['transactions'] ?? 0)) }}</b> 笔</span>
<span>期初合计 <b>{{ $money($meta['opening_sum'] ?? 0) }}</b></span>
@if (!empty($meta['last_synced_at']))
<span>数据时间 <b>{{ $date($meta['last_synced_at']) }}</b></span>
@endif
</div>
</div>
<nav class="ledger-tabbar" role="tablist" aria-label="财务台账页签">
@foreach ($tabs as $key => $tab)
<button type="button" class="ledger-tab {{ $activeTab === $key ? 'is-active' : '' }}" role="tab" aria-selected="{{ $activeTab === $key ? 'true' : 'false' }}" aria-controls="ledger-panel-{{ $key }}" data-ledger-tab="{{ $key }}">
<span class="material-icons-outlined" aria-hidden="true">{{ $tab['icon'] }}</span>
<span>{{ $tab['title'] }}</span>
</button>
@endforeach
</nav>
{{-- 账户总表 --}}
<section class="ledger-panel" id="ledger-panel-accounts" role="tabpanel" data-ledger-panel="accounts" @if ($activeTab !== 'accounts') hidden @endif>
<form class="ledger-toolbar" method="GET" action="{{ $tabUrl('accounts') }}">
<input type="hidden" name="tab" value="accounts">
<div class="ledger-field"><label for="accounts-q">关键词</label><input id="accounts-q" name="accounts_q" value="{{ request('accounts_q') }}" placeholder="公司、账户简称、账号、责任人"></div>
<div class="ledger-actions"><button class="ledger-btn primary" type="submit"><span class="material-icons-outlined">search</span>查询</button><a class="ledger-btn" href="{{ $tabUrl('accounts') }}"><span class="material-icons-outlined">refresh</span>重置</a></div>
</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"><span>当前余额合计</span><b>{{ $money($ledgerAccountSummary['balance'] ?? 0) }}</b></div>
<div class="ledger-summary__item"><span>卡住金额合计</span><b>{{ $money($ledgerAccountSummary['blocked'] ?? 0) }}</b></div>
<div class="ledger-summary__item"><span>每日可出金额口径</span><b>余额 − 卡住金额</b></div>
</div>
<div class="ledger-table-wrap">
<table class="ledger-table">
<thead><tr><th>公司名称</th><th>账户简称</th><th>地区</th><th>账号</th><th>开户行</th><th>开户城市/网点</th><th>账户类型</th><th>公司规划</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>提额目标</th><th>提额状态</th><th>提额动作</th><th>已做动作</th><th>下一步</th><th>备注</th><th>责任人</th></tr></thead>
<tbody>
@forelse ($ledgerAccounts as $row)
<tr>
<td>{{ $row->company_name }}</td><td>{{ $row->account_short }}</td><td>{{ $row->region }}</td><td>{{ $row->account_no }}</td><td>{{ $row->bank_name }}</td><td>{{ $row->bank_city_branch }}</td><td>{{ $row->account_type }}</td><td>{{ $row->company_plan }}</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">{{ $money($row->current_balance) }}</td><td class="num">{{ $money($row->blocked_amount) }}</td><td class="num">{{ $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>
</tr>
@empty
<tr><td colspan="24"><div class="ledger-empty">没有匹配的账户数据。</div></td></tr>
@endforelse
</tbody>
</table>
<div class="ledger-pagination"><span>共 {{ number_format($ledgerAccounts->total()) }} 条 · 第 {{ $ledgerAccounts->currentPage() }} / {{ $ledgerAccounts->lastPage() }} 页</span><span>@if (!$ledgerAccounts->onFirstPage())<a href="{{ $ledgerAccounts->previousPageUrl() }}">上一页</a>@endif @if ($ledgerAccounts->hasMorePages())<a href="{{ $ledgerAccounts->nextPageUrl() }}">下一页</a>@endif</span></div>
</div>
</section>
{{-- 店铺台账 --}}
<section class="ledger-panel" id="ledger-panel-shops" role="tabpanel" data-ledger-panel="shops" @if ($activeTab !== 'shops') hidden @endif>
<form class="ledger-toolbar" method="GET" action="{{ $tabUrl('shops') }}">
<input type="hidden" name="tab" value="shops">
<div class="ledger-field"><label for="shops-q">店铺/平台/公司</label><input id="shops-q" name="shops_q" value="{{ request('shops_q') }}" placeholder="关键词"></div>
<div class="ledger-field"><label for="shops-platform">平台</label><input id="shops-platform" name="shops_platform" value="{{ request('shops_platform') }}" placeholder="如 天猫"></div>
<div class="ledger-field"><label for="shops-company">所属公司</label><input id="shops-company" name="shops_company" value="{{ request('shops_company') }}" placeholder="公司名称"></div>
<div class="ledger-actions"><button class="ledger-btn primary" type="submit"><span class="material-icons-outlined">search</span>查询</button><a class="ledger-btn" href="{{ $tabUrl('shops') }}"><span class="material-icons-outlined">refresh</span>重置</a></div>
</form>
<div class="ledger-summary"><div class="ledger-summary__item"><span>店铺数</span><b>{{ number_format((int) ($ledgerShopSummary['count'] ?? 0)) }}</b></div><div class="ledger-summary__item"><span>店铺余额合计</span><b>{{ $money($ledgerShopSummary['balance'] ?? 0) }}</b></div><div class="ledger-summary__item"><span>余额口径</span><b>所属公司账户合计(待确认)</b></div></div>
<div class="ledger-table-wrap">
<table class="ledger-table" style="min-width:56rem"><thead><tr><th>店铺名称</th><th>平台</th><th>所属公司</th><th>账户简称</th><th>店铺状态</th><th>回款账户</th><th class="num">店铺余额</th><th>备注</th></tr></thead>
<tbody>@forelse ($ledgerShops as $row)<tr><td>{{ $row->shop_name }}</td><td>{{ $row->platform }}</td><td>{{ $row->company_name }}</td><td>{{ $row->account_short }}</td><td><span class="{{ $row->status === '在营' ? 'success' : 'muted' }}">{{ $row->status }}</span></td><td>{{ $row->payout_account }}</td><td class="num">{{ $money($row->shop_balance) }}</td><td>{{ $row->remark }}</td></tr>@empty<tr><td colspan="8"><div class="ledger-empty">没有匹配的店铺数据。</div></td></tr>@endforelse</tbody>
</table>
<div class="ledger-pagination"><span>共 {{ number_format($ledgerShops->total()) }} 条 · 第 {{ $ledgerShops->currentPage() }} / {{ $ledgerShops->lastPage() }} 页</span><span>@if (!$ledgerShops->onFirstPage())<a href="{{ $ledgerShops->previousPageUrl() }}">上一页</a>@endif @if ($ledgerShops->hasMorePages())<a href="{{ $ledgerShops->nextPageUrl() }}">下一页</a>@endif</span></div>
</div>
</section>
{{-- 每日资金流水 --}}
<section class="ledger-panel" id="ledger-panel-flows" role="tabpanel" data-ledger-panel="flows" @if ($activeTab !== 'flows') hidden @endif>
<form class="ledger-toolbar" method="GET" action="{{ $tabUrl('flows') }}">
<input type="hidden" name="tab" value="flows">
<div class="ledger-field"><label for="flows-date">日期</label><input id="flows-date" type="date" name="flows_date" value="{{ request('flows_date') }}"></div>
<div class="ledger-field"><label for="flows-company">公司</label><input id="flows-company" name="flows_company" value="{{ request('flows_company') }}" placeholder="公司名称"></div>
<div class="ledger-field"><label for="flows-account">账户简称</label><input id="flows-account" name="flows_account" value="{{ request('flows_account') }}" placeholder="账户简称"></div>
<div class="ledger-actions"><button class="ledger-btn primary" type="submit"><span class="material-icons-outlined">search</span>查询</button><a class="ledger-btn" href="{{ $tabUrl('flows') }}"><span class="material-icons-outlined">refresh</span>重置</a></div>
</form>
<div class="ledger-summary"><div class="ledger-summary__item"><span>流水条数</span><b>{{ number_format((int) ($ledgerFlowSummary['count'] ?? 0)) }}</b></div><div class="ledger-summary__item"><span>期初余额({{ $ledgerOpeningDate }})</span><b>{{ $money($meta['opening_sum'] ?? 0) }}</b></div><div class="ledger-summary__item"><span>当期入账</span><b>{{ $money($ledgerFlowSummary['inflow'] ?? 0) }}</b></div><div class="ledger-summary__item"><span>当期出账</span><b>{{ $money($ledgerFlowSummary['outflow'] ?? 0) }}</b></div><div class="ledger-summary__item"><span>期末余额合计</span><b>{{ $money($ledgerFlowSummary['closing'] ?? 0) }}</b></div></div>
<div class="ledger-table-wrap">
<table class="ledger-table" style="min-width:62rem"><thead><tr><th>日期</th><th>公司</th><th>账户简称</th><th class="num">期初余额</th><th class="num">当日入账</th><th class="num">当日出账</th><th class="num">期末余额</th><th>异常/备注</th></tr></thead>
<tbody>@forelse ($ledgerFlows as $row)<tr><td>{{ $date($row->flow_date) }}</td><td>{{ $row->company_name }}</td><td>{{ $row->account_short }}</td><td class="num">{{ $money($row->opening_balance) }}</td><td class="num">{{ $row->inflow > 0 ? $money($row->inflow) : '' }}</td><td class="num">{{ $row->outflow > 0 ? $money($row->outflow) : '' }}</td><td class="num">{{ $money($row->closing_balance) }}</td><td>{{ $row->remark }}</td></tr>@empty<tr><td colspan="8"><div class="ledger-empty">当前筛选没有流水数据;期初日会为每个期初账户生成一行。</div></td></tr>@endforelse</tbody>
</table>
<div class="ledger-pagination"><span>共 {{ number_format($ledgerFlows->total()) }} 条 · 第 {{ $ledgerFlows->currentPage() }} / {{ $ledgerFlows->lastPage() }} 页</span><span>@if (!$ledgerFlows->onFirstPage())<a href="{{ $ledgerFlows->previousPageUrl() }}">上一页</a>@endif @if ($ledgerFlows->hasMorePages())<a href="{{ $ledgerFlows->nextPageUrl() }}">下一页</a>@endif</span></div>
</div>
</section>
{{-- 动账明细 --}}
<section class="ledger-panel" id="ledger-panel-transactions" role="tabpanel" data-ledger-panel="transactions" @if ($activeTab !== 'transactions') hidden @endif>
<form class="ledger-toolbar" method="GET" action="{{ $tabUrl('transactions') }}">
<input type="hidden" name="tab" value="transactions">
<div class="ledger-field" style="min-width:15rem"><label for="txns-q">关键词</label><input id="txns-q" name="txns_q" value="{{ request('txns_q') }}" placeholder="账户、明细、审批编号"></div>
<div class="ledger-field"><label for="txns-account">账户简称</label><input id="txns-account" name="txns_account" value="{{ request('txns_account') }}" placeholder="含未匹配"></div>
<div class="ledger-field"><label for="txns-source">来源</label><select id="txns-source" name="txns_source"><option value="">全部</option>@foreach ($sourceLabels as $key => $label)<option value="{{ $key }}" @selected(request('txns_source') === $key)>{{ $label }}</option>@endforeach</select></div>
<div class="ledger-field"><label for="txns-from">日期起</label><input id="txns-from" type="date" name="txns_from" value="{{ request('txns_from') }}"></div>
<div class="ledger-field"><label for="txns-to">日期止</label><input id="txns-to" type="date" name="txns_to" value="{{ request('txns_to') }}"></div>
<div class="ledger-actions"><button class="ledger-btn primary" type="submit"><span class="material-icons-outlined">search</span>查询</button><a class="ledger-btn" href="{{ $tabUrl('transactions') }}"><span class="material-icons-outlined">refresh</span>重置</a></div>
</form>
<div class="ledger-summary"><div class="ledger-summary__item"><span>明细笔数</span><b>{{ number_format((int) ($ledgerTransactionSummary['count'] ?? 0)) }}</b></div><div class="ledger-summary__item"><span>收入合计</span><b>{{ $money($ledgerTransactionSummary['income'] ?? 0) }}</b></div><div class="ledger-summary__item"><span>支出合计</span><b>{{ $money($ledgerTransactionSummary['expense'] ?? 0) }}</b></div><div class="ledger-summary__item"><span>净额</span><b>{{ $money(($ledgerTransactionSummary['income'] ?? 0) - ($ledgerTransactionSummary['expense'] ?? 0)) }}</b></div></div>
<div class="ledger-note">账户简称按“映射表 → 来源原始值 → 未匹配”解析;映射修改后无需重跑明细。</div>
<div class="ledger-table-wrap">
<table class="ledger-table" style="min-width:66rem"><thead><tr><th>日期</th><th>账户简称</th><th>明细</th><th>来源</th><th class="num">收入</th><th class="num">支出</th><th>审批编号/业务单号</th></tr></thead>
<tbody>@forelse ($ledgerTransactions as $row)<tr><td>{{ $date($row->txn_date) }}</td><td class="{{ $row->effective_account_short === '未匹配' ? 'danger' : '' }}">{{ $row->effective_account_short }}</td><td class="wrap">{{ $row->detail }}</td><td><span class="ledger-source-tag">{{ $sourceLabels[$row->source] ?? $row->source }}</span></td><td class="num">{{ $row->income > 0 ? $money($row->income) : '' }}</td><td class="num">{{ $row->expense > 0 ? $money($row->expense) : '' }}</td><td>{{ $row->approval_no }}</td></tr>@empty<tr><td colspan="7"><div class="ledger-empty">没有匹配的动账明细。</div></td></tr>@endforelse</tbody>
</table>
<div class="ledger-pagination"><span>共 {{ number_format($ledgerTransactions->total()) }} 条 · 第 {{ $ledgerTransactions->currentPage() }} / {{ $ledgerTransactions->lastPage() }} 页</span><span>@if (!$ledgerTransactions->onFirstPage())<a href="{{ $ledgerTransactions->previousPageUrl() }}">上一页</a>@endif @if ($ledgerTransactions->hasMorePages())<a href="{{ $ledgerTransactions->nextPageUrl() }}">下一页</a>@endif</span></div>
</div>
</section>
{{-- 账户映射 --}}
<section class="ledger-panel" id="ledger-panel-mapping" role="tabpanel" data-ledger-panel="mapping" @if ($activeTab !== 'mapping') hidden @endif>
@php
$mapping = $ledgerMapping ?? [];
$mapped = (int) ($mapping['all_mapped'] ?? 0);
$total = max((int) ($mapping['all_total'] ?? 0), 1);
$percent = (int) round($mapped / $total * 100);
@endphp
<div class="ledger-summary">
<div class="ledger-summary__item" style="min-width:18rem"><span>映射进度</span><div class="ledger-progress"><div class="ledger-progress__track"><div class="ledger-progress__bar" data-mapping-bar style="width: {{ $percent }}%"></div></div><b data-mapping-progress>{{ $mapped }} / {{ $mapping['all_total'] ?? 0 }}</b></div></div>
<div class="ledger-summary__item"><span>当前筛选</span><b>{{ $mapping['mapped'] ?? 0 }} / {{ $mapping['total'] ?? 0 }}</b></div>
</div>
<form class="ledger-toolbar" method="GET" action="{{ $tabUrl('mapping') }}">
<input type="hidden" name="tab" value="mapping">
<div class="ledger-field"><label for="mapping-source">来源</label><select id="mapping-source" name="mapping_source"><option value="">全部</option>@foreach (($mapping['sources'] ?? []) as $source)<option value="{{ $source }}" @selected(request('mapping_source') === $source)>{{ $sourceLabels[$source] ?? $source }}</option>@endforeach</select></div>
<div class="ledger-field" style="min-width:15rem"><label for="mapping-q">原始值/账户简称</label><input id="mapping-q" name="mapping_q" value="{{ request('mapping_q') }}" placeholder="搜索"></div>
<div class="ledger-field" style="min-width:auto"><label>&nbsp;</label><label style="display:flex;align-items:center;gap:.4rem;height:2.15rem"><input type="checkbox" name="mapping_unmapped" value="1" @checked(request()->boolean('mapping_unmapped'))> 只看未映射</label></div>
<div class="ledger-actions"><button class="ledger-btn primary" type="submit"><span class="material-icons-outlined">search</span>查询</button><a class="ledger-btn" href="{{ $tabUrl('mapping') }}"><span class="material-icons-outlined">refresh</span>重置</a></div>
</form>
<div class="ledger-note">{{ $canEdit ? '映射修改后失焦自动保存,并立即重算账户流水;输入值必须来自账户总表。' : '当前账号为只读权限,不能修改账户映射。' }}</div>
<div class="ledger-table-wrap">
<table class="ledger-table" style="min-width:64rem"><thead><tr><th>来源</th><th>原始值</th><th class="num">笔数</th><th class="num">收入合计</th><th class="num">支出合计</th><th>映射账户简称</th><th>状态</th></tr></thead>
<tbody>@forelse (($mapping['rows'] ?? collect()) as $row)<tr data-mapping-row data-id="{{ $row->id }}"><td><span class="ledger-source-tag">{{ $sourceLabels[$row->source] ?? $row->source }}</span></td><td>{{ $row->source_value }}</td><td class="num">{{ number_format((int) $row->txn_count) }}</td><td class="num">{{ $money($row->income_sum) }}</td><td class="num">{{ $money($row->expense_sum) }}</td><td>@if ($canEdit)<input class="ledger-map-input" list="ledger-account-list" value="{{ $row->account_short }}" placeholder="选择账户简称" data-mapping-input data-id="{{ $row->id }}" data-url="{{ route('business.financial-ledger.mapping.update', ['id' => $row->id]) }}">@else{{ $row->account_short ?: '未匹配' }}@endif</td><td data-mapping-state class="{{ $row->account_short ? 'success' : 'danger' }}">{{ $row->account_short ? '已映射' : '未匹配' }}</td></tr>@empty<tr><td colspan="7"><div class="ledger-empty">没有匹配的映射记录。</div></td></tr>@endforelse</tbody>
</table>
<div class="ledger-pagination"><span>共 {{ number_format($mapping['total'] ?? 0) }} 条 · 第 {{ ($mapping['rows'] ?? null)?->currentPage() ?? 1 }} / {{ ($mapping['rows'] ?? null)?->lastPage() ?? 1 }} 页</span><span>@if (($mapping['rows'] ?? null) && !$mapping['rows']->onFirstPage())<a href="{{ $mapping['rows']->previousPageUrl() }}">上一页</a>@endif @if (($mapping['rows'] ?? null) && $mapping['rows']->hasMorePages())<a href="{{ $mapping['rows']->nextPageUrl() }}">下一页</a>@endif</span></div>
</div>
<datalist id="ledger-account-list">@foreach (($ledgerAccountOptions ?? []) as $option)<option value="{{ $option }}"></option>@endforeach</datalist>
</section>
{{-- 数据来源 --}}
<section class="ledger-panel" id="ledger-panel-sources" role="tabpanel" data-ledger-panel="sources" @if ($activeTab !== 'sources') hidden @endif>
<div class="ledger-note" style="line-height:1.8">
<b>取数边界:</b>动账明细只读 AI 库 <span class="ledger-mono">JSReportReplica.rpt</span> 的视图,不读 base,不直连 JianSun;账户、店铺、公司、期初由财务系统单独导入。当前本地数据为 rpt 实时同步结果,期初日 {{ $ledgerOpeningDate }}。<br>
<b>流水口径:</b>按账户逐日滚动,期末 = 期初 + 当日入账 − 当日出账;期初日无交易也保留一行。<br>
<b>店铺余额:</b>当前按“所属公司名下所有账户最新期末余额合计”计算,待财务确认是否改为回款账户口径。
</div>
<div class="ledger-table-wrap">
<table class="ledger-table" style="min-width:55rem"><thead><tr><th>来源</th><th>流程</th><th class="num">明细笔数</th><th class="num">收入合计</th><th class="num">支出合计</th><th>日期范围</th></tr></thead>
<tbody>@forelse (($ledgerSourceSummary ?? []) as $row)<tr><td><span class="ledger-source-tag">{{ $sourceLabels[$row['source']] ?? $row['source'] }}</span></td><td>{{ $row['process_type'] }}</td><td class="num">{{ number_format((int) $row['rows_count']) }}</td><td class="num">{{ $money($row['income']) }}</td><td class="num">{{ $money($row['expense']) }}</td><td>{{ $date($row['first_date']) }} ~ {{ $date($row['last_date']) }}</td></tr>@empty<tr><td colspan="6"><div class="ledger-empty">尚未同步动账明细。</div></td></tr>@endforelse</tbody>
</table>
</div>
</section>
<div class="ledger-toast" data-ledger-toast role="status" aria-live="polite"></div>
</div>
<script>
(() => {
const page = document.querySelector('[data-ledger-page]');
if (!page) return;
const tabs = Array.from(page.querySelectorAll('[data-ledger-tab]'));
const panels = Array.from(page.querySelectorAll('[data-ledger-panel]'));
const showTab = (name, updateUrl = true) => {
tabs.forEach((tab) => {
const active = tab.dataset.ledgerTab === name;
tab.classList.toggle('is-active', active);
tab.setAttribute('aria-selected', active ? 'true' : 'false');
});
panels.forEach((panel) => { panel.hidden = panel.dataset.ledgerPanel !== name; });
if (updateUrl && window.history && window.history.replaceState) {
const url = new URL(window.location.href);
url.searchParams.set('tab', name);
window.history.replaceState({}, '', url.toString());
}
};
tabs.forEach((tab) => tab.addEventListener('click', () => showTab(tab.dataset.ledgerTab)));
if (window.location.hash === '#mapping') showTab('mapping', false);
const toast = page.querySelector('[data-ledger-toast]');
const notify = (message, error = false) => {
if (!toast) return;
toast.textContent = message;
toast.classList.toggle('is-error', error);
toast.classList.add('is-visible');
window.clearTimeout(notify.timer);
notify.timer = window.setTimeout(() => toast.classList.remove('is-visible'), 2600);
};
const inputs = Array.from(page.querySelectorAll('[data-mapping-input]'));
inputs.forEach((input) => {
let previous = input.value;
input.addEventListener('focus', () => { previous = input.value; });
input.addEventListener('blur', async () => {
const value = input.value.trim();
if (value === previous) return;
const row = input.closest('[data-mapping-row]');
const state = row?.querySelector('[data-mapping-state]');
input.classList.add('is-saving');
input.classList.remove('is-error');
try {
const response = await fetch(input.dataset.url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
},
body: JSON.stringify({ account_short: value }),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.message || '保存失败');
previous = value;
if (state) {
state.textContent = value ? '已映射' : '未匹配';
state.classList.toggle('success', Boolean(value));
state.classList.toggle('danger', !value);
}
const bar = page.querySelector('[data-mapping-bar]');
const progress = page.querySelector('[data-mapping-progress]');
const total = Number(payload.total || 0);
const mapped = Number(payload.mapped || 0);
if (bar) bar.style.width = (total > 0 ? Math.round(mapped / total * 100) : 0) + '%';
if (progress) progress.textContent = mapped + ' / ' + total;
notify('账户映射已保存');
} catch (error) {
input.classList.add('is-error');
input.value = previous;
notify(error.message || '保存失败', true);
} finally {
input.classList.remove('is-saving');
}
});
});
})();
</script>