profileShare

rasmusjy / splitapp-backend-modular-monolith

Read-only snapshot

No repository description.

main default branch 418 files Expires Sep 13, 2026, 9:06 AM
BudgetController.cs 7,004 bytes
1 using SplitApp.WebApp.Application.DTO;
2 using SplitApp.WebApp.Application.Services;
3 using SplitApp.Modules.Trips.Domain.Entities;
4 using SplitApp.Modules.Trips.Domain.Enums;
5 using SplitApp.Modules.Expenses.Domain.Entities;
6 using SplitApp.Modules.Expenses.Domain.Enums;
7 using SplitApp.Modules.Users.Domain.Entities;
8 using SplitApp.Modules.Users.Domain.Entities;
9 using SplitApp.Shared.Kernel.Domain;
10 using SplitApp.Shared.Kernel.Localization;
11 using Microsoft.AspNetCore.Authorization;
12 using Microsoft.AspNetCore.Identity;
13 using Microsoft.AspNetCore.Mvc;
14
15 namespace SplitApp.WebApp.Controllers;
16
17 [Authorize]
18 public class BudgetController : Controller
19 {
20 private readonly ITripService _tripService;
21 private readonly IBudgetCategoryService _budgetCategoryService;
22 private readonly UserManager<AppUser> _userManager;
23
24 public BudgetController(
25 ITripService tripService,
26 IBudgetCategoryService budgetCategoryService,
27 UserManager<AppUser> userManager)
28 {
29 _tripService = tripService;
30 _budgetCategoryService = budgetCategoryService;
31 _userManager = userManager;
32 }
33
34 private Guid GetUserId() => Guid.Parse(_userManager.GetUserId(User)!);
35
36 // GET: Budget?tripId=xxx
37 public async Task<IActionResult> Index(Guid tripId)
38 {
39 var userId = GetUserId();
40 if (!await _tripService.IsParticipantAsync(tripId, userId)) return Forbid();
41
42 var trip = await _tripService.GetByIdAsync(tripId, userId);
43 if (trip == null) return NotFound();
44
45 var categories = await _budgetCategoryService.GetByTripIdAsync(tripId, userId);
46
47 var model = categories.Select(c => new BudgetCategoryViewModel
48 {
49 Id = c.Id,
50 Name = c.Name,
51 IconName = c.IconName,
52 PlannedAmount = c.PlannedAmount ?? 0,
53 SpentAmount = c.SpentAmount,
54 DisplayOrder = c.DisplayOrder
55 }).ToList();
56
57 ViewData["TripId"] = tripId;
58 ViewData["TripName"] = trip.Name;
59 ViewData["TotalPlanned"] = model.Sum(m => m.PlannedAmount);
60 ViewData["TotalSpent"] = model.Sum(m => m.SpentAmount);
61 ViewData["IsOrganizer"] = await _tripService.IsOrganizerAsync(tripId, userId);
62
63 return View(model);
64 }
65
66 // GET: Budget/CreateCategory?tripId=xxx
67 public async Task<IActionResult> CreateCategory(Guid tripId)
68 {
69 var userId = GetUserId();
70 if (!await _tripService.IsOrganizerAsync(tripId, userId)) return Forbid();
71
72 ViewData["TripId"] = tripId;
73 return View(new BudgetCategoryBllDto { TripId = tripId });
74 }
75
76 // POST: Budget/CreateCategory
77 [HttpPost]
78 [ValidateAntiForgeryToken]
79 public async Task<IActionResult> CreateCategory(BudgetCategoryBllDto category, string? name)
80 {
81 var userId = GetUserId();
82
83 category.Name = new LangStr(name ?? "", "en");
84 ModelState.Remove(nameof(BudgetCategory.Name));
85
86 if (string.IsNullOrWhiteSpace(name))
87 ModelState.AddModelError(nameof(BudgetCategory.Name), "Name is required.");
88
89 if (ModelState.IsValid)
90 {
91 var (created, errorCode) = await _budgetCategoryService.CreateAsync(category, userId);
92 if (created == null)
93 {
94 if (errorCode == "forbidden") return Forbid();
95 return NotFound();
96 }
97 return RedirectToAction(nameof(Index), new { tripId = category.TripId });
98 }
99
100 ViewData["TripId"] = category.TripId;
101 return View(category);
102 }
103
104 // GET: Budget/EditCategory/5
105 public async Task<IActionResult> EditCategory(Guid id)
106 {
107 var userId = GetUserId();
108
109 var category = await _budgetCategoryService.GetByIdAsync(id);
110 if (category == null) return NotFound();
111
112 if (!await _tripService.IsOrganizerAsync(category.TripId, userId)) return Forbid();
113
114 ViewData["TripId"] = category.TripId;
115 return View(category);
116 }
117
118 // POST: Budget/EditCategory/5
119 [HttpPost]
120 [ValidateAntiForgeryToken]
121 public async Task<IActionResult> EditCategory(Guid id, BudgetCategoryBllDto category, string? name)
122 {
123 if (id != category.Id) return NotFound();
124
125 var userId = GetUserId();
126
127 var existing = await _budgetCategoryService.GetByIdAsync(id);
128 if (existing == null) return NotFound();
129
130 if (!await _tripService.IsOrganizerAsync(existing.TripId, userId)) return Forbid();
131
132 category.Name = new LangStr(name ?? "", "en");
133 ModelState.Remove(nameof(BudgetCategory.Name));
134
135 if (string.IsNullOrWhiteSpace(name))
136 ModelState.AddModelError(nameof(BudgetCategory.Name), "Name is required.");
137
138 if (ModelState.IsValid)
139 {
140 var (ok, errorCode) = await _budgetCategoryService.UpdateAsync(id, category, userId);
141 if (!ok)
142 {
143 return errorCode switch
144 {
145 "forbidden" => Forbid(),
146 _ => NotFound()
147 };
148 }
149 return RedirectToAction(nameof(Index), new { tripId = existing.TripId });
150 }
151
152 ViewData["TripId"] = existing.TripId;
153 return View(category);
154 }
155
156 // GET: Budget/DeleteCategory/5
157 public async Task<IActionResult> DeleteCategory(Guid id)
158 {
159 var userId = GetUserId();
160
161 var category = await _budgetCategoryService.GetByIdAsync(id);
162 if (category == null) return NotFound();
163
164 if (!await _tripService.IsOrganizerAsync(category.TripId, userId)) return Forbid();
165
166 ViewData["TripId"] = category.TripId;
167 return View(category);
168 }
169
170 // POST: Budget/DeleteCategory/5
171 [HttpPost, ActionName("DeleteCategory")]
172 [ValidateAntiForgeryToken]
173 public async Task<IActionResult> DeleteCategoryConfirmed(Guid id)
174 {
175 var userId = GetUserId();
176
177 var category = await _budgetCategoryService.GetByIdAsync(id);
178 if (category == null) return NotFound();
179
180 var tripId = category.TripId;
181 var (ok, errorCode) = await _budgetCategoryService.DeleteAsync(id, userId);
182 if (!ok)
183 {
184 return errorCode switch
185 {
186 "forbidden" => Forbid(),
187 _ => NotFound()
188 };
189 }
190 return RedirectToAction(nameof(Index), new { tripId });
191 }
192 }
193
194 public class BudgetCategoryViewModel
195 {
196 public Guid Id { get; set; }
197 public string Name { get; set; } = default!;
198 public string? IconName { get; set; }
199 public decimal PlannedAmount { get; set; }
200 public decimal SpentAmount { get; set; }
201 public int DisplayOrder { get; set; }
202
203 public int ProgressPercentage =>
204 PlannedAmount > 0 ? (int)(SpentAmount / PlannedAmount * 100) : 0;
205
206 public string ProgressBarClass =>
207 ProgressPercentage switch
208 {
209 > 90 => "bg-danger",
210 > 70 => "bg-warning",
211 _ => "bg-success"
212 };
213 }
214