profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

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