profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
TripsController.cs 8,872 bytes
1 using App.BLL.DTO;
2 using App.BLL.Services;
3 using App.Domain;
4 using App.Domain.Identity;
5 using Microsoft.AspNetCore.Authorization;
6 using Microsoft.AspNetCore.Identity;
7 using Microsoft.AspNetCore.Mvc;
8 using Microsoft.AspNetCore.Mvc.Rendering;
9 using WebApp.Helpers;
10
11 namespace WebApp.Controllers;
12
13 [Authorize]
14 public class TripsController : Controller
15 {
16 private readonly ITripService _tripService;
17 private readonly IExpenseService _expenseService;
18 private readonly IBudgetCategoryService _budgetCategoryService;
19 private readonly UserManager<AppUser> _userManager;
20
21 public TripsController(
22 ITripService tripService,
23 IExpenseService expenseService,
24 IBudgetCategoryService budgetCategoryService,
25 UserManager<AppUser> userManager)
26 {
27 _tripService = tripService;
28 _expenseService = expenseService;
29 _budgetCategoryService = budgetCategoryService;
30 _userManager = userManager;
31 }
32
33 private Guid GetUserId() => Guid.Parse(_userManager.GetUserId(User)!);
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 = p.User != null ? $"{p.User.FirstName} {p.User.LastName}" : "Unknown",
97 TotalPaid = 0,
98 TotalOwed = 0
99 };
100 }
101
102 foreach (var expense in expensesAll)
103 {
104 var expenseWithSplits = await _expenseService.GetByIdWithDetailsAsync(expense.Id, userId);
105 if (expenseWithSplits == null) continue;
106
107 var expCurrency = expenseWithSplits.Currency?.Code ?? defaultCurrencyCode;
108 var convertedAmount = CurrencyConverter.Convert(expenseWithSplits.Amount, expCurrency, defaultCurrencyCode);
109
110 if (balances.ContainsKey(expenseWithSplits.PaidByUserId))
111 balances[expenseWithSplits.PaidByUserId].TotalPaid += convertedAmount;
112
113 if (expenseWithSplits.Splits != null)
114 {
115 foreach (var split in expenseWithSplits.Splits)
116 {
117 var convertedSplit = CurrencyConverter.Convert(split.Amount, expCurrency, defaultCurrencyCode);
118 if (balances.ContainsKey(split.UserId))
119 balances[split.UserId].TotalOwed += convertedSplit;
120 }
121 }
122 }
123
124 // Calculate budget totals (only category-assigned expenses count against budget)
125 var budgetCategories = await _budgetCategoryService.GetByTripIdAsync(id, userId);
126 var totalPlanned = budgetCategories.Sum(c => c.PlannedAmount ?? 0);
127 var totalBudgetSpent = budgetCategories.Sum(c => c.SpentAmount);
128 var budgetUsedPct = totalPlanned > 0 ? (int)(totalBudgetSpent * 100 / totalPlanned) : 0;
129
130 // Current user's balance
131 var currentUserBalance = balances.ContainsKey(userId) ? balances[userId].NetBalance : 0;
132
133 ViewData["TripId"] = id;
134 ViewData["TripName"] = trip.Name;
135 ViewData["ParticipantCount"] = participants.Count;
136 ViewData["RecentExpenses"] = recentExpenses;
137 ViewData["TotalExpenses"] = totalExpenses;
138 ViewData["UserRole"] = participant?.Role ?? EParticipantRole.Participant;
139 ViewData["Balances"] = balances.Values.OrderByDescending(b => b.NetBalance).ToList();
140 ViewData["CurrentUserBalance"] = currentUserBalance;
141 ViewData["BudgetUsedPct"] = budgetUsedPct;
142 ViewData["TotalPlanned"] = totalPlanned;
143 ViewData["CurrencySymbol"] = trip.DefaultCurrency?.Symbol ?? "\u20ac";
144
145 return View(trip);
146 }
147
148 // GET: Trips/Create
149 public async Task<IActionResult> Create()
150 {
151 await PopulateCurrencyDropdown();
152 return View();
153 }
154
155 // POST: Trips/Create
156 [HttpPost]
157 [ValidateAntiForgeryToken]
158 public async Task<IActionResult> Create(TripBllDto trip)
159 {
160 var userId = GetUserId();
161
162 if (ModelState.IsValid)
163 {
164 var created = await _tripService.CreateTripAsync(trip, userId);
165 return RedirectToAction(nameof(Details), new { id = created.Id });
166 }
167
168 await PopulateCurrencyDropdown(trip.DefaultCurrencyId);
169 return View(trip);
170 }
171
172 // GET: Trips/Edit/5
173 public async Task<IActionResult> Edit(Guid id)
174 {
175 var userId = GetUserId();
176
177 var trip = await _tripService.GetByIdForOrganizerAsync(id, userId);
178 if (trip == null)
179 {
180 // Distinguish not-organizer vs not-found
181 if (!await _tripService.IsOrganizerAsync(id, userId)) return Forbid();
182 return NotFound();
183 }
184
185 await PopulateCurrencyDropdown(trip.DefaultCurrencyId);
186 return View(trip);
187 }
188
189 // POST: Trips/Edit/5
190 [HttpPost]
191 [ValidateAntiForgeryToken]
192 public async Task<IActionResult> Edit(Guid id, TripBllDto trip)
193 {
194 if (id != trip.Id) return NotFound();
195
196 var userId = GetUserId();
197
198 if (!await _tripService.IsOrganizerAsync(id, userId)) return Forbid();
199
200 if (ModelState.IsValid)
201 {
202 var updated = await _tripService.UpdateAsync(trip, userId);
203 if (updated == null) return NotFound();
204 return RedirectToAction(nameof(Details), new { id });
205 }
206
207 await PopulateCurrencyDropdown(trip.DefaultCurrencyId);
208 return View(trip);
209 }
210
211 // GET: Trips/Delete/5
212 public async Task<IActionResult> Delete(Guid id)
213 {
214 var userId = GetUserId();
215
216 if (!await _tripService.IsOrganizerAsync(id, userId)) return Forbid();
217
218 var trip = await _tripService.GetByIdWithDetailsAsync(id, userId);
219 if (trip == null) return NotFound();
220
221 return View(trip);
222 }
223
224 // POST: Trips/Delete/5
225 [HttpPost, ActionName("Delete")]
226 [ValidateAntiForgeryToken]
227 public async Task<IActionResult> DeleteConfirmed(Guid id)
228 {
229 var userId = GetUserId();
230
231 var success = await _tripService.DeleteAsync(id, userId);
232 if (!success)
233 {
234 if (!await _tripService.IsOrganizerAsync(id, userId)) return Forbid();
235 return NotFound();
236 }
237
238 return RedirectToAction(nameof(Index));
239 }
240
241 private async Task PopulateCurrencyDropdown(Guid? selectedId = null)
242 {
243 var currencies = await _tripService.GetAllCurrenciesAsync();
244 ViewData["DefaultCurrencyId"] = new SelectList(currencies, "Id", "Code", selectedId);
245 }
246 }
247
248 public class TripIndexViewModel
249 {
250 public Guid Id { get; set; }
251 public string Name { get; set; } = default!;
252 public string? Destination { get; set; }
253 public ETripStatus Status { get; set; }
254 public DateTime? StartDate { get; set; }
255 public DateTime? EndDate { get; set; }
256 public EParticipantRole Role { get; set; }
257 public string CurrencyCode { get; set; } = default!;
258 }
259