|
2 | 2 | @using EstateManagementUI.BlazorServer.Factories |
3 | 3 | @using EstateManagementUI.BusinessLogic.Requests |
4 | 4 | @rendermode InteractiveServer |
| 5 | +@inherits AuthorizedComponentBase |
5 | 6 | @inject IMediator Mediator |
6 | 7 | @inject NavigationManager Navigation |
7 | 8 | @inject ILogger<ProductPerformance> Logger |
|
67 | 68 | <div class="flex items-end"> |
68 | 69 | <button @onclick="LoadData" class="btn btn-primary"> |
69 | 70 | <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
70 | | - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path> |
| 71 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path> |
71 | 72 | </svg> |
72 | | - Refresh |
| 73 | + Apply Filters |
73 | 74 | </button> |
74 | 75 | </div> |
75 | 76 | </div> |
|
86 | 87 | </div> |
87 | 88 | <div class="info-box-content"> |
88 | 89 | <span class="info-box-text">Total Products</span> |
89 | | - <span class="info-box-number">@totalProducts</span> |
| 90 | + <span class="info-box-number">@performanceData.Summary.TotalProducts</span> |
90 | 91 | </div> |
91 | 92 | </div> |
92 | 93 |
|
|
98 | 99 | </div> |
99 | 100 | <div class="info-box-content"> |
100 | 101 | <span class="info-box-text">Total Transactions</span> |
101 | | - <span class="info-box-number">@totalTransactions.ToString("N0")</span> |
| 102 | + <span class="info-box-number">@performanceData.Summary.TotalCount.ToString("N0")</span> |
102 | 103 | </div> |
103 | 104 | </div> |
104 | 105 |
|
|
110 | 111 | </div> |
111 | 112 | <div class="info-box-content"> |
112 | 113 | <span class="info-box-text">Total Value</span> |
113 | | - <span class="info-box-number">@totalValue.ToString("C")</span> |
| 114 | + <span class="info-box-number">@performanceData.Summary.TotalValue.ToString("C")</span> |
114 | 115 | </div> |
115 | 116 | </div> |
116 | 117 |
|
|
122 | 123 | </div> |
123 | 124 | <div class="info-box-content"> |
124 | 125 | <span class="info-box-text">Average per Product</span> |
125 | | - <span class="info-box-number">@averageValuePerProduct.ToString("C")</span> |
| 126 | + <span class="info-box-number">@performanceData.Summary.AveragePerProduct.ToString("C")</span> |
126 | 127 | </div> |
127 | 128 | </div> |
128 | 129 | </div> |
|
149 | 150 | </div> |
150 | 151 | </div> |
151 | 152 | <div class="card-body"> |
152 | | - @if (performanceData != null && performanceData.Any()) |
| 153 | + @if (performanceData != null && performanceData.ProductDetails.Any()) |
153 | 154 | { |
154 | 155 | @if (!showChart) |
155 | 156 | { |
|
166 | 167 | </tr> |
167 | 168 | </thead> |
168 | 169 | <tbody> |
169 | | - @foreach (var item in performanceData) |
| 170 | + @foreach (var item in performanceData.ProductDetails) |
170 | 171 | { |
171 | 172 | <tr> |
172 | 173 | <td class="font-medium">@item.ProductName</td> |
173 | 174 | <td class="text-right">@item.TransactionCount.ToString("N0")</td> |
174 | 175 | <td class="text-right">@item.TransactionValue.ToString("C")</td> |
175 | 176 | <td class="text-right"> |
176 | | - <span class="font-medium text-admin-primary">@item.PercentageContribution.ToString("F2")%</span> |
| 177 | + <span class="font-medium text-admin-primary">@item.PercentageOfTotal.ToString("F2")%</span> |
177 | 178 | </td> |
178 | 179 | <td> |
179 | 180 | <div class="w-full bg-gray-200 rounded-full h-2.5"> |
180 | 181 | <div class="bg-admin-primary h-2.5 rounded-full" |
181 | | - style="width: @(item.PercentageContribution)%"></div> |
| 182 | + style="width: @(item.PercentageOfTotal)%"></div> |
182 | 183 | </div> |
183 | 184 | </td> |
184 | 185 | </tr> |
185 | 186 | } |
186 | 187 | </tbody> |
187 | | - <tfoot> |
| 188 | + @* <tfoot> |
188 | 189 | <tr class="font-bold bg-gray-50"> |
189 | 190 | <td>Total</td> |
190 | 191 | <td class="text-right">@totalTransactions.ToString("N0")</td> |
191 | 192 | <td class="text-right">@totalValue.ToString("C")</td> |
192 | 193 | <td class="text-right">@totalPercentage.ToString("F2")%</td> |
193 | 194 | <td></td> |
194 | 195 | </tr> |
195 | | - </tfoot> |
| 196 | + </tfoot> *@ |
196 | 197 | </table> |
197 | 198 | </div> |
198 | 199 | } |
|
201 | 202 | <!-- Chart View --> |
202 | 203 | <div class="space-y-4"> |
203 | 204 | <h4 class="font-semibold text-lg mb-4">Transaction Value by Product</h4> |
204 | | - @foreach (var item in performanceData) |
| 205 | + @foreach (var item in performanceData.ProductDetails) |
205 | 206 | { |
206 | 207 | <div class="mb-4"> |
207 | 208 | <div class="flex justify-between mb-1"> |
208 | 209 | <span class="text-sm font-medium text-gray-700">@item.ProductName</span> |
209 | 210 | <div class="text-sm text-gray-600"> |
210 | 211 | <span class="font-semibold">@item.TransactionValue.ToString("C")</span> |
211 | | - <span class="text-admin-primary ml-2">(@item.PercentageContribution.ToString("F2")%)</span> |
| 212 | + <span class="text-admin-primary ml-2">(@item.PercentageOfTotal.ToString("F2")%)</span> |
212 | 213 | </div> |
213 | 214 | </div> |
214 | 215 | <div class="w-full bg-gray-200 rounded-full h-8"> |
215 | 216 | <div class="bg-gradient-to-r from-admin-primary to-admin-secondary h-8 rounded-full flex items-center justify-end pr-3" |
216 | | - style="width: @(item.PercentageContribution)%"> |
| 217 | + style="width: @(item.PercentageOfTotal)%"> |
217 | 218 | <span class="text-white text-xs font-bold">@item.TransactionCount.ToString("N0")</span> |
218 | 219 | </div> |
219 | 220 | </div> |
|
223 | 224 | } |
224 | 225 |
|
225 | 226 | <!-- Validation Message --> |
226 | | - @if (Math.Abs(totalPercentage - 100) < 0.01m) |
| 227 | + @if (Math.Abs(performanceData.ProductDetails.Sum(p=> p.PercentageOfTotal) - 100) < 0.01m) |
227 | 228 | { |
228 | 229 | <div class="mt-4 p-3 bg-green-50 border border-green-200 rounded-lg"> |
229 | 230 | <div class="flex items-center text-green-700"> |
|
241 | 242 | <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
242 | 243 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path> |
243 | 244 | </svg> |
244 | | - <span class="text-sm font-medium">Warning: Percentages sum to @totalPercentage.ToString("F2")%</span> |
| 245 | + <span class="text-sm font-medium">Warning: Percentages sum to @performanceData.ProductDetails.Sum(p => p.PercentageOfTotal).ToString("F2")%</span> |
245 | 246 | </div> |
246 | 247 | </div> |
247 | 248 | } |
|
260 | 261 | } |
261 | 262 | </div> |
262 | 263 |
|
263 | | -@code { |
264 | | - private bool isLoading = true; |
265 | | - private string? errorMessage; |
266 | | - private bool showChart = false; |
267 | | - |
268 | | - // Filter states |
269 | | - private DateOnly _startDate = DateOnly.FromDateTime(DateTime.Now.AddDays(-30)); |
270 | | - private DateOnly _endDate = DateOnly.FromDateTime(DateTime.Now); |
271 | | - |
272 | | - // Data |
273 | | - private List<ProductPerformanceModel>? performanceData; |
274 | | - |
275 | | - // KPIs |
276 | | - private int totalProducts = 0; |
277 | | - private int totalTransactions = 0; |
278 | | - private decimal totalValue = 0; |
279 | | - private decimal averageValuePerProduct = 0; |
280 | | - private decimal totalPercentage = 0; |
281 | | - |
282 | | - protected override async Task OnInitializedAsync() |
283 | | - { |
284 | | - await LoadData(); |
285 | | - } |
286 | | - |
287 | | - private async Task LoadData() |
288 | | - { |
289 | | - try |
290 | | - { |
291 | | - isLoading = true; |
292 | | - errorMessage = null; |
293 | | - StateHasChanged(); |
294 | | - |
295 | | - var correlationId = new CorrelationId(Guid.NewGuid()); |
296 | | - var estateId = Guid.Parse("11111111-1111-1111-1111-111111111111"); |
297 | | - var accessToken = "stubbed-token"; |
298 | | - |
299 | | - var startDate = _startDate.ToDateTime(TimeOnly.MinValue); |
300 | | - var endDate = _endDate.ToDateTime(TimeOnly.MaxValue); |
301 | | - |
302 | | - var result = await Mediator.Send(new Queries.GetProductPerformanceQuery( |
303 | | - correlationId, |
304 | | - accessToken, |
305 | | - estateId, |
306 | | - startDate, |
307 | | - endDate |
308 | | - )); |
309 | | - |
310 | | - if (result.IsSuccess && result.Data != null) |
311 | | - { |
312 | | - performanceData = ModelFactory.ConvertFrom(result.Data); |
313 | | - CalculateKPIs(); |
314 | | - } |
315 | | - else |
316 | | - { |
317 | | - errorMessage = result.Message ?? "Failed to load product performance data"; |
318 | | - } |
319 | | - } |
320 | | - catch (Exception ex) |
321 | | - { |
322 | | - errorMessage = $"Failed to load data: {ex.Message}"; |
323 | | - Logger.LogError(ex, "Error loading product performance data"); |
324 | | - } |
325 | | - finally |
326 | | - { |
327 | | - isLoading = false; |
328 | | - StateHasChanged(); |
329 | | - } |
330 | | - } |
331 | | - |
332 | | - private void CalculateKPIs() |
333 | | - { |
334 | | - if (performanceData == null || !performanceData.Any()) |
335 | | - { |
336 | | - totalProducts = 0; |
337 | | - totalTransactions = 0; |
338 | | - totalValue = 0; |
339 | | - averageValuePerProduct = 0; |
340 | | - totalPercentage = 0; |
341 | | - return; |
342 | | - } |
343 | | - |
344 | | - totalProducts = performanceData.Count; |
345 | | - totalTransactions = performanceData.Sum(p => p.TransactionCount); |
346 | | - totalValue = performanceData.Sum(p => p.TransactionValue); |
347 | | - averageValuePerProduct = totalProducts > 0 ? totalValue / totalProducts : 0; |
348 | | - totalPercentage = performanceData.Sum(p => p.PercentageContribution); |
349 | | - } |
350 | | -} |
351 | | - |
0 commit comments