CaiWuXiTong/modules/EcommerceFinance/View/FinanceLedgerPageData.php

264 lines
11 KiB
PHP

<?php
namespace Modules\EcommerceFinance\View;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
/**
* 财务台账页面数据。
*
* 所有页签一次性读取,页签切换只做前端显示/隐藏;各页签使用独立的分页
* 参数,避免在同一 URL 上互相覆盖。
*/
class FinanceLedgerPageData
{
public const TABS = [
'accounts' => ['title' => '账户总表', 'icon' => 'account_balance'],
'shops' => ['title' => '店铺台账', 'icon' => 'storefront'],
'flows' => ['title' => '每日资金流水', 'icon' => 'waterfall_chart'],
'transactions' => ['title' => '动账明细', 'icon' => 'receipt_long'],
'mapping' => ['title' => '账户映射', 'icon' => 'alt_route'],
'sources' => ['title' => '数据来源', 'icon' => 'database'],
];
public function page(?string $tab = null): array
{
$tab = (string) (request('tab') ?: $tab);
$tab = array_key_exists($tab, self::TABS) ? $tab : 'accounts';
$openingDate = (string) config('finance_ledger.opening_date', '2026-10-17');
$accounts = $this->accounts();
$shops = $this->shops();
$flows = $this->flows();
$transactions = $this->transactions();
return [
'view' => 'business.pages.financial-ledger.ledger',
'ledgerTab' => $tab,
'ledgerTabs' => self::TABS,
'ledgerOpeningDate' => $openingDate,
'ledgerAccounts' => $accounts['rows'],
'ledgerAccountSummary' => $accounts['summary'],
'ledgerShops' => $shops['rows'],
'ledgerShopSummary' => $shops['summary'],
'ledgerFlows' => $flows['rows'],
'ledgerFlowSummary' => $flows['summary'],
'ledgerTransactions' => $transactions['rows'],
'ledgerTransactionSummary' => $transactions['summary'],
'ledgerMapping' => $this->mapping(),
'ledgerSourceSummary' => $this->sourceSummary(),
'ledgerMeta' => $this->meta(),
'ledgerAccountOptions' => $this->accountOptions(),
'ledgerCanEditMapping' => \App\Utilities\FinanceLedgerAccess::canEditMapping(user()),
];
}
private function accounts(): array
{
$query = $this->db()->table('fin_accounts');
$keyword = trim((string) request('accounts_q', ''));
if ($keyword !== '') {
$query->where(function (Builder $builder) use ($keyword): void {
foreach (['company_name', 'account_short', 'account_no', 'owner'] as $column) {
$builder->orWhere($column, 'like', '%' . $keyword . '%');
}
});
}
$summaryQuery = clone $query;
$summary = [
'count' => (clone $query)->count(),
'balance' => round((float) (clone $summaryQuery)->sum('current_balance'), 2),
'blocked' => round((float) (clone $summaryQuery)->sum('blocked_amount'), 2),
];
$rows = $query
->orderBy('company_name')
->orderBy('account_short')
->paginate(100, ['*'], 'accounts_page')
->withQueryString();
return ['rows' => $rows, 'summary' => $summary];
}
private function shops(): array
{
$query = $this->db()->table('fin_shops');
foreach (['shops_q' => ['shop_name', 'platform', 'company_name'], 'shops_platform' => ['platform'], 'shops_company' => ['company_name']] as $param => $columns) {
$value = trim((string) request($param, ''));
if ($value === '') {
continue;
}
$query->where(function (Builder $builder) use ($columns, $value): void {
foreach ($columns as $column) {
$builder->orWhere($column, 'like', '%' . $value . '%');
}
});
}
$summary = [
'count' => (clone $query)->count(),
'balance' => round((float) (clone $query)->sum('shop_balance'), 2),
];
return [
'rows' => $query->orderBy('company_name')->orderBy('shop_name')
->paginate(100, ['*'], 'shops_page')->withQueryString(),
'summary' => $summary,
];
}
private function flows(): array
{
$query = $this->db()->table('fin_daily_flows');
foreach (['flows_date' => 'flow_date', 'flows_company' => 'company_name', 'flows_account' => 'account_short'] as $param => $column) {
$value = trim((string) request($param, ''));
if ($value !== '') {
$query->where($column, $param === 'flows_company' || $param === 'flows_account' ? 'like' : '=', $param === 'flows_company' || $param === 'flows_account' ? '%' . $value . '%' : $value);
}
}
$summary = [
'count' => (clone $query)->count(),
'closing' => round((float) (clone $query)->sum('closing_balance'), 2),
'inflow' => round((float) (clone $query)->sum('inflow'), 2),
'outflow' => round((float) (clone $query)->sum('outflow'), 2),
];
return [
'rows' => $query->orderBy('flow_date')->orderBy('account_short')
->paginate(100, ['*'], 'flows_page')->withQueryString(),
'summary' => $summary,
];
}
private function transactions(): array
{
$query = $this->transactionQuery();
$keyword = trim((string) request('txns_q', ''));
if ($keyword !== '') {
$query->where(function (Builder $builder) use ($keyword): void {
$builder->where('t.detail', 'like', '%' . $keyword . '%')
->orWhere('t.approval_no', 'like', '%' . $keyword . '%')
->orWhere('t.account_short', 'like', '%' . $keyword . '%')
->orWhere('m.account_short', 'like', '%' . $keyword . '%');
});
}
$account = trim((string) request('txns_account', ''));
if ($account !== '') {
$query->whereRaw("COALESCE(NULLIF(m.account_short,''), NULLIF(t.account_short,''), '未匹配') LIKE ?", ['%' . $account . '%']);
}
$from = trim((string) request('txns_from', ''));
if ($from !== '') {
$query->where('t.txn_date', '>=', $from);
}
$to = trim((string) request('txns_to', ''));
if ($to !== '') {
$query->where('t.txn_date', '<=', $to);
}
$source = trim((string) request('txns_source', ''));
if ($source !== '') {
$query->where('t.source', $source);
}
$summary = [
'count' => (clone $query)->count(),
'income' => round((float) (clone $query)->sum('t.income'), 2),
'expense' => round((float) (clone $query)->sum('t.expense'), 2),
];
return [
'rows' => $query->orderByDesc('t.txn_date')->orderByDesc('t.id')
->paginate(100, ['*'], 'txns_page')->withQueryString(),
'summary' => $summary,
];
}
private function transactionQuery(): Builder
{
return $this->db()->table('fin_transactions as t')
->leftJoin('fin_account_map as m', function ($join): void {
$join->on('m.source', '=', 't.source')->on('m.source_value', '=', 't.source_value');
})
->selectRaw("t.*, COALESCE(NULLIF(m.account_short,''), NULLIF(t.account_short,''), '未匹配') AS effective_account_short");
}
private function mapping(): array
{
$query = $this->db()->table('fin_account_map');
$source = trim((string) request('mapping_source', ''));
if ($source !== '') {
$query->where('source', $source);
}
$keyword = trim((string) request('mapping_q', ''));
if ($keyword !== '') {
$query->where(function (Builder $builder) use ($keyword): void {
$builder->where('source_value', 'like', '%' . $keyword . '%')
->orWhere('account_short', 'like', '%' . $keyword . '%');
});
}
if (request()->boolean('mapping_unmapped')) {
$query->where(function (Builder $builder): void {
$builder->whereNull('account_short')->orWhere('account_short', '');
});
}
$total = (clone $query)->count();
$mapped = (clone $query)->whereNotNull('account_short')->where('account_short', '<>', '')->count();
return [
'rows' => $query->orderByDesc('txn_count')->orderBy('source_value')
->paginate(100, ['*'], 'mapping_page')->withQueryString(),
'total' => $total,
'mapped' => $mapped,
'all_total' => $this->db()->table('fin_account_map')->count(),
'all_mapped' => $this->db()->table('fin_account_map')->whereNotNull('account_short')->where('account_short', '<>', '')->count(),
'sources' => $this->db()->table('fin_account_map')->select('source')->distinct()->orderBy('source')->pluck('source'),
];
}
private function sourceSummary(): array
{
return $this->db()->table('fin_transactions')
->selectRaw('source, process_type, COUNT(*) AS rows_count, SUM(income) AS income, SUM(expense) AS expense, MIN(txn_date) AS first_date, MAX(txn_date) AS last_date')
->groupBy('source', 'process_type')
->orderBy('source')
->orderByDesc('rows_count')
->get()
->map(static fn ($row): array => (array) $row)
->all();
}
private function meta(): array
{
return [
'accounts' => $this->db()->table('fin_accounts')->count(),
'shops' => $this->db()->table('fin_shops')->count(),
'companies' => $this->db()->table('fin_companies')->count(),
'opening_rows' => $this->db()->table('fin_account_opening')->count(),
'opening_sum' => round((float) $this->db()->table('fin_account_opening')->sum('opening_balance'), 2),
'transactions' => $this->db()->table('fin_transactions')->count(),
'income' => round((float) $this->db()->table('fin_transactions')->sum('income'), 2),
'expense' => round((float) $this->db()->table('fin_transactions')->sum('expense'), 2),
'flows' => $this->db()->table('fin_daily_flows')->count(),
'mapping_total' => $this->db()->table('fin_account_map')->count(),
'mapping_done' => $this->db()->table('fin_account_map')->whereNotNull('account_short')->where('account_short', '<>', '')->count(),
'last_synced_at' => $this->db()->table('fin_transactions')->max('synced_at'),
];
}
private function accountOptions(): array
{
return $this->db()->table('fin_accounts')
->select('account_short')->distinct()->orderBy('account_short')->pluck('account_short')->all();
}
private function db()
{
return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
}
}