with(['member', 'submittedBy', 'paymentOrder']); // Filter by status if ($request->filled('status')) { $query->where('status', $request->status); } // 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'], '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'], '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(); foreach ($cashiers as $cashier) { Mail::to($cashier->email)->queue(new FinanceDocumentSubmitted($document)); } return redirect() ->route('admin.finance.index') ->with('status', '報銷申請單已提交。金額級別:' . $document->getAmountTierText()); } public function show(FinanceDocument $financeDocument) { $financeDocument->load([ 'member', 'submittedBy', // 新工作流程 relationships 'approvedBySecretary', 'approvedByChair', 'approvedByBoardMeeting', 'requesterConfirmedBy', 'cashierConfirmedBy', 'accountantRecordedBy', // Legacy relationships 'approvedByCashier', 'approvedByAccountant', 'rejectedBy', 'chartOfAccount', 'budgetItem', '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(); // 新工作流程:秘書長 → 理事長 → 董理事會 $isSecretary = $user->hasRole('secretary_general'); $isChair = $user->hasRole('finance_chair'); $isBoardMember = $user->hasRole('finance_board_member'); $isAdmin = $user->hasRole('admin'); // 秘書長審核(第一階段) if ($financeDocument->canBeApprovedBySecretary($user) && ($isSecretary || $isAdmin)) { $financeDocument->update([ 'approved_by_secretary_id' => $user->id, 'secretary_approved_at' => now(), 'status' => FinanceDocument::STATUS_APPROVED_SECRETARY, ]); AuditLogger::log('finance_document.approved_by_secretary', $financeDocument, [ 'approved_by' => $user->name, 'amount_tier' => $financeDocument->amount_tier, ]); // 小額:審核完成 if ($financeDocument->amount_tier === FinanceDocument::AMOUNT_TIER_SMALL) { // 通知申請人審核已完成,可以領款 Mail::to($financeDocument->submittedBy->email)->queue(new FinanceDocumentFullyApproved($financeDocument)); return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '秘書長已核准。小額申請審核完成,申請人可向出納領款。'); } // 中額/大額:送交理事長 $chairs = User::role('finance_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($user) && ($isChair || $isAdmin)) { $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, ]); // 中額:審核完成 if ($financeDocument->amount_tier === FinanceDocument::AMOUNT_TIER_MEDIUM) { Mail::to($financeDocument->submittedBy->email)->queue(new FinanceDocumentFullyApproved($financeDocument)); return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '理事長已核准。中額申請審核完成,申請人可向出納領款。'); } // 大額:送交董理事會 $boardMembers = User::role('finance_board_member')->get(); foreach ($boardMembers as $member) { Mail::to($member->email)->queue(new FinanceDocumentApprovedByAccountant($financeDocument)); } return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '理事長已核准。大額申請需送交董理事會審核。'); } // 董理事會審核(第三階段:大額) if ($financeDocument->canBeApprovedByBoard($user) && ($isBoardMember || $isAdmin)) { $financeDocument->update([ 'board_meeting_approved_by_id' => $user->id, 'board_meeting_approved_at' => now(), 'status' => FinanceDocument::STATUS_APPROVED_BOARD, ]); AuditLogger::log('finance_document.approved_by_board', $financeDocument, [ 'approved_by' => $user->name, 'amount_tier' => $financeDocument->amount_tier, ]); Mail::to($financeDocument->submittedBy->email)->queue(new FinanceDocumentFullyApproved($financeDocument)); return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '董理事會已核准。審核流程完成,申請人可向出納領款。'); } abort(403, '您無權在此階段審核此文件。'); } /** * 出帳確認(雙重確認:申請人 + 出納) */ public function confirmDisbursement(Request $request, FinanceDocument $financeDocument) { $user = $request->user(); $isRequester = $financeDocument->submitted_by_user_id === $user->id; $isCashier = $user->hasRole('finance_cashier'); $isAdmin = $user->hasRole('admin'); // 申請人確認 if ($isRequester && $financeDocument->canRequesterConfirmDisbursement($user)) { $financeDocument->update([ 'requester_confirmed_at' => now(), 'requester_confirmed_by_id' => $user->id, ]); AuditLogger::log('finance_document.requester_confirmed_disbursement', $financeDocument, [ 'confirmed_by' => $user->name, ]); // 檢查是否雙重確認完成 if ($financeDocument->isDisbursementComplete()) { $financeDocument->update(['disbursement_status' => FinanceDocument::DISBURSEMENT_COMPLETED]); return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '出帳確認完成。等待會計入帳。'); } return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '申請人已確認領款。等待出納確認。'); } // 出納確認 if (($isCashier || $isAdmin) && $financeDocument->canCashierConfirmDisbursement()) { $financeDocument->update([ 'cashier_confirmed_at' => now(), 'cashier_confirmed_by_id' => $user->id, ]); AuditLogger::log('finance_document.cashier_confirmed_disbursement', $financeDocument, [ 'confirmed_by' => $user->name, ]); // 檢查是否雙重確認完成 if ($financeDocument->isDisbursementComplete()) { $financeDocument->update(['disbursement_status' => FinanceDocument::DISBURSEMENT_COMPLETED]); return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '出帳確認完成。等待會計入帳。'); } return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '出納已確認出帳。等待申請人確認。'); } abort(403, '您無權確認此出帳。'); } /** * 入帳確認(會計) */ public function confirmRecording(Request $request, FinanceDocument $financeDocument) { $user = $request->user(); $isAccountant = $user->hasRole('finance_accountant'); $isAdmin = $user->hasRole('admin'); if (!$financeDocument->canAccountantConfirmRecording()) { abort(403, '此文件尚未完成出帳確認,無法入帳。'); } if (!$isAccountant && !$isAdmin) { abort(403, '只有會計可以確認入帳。'); } $financeDocument->update([ 'accountant_recorded_at' => now(), 'accountant_recorded_by_id' => $user->id, 'recording_status' => FinanceDocument::RECORDING_COMPLETED, ]); // 自動產生會計分錄 $financeDocument->autoGenerateAccountingEntries(); AuditLogger::log('finance_document.accountant_confirmed_recording', $financeDocument, [ 'confirmed_by' => $user->name, ]); return redirect() ->route('admin.finance.show', $financeDocument) ->with('status', '會計已確認入帳。財務流程完成。'); } 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('admin') || $user->hasRole('secretary_general') || $user->hasRole('finance_cashier') || $user->hasRole('finance_accountant') || $user->hasRole('finance_chair') || $user->hasRole('finance_board_member'); 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); } }