profileShare

rasmusjy / splitapp-backend-microservices

Read-only snapshot

No repository description.

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