687 lines
30 KiB
PHP
687 lines
30 KiB
PHP
<?php
|
|
|
|
namespace Modules\EcommerceFinance\View;
|
|
|
|
use App\Utilities\FinanceLedgerAccountStatus;
|
|
use App\Utilities\FinanceLedgerAccountShouldKeep;
|
|
use App\Utilities\FinanceLedgerAccountType;
|
|
use App\Utilities\FinanceLedgerAccess;
|
|
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'],
|
|
];
|
|
|
|
private const ACCOUNT_FILTER_EMPTY = '__empty__';
|
|
|
|
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(),
|
|
'ledgerAccountFilterOptions' => $this->accountFilterOptions(),
|
|
'ledgerAccountStatusOptions' => FinanceLedgerAccountStatus::OPTIONS,
|
|
'ledgerAccountPlanOptions' => $this->accountPlanOptions(),
|
|
'ledgerAccountKeepOptions' => FinanceLedgerAccountShouldKeep::OPTIONS,
|
|
'ledgerCanEditMapping' => FinanceLedgerAccess::canEditMapping(user()),
|
|
'ledgerCanEditAccountStatus' => FinanceLedgerAccess::canEditAccountStatus(user()),
|
|
'ledgerCanEditAccountType' => FinanceLedgerAccess::canEditAccountType(user()),
|
|
];
|
|
}
|
|
|
|
private function accounts(): array
|
|
{
|
|
$query = $this->db()->table('fin_accounts as a');
|
|
$keyword = trim((string) request('accounts_q', ''));
|
|
if ($keyword !== '') {
|
|
$query->where(function (Builder $builder) use ($keyword): void {
|
|
foreach (['a.company_name', 'a.account_short', 'a.account_no', 'a.owner'] as $column) {
|
|
$builder->orWhere($column, 'like', '%' . $keyword . '%');
|
|
}
|
|
});
|
|
}
|
|
|
|
$accountShort = trim((string) request('accounts_short', ''));
|
|
if ($accountShort !== '') {
|
|
$query->where('a.account_short', 'like', '%' . $accountShort . '%');
|
|
}
|
|
|
|
$status = trim((string) request('accounts_status', ''));
|
|
if ($status !== '' && array_key_exists($status, FinanceLedgerAccountStatus::OPTIONS)) {
|
|
if ($status === FinanceLedgerAccountStatus::NORMAL) {
|
|
$nonNormalStatuses = array_values(array_diff(
|
|
array_keys(FinanceLedgerAccountStatus::OPTIONS),
|
|
[FinanceLedgerAccountStatus::NORMAL],
|
|
));
|
|
$query->whereNotIn(
|
|
'a.account_short',
|
|
$this->db()->table('fin_account_statuses')
|
|
->select('account_short')
|
|
->whereIn('status', $nonNormalStatuses),
|
|
);
|
|
} else {
|
|
$query->whereIn(
|
|
'a.account_short',
|
|
$this->db()->table('fin_account_statuses')
|
|
->select('account_short')
|
|
->where('status', $status),
|
|
);
|
|
}
|
|
}
|
|
|
|
$region = trim((string) request('accounts_region', ''));
|
|
if ($region !== '') {
|
|
if ($region === self::ACCOUNT_FILTER_EMPTY) {
|
|
$query->where(function (Builder $builder): void {
|
|
$builder->whereNull('a.region')->orWhere('a.region', '');
|
|
});
|
|
} else {
|
|
$query->where('a.region', $region);
|
|
}
|
|
}
|
|
|
|
$accountType = trim((string) request('accounts_type', ''));
|
|
if ($accountType !== '') {
|
|
$query->where(function (Builder $effectiveType) use ($accountType): void {
|
|
$effectiveType->where(function (Builder $baseType) use ($accountType): void {
|
|
$baseType->whereNotIn(
|
|
'a.account_short',
|
|
$this->db()->table('fin_account_type_overrides')->select('account_short'),
|
|
);
|
|
if ($accountType === self::ACCOUNT_FILTER_EMPTY) {
|
|
$baseType->where(function (Builder $blank): void {
|
|
$blank->whereNull('a.account_type')->orWhere('a.account_type', '');
|
|
});
|
|
} else {
|
|
$baseType->where('a.account_type', $accountType);
|
|
}
|
|
});
|
|
|
|
$overrideType = $this->db()->table('fin_account_type_overrides')->select('account_short');
|
|
if ($accountType === self::ACCOUNT_FILTER_EMPTY) {
|
|
$overrideType->where(function (Builder $blank): void {
|
|
$blank->whereNull('account_type')->orWhere('account_type', '');
|
|
});
|
|
} else {
|
|
$overrideType->where('account_type', $accountType);
|
|
}
|
|
$effectiveType->orWhereIn('a.account_short', $overrideType);
|
|
});
|
|
}
|
|
|
|
$balanceRange = trim((string) request('accounts_balance', ''));
|
|
if ($balanceRange === '0-5') {
|
|
$query->where('a.current_balance', '>=', 0)->where('a.current_balance', '<', 50000);
|
|
} elseif ($balanceRange === '5-20') {
|
|
$query->where('a.current_balance', '>=', 50000)->where('a.current_balance', '<', 200000);
|
|
} elseif ($balanceRange === '20-plus') {
|
|
$query->where('a.current_balance', '>=', 200000);
|
|
}
|
|
|
|
$headerColumns = $this->accountHeaderColumns();
|
|
$this->applyLedgerHeaderFilter($query, 'accounts', $headerColumns);
|
|
$summaryQuery = clone $query;
|
|
$summary = [
|
|
'count' => (clone $query)->count(),
|
|
'unretained' => FinanceLedgerAccountShouldKeep::applyUnretainedCondition(
|
|
clone $query,
|
|
'a.account_should_keep',
|
|
'a.company_plan',
|
|
)->count(),
|
|
'current_balance' => round((float) (clone $summaryQuery)->sum('a.current_balance'), 2),
|
|
'balance' => round((float) (clone $summaryQuery)->sum('a.daily_available'), 2),
|
|
'blocked' => round((float) (clone $summaryQuery)->sum('a.blocked_amount'), 2),
|
|
];
|
|
|
|
$sorted = $this->applyLedgerHeaderSort($query, 'accounts', $headerColumns);
|
|
if (! $sorted) {
|
|
$query->orderBy('a.company_name')->orderBy('a.account_short');
|
|
}
|
|
$rows = $query
|
|
->paginate(100, ['*'], 'accounts_page')
|
|
->withQueryString();
|
|
|
|
$accountShorts = $rows->pluck('account_short')->filter()->unique()->values()->all();
|
|
$statuses = $accountShorts === []
|
|
? collect()
|
|
: $this->db()->table('fin_account_statuses')
|
|
->whereIn('account_short', $accountShorts)
|
|
->pluck('status', 'account_short');
|
|
$typeOverrides = $accountShorts === []
|
|
? collect()
|
|
: $this->db()->table('fin_account_type_overrides')
|
|
->whereIn('account_short', $accountShorts)
|
|
->pluck('account_type', 'account_short');
|
|
$rows->getCollection()->transform(static function ($row) use ($statuses, $typeOverrides) {
|
|
$row->account_status = FinanceLedgerAccountStatus::normalize($statuses[$row->account_short] ?? null);
|
|
if ($typeOverrides->has($row->account_short)) {
|
|
$row->account_type = (string) ($typeOverrides->get($row->account_short) ?? '');
|
|
}
|
|
$row->account_should_keep = FinanceLedgerAccountShouldKeep::display(
|
|
$row->account_should_keep ?? null,
|
|
$row->company_plan ?? null,
|
|
);
|
|
|
|
return $row;
|
|
});
|
|
|
|
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 . '%');
|
|
}
|
|
});
|
|
}
|
|
|
|
$headerColumns = $this->shopHeaderColumns();
|
|
$this->applyLedgerHeaderFilter($query, 'shops', $headerColumns);
|
|
$summary = [
|
|
'count' => (clone $query)->count(),
|
|
'balance' => round((float) (clone $query)->sum('shop_balance'), 2),
|
|
];
|
|
|
|
$sorted = $this->applyLedgerHeaderSort($query, 'shops', $headerColumns);
|
|
if (! $sorted) {
|
|
$query->orderBy('company_name')->orderBy('shop_name');
|
|
}
|
|
|
|
return [
|
|
'rows' => $query
|
|
->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);
|
|
}
|
|
}
|
|
|
|
$headerColumns = $this->flowHeaderColumns();
|
|
$this->applyLedgerHeaderFilter($query, 'flows', $headerColumns);
|
|
$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),
|
|
];
|
|
|
|
$sorted = $this->applyLedgerHeaderSort($query, 'flows', $headerColumns);
|
|
if (! $sorted) {
|
|
$query->orderBy('flow_date')->orderBy('account_short');
|
|
}
|
|
|
|
return [
|
|
'rows' => $query
|
|
->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 !== '') {
|
|
$effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort'];
|
|
$query->whereRaw($effectiveAccount . ' 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);
|
|
}
|
|
|
|
$headerColumns = $this->transactionHeaderColumns();
|
|
$this->applyLedgerHeaderFilter($query, 'transactions', $headerColumns);
|
|
$summary = [
|
|
'count' => (clone $query)->count(),
|
|
'income' => round((float) (clone $query)->sum('t.income'), 2),
|
|
'expense' => round((float) (clone $query)->sum('t.expense'), 2),
|
|
];
|
|
|
|
$sorted = $this->applyLedgerHeaderSort($query, 'transactions', $headerColumns);
|
|
if (! $sorted) {
|
|
$query->orderByDesc('t.txn_date')->orderByDesc('t.id');
|
|
}
|
|
|
|
return [
|
|
'rows' => $query
|
|
->paginate(100, ['*'], 'txns_page')->withQueryString(),
|
|
'summary' => $summary,
|
|
];
|
|
}
|
|
|
|
private function transactionQuery(): Builder
|
|
{
|
|
$effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort'];
|
|
|
|
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');
|
|
})
|
|
->select([
|
|
't.id',
|
|
't.txn_date',
|
|
't.account_short',
|
|
't.detail',
|
|
't.income',
|
|
't.expense',
|
|
't.approval_no',
|
|
't.source',
|
|
't.source_value',
|
|
't.process_type',
|
|
't.row_key',
|
|
't.synced_at',
|
|
't.created_at',
|
|
't.updated_at',
|
|
])
|
|
->selectRaw($effectiveAccount . ' 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', '');
|
|
});
|
|
}
|
|
|
|
$headerColumns = $this->mappingHeaderColumns();
|
|
$this->applyLedgerHeaderFilter($query, 'mapping', $headerColumns);
|
|
$total = (clone $query)->count();
|
|
$mapped = (clone $query)->whereNotNull('account_short')->where('account_short', '<>', '')->count();
|
|
$sorted = $this->applyLedgerHeaderSort($query, 'mapping', $headerColumns);
|
|
if (! $sorted) {
|
|
$query->orderByDesc('txn_count')->orderBy('source_value');
|
|
}
|
|
|
|
return [
|
|
'rows' => $query
|
|
->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
|
|
{
|
|
$rows = $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 function ($row): array {
|
|
$data = (array) $row;
|
|
$data['date_range'] = trim((string) ($data['first_date'] ?? '') . ' ~ ' . (string) ($data['last_date'] ?? ''));
|
|
|
|
return $data;
|
|
})
|
|
->all();
|
|
|
|
return $this->filterAndSortHeaderRows($rows, 'sources', $this->sourceHeaderColumns());
|
|
}
|
|
|
|
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 accountFilterOptions(): array
|
|
{
|
|
$baseTypes = $this->db()->table('fin_accounts')
|
|
->whereNotNull('account_type')->where('account_type', '<>', '')
|
|
->distinct()->orderBy('account_type')->pluck('account_type')->all();
|
|
$overrideTypes = $this->db()->table('fin_account_type_overrides')
|
|
->whereNotNull('account_type')->where('account_type', '<>', '')
|
|
->distinct()->orderBy('account_type')->pluck('account_type')->all();
|
|
$types = FinanceLedgerAccountType::options($baseTypes, $overrideTypes);
|
|
|
|
return [
|
|
'regions' => $this->db()->table('fin_accounts')
|
|
->whereNotNull('region')->where('region', '<>', '')
|
|
->distinct()->orderBy('region')->pluck('region')->all(),
|
|
'types' => $types,
|
|
];
|
|
}
|
|
|
|
private function accountPlanOptions(): array
|
|
{
|
|
return $this->db()->table('fin_accounts')
|
|
->whereNotNull('company_plan')
|
|
->where('company_plan', '<>', '')
|
|
->distinct()
|
|
->orderBy('company_plan')
|
|
->pluck('company_plan')
|
|
->map(static fn (mixed $value): string => trim((string) $value))
|
|
->filter()
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
private function applyLedgerHeaderFilter(Builder $query, string $tab, array $columns): void
|
|
{
|
|
$column = trim((string) request($tab . '_filter_column', ''));
|
|
$value = trim((string) request($tab . '_filter_value', ''));
|
|
if ($value === '' || ! isset($columns[$column])) {
|
|
return;
|
|
}
|
|
|
|
$definition = $columns[$column];
|
|
$expression = $definition['filter'] ?? $definition['sort'] ?? null;
|
|
if (! is_string($expression) || $expression === '') {
|
|
return;
|
|
}
|
|
|
|
$type = $definition['type'] ?? 'text';
|
|
if ($type === 'number') {
|
|
if (is_numeric($value)) {
|
|
$query->whereRaw($expression . ' = ?', [$value]);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (in_array($type, ['enum', 'date'], true)) {
|
|
$query->whereRaw($expression . ' = ?', [$value]);
|
|
|
|
return;
|
|
}
|
|
|
|
$query->whereRaw($expression . ' LIKE ?', ['%' . $value . '%']);
|
|
}
|
|
|
|
private function applyLedgerHeaderSort(Builder $query, string $tab, array $columns): bool
|
|
{
|
|
$column = trim((string) request($tab . '_sort', ''));
|
|
if (! isset($columns[$column])) {
|
|
return false;
|
|
}
|
|
|
|
$expression = $columns[$column]['sort'] ?? null;
|
|
if (! is_string($expression) || $expression === '') {
|
|
return false;
|
|
}
|
|
|
|
$direction = strtolower((string) request($tab . '_direction', 'asc')) === 'desc' ? 'desc' : 'asc';
|
|
$query->orderByRaw($expression . ' ' . $direction);
|
|
|
|
return true;
|
|
}
|
|
|
|
private function filterAndSortHeaderRows(array $rows, string $tab, array $columns): array
|
|
{
|
|
$filterColumn = trim((string) request($tab . '_filter_column', ''));
|
|
$filterValue = trim((string) request($tab . '_filter_value', ''));
|
|
if ($filterValue !== '' && isset($columns[$filterColumn])) {
|
|
$valueColumn = $columns[$filterColumn]['value'] ?? $filterColumn;
|
|
$type = $columns[$filterColumn]['type'] ?? 'text';
|
|
$rows = array_values(array_filter(
|
|
$rows,
|
|
static function (array $row) use ($valueColumn, $filterValue, $type): bool {
|
|
$value = trim((string) ($row[$valueColumn] ?? ''));
|
|
if ($type === 'number') {
|
|
return is_numeric($filterValue) && is_numeric($value) && (float) $value === (float) $filterValue;
|
|
}
|
|
if (in_array($type, ['enum', 'date'], true)) {
|
|
return $value === $filterValue;
|
|
}
|
|
|
|
return mb_stripos($value, $filterValue) !== false;
|
|
},
|
|
));
|
|
}
|
|
|
|
$sortColumn = trim((string) request($tab . '_sort', ''));
|
|
if (isset($columns[$sortColumn])) {
|
|
$valueColumn = $columns[$sortColumn]['value'] ?? $sortColumn;
|
|
$direction = strtolower((string) request($tab . '_direction', 'asc')) === 'desc' ? -1 : 1;
|
|
$type = $columns[$sortColumn]['type'] ?? 'text';
|
|
usort($rows, static function (array $left, array $right) use ($valueColumn, $direction, $type): int {
|
|
$leftValue = $left[$valueColumn] ?? null;
|
|
$rightValue = $right[$valueColumn] ?? null;
|
|
if ($type === 'number') {
|
|
return ((float) $leftValue <=> (float) $rightValue) * $direction;
|
|
}
|
|
|
|
return strnatcasecmp((string) $leftValue, (string) $rightValue) * $direction;
|
|
});
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
private function accountHeaderColumns(): array
|
|
{
|
|
$a = $this->queryAlias('a');
|
|
$statusTable = $this->tableName('fin_account_statuses');
|
|
$typeTable = $this->tableName('fin_account_type_overrides');
|
|
$statusExpression = "COALESCE((SELECT NULLIF(s.status, '') FROM {$statusTable} s WHERE s.account_short = {$a}.account_short LIMIT 1), 'normal')";
|
|
$typeExpression = "COALESCE((SELECT NULLIF(o.account_type, '') FROM {$typeTable} o WHERE o.account_short = {$a}.account_short LIMIT 1), NULLIF({$a}.account_type, ''), '')";
|
|
$keepExpression = "COALESCE(NULLIF({$a}.account_should_keep, ''), CASE WHEN COALESCE({$a}.company_plan, '') = '' THEN '' WHEN {$a}.company_plan LIKE '%保留%' THEN '是' ELSE '否' END)";
|
|
|
|
return [
|
|
'company_name' => ['sort' => $a . '.company_name', 'type' => 'text'],
|
|
'account_short' => ['sort' => $a . '.account_short', 'type' => 'text'],
|
|
'account_type' => ['sort' => $typeExpression, 'type' => 'enum'],
|
|
'account_status' => ['sort' => $statusExpression, 'type' => 'enum'],
|
|
'region' => ['sort' => $a . '.region', 'type' => 'text'],
|
|
'account_no' => ['sort' => $a . '.account_no', 'type' => 'text'],
|
|
'bank_name' => ['sort' => $a . '.bank_name', 'type' => 'text'],
|
|
'bank_city_branch' => ['sort' => $a . '.bank_city_branch', 'type' => 'text'],
|
|
'company_plan' => ['sort' => $a . '.company_plan', 'type' => 'enum'],
|
|
'account_should_keep' => ['sort' => $keepExpression, 'type' => 'enum'],
|
|
'account_last4' => ['sort' => $a . '.account_last4', 'type' => 'text'],
|
|
'single_limit' => ['sort' => "CAST({$a}.single_limit AS DECIMAL(18, 2))", 'filter' => $a . '.single_limit', 'type' => 'number'],
|
|
'daily_limit' => ['sort' => "CAST({$a}.daily_limit AS DECIMAL(18, 2))", 'filter' => $a . '.daily_limit', 'type' => 'number'],
|
|
'monthly_limit' => ['sort' => "CAST({$a}.monthly_limit AS DECIMAL(18, 2))", 'filter' => $a . '.monthly_limit', 'type' => 'number'],
|
|
'yearly_limit' => ['sort' => "CAST({$a}.yearly_limit AS DECIMAL(18, 2))", 'filter' => $a . '.yearly_limit', 'type' => 'number'],
|
|
'current_balance' => ['sort' => $a . '.current_balance', 'type' => 'number'],
|
|
'daily_available' => ['sort' => $a . '.daily_available', 'type' => 'number'],
|
|
'blocked_amount' => ['sort' => $a . '.blocked_amount', 'type' => 'number'],
|
|
'daily_withdraw' => ['sort' => $a . '.daily_available', 'type' => 'number'],
|
|
'clearing_days' => ['sort' => $a . '.clearing_days', 'type' => 'number'],
|
|
'raise_target' => ['sort' => "CAST({$a}.raise_target AS DECIMAL(18, 2))", 'filter' => $a . '.raise_target', 'type' => 'number'],
|
|
'raise_status' => ['sort' => $a . '.raise_status', 'type' => 'text'],
|
|
'raise_actions' => ['sort' => $a . '.raise_actions', 'type' => 'text'],
|
|
'done_actions' => ['sort' => $a . '.done_actions', 'type' => 'text'],
|
|
'next_action' => ['sort' => $a . '.next_action', 'type' => 'text'],
|
|
'remark' => ['sort' => $a . '.remark', 'type' => 'text'],
|
|
'owner' => ['sort' => $a . '.owner', 'type' => 'text'],
|
|
];
|
|
}
|
|
|
|
private function shopHeaderColumns(): array
|
|
{
|
|
return [
|
|
'shop_name' => ['sort' => 'shop_name', 'type' => 'text'],
|
|
'platform' => ['sort' => 'platform', 'type' => 'text'],
|
|
'company_name' => ['sort' => 'company_name', 'type' => 'text'],
|
|
'account_short' => ['sort' => 'account_short', 'type' => 'text'],
|
|
'status' => ['sort' => 'status', 'type' => 'text'],
|
|
'payout_account' => ['sort' => 'payout_account', 'type' => 'text'],
|
|
'shop_balance' => ['sort' => 'shop_balance', 'type' => 'number'],
|
|
'remark' => ['sort' => 'remark', 'type' => 'text'],
|
|
];
|
|
}
|
|
|
|
private function flowHeaderColumns(): array
|
|
{
|
|
return [
|
|
'flow_date' => ['sort' => 'flow_date', 'type' => 'date'],
|
|
'company_name' => ['sort' => 'company_name', 'type' => 'text'],
|
|
'account_short' => ['sort' => 'account_short', 'type' => 'text'],
|
|
'opening_balance' => ['sort' => 'opening_balance', 'type' => 'number'],
|
|
'inflow' => ['sort' => 'inflow', 'type' => 'number'],
|
|
'outflow' => ['sort' => 'outflow', 'type' => 'number'],
|
|
'closing_balance' => ['sort' => 'closing_balance', 'type' => 'number'],
|
|
'remark' => ['sort' => 'remark', 'type' => 'text'],
|
|
];
|
|
}
|
|
|
|
private function transactionHeaderColumns(): array
|
|
{
|
|
$t = $this->queryAlias('t');
|
|
$m = $this->queryAlias('m');
|
|
$effectiveAccount = "COALESCE(NULLIF({$m}.account_short, ''), NULLIF({$t}.account_short, ''), '未匹配')";
|
|
|
|
return [
|
|
'txn_date' => ['sort' => $t . '.txn_date', 'type' => 'date'],
|
|
'effective_account_short' => ['sort' => $effectiveAccount, 'type' => 'text'],
|
|
'detail' => ['sort' => $t . '.detail', 'type' => 'text'],
|
|
'source' => ['sort' => $t . '.source', 'type' => 'enum'],
|
|
'income' => ['sort' => $t . '.income', 'type' => 'number'],
|
|
'expense' => ['sort' => $t . '.expense', 'type' => 'number'],
|
|
'approval_no' => ['sort' => $t . '.approval_no', 'type' => 'text'],
|
|
];
|
|
}
|
|
|
|
private function mappingHeaderColumns(): array
|
|
{
|
|
$mappingStatus = "CASE WHEN COALESCE(account_short, '') = '' THEN 'unmapped' ELSE 'mapped' END";
|
|
|
|
return [
|
|
'source' => ['sort' => 'source', 'type' => 'enum'],
|
|
'source_value' => ['sort' => 'source_value', 'type' => 'text'],
|
|
'txn_count' => ['sort' => 'txn_count', 'type' => 'number'],
|
|
'income_sum' => ['sort' => 'income_sum', 'type' => 'number'],
|
|
'expense_sum' => ['sort' => 'expense_sum', 'type' => 'number'],
|
|
'account_short' => ['sort' => 'account_short', 'type' => 'text'],
|
|
'mapping_status' => ['sort' => $mappingStatus, 'type' => 'enum'],
|
|
];
|
|
}
|
|
|
|
private function sourceHeaderColumns(): array
|
|
{
|
|
return [
|
|
'source' => ['value' => 'source', 'type' => 'enum'],
|
|
'process_type' => ['value' => 'process_type', 'type' => 'text'],
|
|
'rows_count' => ['value' => 'rows_count', 'type' => 'number'],
|
|
'income' => ['value' => 'income', 'type' => 'number'],
|
|
'expense' => ['value' => 'expense', 'type' => 'number'],
|
|
'date_range' => ['value' => 'date_range', 'type' => 'text'],
|
|
];
|
|
}
|
|
|
|
private function db()
|
|
{
|
|
return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
|
}
|
|
|
|
private function tableName(string $table): string
|
|
{
|
|
return $this->db()->getTablePrefix() . $table;
|
|
}
|
|
|
|
private function queryAlias(string $alias): string
|
|
{
|
|
return $this->db()->getTablePrefix() . $alias;
|
|
}
|
|
}
|