diff --git a/Webshop.Api/Controllers/Admin/AdminAnalyticsController.cs b/Webshop.Api/Controllers/Admin/AdminAnalyticsController.cs new file mode 100644 index 0000000..777c5a1 --- /dev/null +++ b/Webshop.Api/Controllers/Admin/AdminAnalyticsController.cs @@ -0,0 +1,36 @@ +// src/Webshop.Api/Controllers/Admin/AdminAnalyticsController.cs +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Webshop.Application.DTOs.Admin; +using Webshop.Application.Services.Admin.Interfaces; + +namespace Webshop.Api.Controllers.Admin +{ + /// + /// Stellt aggregierte Analysedaten für das Admin-Dashboard bereit. + /// + [ApiController] + [Route("api/v1/admin/[controller]")] + [Authorize(Roles = "Admin")] + public class AdminAnalyticsController : ControllerBase + { + private readonly IAdminAnalyticsService _analyticsService; + + public AdminAnalyticsController(IAdminAnalyticsService analyticsService) + { + _analyticsService = analyticsService; + } + + /// + /// Ruft die aggregierten Analysedaten und KPIs für den Admin-Bereich ab. + /// + /// Der Zeitraum für die Statistiken (Last7Days, Last30Days, AllTime). Standard ist Last30Days. + [HttpGet] + public async Task> GetAnalytics([FromQuery] AnalyticsPeriod period = AnalyticsPeriod.Last30Days) + { + var stats = await _analyticsService.GetAnalyticsAsync(period); + return Ok(stats); + } + } +} \ No newline at end of file diff --git a/Webshop.Api/Program.cs b/Webshop.Api/Program.cs index 7eb282f..c277e72 100644 --- a/Webshop.Api/Program.cs +++ b/Webshop.Api/Program.cs @@ -149,6 +149,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Controller und API-Infrastruktur builder.Services.AddControllers() diff --git a/Webshop.Application/DTOs/Admin/AnalyticsDto.cs b/Webshop.Application/DTOs/Admin/AnalyticsDto.cs new file mode 100644 index 0000000..80c047e --- /dev/null +++ b/Webshop.Application/DTOs/Admin/AnalyticsDto.cs @@ -0,0 +1,114 @@ +// src/Webshop.Application/DTOs/Admin/AnalyticsDto.cs +using System.Collections.Generic; + +namespace Webshop.Application.DTOs.Admin +{ + /// + /// Fasst alle relevanten Analysedaten und KPIs für den Admin-Bereich zusammen. + /// + public class AnalyticsDto + { + /// + /// Die wichtigsten Kennzahlen (Key Performance Indicators). + /// + public KpiSummaryDto KpiSummary { get; set; } = new KpiSummaryDto(); + + /// + /// Eine Liste von Datenpunkten für Umsatz-Charts (z.B. Umsatz pro Tag). + /// + public List SalesOverTime { get; set; } = new List(); + + /// + /// Die Top 10 der meistverkauften Produkte im ausgewählten Zeitraum. + /// + public List TopPerformingProducts { get; set; } = new List(); + + /// + /// Informationen zum aktuellen Lagerbestand. + /// + public InventoryStatusDto InventoryStatus { get; set; } = new InventoryStatusDto(); + } + + /// + /// Die wichtigsten Kennzahlen (Key Performance Indicators). + /// + public class KpiSummaryDto + { + /// + /// Der Gesamtumsatz (netto) im ausgewählten Zeitraum. + /// + public decimal TotalRevenue { get; set; } + + /// + /// Die Gesamtzahl der Bestellungen im ausgewählten Zeitraum. + /// + public int TotalOrders { get; set; } + + /// + /// Die Gesamtzahl aller registrierten Kunden. + /// + public int TotalCustomers { get; set; } + + /// + /// Die Anzahl der Neukunden, die sich im ausgewählten Zeitraum registriert haben. + /// + public int NewCustomersThisPeriod { get; set; } + + /// + /// Der durchschnittliche Wert einer Bestellung im ausgewählten Zeitraum. + /// + public decimal AverageOrderValue { get; set; } + } + + /// + /// Ein Datenpunkt für Umsatz-Charts (z.B. Umsatz pro Tag). + /// + public class SalesDataPointDto + { + /// + /// Das Datum des Datenpunkts im Format 'yyyy-MM-dd'. + /// + public string Date { get; set; } = string.Empty; + + /// + /// Der an diesem Tag generierte Umsatz. + /// + public decimal Revenue { get; set; } + } + + /// + /// Repräsentiert ein gut verkauftes Produkt. + /// + public class TopProductDto + { + public Guid ProductId { get; set; } + public string Name { get; set; } = string.Empty; + public string Sku { get; set; } = string.Empty; + + /// + /// Die Gesamtzahl der verkauften Einheiten im ausgewählten Zeitraum. + /// + public int UnitsSold { get; set; } + + /// + /// Der Gesamtumsatz, der mit diesem Produkt im ausgewählten Zeitraum erzielt wurde. + /// + public decimal TotalRevenue { get; set; } + } + + /// + /// Informationen zum Lagerbestand. + /// + public class InventoryStatusDto + { + /// + /// Die Anzahl der Produkte, deren Lagerbestand unter dem in den Einstellungen definierten Schwellenwert liegt. + /// + public int ProductsWithLowStock { get; set; } + + /// + /// Der prozentuale Anteil der Produkte, die auf Lager sind (Menge > 0). + /// + public double OverallStockAvailabilityPercentage { get; set; } + } +} \ No newline at end of file diff --git a/Webshop.Application/Services/Admin/AdminAnalyticsService.cs b/Webshop.Application/Services/Admin/AdminAnalyticsService.cs new file mode 100644 index 0000000..bd2b051 --- /dev/null +++ b/Webshop.Application/Services/Admin/AdminAnalyticsService.cs @@ -0,0 +1,116 @@ +// src/Webshop.Application/Services/Admin/AdminAnalyticsService.cs +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Webshop.Application.DTOs.Admin; +using Webshop.Application.Services.Admin.Interfaces; +using Webshop.Domain.Enums; +using Webshop.Domain.Interfaces; +using Webshop.Infrastructure.Data; + +namespace Webshop.Application.Services.Admin +{ + public class AdminAnalyticsService : IAdminAnalyticsService + { + private readonly ApplicationDbContext _context; + private readonly ISettingService _settingService; + + public AdminAnalyticsService(ApplicationDbContext context, ISettingService settingService) + { + _context = context; + _settingService = settingService; + } + + public async Task GetAnalyticsAsync(AnalyticsPeriod period) + { + var startDate = GetStartDateFromPeriod(period); + + var filteredOrders = await _context.Orders + .Where(o => o.OrderDate >= startDate && o.OrderStatus != OrderStatus.Cancelled.ToString()) + .ToListAsync(); + + var analyticsDto = new AnalyticsDto + { + KpiSummary = await GetKpiSummaryAsync(startDate, filteredOrders), + SalesOverTime = GetSalesOverTime(filteredOrders), + TopPerformingProducts = await GetTopPerformingProductsAsync(startDate), + InventoryStatus = await GetInventoryStatusAsync() + }; + + return analyticsDto; + } + + private async Task GetKpiSummaryAsync(DateTimeOffset startDate, List filteredOrders) + { + var totalRevenue = filteredOrders.Sum(o => o.OrderTotal); + var totalOrders = filteredOrders.Count; + + return new KpiSummaryDto + { + TotalRevenue = totalRevenue, + TotalOrders = totalOrders, + TotalCustomers = await _context.Customers.CountAsync(), + NewCustomersThisPeriod = await _context.Users.CountAsync(u => u.CreatedDate >= startDate), + AverageOrderValue = totalOrders > 0 ? totalRevenue / totalOrders : 0 + }; + } + + private List GetSalesOverTime(List orders) + { + return orders + .GroupBy(o => o.OrderDate.Date) + .Select(g => new SalesDataPointDto + { + Date = g.Key.ToString("yyyy-MM-dd"), + Revenue = g.Sum(o => o.OrderTotal) + }) + .OrderBy(dp => dp.Date) + .ToList(); + } + + private async Task> GetTopPerformingProductsAsync(DateTimeOffset startDate) + { + return await _context.OrderItems + .Where(oi => oi.Order.OrderDate >= startDate && oi.ProductId.HasValue) + .GroupBy(oi => new { oi.ProductId, oi.ProductName, oi.ProductSKU }) + .Select(g => new TopProductDto + { + ProductId = g.Key.ProductId!.Value, + Name = g.Key.ProductName, + Sku = g.Key.ProductSKU, + UnitsSold = g.Sum(oi => oi.Quantity), + TotalRevenue = g.Sum(oi => oi.TotalPrice) + }) + .OrderByDescending(p => p.UnitsSold) + .Take(10) + .ToListAsync(); + } + + private async Task GetInventoryStatusAsync() + { + var lowStockThreshold = await _settingService.GetSettingValueAsync("LowStockThreshold", 10); + + var totalProducts = await _context.Products.CountAsync(p => p.IsActive); + var productsInStock = await _context.Products.CountAsync(p => p.IsActive && p.StockQuantity > 0); + var productsWithLowStock = await _context.Products.CountAsync(p => p.IsActive && p.StockQuantity > 0 && p.StockQuantity <= lowStockThreshold); + + return new InventoryStatusDto + { + ProductsWithLowStock = productsWithLowStock, + OverallStockAvailabilityPercentage = totalProducts > 0 ? Math.Round((double)productsInStock / totalProducts * 100, 2) : 0 + }; + } + + private DateTimeOffset GetStartDateFromPeriod(AnalyticsPeriod period) + { + return period switch + { + AnalyticsPeriod.Last7Days => DateTimeOffset.UtcNow.AddDays(-7), + AnalyticsPeriod.Last30Days => DateTimeOffset.UtcNow.AddDays(-30), + _ => DateTimeOffset.MinValue + }; + } + } +} \ No newline at end of file diff --git a/Webshop.Application/Services/Admin/Interfaces/IAdminAnalyticsService.cs b/Webshop.Application/Services/Admin/Interfaces/IAdminAnalyticsService.cs new file mode 100644 index 0000000..c44fa64 --- /dev/null +++ b/Webshop.Application/Services/Admin/Interfaces/IAdminAnalyticsService.cs @@ -0,0 +1,13 @@ +// src/Webshop.Application/Services/Admin/Interfaces/IAdminAnalyticsService.cs +using System.Threading.Tasks; +using Webshop.Application.DTOs.Admin; + +namespace Webshop.Application.Services.Admin.Interfaces +{ + public enum AnalyticsPeriod { Last7Days, Last30Days, AllTime } + + public interface IAdminAnalyticsService + { + Task GetAnalyticsAsync(AnalyticsPeriod period); + } +} \ No newline at end of file