From 440483e32d481cc7755f92bcda152fa5fc33bd8e Mon Sep 17 00:00:00 2001 From: shanshi <3316401569@qq.com> Date: Fri, 6 Nov 2026 12:00:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E8=B4=A2=E5=8A=A1?= =?UTF-8?q?=E5=8F=B0=E8=B4=A6=E5=8A=A8=E8=B4=A6=E6=B1=87=E6=80=BB=E5=8F=A3?= =?UTF-8?q?=E5=BE=84=20(#931)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../View/FinanceLedgerPageData.php | 90 +++++++++++++++++-- .../pages/financial-ledger/ledger.blade.php | 29 ++++-- .../FinanceLedgerHeaderControlsTest.php | 39 ++++++++ 3 files changed, 144 insertions(+), 14 deletions(-) diff --git a/modules/EcommerceFinance/View/FinanceLedgerPageData.php b/modules/EcommerceFinance/View/FinanceLedgerPageData.php index 24464ab5..7d76e117 100644 --- a/modules/EcommerceFinance/View/FinanceLedgerPageData.php +++ b/modules/EcommerceFinance/View/FinanceLedgerPageData.php @@ -8,6 +8,7 @@ use App\Utilities\FinanceLedgerAccountStatus; use App\Utilities\FinanceLedgerAccountShouldKeep; use App\Utilities\FinanceLedgerAccountType; use App\Utilities\FinanceLedgerAccess; +use Carbon\CarbonImmutable; use Illuminate\Database\Query\Builder; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\Paginator; @@ -582,7 +583,7 @@ class FinanceLedgerPageData $this->applyTransactionFilters($query, $accountShort, $period); $headerColumns = $this->transactionHeaderColumns(); - $summary = $this->transactionSummaryMetrics($query); + $summary = $this->transactionSummaryMetrics($query, $accountShort, $period); $sorted = $this->applyLedgerHeaderSort($query, 'transactions', $headerColumns); if (! $sorted) { $query->orderByDesc('t.txn_date')->orderByDesc('t.id'); @@ -597,14 +598,16 @@ class FinanceLedgerPageData public function transactionSummary(): array { + $view = $this->transactionView(); + $period = $this->dateRange('txns'); $query = $this->transactionQuery(); $this->applyTransactionFilters( $query, - $this->transactionView()['account_short'], - $this->dateRange('txns'), + $view['account_short'], + $period, ); - return $this->transactionSummaryMetrics($query); + return $this->transactionSummaryMetrics($query, $view['account_short'], $period); } private function applyTransactionFilters( @@ -707,15 +710,92 @@ class FinanceLedgerPageData } } - private function transactionSummaryMetrics(Builder $query): array + private function transactionSummaryMetrics(Builder $query, ?string $accountShort, array $period): array { return [ 'count' => (clone $query)->count(), + 'previous_closing' => $this->transactionPreviousMonthClosing($accountShort, $period), 'income' => round((float) (clone $query)->sum('t.income'), 2), 'expense' => round((float) (clone $query)->sum('t.expense'), 2), + 'current_balance' => $this->transactionCurrentBalance($accountShort), ]; } + private function transactionCurrentBalance(?string $accountShort): ?float + { + $query = $this->db()->table('fin_accounts'); + if ($accountShort !== null) { + $query->where('account_short', $accountShort); + } else { + $account = trim((string) request('txns_account', '')); + if ($account !== '') { + $query->where('account_short', 'like', '%' . $account . '%'); + } + } + if (! (clone $query)->exists()) { + return null; + } + + return round((float) (clone $query)->sum('current_balance'), 2); + } + + private function transactionPreviousMonthClosing(?string $accountShort, array $period): ?float + { + $from = $period['from'] ?? null; + if ($from === null) { + return null; + } + + $monthStart = CarbonImmutable::parse($from)->startOfMonth(); + $monthEnd = $monthStart->endOfMonth(); + $previousMonthEnd = $monthStart->subDay(); + $base = $this->db()->table('fin_daily_flows as f'); + $this->applyTransactionFlowAccountScope($base, $accountShort); + + $balances = []; + $priorRows = (clone $base) + ->where('f.flow_date', '>=', $previousMonthEnd->toDateString()) + ->where('f.flow_date', '<=', $previousMonthEnd->toDateString()) + ->get(['f.account_short', 'f.closing_balance']); + foreach ($priorRows as $row) { + $account = (string) $row->account_short; + $balances[$account] = ($balances[$account] ?? 0.0) + (float) $row->closing_balance; + } + + $firstFlowDates = (clone $base) + ->where('f.flow_date', '>=', $monthStart->toDateString()) + ->where('f.flow_date', '<=', $monthEnd->toDateString()) + ->selectRaw('account_short, MIN(flow_date) AS first_flow_date') + ->groupBy('account_short'); + $openingRows = (clone $base) + ->joinSub($firstFlowDates, 'first_flow', function ($join): void { + $join->on('first_flow.account_short', '=', 'f.account_short') + ->on('first_flow.first_flow_date', '=', 'f.flow_date'); + }) + ->get(['f.account_short', 'f.opening_balance']); + foreach ($openingRows as $row) { + $account = (string) $row->account_short; + if (! array_key_exists($account, $balances)) { + $balances[$account] = (float) $row->opening_balance; + } + } + + return $balances === [] ? null : round(array_sum($balances), 2); + } + + private function applyTransactionFlowAccountScope(Builder $query, ?string $accountShort): void + { + if ($accountShort !== null) { + $query->where('f.account_short', $accountShort); + return; + } + + $account = trim((string) request('txns_account', '')); + if ($account !== '') { + $query->where('f.account_short', 'like', '%' . $account . '%'); + } + } + private function transactionQuery(): Builder { $effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort']; diff --git a/resources/views/business/pages/financial-ledger/ledger.blade.php b/resources/views/business/pages/financial-ledger/ledger.blade.php index 4236294f..ac73ea02 100644 --- a/resources/views/business/pages/financial-ledger/ledger.blade.php +++ b/resources/views/business/pages/financial-ledger/ledger.blade.php @@ -354,6 +354,9 @@ .ledger-download-status { flex-basis:100%; color:var(--vip-success, #53793E); font-size:.68rem; font-weight:600; line-height:1.4; text-align:right; } .ledger-summary { display:flex; flex-wrap:wrap; gap:.65rem; margin-bottom:.85rem; } .ledger-summary__item { display:flex; flex:0 0 14rem; flex-direction:column; justify-content:center; gap:.2rem; width:14rem; min-width:14rem; height:3.4rem; padding:.6rem .8rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.55rem; background:var(--vip-content-accent-soft, #F7F0DE); box-shadow:0 1px 2px rgba(16,24,40,.04); } + #ledger-panel-transactions [data-ledger-transaction-summary] { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); } + #ledger-panel-transactions [data-ledger-transaction-summary] .ledger-summary__item { flex:none; width:auto; min-width:0; } + @media (max-width: 560px) { #ledger-panel-transactions [data-ledger-transaction-summary] { grid-template-columns:repeat(2,minmax(0,1fr)); } } .ledger-summary__item--available { background:#f3f7ee; } .ledger-summary__item--blocked { background:#fff5f5; } .ledger-summary__item--link { color:inherit; text-decoration:none; transition:border-color .15s ease, box-shadow .15s ease; } @@ -1128,7 +1131,7 @@ ])
-