profileShare

rasmusjy / splitapp-backend-microservices

Read-only snapshot

No repository description.

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