264 lines
11 KiB
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'));
|
|
}
|
|
}
|