Initial commit
This commit is contained in:
315
app/Http/Controllers/FinanceDocumentController.php
Normal file
315
app/Http/Controllers/FinanceDocumentController.php
Normal file
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Mail\FinanceDocumentApprovedByAccountant;
|
||||
use App\Mail\FinanceDocumentApprovedByCashier;
|
||||
use App\Mail\FinanceDocumentFullyApproved;
|
||||
use App\Mail\FinanceDocumentRejected;
|
||||
use App\Mail\FinanceDocumentSubmitted;
|
||||
use App\Models\FinanceDocument;
|
||||
use App\Models\Member;
|
||||
use App\Models\User;
|
||||
use App\Support\AuditLogger;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class FinanceDocumentController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = FinanceDocument::query()
|
||||
->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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user