273 lines
9.3 KiB
PHP
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]);
|
|
}
|
|
}
|