33namespace App \Modules \Finance \Http \Controllers ;
44
55use App \Http \Controllers \Controller ;
6- use App \Modules \Finance \Models \Account ;
76use App \Modules \Finance \Models \Budget ;
7+ use App \Modules \Finance \Models \BudgetLine ;
88use Illuminate \Http \RedirectResponse ;
99use Illuminate \Http \Request ;
10- use Illuminate \Support \Facades \DB ;
1110use Illuminate \Validation \Rule ;
1211use Inertia \Inertia ;
1312use Inertia \Response ;
@@ -18,177 +17,75 @@ public function index(Request $request): Response
1817 {
1918 $ this ->authorize ('viewAny ' , Budget::class);
2019
21- $ budgets = Budget::withCount ('lines ' )
22- ->orderByDesc ('fiscal_year ' )
20+ $ query = Budget::withCount ('lines ' );
21+
22+ if ($ request ->filled ('fiscal_year ' )) {
23+ $ query ->where ('fiscal_year ' , (int ) $ request ->input ('fiscal_year ' ));
24+ }
25+
26+ if ($ request ->filled ('status ' )) {
27+ $ query ->where ('status ' , $ request ->input ('status ' ));
28+ }
29+
30+ $ budgets = $ query ->orderByDesc ('fiscal_year ' )
2331 ->orderByDesc ('id ' )
24- ->paginate (15 )
25- ->through (fn ($ b ) => [
26- 'id ' => $ b ->id ,
27- 'name ' => $ b ->name ,
28- 'fiscal_year ' => $ b ->fiscal_year ?? $ b ->year ,
29- 'year ' => $ b ->year ,
30- 'period_type ' => $ b ->period_type ,
31- 'status ' => $ b ->status ,
32- 'lines_count ' => $ b ->lines_count ,
33- 'total_budgeted ' => null ,
34- ]);
32+ ->paginate (20 );
3533
3634 return Inertia::render ('Finance/Budgets/Index ' , [
37- 'budgets ' => $ budgets ,
38- 'breadcrumbs ' => [
39- ['label ' => 'Finance ' ],
40- ['label ' => 'Budgets ' ],
41- ],
35+ 'budgets ' => $ budgets ,
36+ 'filters ' => $ request ->only (['fiscal_year ' , 'status ' ]),
4237 ]);
4338 }
4439
4540 public function create (): Response
4641 {
4742 $ this ->authorize ('create ' , Budget::class);
4843
49- $ accounts = Account::whereIn ('type ' , ['income ' , 'expense ' ])
50- ->where ('is_active ' , true )
51- ->orderBy ('code ' )
52- ->get (['id ' , 'code ' , 'name ' , 'type ' ]);
53-
54- return Inertia::render ('Finance/Budgets/Create ' , [
55- 'accounts ' => $ accounts ,
56- 'breadcrumbs ' => [
57- ['label ' => 'Finance ' ],
58- ['label ' => 'Budgets ' , 'href ' => '/finance/budgets ' ],
59- ['label ' => 'New Budget ' ],
60- ],
61- ]);
44+ return Inertia::render ('Finance/Budgets/Create ' );
6245 }
6346
6447 public function store (Request $ request ): RedirectResponse
6548 {
6649 $ this ->authorize ('create ' , Budget::class);
6750
68- $ tenantId = app ('tenant ' )->id ;
69- $ fiscalYear = $ request ->input ('fiscal_year ' ) ?? $ request ->input ('year ' );
70-
7151 $ validated = $ request ->validate ([
72- 'name ' => [
73- 'required ' ,
74- 'string ' ,
75- 'max:255 ' ,
76- Rule::unique ('budgets ' )->where (fn ($ q ) => $ q
77- ->where ('fiscal_year ' , $ fiscalYear )
78- ->where ('tenant_id ' , $ tenantId )
79- ->whereNull ('deleted_at ' )
80- ),
81- ],
82- 'fiscal_year ' => ['required ' , 'integer ' , 'min:2000 ' , 'max:2100 ' ],
83- 'period_type ' => ['required ' , Rule::in (['annual ' , 'quarterly ' , 'monthly ' ])],
52+ 'name ' => ['required ' , 'string ' , 'max:255 ' ],
53+ 'fiscal_year ' => ['required ' , 'integer ' ],
54+ 'period_type ' => ['nullable ' , Rule::in (['annual ' , 'quarterly ' , 'monthly ' ])],
8455 'notes ' => ['nullable ' , 'string ' ],
85- 'lines ' => ['required ' , 'array ' , 'min:1 ' ],
86- 'lines.*.account_id ' => ['required ' , Rule::exists ('accounts ' , 'id ' )],
87- 'lines.*.period ' => ['required ' , 'integer ' , 'min:0 ' , 'max:12 ' ],
88- 'lines.*.amount ' => ['required ' , 'numeric ' , 'min:0 ' ],
8956 ]);
9057
91- $ fy = $ validated ['fiscal_year ' ];
92-
93- $ budget = DB ::transaction (function () use ($ validated , $ request , $ tenantId , $ fy ) {
94- $ budget = Budget::create ([
95- 'tenant_id ' => $ tenantId ,
96- 'name ' => $ validated ['name ' ],
97- 'fiscal_year ' => $ fy ,
98- 'year ' => $ fy ,
99- 'period_type ' => $ validated ['period_type ' ],
100- 'notes ' => $ validated ['notes ' ] ?? null ,
101- 'status ' => 'draft ' ,
102- 'created_by ' => $ request ->user ()->id ,
103- ]);
104-
105- foreach ($ validated ['lines ' ] as $ line ) {
106- $ budget ->lines ()->create ([
107- 'tenant_id ' => $ tenantId ,
108- 'account_id ' => $ line ['account_id ' ],
109- 'period ' => $ line ['period ' ],
110- 'amount ' => $ line ['amount ' ],
111- 'notes ' => $ line ['notes ' ] ?? null ,
112- ]);
113- }
114-
115- return $ budget ;
116- });
117-
118- return redirect ()->route ('finance.budgets.show ' , $ budget )
119- ->with ('success ' , 'Budget created successfully. ' );
58+ $ budget = Budget::create ([
59+ 'tenant_id ' => app ('tenant ' )->id ,
60+ 'name ' => $ validated ['name ' ],
61+ 'fiscal_year ' => $ validated ['fiscal_year ' ],
62+ 'year ' => $ validated ['fiscal_year ' ],
63+ 'period_type ' => $ validated ['period_type ' ] ?? 'annual ' ,
64+ 'notes ' => $ validated ['notes ' ] ?? null ,
65+ 'status ' => 'draft ' ,
66+ ]);
67+
68+ return redirect ()->route ('finance.budgets.show ' , $ budget );
12069 }
12170
12271 public function show (Budget $ budget ): Response
12372 {
12473 $ this ->authorize ('view ' , $ budget );
125- $ budget ->load (['lines.account ' ]);
126-
127- $ tenantId = request ()->user ()->tenant_id ;
128- $ year = $ budget ->fiscal_year ?? $ budget ->year ;
129-
130- // Compute actuals from posted journal entries for this fiscal year
131- $ actuals = DB ::table ('journal_lines ' )
132- ->join ('journal_entries ' , 'journal_lines.journal_entry_id ' , '= ' , 'journal_entries.id ' )
133- ->join ('accounts ' , 'journal_lines.account_id ' , '= ' , 'accounts.id ' )
134- ->where ('journal_entries.tenant_id ' , $ tenantId )
135- ->where ('journal_entries.status ' , 'posted ' )
136- ->whereYear ('journal_entries.date ' , $ year )
137- ->whereIn ('accounts.type ' , ['income ' , 'expense ' ])
138- ->select (
139- 'journal_lines.account_id ' ,
140- 'accounts.type ' ,
141- DB ::raw ('SUM(journal_lines.debit) as total_debit ' ),
142- DB ::raw ('SUM(journal_lines.credit) as total_credit ' ),
143- )
144- ->groupBy ('journal_lines.account_id ' , 'accounts.type ' )
145- ->get ()
146- ->keyBy ('account_id ' );
147-
148- $ lines = $ budget ->lines ->map (function ($ line ) use ($ actuals ) {
149- $ actual = $ actuals ->get ($ line ->account_id );
150- $ actualAmount = 0 ;
151- if ($ actual ) {
152- $ actualAmount = $ actual ->type === 'income '
153- ? (float ) $ actual ->total_credit - (float ) $ actual ->total_debit
154- : (float ) $ actual ->total_debit - (float ) $ actual ->total_credit ;
155- }
156- $ variance = $ actualAmount - (float ) $ line ->amount ;
157- $ variancePct = $ line ->amount != 0 ? round ($ variance / $ line ->amount * 100 , 1 ) : null ;
158-
159- return [
160- 'id ' => $ line ->id ,
161- 'account_id ' => $ line ->account_id ,
162- 'account_code ' => $ line ->account ->code ,
163- 'account_name ' => $ line ->account ->name ,
164- 'account_type ' => $ line ->account ->type ,
165- 'period ' => $ line ->period ,
166- 'budget ' => round ((float ) $ line ->amount , 2 ),
167- 'actual ' => round ($ actualAmount , 2 ),
168- 'variance ' => round ($ variance , 2 ),
169- 'variance_pct ' => $ variancePct ,
170- ];
171- });
74+
75+ $ budget ->load ('lines ' );
17276
17377 return Inertia::render ('Finance/Budgets/Show ' , [
174- 'budget ' => [
175- 'id ' => $ budget ->id ,
176- 'name ' => $ budget ->name ,
177- 'fiscal_year ' => $ budget ->fiscal_year ?? $ budget ->year ,
178- 'year ' => $ budget ->year ,
179- 'period_type ' => $ budget ->period_type ,
180- 'status ' => $ budget ->status ,
181- 'notes ' => $ budget ->notes ,
182- ],
183- 'lines ' => $ lines ->values (),
184- 'total_budget ' => $ lines ->sum ('budget ' ),
185- 'total_actual ' => $ lines ->sum ('actual ' ),
186- 'total_variance ' => round ($ lines ->sum ('variance ' ), 2 ),
187- 'breadcrumbs ' => [
188- ['label ' => 'Finance ' ],
189- ['label ' => 'Budgets ' , 'href ' => '/finance/budgets ' ],
190- ['label ' => $ budget ->name ],
191- ],
78+ 'budget ' => array_merge ($ budget ->toArray (), [
79+ 'total_budgeted ' => $ budget ->total_budgeted ,
80+ 'total_actual ' => $ budget ->total_actual ,
81+ 'total_variance ' => $ budget ->total_variance ,
82+ 'variance_percent ' => $ budget ->variance_percent ,
83+ 'lines ' => $ budget ->lines ->map (fn ($ line ) => array_merge ($ line ->toArray (), [
84+ 'variance ' => $ line ->variance ,
85+ 'variance_percent ' => $ line ->variance_percent ,
86+ 'is_over_budget ' => $ line ->is_over_budget ,
87+ ]))->values (),
88+ ]),
19289 ]);
19390 }
19491
@@ -198,25 +95,71 @@ public function destroy(Budget $budget): RedirectResponse
19895
19996 $ budget ->delete ();
20097
201- return redirect ()->route ('finance.budgets.index ' )
202- ->with ('success ' , 'Budget deleted. ' );
98+ return redirect ()->route ('finance.budgets.index ' );
20399 }
204100
205- public function activate (Budget $ budget ): RedirectResponse
101+ public function activate (Request $ request , Budget $ budget ): RedirectResponse
206102 {
207103 $ this ->authorize ('update ' , $ budget );
208104
209105 $ budget ->activate ();
210106
211- return redirect ()->back ()-> with ( ' success ' , ' Budget activated. ' ) ;
107+ return redirect ()->back ();
212108 }
213109
214- public function close (Budget $ budget ): RedirectResponse
110+ public function close (Request $ request , Budget $ budget ): RedirectResponse
215111 {
216112 $ this ->authorize ('update ' , $ budget );
217113
218114 $ budget ->close ();
219115
220- return redirect ()->back ()->with ('success ' , 'Budget closed. ' );
116+ return redirect ()->back ();
117+ }
118+
119+ public function addLine (Request $ request , Budget $ budget ): RedirectResponse
120+ {
121+ $ this ->authorize ('update ' , $ budget );
122+
123+ $ validated = $ request ->validate ([
124+ 'category ' => ['required ' , 'string ' , 'max:255 ' ],
125+ 'line_type ' => ['required ' , Rule::in (['income ' , 'expense ' ])],
126+ 'period_number ' => ['required ' , 'integer ' , 'min:1 ' ],
127+ 'budgeted_amount ' => ['required ' , 'numeric ' , 'min:0 ' ],
128+ 'notes ' => ['nullable ' , 'string ' ],
129+ ]);
130+
131+ $ budget ->lines ()->create ([
132+ 'tenant_id ' => app ('tenant ' )->id ,
133+ 'category ' => $ validated ['category ' ],
134+ 'line_type ' => $ validated ['line_type ' ],
135+ 'period_number ' => $ validated ['period_number ' ],
136+ 'budgeted_amount ' => $ validated ['budgeted_amount ' ],
137+ 'actual_amount ' => 0 ,
138+ 'notes ' => $ validated ['notes ' ] ?? null ,
139+ ]);
140+
141+ return redirect ()->back ();
142+ }
143+
144+ public function updateActual (Request $ request , Budget $ budget , BudgetLine $ line ): RedirectResponse
145+ {
146+ $ this ->authorize ('update ' , $ budget );
147+
148+ $ validated = $ request ->validate ([
149+ 'actual_amount ' => ['required ' , 'numeric ' , 'min:0 ' ],
150+ ]);
151+
152+ $ line ->update (['actual_amount ' => $ validated ['actual_amount ' ]]);
153+
154+ return redirect ()->back ();
155+ }
156+
157+ public function removeLine (Request $ request , Budget $ budget , BudgetLine $ line ): RedirectResponse
158+ {
159+ $ this ->authorize ('update ' , $ budget );
160+
161+ $ line ->delete ();
162+
163+ return redirect ()->back ();
221164 }
222165}
0 commit comments