|
3 | 3 | namespace App\Modules\HR\Http\Controllers; |
4 | 4 |
|
5 | 5 | use App\Http\Controllers\Controller; |
6 | | -use App\Modules\HR\Http\Requests\StoreExpenseClaimRequest; |
7 | | -use App\Modules\HR\Http\Resources\ExpenseClaimResource; |
8 | 6 | use App\Modules\HR\Models\Employee; |
9 | 7 | use App\Modules\HR\Models\ExpenseClaim; |
| 8 | +use App\Modules\HR\Models\ExpenseClaimItem; |
10 | 9 | use Illuminate\Http\RedirectResponse; |
11 | 10 | use Illuminate\Http\Request; |
12 | | -use Illuminate\Support\Facades\Auth; |
13 | 11 | use Inertia\Inertia; |
14 | 12 | use Inertia\Response; |
15 | 13 |
|
16 | 14 | class ExpenseClaimController extends Controller |
17 | 15 | { |
18 | | - const CATEGORIES = ['travel', 'meals', 'supplies', 'accommodation', 'other']; |
19 | | - |
20 | 16 | public function index(Request $request): Response |
21 | 17 | { |
22 | 18 | $this->authorize('viewAny', ExpenseClaim::class); |
23 | 19 |
|
24 | | - $claims = ExpenseClaim::with(['employee', 'reviewer']) |
| 20 | + $claims = ExpenseClaim::with(['employee']) |
25 | 21 | ->when($request->status, fn ($q) => $q->where('status', $request->status)) |
26 | 22 | ->orderBy('created_at', 'desc') |
27 | | - ->paginate(25) |
| 23 | + ->paginate(15) |
28 | 24 | ->withQueryString(); |
29 | 25 |
|
30 | 26 | return Inertia::render('HR/ExpenseClaims/Index', [ |
31 | | - 'claims' => ExpenseClaimResource::collection($claims), |
32 | | - 'filters' => $request->only(['status']), |
33 | | - 'categories' => self::CATEGORIES, |
34 | | - 'breadcrumbs' => [ |
35 | | - ['label' => 'HR'], |
36 | | - ['label' => 'Expense Claims', 'href' => route('hr.expense-claims.index')], |
37 | | - ], |
| 27 | + 'claims' => $claims, |
| 28 | + 'filters' => $request->only(['status']), |
38 | 29 | ]); |
39 | 30 | } |
40 | 31 |
|
41 | 32 | public function create(): Response |
42 | 33 | { |
43 | 34 | $this->authorize('create', ExpenseClaim::class); |
44 | 35 |
|
| 36 | + $employees = Employee::where('status', 'active') |
| 37 | + ->orderBy('last_name') |
| 38 | + ->get(['id', 'first_name', 'last_name']); |
| 39 | + |
45 | 40 | return Inertia::render('HR/ExpenseClaims/Create', [ |
46 | | - 'employees' => Employee::active()->orderBy('last_name')->get()->map(fn ($e) => [ |
47 | | - 'id' => $e->id, 'full_name' => $e->full_name, |
48 | | - ]), |
49 | | - 'categories' => self::CATEGORIES, |
50 | | - 'breadcrumbs' => [ |
51 | | - ['label' => 'HR'], |
52 | | - ['label' => 'Expense Claims', 'href' => route('hr.expense-claims.index')], |
53 | | - ['label' => 'New Claim'], |
54 | | - ], |
| 41 | + 'employees' => $employees, |
55 | 42 | ]); |
56 | 43 | } |
57 | 44 |
|
58 | | - public function store(StoreExpenseClaimRequest $request): RedirectResponse |
| 45 | + public function store(Request $request): RedirectResponse |
59 | 46 | { |
60 | 47 | $this->authorize('create', ExpenseClaim::class); |
61 | 48 |
|
62 | | - $data = $request->validated(); |
| 49 | + $validated = $request->validate([ |
| 50 | + 'title' => 'required|string', |
| 51 | + 'employee_id' => 'required|exists:employees,id', |
| 52 | + 'description' => 'nullable|string', |
| 53 | + ]); |
63 | 54 |
|
64 | 55 | $claim = ExpenseClaim::create([ |
65 | | - ...$data, |
66 | | - 'tenant_id' => auth()->user()->tenant_id, |
67 | | - 'status' => 'draft', |
68 | | - 'created_by' => Auth::id(), |
| 56 | + 'tenant_id' => auth()->user()->tenant_id, |
| 57 | + 'employee_id' => $validated['employee_id'], |
| 58 | + 'title' => $validated['title'], |
| 59 | + 'description' => $validated['description'] ?? null, |
| 60 | + 'status' => 'draft', |
| 61 | + 'total_amount' => 0, |
69 | 62 | ]); |
70 | 63 |
|
71 | | - return redirect()->route('hr.expense-claims.show', $claim) |
72 | | - ->with('success', 'Expense claim created.'); |
| 64 | + return redirect()->route('hr.expense-claims.show', $claim); |
73 | 65 | } |
74 | 66 |
|
75 | 67 | public function show(ExpenseClaim $expenseClaim): Response |
76 | 68 | { |
77 | 69 | $this->authorize('view', $expenseClaim); |
78 | 70 |
|
79 | | - $expenseClaim->load(['employee', 'submitter', 'reviewer']); |
| 71 | + $expenseClaim->load(['items', 'employee', 'approvedBy']); |
80 | 72 |
|
81 | 73 | return Inertia::render('HR/ExpenseClaims/Show', [ |
82 | | - 'claim' => new ExpenseClaimResource($expenseClaim), |
83 | | - 'categories' => self::CATEGORIES, |
84 | | - 'can' => [ |
85 | | - 'update' => auth()->user()->can('update', $expenseClaim), |
86 | | - 'delete' => auth()->user()->can('delete', $expenseClaim), |
87 | | - 'approve' => auth()->user()->can('approve', $expenseClaim), |
88 | | - ], |
89 | | - 'breadcrumbs' => [ |
90 | | - ['label' => 'HR'], |
91 | | - ['label' => 'Expense Claims', 'href' => route('hr.expense-claims.index')], |
92 | | - ['label' => $expenseClaim->title], |
93 | | - ], |
| 74 | + 'expenseClaim' => $expenseClaim, |
94 | 75 | ]); |
95 | 76 | } |
96 | 77 |
|
| 78 | + public function destroy(ExpenseClaim $expenseClaim): RedirectResponse |
| 79 | + { |
| 80 | + $this->authorize('delete', $expenseClaim); |
| 81 | + |
| 82 | + $expenseClaim->delete(); |
| 83 | + |
| 84 | + return redirect()->route('hr.expense-claims.index'); |
| 85 | + } |
| 86 | + |
97 | 87 | public function submit(ExpenseClaim $expenseClaim): RedirectResponse |
98 | 88 | { |
99 | 89 | $this->authorize('update', $expenseClaim); |
100 | 90 |
|
101 | | - try { |
102 | | - $expenseClaim->submit(); |
103 | | - } catch (\DomainException $e) { |
104 | | - return back()->withErrors(['status' => $e->getMessage()]); |
105 | | - } |
| 91 | + $expenseClaim->submit(); |
106 | 92 |
|
107 | 93 | return back()->with('success', 'Expense claim submitted.'); |
108 | 94 | } |
109 | 95 |
|
110 | | - public function approve(ExpenseClaim $expenseClaim, Request $request): RedirectResponse |
| 96 | + public function approve(ExpenseClaim $expenseClaim): RedirectResponse |
111 | 97 | { |
112 | | - $this->authorize('approve', $expenseClaim); |
113 | | - |
114 | | - $request->validate([ |
115 | | - 'notes' => 'nullable|string', |
116 | | - ]); |
| 98 | + $this->authorize('update', $expenseClaim); |
117 | 99 |
|
118 | | - try { |
119 | | - $expenseClaim->approve($request->input('notes', '')); |
120 | | - } catch (\DomainException $e) { |
121 | | - return back()->withErrors(['status' => $e->getMessage()]); |
122 | | - } |
| 100 | + $expenseClaim->approve(auth()->user()); |
123 | 101 |
|
124 | 102 | return back()->with('success', 'Expense claim approved.'); |
125 | 103 | } |
126 | 104 |
|
127 | | - public function reject(ExpenseClaim $expenseClaim, Request $request): RedirectResponse |
| 105 | + public function reject(Request $request, ExpenseClaim $expenseClaim): RedirectResponse |
128 | 106 | { |
129 | | - $this->authorize('approve', $expenseClaim); |
| 107 | + $this->authorize('update', $expenseClaim); |
130 | 108 |
|
131 | | - $request->validate([ |
132 | | - 'notes' => 'required|string', |
| 109 | + $validated = $request->validate([ |
| 110 | + 'reason' => 'required|string', |
133 | 111 | ]); |
134 | 112 |
|
135 | | - try { |
136 | | - $expenseClaim->reject($request->input('notes', '')); |
137 | | - } catch (\DomainException $e) { |
138 | | - return back()->withErrors(['status' => $e->getMessage()]); |
139 | | - } |
| 113 | + $expenseClaim->reject($validated['reason']); |
140 | 114 |
|
141 | 115 | return back()->with('success', 'Expense claim rejected.'); |
142 | 116 | } |
143 | 117 |
|
144 | | - public function reimburse(ExpenseClaim $expenseClaim): RedirectResponse |
| 118 | + public function markPaid(ExpenseClaim $expenseClaim): RedirectResponse |
145 | 119 | { |
146 | | - $this->authorize('approve', $expenseClaim); |
| 120 | + $this->authorize('update', $expenseClaim); |
147 | 121 |
|
148 | | - try { |
149 | | - $expenseClaim->reimburse(); |
150 | | - } catch (\DomainException $e) { |
151 | | - return back()->withErrors(['status' => $e->getMessage()]); |
152 | | - } |
| 122 | + $expenseClaim->markPaid(); |
153 | 123 |
|
154 | | - return back()->with('success', 'Expense claim marked as reimbursed.'); |
| 124 | + return back(); |
155 | 125 | } |
156 | 126 |
|
157 | | - public function destroy(ExpenseClaim $expenseClaim): RedirectResponse |
| 127 | + public function addItem(Request $request, ExpenseClaim $expenseClaim): RedirectResponse |
158 | 128 | { |
159 | | - $this->authorize('delete', $expenseClaim); |
| 129 | + $this->authorize('update', $expenseClaim); |
160 | 130 |
|
161 | | - $expenseClaim->delete(); |
| 131 | + $validated = $request->validate([ |
| 132 | + 'category' => 'required|string|max:50', |
| 133 | + 'description' => 'required|string', |
| 134 | + 'amount' => 'required|numeric|min:0.01', |
| 135 | + 'expense_date' => 'required|date', |
| 136 | + 'receipt_reference' => 'nullable|string', |
| 137 | + ]); |
| 138 | + |
| 139 | + $expenseClaim->items()->create([ |
| 140 | + 'tenant_id' => auth()->user()->tenant_id, |
| 141 | + 'expense_claim_id' => $expenseClaim->id, |
| 142 | + 'category' => $validated['category'], |
| 143 | + 'description' => $validated['description'], |
| 144 | + 'amount' => $validated['amount'], |
| 145 | + 'expense_date' => $validated['expense_date'], |
| 146 | + 'receipt_reference' => $validated['receipt_reference'] ?? null, |
| 147 | + ]); |
| 148 | + |
| 149 | + $expenseClaim->recalculateTotal(); |
| 150 | + |
| 151 | + return back()->with('success', 'Item added.'); |
| 152 | + } |
| 153 | + |
| 154 | + public function removeItem(ExpenseClaim $expenseClaim, ExpenseClaimItem $item): RedirectResponse |
| 155 | + { |
| 156 | + $this->authorize('update', $expenseClaim); |
| 157 | + |
| 158 | + $item->delete(); |
| 159 | + |
| 160 | + $expenseClaim->recalculateTotal(); |
162 | 161 |
|
163 | | - return redirect()->route('hr.expense-claims.index') |
164 | | - ->with('success', 'Expense claim deleted.'); |
| 162 | + return back(); |
165 | 163 | } |
166 | 164 | } |
0 commit comments