with(['member', 'submittedBy', 'paymentOrder']); // Filter by status if ($request->filled('status')) { $query->where('status', $request->status); } // Filter by request type if ($request->filled('request_type')) { $query->where('request_type', $request->request_type); } // Filter by amount tier if ($request->filled('amount_tier')) { $query->where('amount_tier', $request->amount_tier); } // Filter by workflow stage if ($request->filled('workflow_stage')) { $stage = $request->workflow_stage; if ($stage === 'approval') { $query->whereNull('payment_order_created_at'); } elseif ($stage === 'payment') { $query->whereNotNull('payment_order_created_at') ->whereNull('payment_executed_at'); } elseif ($stage === 'recording') { $query->whereNotNull('payment_executed_at') ->where(function($q) { $q->whereNull('cashier_ledger_entry_id') ->orWhereNull('accounting_transaction_id'); }); } elseif ($stage === 'completed') { $query->whereNotNull('cashier_ledger_entry_id') ->whereNotNull('accounting_transaction_id'); } } $documents = $query->orderByDesc('created_at')->paginate(15); return view('admin.finance.index', [ 'documents' => $documents, ]); } public function create(Request $request) { $members = Member::orderBy('full_name')->get(); return view('admin.finance.create', [ 'members' => $members, ]); } public function store(Request $request) { $validated = $request->validate([ 'member_id' => ['nullable', 'exists:members,id'], 'title' => ['required', 'string', 'max:255'], 'amount' => ['required', 'numeric', 'min:0'], 'request_type' => ['required', 'in:expense_reimbursement,advance_payment,purchase_request,petty_cash'], 'description' => ['nullable', 'string'], 'attachment' => ['nullable', 'file', 'max:10240'], // 10MB max ]); $attachmentPath = null; if ($request->hasFile('attachment')) { $attachmentPath = $request->file('attachment')->store('finance-documents', 'local'); } // Create document first to use its determineAmountTier method $document = new FinanceDocument([ 'member_id' => $validated['member_id'] ?? null, 'submitted_by_user_id' => $request->user()->id, 'title' => $validated['title'], 'amount' => $validated['amount'], 'request_type' => $validated['request_type'], 'description' => $validated['description'] ?? null, 'attachment_path' => $attachmentPath, 'status' => FinanceDocument::STATUS_PENDING, 'submitted_at' => now(), ]); // Determine amount tier $document->amount_tier = $document->determineAmountTier(); // Set if requires board meeting $document->requires_board_meeting = $document->needsBoardMeetingApproval(); // Save the document $document->save(); AuditLogger::log('finance_document.created', $document, $validated); // Send email notification to finance cashiers $cashiers = User::role('finance_cashier')->get(); if ($cashiers->isEmpty()) { // Fallback to old cashier role for backward compatibility $cashiers = User::role('cashier')->get(); } foreach ($cashiers as $cashier) { Mail::to($cashier->email)->queue(new FinanceDocumentSubmitted($document)); } return redirect() ->route('admin.finance.index') ->with('status', '財務申請單已提交。申請類型:' . $document->getRequestTypeText() . ',金額級別:' . $document->getAmountTierText()); } public function show(FinanceDocument $financeDocument) { $financeDocument->load([ 'member', 'submittedBy', 'approvedByCashier', 'approvedByAccountant', 'approvedByChair', 'rejectedBy', 'chartOfAccount', 'budgetItem', 'approvedByBoardMeeting', 'paymentOrderCreatedByAccountant', 'paymentVerifiedByCashier', 'paymentExecutedByCashier', 'paymentOrder.createdByAccountant', 'paymentOrder.verifiedByCashier', 'paymentOrder.executedByCashier', 'cashierLedgerEntry.recordedByCashier', 'accountingTransaction', ]); return view('admin.finance.show', [ 'document' => $financeDocument, ]); } public function approve(Request $request, FinanceDocument $financeDocument) { $user = $request->user(); // Check if user has any finance approval permissions $isCashier = $user->hasRole('finance_cashier') || $user->hasRole('cashier'); $isAccountant = $user->hasRole('finance_accountant') || $user->hasRole('accountant'); $isChair = $user->hasRole('finance_chair') || $user->hasRole('chair'); // Determine which level of approval based on current status and user role if ($financeDocument->canBeApprovedByCashier() && $isCashier) { $financeDocument->update([ 'approved_by_cashier_id' => $user->id, 'cashier_approved_at' => now(), 'status' => FinanceDocument::STATUS_APPROVED_CASHIER, ]); AuditLogger::log('finance_document.approved_by_cashier', $financeDocument, [ 'approved_by' => $user->name, 'amount_tier' => $financeDocument->amount_tier, ]); // Send email notification to accountants $accountants = User::role('finance_accountant')->get(); if ($accountants->isEmpty()) { $accountants = User::role('accountant')->get(); } foreach ($accountants as $accountant) { Mail::to($accountant->email)->queue(new FinanceDocumentApprovedByCashier($financeDocument)); } return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '出納已審核通過。已送交會計審核。'); } if ($financeDocument->canBeApprovedByAccountant() && $isAccountant) { $financeDocument->update([ 'approved_by_accountant_id' => $user->id, 'accountant_approved_at' => now(), 'status' => FinanceDocument::STATUS_APPROVED_ACCOUNTANT, ]); AuditLogger::log('finance_document.approved_by_accountant', $financeDocument, [ 'approved_by' => $user->name, 'amount_tier' => $financeDocument->amount_tier, ]); // For small amounts, approval is complete (no chair needed) if ($financeDocument->amount_tier === FinanceDocument::AMOUNT_TIER_SMALL) { return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '會計已審核通過。小額申請審核完成,可以製作付款單。'); } // For medium and large amounts, send to chair $chairs = User::role('finance_chair')->get(); if ($chairs->isEmpty()) { $chairs = User::role('chair')->get(); } foreach ($chairs as $chair) { Mail::to($chair->email)->queue(new FinanceDocumentApprovedByAccountant($financeDocument)); } return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '會計已審核通過。已送交理事長審核。'); } if ($financeDocument->canBeApprovedByChair() && $isChair) { $financeDocument->update([ 'approved_by_chair_id' => $user->id, 'chair_approved_at' => now(), 'status' => FinanceDocument::STATUS_APPROVED_CHAIR, ]); AuditLogger::log('finance_document.approved_by_chair', $financeDocument, [ 'approved_by' => $user->name, 'amount_tier' => $financeDocument->amount_tier, 'requires_board_meeting' => $financeDocument->requires_board_meeting, ]); // For large amounts, notify that board meeting approval is still needed if ($financeDocument->requires_board_meeting && !$financeDocument->board_meeting_approved_at) { return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '理事長已審核通過。大額申請仍需理事會核准。'); } // For medium amounts or large amounts with board approval, complete Mail::to($financeDocument->submittedBy->email)->queue(new FinanceDocumentFullyApproved($financeDocument)); return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '審核流程完成。會計可以製作付款單。'); } abort(403, 'You are not authorized to approve this document at this stage.'); } public function reject(Request $request, FinanceDocument $financeDocument) { $validated = $request->validate([ 'rejection_reason' => ['required', 'string', 'max:1000'], ]); $user = $request->user(); // Can be rejected by cashier, accountant, or chair at any stage (except if already rejected or fully approved) if ($financeDocument->isRejected() || $financeDocument->isFullyApproved()) { abort(403, '此文件無法駁回。'); } // Check if user has permission to reject $canReject = $user->hasRole('finance_cashier') || $user->hasRole('cashier') || $user->hasRole('finance_accountant') || $user->hasRole('accountant') || $user->hasRole('finance_chair') || $user->hasRole('chair'); if (!$canReject) { abort(403, '您無權駁回此文件。'); } $financeDocument->update([ 'rejected_by_user_id' => $user->id, 'rejected_at' => now(), 'rejection_reason' => $validated['rejection_reason'], 'status' => FinanceDocument::STATUS_REJECTED, ]); AuditLogger::log('finance_document.rejected', $financeDocument, [ 'rejected_by' => $user->name, 'reason' => $validated['rejection_reason'], 'amount_tier' => $financeDocument->amount_tier, ]); // Send email notification to submitter (rejected) Mail::to($financeDocument->submittedBy->email)->queue(new FinanceDocumentRejected($financeDocument)); return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '財務申請單已駁回。'); } public function download(FinanceDocument $financeDocument) { if (!$financeDocument->attachment_path) { abort(404, 'No attachment found.'); } $path = storage_path('app/' . $financeDocument->attachment_path); if (!file_exists($path)) { abort(404, 'Attachment file not found.'); } return response()->download($path); } }