Files
usher-manage-stack/app/Http/Controllers/TransactionController.php
2025-11-20 23:21:05 +08:00

273 lines
9.3 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Budget;
use App\Models\BudgetItem;
use App\Models\ChartOfAccount;
use App\Models\Transaction;
use App\Support\AuditLogger;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TransactionController extends Controller
{
public function index(Request $request)
{
$query = Transaction::query()
->with(['chartOfAccount', 'budgetItem.budget', 'createdBy']);
// Filter by transaction type
if ($type = $request->string('transaction_type')->toString()) {
$query->where('transaction_type', $type);
}
// Filter by account
if ($accountId = $request->integer('chart_of_account_id')) {
$query->where('chart_of_account_id', $accountId);
}
// Filter by budget
if ($budgetId = $request->integer('budget_id')) {
$query->whereHas('budgetItem', fn($q) => $q->where('budget_id', $budgetId));
}
// Filter by date range
if ($startDate = $request->date('start_date')) {
$query->where('transaction_date', '>=', $startDate);
}
if ($endDate = $request->date('end_date')) {
$query->where('transaction_date', '<=', $endDate);
}
// Search description
if ($search = $request->string('search')->toString()) {
$query->where(function ($q) use ($search) {
$q->where('description', 'like', "%{$search}%")
->orWhere('reference_number', 'like', "%{$search}%");
});
}
$transactions = $query->orderByDesc('transaction_date')
->orderByDesc('created_at')
->paginate(20);
// Get filter options
$accounts = ChartOfAccount::where('is_active', true)
->whereIn('account_type', ['income', 'expense'])
->orderBy('account_code')
->get();
$budgets = Budget::orderByDesc('fiscal_year')->get();
// Calculate totals
$totalIncome = (clone $query)->income()->sum('amount');
$totalExpense = (clone $query)->expense()->sum('amount');
return view('admin.transactions.index', [
'transactions' => $transactions,
'accounts' => $accounts,
'budgets' => $budgets,
'totalIncome' => $totalIncome,
'totalExpense' => $totalExpense,
]);
}
public function create(Request $request)
{
// Get active budgets
$budgets = Budget::whereIn('status', [Budget::STATUS_ACTIVE, Budget::STATUS_APPROVED])
->orderByDesc('fiscal_year')
->get();
// Get income and expense accounts
$incomeAccounts = ChartOfAccount::where('account_type', 'income')
->where('is_active', true)
->orderBy('account_code')
->get();
$expenseAccounts = ChartOfAccount::where('account_type', 'expense')
->where('is_active', true)
->orderBy('account_code')
->get();
// Pre-select budget if provided
$selectedBudgetId = $request->integer('budget_id');
return view('admin.transactions.create', [
'budgets' => $budgets,
'incomeAccounts' => $incomeAccounts,
'expenseAccounts' => $expenseAccounts,
'selectedBudgetId' => $selectedBudgetId,
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'chart_of_account_id' => ['required', 'exists:chart_of_accounts,id'],
'transaction_date' => ['required', 'date'],
'amount' => ['required', 'numeric', 'min:0.01'],
'transaction_type' => ['required', 'in:income,expense'],
'description' => ['required', 'string', 'max:255'],
'reference_number' => ['nullable', 'string', 'max:255'],
'budget_item_id' => ['nullable', 'exists:budget_items,id'],
'notes' => ['nullable', 'string'],
]);
DB::transaction(function () use ($validated, $request) {
$transaction = Transaction::create([
...$validated,
'created_by_user_id' => $request->user()->id,
]);
// Update budget item actual amount if linked
if ($transaction->budget_item_id) {
$this->updateBudgetItemActual($transaction->budget_item_id);
}
AuditLogger::log('transaction.created', $transaction, [
'user' => $request->user()->name,
'amount' => $validated['amount'],
'type' => $validated['transaction_type'],
]);
});
return redirect()
->route('admin.transactions.index')
->with('status', __('Transaction recorded successfully.'));
}
public function show(Transaction $transaction)
{
$transaction->load([
'chartOfAccount',
'budgetItem.budget',
'budgetItem.chartOfAccount',
'financeDocument',
'membershipPayment',
'createdBy',
]);
return view('admin.transactions.show', [
'transaction' => $transaction,
]);
}
public function edit(Transaction $transaction)
{
// Only allow editing if not linked to finance document or payment
if ($transaction->finance_document_id || $transaction->membership_payment_id) {
return redirect()
->route('admin.transactions.show', $transaction)
->with('error', __('Cannot edit auto-generated transactions.'));
}
$budgets = Budget::whereIn('status', [Budget::STATUS_ACTIVE, Budget::STATUS_APPROVED])
->orderByDesc('fiscal_year')
->get();
$incomeAccounts = ChartOfAccount::where('account_type', 'income')
->where('is_active', true)
->orderBy('account_code')
->get();
$expenseAccounts = ChartOfAccount::where('account_type', 'expense')
->where('is_active', true)
->orderBy('account_code')
->get();
return view('admin.transactions.edit', [
'transaction' => $transaction,
'budgets' => $budgets,
'incomeAccounts' => $incomeAccounts,
'expenseAccounts' => $expenseAccounts,
]);
}
public function update(Request $request, Transaction $transaction)
{
// Only allow editing if not auto-generated
if ($transaction->finance_document_id || $transaction->membership_payment_id) {
abort(403, 'Cannot edit auto-generated transactions.');
}
$validated = $request->validate([
'chart_of_account_id' => ['required', 'exists:chart_of_accounts,id'],
'transaction_date' => ['required', 'date'],
'amount' => ['required', 'numeric', 'min:0.01'],
'description' => ['required', 'string', 'max:255'],
'reference_number' => ['nullable', 'string', 'max:255'],
'budget_item_id' => ['nullable', 'exists:budget_items,id'],
'notes' => ['nullable', 'string'],
]);
DB::transaction(function () use ($transaction, $validated, $request) {
$oldBudgetItemId = $transaction->budget_item_id;
$transaction->update($validated);
// Update budget item actuals
if ($oldBudgetItemId) {
$this->updateBudgetItemActual($oldBudgetItemId);
}
if ($transaction->budget_item_id && $transaction->budget_item_id != $oldBudgetItemId) {
$this->updateBudgetItemActual($transaction->budget_item_id);
}
AuditLogger::log('transaction.updated', $transaction, [
'user' => $request->user()->name,
]);
});
return redirect()
->route('admin.transactions.show', $transaction)
->with('status', __('Transaction updated successfully.'));
}
public function destroy(Request $request, Transaction $transaction)
{
// Only allow deleting if not auto-generated
if ($transaction->finance_document_id || $transaction->membership_payment_id) {
abort(403, 'Cannot delete auto-generated transactions.');
}
$budgetItemId = $transaction->budget_item_id;
DB::transaction(function () use ($transaction, $budgetItemId, $request) {
$transaction->delete();
// Update budget item actual
if ($budgetItemId) {
$this->updateBudgetItemActual($budgetItemId);
}
AuditLogger::log('transaction.deleted', null, [
'user' => $request->user()->name,
'description' => $transaction->description,
'amount' => $transaction->amount,
]);
});
return redirect()
->route('admin.transactions.index')
->with('status', __('Transaction deleted successfully.'));
}
/**
* Update budget item actual amount based on all transactions
*/
protected function updateBudgetItemActual(int $budgetItemId): void
{
$budgetItem = BudgetItem::find($budgetItemId);
if (!$budgetItem) {
return;
}
$actualAmount = Transaction::where('budget_item_id', $budgetItemId)
->sum('amount');
$budgetItem->update(['actual_amount' => $actualAmount]);
}
}