profileShare

rasmusjy / splitapp-backend-modular-monolith

Read-only snapshot

No repository description.

main default branch 418 files Expires Sep 13, 2026, 9:06 AM
Index.cshtml 13,550 bytes
1 @model SplitApp.WebApp.Controllers.SettlementIndexViewModel
2
3 @{
4 ViewData["Title"] = Localizer["Settlement"];
5 var maxBalance = Model.Balances.Any() ? Model.Balances.Max(b => Math.Abs(b.NetBalance)) : 1m;
6 }
7
8 <!-- Page Header -->
9 <div class="sa-gradient-header">
10 <div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
11 <div>
12 <h1 style="margin-bottom: 4px;">@Localizer["Settlement"]</h1>
13 <p class="text-muted mb-0">
14 @Model.TripName
15 @if (Model.TripStatus != "Active" && Model.LatestPlan != null)
16 {
17 var statusBadge = Model.LatestPlan.Status switch
18 {
19 ESettlementStatus.Completed => "sa-badge-success",
20 ESettlementStatus.InProgress => "sa-badge-warning",
21 _ => "sa-badge-info"
22 };
23 var statusIcon = Model.LatestPlan.Status switch
24 {
25 ESettlementStatus.Completed => "bi-check-circle-fill",
26 ESettlementStatus.InProgress => "bi-hourglass-split",
27 _ => "bi-lock-fill"
28 };
29 <span class="sa-badge @statusBadge ms-2" style="font-size: 0.75rem;">
30 <i class="bi @statusIcon me-1"></i>@SplitApp.WebApp.Hosting.Helpers.EnumHelper.GetDisplayName(Model.LatestPlan.Status)
31 </span>
32 }
33 else if (Model.TripStatus != "Active")
34 {
35 <span class="sa-badge sa-badge-info ms-2" style="font-size: 0.75rem;">
36 <i class="bi bi-lock-fill me-1"></i>@Localizer["Pending"]
37 </span>
38 }
39 </p>
40 </div>
41 <div class="d-flex gap-2">
42 @if (Model.IsOrganizer && Model.TripStatus == "Active")
43 {
44 <form asp-action="Finalize" asp-route-tripId="@Model.TripId" method="post" style="display:inline">
45 <button type="submit" class="sa-btn sa-btn-sm" style="background: #fff; color: var(--sa-success);">
46 <i class="bi bi-check-circle"></i> @Localizer["Finalize Trip"]
47 </button>
48 </form>
49 }
50 @if (Model.IsOrganizer && Model.TripStatus != "Active" && Model.LatestPlan != null && Model.LatestPlan.Status != ESettlementStatus.Completed)
51 {
52 <form asp-action="Reopen" asp-route-tripId="@Model.TripId" method="post" style="display:inline">
53 <button type="submit" class="sa-btn sa-btn-sm" style="background: rgba(255,255,255,0.15); color: rgba(255,255,255,0.8);">
54 <i class="bi bi-unlock"></i> @Localizer["Reopen"]
55 </button>
56 </form>
57 }
58 <a asp-controller="Trips" asp-action="Details" asp-route-id="@Model.TripId" class="sa-btn sa-btn-sm" style="background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.9);">
59 <i class="bi bi-arrow-left"></i>
60 </a>
61 </div>
62 </div>
63 </div>
64
65 <!-- Balances -->
66 <div class="sa-card-static mb-4">
67 <div class="sa-card-header">
68 <i class="bi bi-bar-chart me-2"></i>@Localizer["Balances"]
69 </div>
70 <div class="sa-card-body">
71 @foreach (var balance in Model.Balances)
72 {
73 var pct = maxBalance > 0 ? (int)(Math.Abs(balance.NetBalance) * 45 / maxBalance) : 0;
74 <div class="d-flex align-items-center gap-3 mb-3">
75 <div style="width: 120px; font-weight: 600; font-size: 0.9rem;" class="sa-truncate">
76 @balance.UserName
77 </div>
78 <div style="flex: 1; position: relative; height: 28px; display: flex; align-items: center;">
79 <!-- Center line -->
80 <div style="position: absolute; left: 50%; top: 2px; bottom: 2px; width: 2px; background: var(--sa-gray-200); transform: translateX(-50%);"></div>
81 @if (balance.NetBalance >= 0)
82 {
83 <div style="position: absolute; left: 50%; height: 20px; width: @pct%; background: linear-gradient(90deg, var(--sa-success), #16a34a); border-radius: 0 var(--sa-radius-full) var(--sa-radius-full) 0; transition: width 0.8s ease;"></div>
84 }
85 else
86 {
87 <div style="position: absolute; right: 50%; height: 20px; width: @pct%; background: linear-gradient(270deg, var(--sa-danger), #dc2626); border-radius: var(--sa-radius-full) 0 0 var(--sa-radius-full); transition: width 0.8s ease;"></div>
88 }
89 </div>
90 <div style="width: 100px; text-align: right;">
91 <span class="sa-amount @(balance.NetBalance >= 0 ? "sa-amount-positive" : "sa-amount-negative")" style="font-size: 0.95rem;">
92 @(balance.NetBalance >= 0 ? "+" : "")@Model.CurrencySymbol@balance.NetBalance.ToString("N2")
93 </span>
94 </div>
95 </div>
96 }
97 </div>
98 </div>
99
100 <!-- Settlement Plan -->
101 @if (Model.LatestPlan != null && Model.LatestPlan.Payments != null && Model.LatestPlan.Payments.Any() && Model.TripStatus != "Active")
102 {
103 <div class="sa-card-static">
104 <div class="sa-card-header d-flex justify-content-between align-items-center">
105 <span><i class="bi bi-check2-circle me-2"></i>@Localizer["Settlement Plan"]</span>
106 @{
107 var planBadge = Model.LatestPlan.Status switch
108 {
109 ESettlementStatus.Completed => "sa-badge-success",
110 ESettlementStatus.InProgress => "sa-badge-warning",
111 _ => "sa-badge-neutral"
112 };
113 }
114 <span class="sa-badge @planBadge">@SplitApp.WebApp.Hosting.Helpers.EnumHelper.GetDisplayName(Model.LatestPlan.Status)</span>
115 </div>
116 <div class="sa-card-body">
117 <div class="d-flex flex-column gap-3">
118 @foreach (var payment in Model.LatestPlan.Payments.OrderByDescending(p => p.Amount))
119 {
120 var fromName = payment.FromUser != null ? $"{payment.FromUser.FirstName} {payment.FromUser.LastName}" : "?";
121 var toName = payment.ToUser != null ? $"{payment.ToUser.FirstName} {payment.ToUser.LastName}" : "?";
122 var fromInitial = payment.FromUser != null ? $"{payment.FromUser.FirstName?[0]}{payment.FromUser.LastName?[0]}" : "?";
123 var toInitial = payment.ToUser != null ? $"{payment.ToUser.FirstName?[0]}{payment.ToUser.LastName?[0]}" : "?";
124
125 <div class="sa-settlement-card">
126 <div class="d-flex align-items-center gap-2" style="min-width: 0; flex: 1;">
127 <span class="sa-avatar sa-avatar-sm sa-avatar-5" style="border: none;" title="@fromName">@fromInitial</span>
128 <span class="sa-truncate" style="font-weight: 600; font-size: 0.9rem;">@fromName</span>
129 </div>
130 <div class="d-flex flex-column align-items-center" style="flex-shrink: 0;">
131 <i class="bi bi-arrow-right sa-settlement-arrow"></i>
132 <span class="sa-settlement-amount">@Model.CurrencySymbol@payment.Amount.ToString("N2")</span>
133 </div>
134 <div class="d-flex align-items-center gap-2" style="min-width: 0; flex: 1; justify-content: flex-end;">
135 <span class="sa-truncate" style="font-weight: 600; font-size: 0.9rem;">@toName</span>
136 <span class="sa-avatar sa-avatar-sm sa-avatar-2" style="border: none;" title="@toName">@toInitial</span>
137 </div>
138 <div style="flex-shrink: 0; min-width: 110px; text-align: right;">
139 @if (payment.Status == EPaymentStatus.Confirmed)
140 {
141 <span class="sa-badge sa-badge-solid-success"><i class="bi bi-check2-all me-1"></i>@Localizer["Confirmed"]</span>
142 }
143 else if (payment.Status == EPaymentStatus.MarkedPaid)
144 {
145 @if (payment.ToUserId == Model.CurrentUserId)
146 {
147 <form asp-action="ConfirmReceipt" asp-route-paymentId="@payment.Id" method="post" style="display:inline">
148 <button type="submit" class="sa-btn sa-btn-sm sa-btn-pill" style="background: var(--sa-info); color: #fff;">
149 <i class="bi bi-hand-thumbs-up"></i> @Localizer["Confirm"]
150 </button>
151 </form>
152 }
153 else
154 {
155 <span class="sa-badge sa-badge-warning"><i class="bi bi-hourglass-split me-1"></i>@Localizer["Awaiting Confirmation"]</span>
156 }
157 }
158 else
159 {
160 @if (payment.FromUserId == Model.CurrentUserId)
161 {
162 <form asp-action="MarkPaid" asp-route-paymentId="@payment.Id" method="post" style="display:inline">
163 <button type="submit" class="sa-btn sa-btn-success sa-btn-sm sa-btn-pill">
164 <i class="bi bi-check-lg"></i> @Localizer["Mark Paid"]
165 </button>
166 </form>
167 }
168 else
169 {
170 <span class="sa-badge sa-badge-neutral"><i class="bi bi-clock me-1"></i>@Localizer["Pending"]</span>
171 }
172 }
173 </div>
174 </div>
175 }
176 </div>
177
178 <div class="sa-divider"></div>
179 <div class="d-flex justify-content-between" style="font-weight: 700;">
180 <span>@Localizer["Total"]</span>
181 <span>@Model.CurrencySymbol@Model.LatestPlan.TotalAmount.ToString("N2")</span>
182 </div>
183 </div>
184 </div>
185 }
186 else if (Model.PreviewPayments.Any())
187 {
188 <div class="sa-card-static">
189 <div class="sa-card-header d-flex justify-content-between align-items-center">
190 <span><i class="bi bi-eye me-2"></i>@Localizer["Suggested Payments"]</span>
191 <span class="sa-badge sa-badge-neutral">@Localizer["Preview"]</span>
192 </div>
193 <div class="sa-card-body">
194 <div class="d-flex flex-column gap-3">
195 @foreach (var payment in Model.PreviewPayments.OrderByDescending(p => p.Amount))
196 {
197 var fromInitial = payment.FromUserName.Length >= 2 ? payment.FromUserName[..2].ToUpper() : "?";
198 var toInitial = payment.ToUserName.Length >= 2 ? payment.ToUserName[..2].ToUpper() : "?";
199
200 <div class="sa-settlement-card">
201 <div class="d-flex align-items-center gap-2" style="min-width: 0; flex: 1;">
202 <span class="sa-avatar sa-avatar-sm sa-avatar-5" style="border: none;">@fromInitial</span>
203 <span class="sa-truncate" style="font-weight: 600; font-size: 0.9rem;">@payment.FromUserName</span>
204 </div>
205 <div class="d-flex flex-column align-items-center" style="flex-shrink: 0;">
206 <i class="bi bi-arrow-right sa-settlement-arrow"></i>
207 <span class="sa-settlement-amount">@Model.CurrencySymbol@payment.Amount.ToString("N2")</span>
208 </div>
209 <div class="d-flex align-items-center gap-2" style="min-width: 0; flex: 1; justify-content: flex-end;">
210 <span class="sa-truncate" style="font-weight: 600; font-size: 0.9rem;">@payment.ToUserName</span>
211 <span class="sa-avatar sa-avatar-sm sa-avatar-2" style="border: none;">@toInitial</span>
212 </div>
213 </div>
214 }
215 </div>
216 <div class="sa-divider"></div>
217 <p style="font-size: 0.8rem; color: var(--sa-gray-400); margin: 0;">
218 <i class="bi bi-info-circle me-1"></i>@Localizer["These are suggested payments. Finalize the trip to lock them in."]
219 </p>
220 </div>
221 </div>
222 }
223 else
224 {
225 <div class="sa-card-static">
226 <div class="sa-empty" style="padding: var(--sa-space-8) var(--sa-space-4);">
227 <div class="sa-empty-icon" style="font-size: 2.5rem; color: var(--sa-success);"><i class="bi bi-check-circle-fill"></i></div>
228 <div class="sa-empty-title" style="font-size: 1rem;">@Localizer["All settled up!"]</div>
229 <p class="sa-empty-text" style="font-size: 0.875rem;">@Localizer["No payments needed — everyone is even."]</p>
230 </div>
231 </div>
232 }
233