profileShare

rasmusjy / splitapp-backend-modular-monolith

Read-only snapshot

No repository description.

main default branch 418 files Expires Sep 13, 2026, 9:06 AM
TripsController.cs 9,235 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 Microsoft.AspNetCore.Authorization;
10 using Microsoft.AspNetCore.Identity;
11 using Microsoft.AspNetCore.Mvc;
12 using Microsoft.AspNetCore.Mvc.Rendering;
13 using SplitApp.WebApp.Hosting.Helpers;
14
15 namespace SplitApp.WebApp.Controllers;
16
17 [Authorize]
18 public class TripsController : Controller
19 {
20 private readonly ITripService _tripService;
21 private readonly IExpenseService _expenseService;
22 private readonly IBudgetCategoryService _budgetCategoryService;
23 private readonly UserManager<AppUser> _userManager;
24
25 public TripsController(
26 ITripService tripService,
27 IExpenseService expenseService,
28 IBudgetCategoryService budgetCategoryService,
29 UserManager<AppUser> userManager)
30 {
31 _tripService = tripService;
32 _expenseService = expenseService;
33 _budgetCategoryService = budgetCategoryService;
34 _userManager = userManager;
35 }
36
37 private Guid GetUserId() => Guid.Parse(_userManager.GetUserId(User)!);
38
39 // GET: Trips
40 public async Task<IActionResult> Index()
41 {
42 var userId = GetUserId();
43
44 var trips = await _tripService.GetUserTripsAsync(userId);
45
46 var model = new List<TripIndexViewModel>();
47 foreach (var trip in trips)
48 {
49 var participant = trip.Participants?.FirstOrDefault(p => p.UserId == userId && p.IsActive);
50 if (participant == null) continue;
51
52 model.Add(new TripIndexViewModel
53 {
54 Id = trip.Id,
55 Name = trip.Name,
56 Destination = trip.Destination,
57 Status = trip.Status,
58 StartDate = trip.StartDate,
59 EndDate = trip.EndDate,
60 Role = participant.Role,
61 CurrencyCode = trip.DefaultCurrency?.Code ?? ""
62 });
63 }
64
65 return View(model);
66 }
67
68 // GET: Trips/Details/5
69 public async Task<IActionResult> Details(Guid id)
70 {
71 var userId = GetUserId();
72
73 var trip = await _tripService.GetByIdWithDetailsAsync(id, userId);
74 if (trip == null) return NotFound();
75
76 // Get participant role
77 var participants = await _tripService.GetParticipantsAsync(id, userId);
78 var participant = participants.FirstOrDefault(p => p.UserId == userId);
79
80 var defaultCurrencyCode = trip.DefaultCurrency?.Code ?? "EUR";
81
82 // Get expenses with details for balance calculation
83 var expensesAll = await _expenseService.GetByTripIdAsync(id, userId);
84
85 var recentExpenses = expensesAll
86 .OrderByDescending(e => e.ExpenseDate)
87 .Take(5)
88 .ToList();
89
90 var totalExpenses = expensesAll.Sum(e =>
91 CurrencyConverter.Convert(e.Amount, e.Currency?.Code ?? defaultCurrencyCode, defaultCurrencyCode));
92
93 // Calculate balances for each participant
94 var balances = new Dictionary<Guid, SettlementBalanceViewModel>();
95 foreach (var p in participants)
96 {
97 balances[p.UserId] = new SettlementBalanceViewModel
98 {
99 UserId = p.UserId,
100 UserName = !string.IsNullOrWhiteSpace(p.User?.FullName)
101 ? p.User!.FullName
102 : (p.User?.Email ?? "Unknown"),
103 TotalPaid = 0,
104 TotalOwed = 0
105 };
106 }
107
108 foreach (var expense in expensesAll)
109 {
110 var expenseWithSplits = await _expenseService.GetByIdWithDetailsAsync(expense.Id, userId);
111 if (expenseWithSplits == null) continue;
112
113 var expCurrency = expenseWithSplits.Currency?.Code ?? defaultCurrencyCode;
114 var convertedAmount = CurrencyConverter.Convert(expenseWithSplits.Amount, expCurrency, defaultCurrencyCode);
115
116 if (balances.ContainsKey(expenseWithSplits.PaidByUserId))
117 balances[expenseWithSplits.PaidByUserId].TotalPaid += convertedAmount;
118
119 if (expenseWithSplits.Splits != null)
120 {
121 foreach (var split in expenseWithSplits.Splits)
122 {
123 var convertedSplit = CurrencyConverter.Convert(split.Amount, expCurrency, defaultCurrencyCode);
124 if (balances.ContainsKey(split.UserId))
125 balances[split.UserId].TotalOwed += convertedSplit;
126 }
127 }
128 }
129
130 // Calculate budget totals (only category-assigned expenses count against budget)
131 var budgetCategories = await _budgetCategoryService.GetByTripIdAsync(id, userId);
132 var totalPlanned = budgetCategories.Sum(c => c.PlannedAmount ?? 0);
133 var totalBudgetSpent = budgetCategories.Sum(c => c.SpentAmount);
134 var budgetUsedPct = totalPlanned > 0 ? (int)(totalBudgetSpent * 100 / totalPlanned) : 0;
135
136 // Current user's balance
137 var currentUserBalance = balances.ContainsKey(userId) ? balances[userId].NetBalance : 0;
138
139 ViewData["TripId"] = id;
140 ViewData["TripName"] = trip.Name;
141 ViewData["ParticipantCount"] = participants.Count;
142 ViewData["RecentExpenses"] = recentExpenses;
143 ViewData["TotalExpenses"] = totalExpenses;
144 ViewData["UserRole"] = participant?.Role ?? EParticipantRole.Participant;
145 ViewData["Balances"] = balances.Values.OrderByDescending(b => b.NetBalance).ToList();
146 ViewData["CurrentUserBalance"] = currentUserBalance;
147 ViewData["BudgetUsedPct"] = budgetUsedPct;
148 ViewData["TotalPlanned"] = totalPlanned;
149 ViewData["CurrencySymbol"] = trip.DefaultCurrency?.Symbol ?? "\u20ac";
150
151 return View(trip);
152 }
153
154 // GET: Trips/Create
155 public async Task<IActionResult> Create()
156 {
157 await PopulateCurrencyDropdown();
158 return View();
159 }
160
161 // POST: Trips/Create
162 [HttpPost]
163 [ValidateAntiForgeryToken]
164 public async Task<IActionResult> Create(TripBllDto trip)
165 {
166 var userId = GetUserId();
167
168 if (ModelState.IsValid)
169 {
170 var created = await _tripService.CreateTripAsync(trip, userId);
171 return RedirectToAction(nameof(Details), new { id = created.Id });
172 }
173
174 await PopulateCurrencyDropdown(trip.DefaultCurrencyId);
175 return View(trip);
176 }
177
178 // GET: Trips/Edit/5
179 public async Task<IActionResult> Edit(Guid id)
180 {
181 var userId = GetUserId();
182
183 var trip = await _tripService.GetByIdForOrganizerAsync(id, userId);
184 if (trip == null)
185 {
186 // Distinguish not-organizer vs not-found
187 if (!await _tripService.IsOrganizerAsync(id, userId)) return Forbid();
188 return NotFound();
189 }
190
191 await PopulateCurrencyDropdown(trip.DefaultCurrencyId);
192 return View(trip);
193 }
194
195 // POST: Trips/Edit/5
196 [HttpPost]
197 [ValidateAntiForgeryToken]
198 public async Task<IActionResult> Edit(Guid id, TripBllDto trip)
199 {
200 if (id != trip.Id) return NotFound();
201
202 var userId = GetUserId();
203
204 if (!await _tripService.IsOrganizerAsync(id, userId)) return Forbid();
205
206 if (ModelState.IsValid)
207 {
208 var updated = await _tripService.UpdateAsync(trip, userId);
209 if (updated == null) return NotFound();
210 return RedirectToAction(nameof(Details), new { id });
211 }
212
213 await PopulateCurrencyDropdown(trip.DefaultCurrencyId);
214 return View(trip);
215 }
216
217 // GET: Trips/Delete/5
218 public async Task<IActionResult> Delete(Guid id)
219 {
220 var userId = GetUserId();
221
222 if (!await _tripService.IsOrganizerAsync(id, userId)) return Forbid();
223
224 var trip = await _tripService.GetByIdWithDetailsAsync(id, userId);
225 if (trip == null) return NotFound();
226
227 return View(trip);
228 }
229
230 // POST: Trips/Delete/5
231 [HttpPost, ActionName("Delete")]
232 [ValidateAntiForgeryToken]
233 public async Task<IActionResult> DeleteConfirmed(Guid id)
234 {
235 var userId = GetUserId();
236
237 var success = await _tripService.DeleteAsync(id, userId);
238 if (!success)
239 {
240 if (!await _tripService.IsOrganizerAsync(id, userId)) return Forbid();
241 return NotFound();
242 }
243
244 return RedirectToAction(nameof(Index));
245 }
246
247 private async Task PopulateCurrencyDropdown(Guid? selectedId = null)
248 {
249 var currencies = await _tripService.GetAllCurrenciesAsync();
250 ViewData["DefaultCurrencyId"] = new SelectList(currencies, "Id", "Code", selectedId);
251 }
252 }
253
254 public class TripIndexViewModel
255 {
256 public Guid Id { get; set; }
257 public string Name { get; set; } = default!;
258 public string? Destination { get; set; }
259 public ETripStatus Status { get; set; }
260 public DateTime? StartDate { get; set; }
261 public DateTime? EndDate { get; set; }
262 public EParticipantRole Role { get; set; }
263 public string CurrencyCode { get; set; } = default!;
264 }
265