Initial commit

This commit is contained in:
2025-11-20 23:21:05 +08:00
commit 13bc6db529
378 changed files with 54527 additions and 0 deletions

View File

@@ -0,0 +1,269 @@
<?php
namespace App\Http\Controllers;
use App\Models\Budget;
use App\Models\BudgetItem;
use App\Models\ChartOfAccount;
use App\Support\AuditLogger;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class BudgetController extends Controller
{
public function index(Request $request)
{
$query = Budget::query()->with('createdBy', 'approvedBy');
// Filter by fiscal year
if ($fiscalYear = $request->integer('fiscal_year')) {
$query->where('fiscal_year', $fiscalYear);
}
// Filter by status
if ($status = $request->string('status')->toString()) {
$query->where('status', $status);
}
$budgets = $query->orderByDesc('fiscal_year')
->orderByDesc('created_at')
->paginate(15);
// Get unique fiscal years for filter dropdown
$fiscalYears = Budget::select('fiscal_year')
->distinct()
->orderByDesc('fiscal_year')
->pluck('fiscal_year');
return view('admin.budgets.index', [
'budgets' => $budgets,
'fiscalYears' => $fiscalYears,
]);
}
public function create()
{
return view('admin.budgets.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'fiscal_year' => ['required', 'integer', 'min:2000', 'max:2100'],
'name' => ['required', 'string', 'max:255'],
'period_type' => ['required', 'in:annual,quarterly,monthly'],
'period_start' => ['required', 'date'],
'period_end' => ['required', 'date', 'after:period_start'],
'notes' => ['nullable', 'string'],
]);
$budget = Budget::create([
...$validated,
'status' => Budget::STATUS_DRAFT,
'created_by_user_id' => $request->user()->id,
]);
AuditLogger::log('budget.created', $budget, $validated);
return redirect()
->route('admin.budgets.edit', $budget)
->with('status', __('Budget created successfully. Add budget items below.'));
}
public function show(Budget $budget)
{
$budget->load([
'createdBy',
'approvedBy',
'budgetItems.chartOfAccount',
'budgetItems' => fn($q) => $q->orderBy('chart_of_account_id'),
]);
// Group budget items by account type
$incomeItems = $budget->budgetItems->filter(fn($item) => $item->chartOfAccount->isIncome());
$expenseItems = $budget->budgetItems->filter(fn($item) => $item->chartOfAccount->isExpense());
return view('admin.budgets.show', [
'budget' => $budget,
'incomeItems' => $incomeItems,
'expenseItems' => $expenseItems,
]);
}
public function edit(Budget $budget)
{
if (!$budget->canBeEdited()) {
return redirect()
->route('admin.budgets.show', $budget)
->with('error', __('This budget cannot be edited.'));
}
$budget->load(['budgetItems.chartOfAccount']);
// Get all active 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();
return view('admin.budgets.edit', [
'budget' => $budget,
'incomeAccounts' => $incomeAccounts,
'expenseAccounts' => $expenseAccounts,
]);
}
public function update(Request $request, Budget $budget)
{
if (!$budget->canBeEdited()) {
abort(403, 'This budget cannot be edited.');
}
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'period_start' => ['required', 'date'],
'period_end' => ['required', 'date', 'after:period_start'],
'notes' => ['nullable', 'string'],
'budget_items' => ['nullable', 'array'],
'budget_items.*.chart_of_account_id' => ['required', 'exists:chart_of_accounts,id'],
'budget_items.*.budgeted_amount' => ['required', 'numeric', 'min:0'],
'budget_items.*.notes' => ['nullable', 'string'],
]);
DB::transaction(function () use ($budget, $validated, $request) {
// Update budget
$budget->update([
'name' => $validated['name'],
'period_start' => $validated['period_start'],
'period_end' => $validated['period_end'],
'notes' => $validated['notes'] ?? null,
]);
// Delete existing budget items and recreate
$budget->budgetItems()->delete();
// Create new budget items
if (!empty($validated['budget_items'])) {
foreach ($validated['budget_items'] as $itemData) {
if ($itemData['budgeted_amount'] > 0) {
BudgetItem::create([
'budget_id' => $budget->id,
'chart_of_account_id' => $itemData['chart_of_account_id'],
'budgeted_amount' => $itemData['budgeted_amount'],
'notes' => $itemData['notes'] ?? null,
]);
}
}
}
AuditLogger::log('budget.updated', $budget, [
'user' => $request->user()->name,
'items_count' => count($validated['budget_items'] ?? []),
]);
});
return redirect()
->route('admin.budgets.show', $budget)
->with('status', __('Budget updated successfully.'));
}
public function submit(Request $request, Budget $budget)
{
if (!$budget->isDraft()) {
abort(403, 'Only draft budgets can be submitted.');
}
if ($budget->budgetItems()->count() === 0) {
return redirect()
->route('admin.budgets.edit', $budget)
->with('error', __('Cannot submit budget without budget items.'));
}
$budget->update(['status' => Budget::STATUS_SUBMITTED]);
AuditLogger::log('budget.submitted', $budget, ['submitted_by' => $request->user()->name]);
return redirect()
->route('admin.budgets.show', $budget)
->with('status', __('Budget submitted for approval.'));
}
public function approve(Request $request, Budget $budget)
{
if (!$budget->canBeApproved()) {
abort(403, 'This budget cannot be approved.');
}
// Check if user has permission (admin or chair)
$user = $request->user();
if (!$user->hasRole('chair') && !$user->is_admin && !$user->hasRole('admin')) {
abort(403, 'Only chair can approve budgets.');
}
$budget->update([
'status' => Budget::STATUS_APPROVED,
'approved_by_user_id' => $user->id,
'approved_at' => now(),
]);
AuditLogger::log('budget.approved', $budget, ['approved_by' => $user->name]);
return redirect()
->route('admin.budgets.show', $budget)
->with('status', __('Budget approved successfully.'));
}
public function activate(Request $request, Budget $budget)
{
if (!$budget->isApproved()) {
abort(403, 'Only approved budgets can be activated.');
}
$budget->update(['status' => Budget::STATUS_ACTIVE]);
AuditLogger::log('budget.activated', $budget, ['activated_by' => $request->user()->name]);
return redirect()
->route('admin.budgets.show', $budget)
->with('status', __('Budget activated successfully.'));
}
public function close(Request $request, Budget $budget)
{
if (!$budget->isActive()) {
abort(403, 'Only active budgets can be closed.');
}
$budget->update(['status' => Budget::STATUS_CLOSED]);
AuditLogger::log('budget.closed', $budget, ['closed_by' => $request->user()->name]);
return redirect()
->route('admin.budgets.show', $budget)
->with('status', __('Budget closed successfully.'));
}
public function destroy(Request $request, Budget $budget)
{
if (!$budget->isDraft()) {
abort(403, 'Only draft budgets can be deleted.');
}
$fiscalYear = $budget->fiscal_year;
$budget->delete();
AuditLogger::log('budget.deleted', null, [
'fiscal_year' => $fiscalYear,
'deleted_by' => $request->user()->name,
]);
return redirect()
->route('admin.budgets.index')
->with('status', __('Budget deleted successfully.'));
}
}