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