| 1 | using System;
|
|---|
| 2 | using System.Collections.Generic;
|
|---|
| 3 | using System.Linq;
|
|---|
| 4 | using System.Threading.Tasks;
|
|---|
| 5 | using Microsoft.EntityFrameworkCore;
|
|---|
| 6 | using StockMaster.Data;
|
|---|
| 7 | using StockMaster.ViewModels;
|
|---|
| 8 |
|
|---|
| 9 | namespace StockMaster.Services
|
|---|
| 10 | {
|
|---|
| 11 | public class ReportService : IReportService
|
|---|
| 12 | {
|
|---|
| 13 | private readonly StockDbContext _context;
|
|---|
| 14 |
|
|---|
| 15 | public ReportService(StockDbContext context)
|
|---|
| 16 | {
|
|---|
| 17 | _context = context;
|
|---|
| 18 | }
|
|---|
| 19 |
|
|---|
| 20 | public async Task<List<StockByWarehouseViewModel>> GetStockByWarehouseAsync()
|
|---|
| 21 | {
|
|---|
| 22 | return await _context.WarehouseStocks
|
|---|
| 23 | .Include(ws => ws.Warehouse)
|
|---|
| 24 | .Include(ws => ws.Product)
|
|---|
| 25 | .GroupBy(ws => new { ws.Warehouse.WarehouseId, ws.Warehouse.Name })
|
|---|
| 26 | .Select(g => new StockByWarehouseViewModel
|
|---|
| 27 | {
|
|---|
| 28 | WarehouseName = g.Key.Name,
|
|---|
| 29 | TotalUnits = g.Sum(ws => ws.QuantityOnHand),
|
|---|
| 30 | TotalStockValue = g.Sum(ws => ws.QuantityOnHand * ws.Product.UnitPrice)
|
|---|
| 31 | })
|
|---|
| 32 | .OrderByDescending(r => r.TotalStockValue)
|
|---|
| 33 | .ToListAsync();
|
|---|
| 34 | }
|
|---|
| 35 |
|
|---|
| 36 | public async Task<List<ProductRevenueViewModel>> GetProductRevenueAsync()
|
|---|
| 37 | {
|
|---|
| 38 | return await _context.SaleItems
|
|---|
| 39 | .Include(si => si.Product)
|
|---|
| 40 | .GroupBy(si => new { si.Product.ProductId, si.Product.Name })
|
|---|
| 41 | .Select(g => new ProductRevenueViewModel
|
|---|
| 42 | {
|
|---|
| 43 | ProductName = g.Key.Name,
|
|---|
| 44 | TotalUnitsSold = g.Sum(si => si.Quantity),
|
|---|
| 45 | TotalRevenue = g.Sum(si => si.Quantity * si.UnitPriceAtSale)
|
|---|
| 46 | })
|
|---|
| 47 | .OrderByDescending(r => r.TotalRevenue)
|
|---|
| 48 | .ToListAsync();
|
|---|
| 49 | }
|
|---|
| 50 |
|
|---|
| 51 | public async Task<List<POStatusViewModel>> GetPOStatusAsync()
|
|---|
| 52 | {
|
|---|
| 53 | return await _context.PurchaseOrderItems
|
|---|
| 54 | .Include(poi => poi.PurchaseOrder)
|
|---|
| 55 | .Include(poi => poi.Product)
|
|---|
| 56 | .Select(poi => new POStatusViewModel
|
|---|
| 57 | {
|
|---|
| 58 | PoId = poi.PoId,
|
|---|
| 59 | Status = poi.PurchaseOrder.Status,
|
|---|
| 60 | ProductName = poi.Product.Name,
|
|---|
| 61 | OrderedQuantity = poi.Quantity,
|
|---|
| 62 | ReceivedQuantity = poi.ReceivedQuantity,
|
|---|
| 63 | PendingQuantity = poi.Quantity - poi.ReceivedQuantity
|
|---|
| 64 | })
|
|---|
| 65 | .OrderBy(r => r.PoId)
|
|---|
| 66 | .ToListAsync();
|
|---|
| 67 | }
|
|---|
| 68 |
|
|---|
| 69 | public async Task<List<CategoryRevenueViewModel>> GetCategoryRevenueAsync()
|
|---|
| 70 | {
|
|---|
| 71 | return await _context.SaleItems
|
|---|
| 72 | .Include(si => si.Product)
|
|---|
| 73 | .ThenInclude(p => p.Category)
|
|---|
| 74 | .GroupBy(si => new { si.Product.Category.CategoryId, si.Product.Category.Name })
|
|---|
| 75 | .Select(g => new CategoryRevenueViewModel
|
|---|
| 76 | {
|
|---|
| 77 | CategoryName = g.Key.Name,
|
|---|
| 78 | TotalRevenue = g.Sum(si => si.Quantity * si.UnitPriceAtSale)
|
|---|
| 79 | })
|
|---|
| 80 | .OrderByDescending(r => r.TotalRevenue)
|
|---|
| 81 | .ToListAsync();
|
|---|
| 82 | }
|
|---|
| 83 |
|
|---|
| 84 | public async Task<List<WarehouseCapacityViewModel>> GetWarehouseCapacityAsync()
|
|---|
| 85 | {
|
|---|
| 86 | var warehouses = await _context.Warehouses.ToListAsync();
|
|---|
| 87 | var stocks = await _context.WarehouseStocks.ToListAsync();
|
|---|
| 88 |
|
|---|
| 89 | var result = new List<WarehouseCapacityViewModel>();
|
|---|
| 90 |
|
|---|
| 91 | foreach (var w in warehouses)
|
|---|
| 92 | {
|
|---|
| 93 | var currentStock = stocks.Where(ws => ws.WarehouseId == w.WarehouseId).Sum(ws => ws.QuantityOnHand);
|
|---|
| 94 | double percentage = w.Capacity > 0 ? ((double)currentStock / w.Capacity) * 100 : 0;
|
|---|
| 95 |
|
|---|
| 96 | result.Add(new WarehouseCapacityViewModel
|
|---|
| 97 | {
|
|---|
| 98 | WarehouseName = w.Name,
|
|---|
| 99 | Capacity = w.Capacity,
|
|---|
| 100 | UnitsInStock = currentStock,
|
|---|
| 101 | OccupancyPercentage = Math.Round(percentage, 2)
|
|---|
| 102 | });
|
|---|
| 103 | }
|
|---|
| 104 | return result;
|
|---|
| 105 | }
|
|---|
| 106 |
|
|---|
| 107 | public async Task<List<StagnantProductViewModel>> GetStagnantProductsAsync()
|
|---|
| 108 | {
|
|---|
| 109 | var ninetyDaysAgo = DateTime.Now.AddDays(-90);
|
|---|
| 110 | var products = await _context.Products.Include(p => p.Category).ToListAsync();
|
|---|
| 111 | var stocks = await _context.WarehouseStocks.ToListAsync();
|
|---|
| 112 |
|
|---|
| 113 | var recentSales = await _context.SaleItems
|
|---|
| 114 | .Include(si => si.Sale)
|
|---|
| 115 | .Where(si => si.Sale.DateTime >= ninetyDaysAgo)
|
|---|
| 116 | .Select(si => si.ProductId)
|
|---|
| 117 | .Distinct()
|
|---|
| 118 | .ToListAsync();
|
|---|
| 119 |
|
|---|
| 120 | var result = new List<StagnantProductViewModel>();
|
|---|
| 121 |
|
|---|
| 122 | foreach (var p in products)
|
|---|
| 123 | {
|
|---|
| 124 | if (!recentSales.Contains(p.ProductId))
|
|---|
| 125 | {
|
|---|
| 126 | var totalStock = stocks.Where(ws => ws.ProductId == p.ProductId).Sum(ws => ws.QuantityOnHand);
|
|---|
| 127 | if (totalStock > 0)
|
|---|
| 128 | {
|
|---|
| 129 | result.Add(new StagnantProductViewModel
|
|---|
| 130 | {
|
|---|
| 131 | ProductName = p.Name,
|
|---|
| 132 | Sku = p.Sku,
|
|---|
| 133 | QuantityOnHand = totalStock,
|
|---|
| 134 | LastSoldDate = "No sales in 90 days"
|
|---|
| 135 | });
|
|---|
| 136 | }
|
|---|
| 137 | }
|
|---|
| 138 | }
|
|---|
| 139 | return result;
|
|---|
| 140 | }
|
|---|
| 141 |
|
|---|
| 142 | public async Task<List<StockSufficiencyViewModel>> GetStockSufficiencyAsync()
|
|---|
| 143 | {
|
|---|
| 144 | var sixtyDaysAgo = DateTime.Now.AddDays(-60);
|
|---|
| 145 |
|
|---|
| 146 | var recentSales = await _context.SaleItems
|
|---|
| 147 | .Include(si => si.Sale)
|
|---|
| 148 | .Where(si => si.Sale.DateTime >= sixtyDaysAgo)
|
|---|
| 149 | .ToListAsync();
|
|---|
| 150 |
|
|---|
| 151 | var stocks = await _context.WarehouseStocks.ToListAsync();
|
|---|
| 152 | var products = await _context.Products.Where(p => p.IsActive).ToListAsync();
|
|---|
| 153 |
|
|---|
| 154 | var report = new List<StockSufficiencyViewModel>();
|
|---|
| 155 |
|
|---|
| 156 | foreach (var p in products)
|
|---|
| 157 | {
|
|---|
| 158 | var sold = recentSales.Where(x => x.ProductId == p.ProductId).Sum(x => x.Quantity);
|
|---|
| 159 | var currentStock = stocks.Where(x => x.ProductId == p.ProductId).Sum(x => x.QuantityOnHand);
|
|---|
| 160 |
|
|---|
| 161 | double avgDaily = (double)sold / 60.0;
|
|---|
| 162 | double projected30 = avgDaily * 30.0;
|
|---|
| 163 |
|
|---|
| 164 | report.Add(new StockSufficiencyViewModel
|
|---|
| 165 | {
|
|---|
| 166 | ProductName = p.Name,
|
|---|
| 167 | SoldLast60Days = sold,
|
|---|
| 168 | AvgDailySales = Math.Round(avgDaily, 2),
|
|---|
| 169 | ProjectedNext30Days = Math.Round(projected30, 2),
|
|---|
| 170 | CurrentTotalStock = currentStock,
|
|---|
| 171 | StockStatus = currentStock >= projected30 ? "SUFFICIENT" : "INSUFFICIENT"
|
|---|
| 172 | });
|
|---|
| 173 | }
|
|---|
| 174 |
|
|---|
| 175 | return report.OrderBy(r => r.StockStatus).ThenByDescending(r => r.ProjectedNext30Days).ToList();
|
|---|
| 176 | }
|
|---|
| 177 |
|
|---|
| 178 | public async Task<List<AnnualSalesReportViewModel>> GetAnnualSalesReportAsync()
|
|---|
| 179 | {
|
|---|
| 180 | var twelveMonthsAgo = DateTime.Now.AddMonths(-11).Date;
|
|---|
| 181 |
|
|---|
| 182 | var rawData = await _context.Sales
|
|---|
| 183 | .Include(s => s.Warehouse)
|
|---|
| 184 | .Include(s => s.SaleItems)
|
|---|
| 185 | .ThenInclude(si => si.Product)
|
|---|
| 186 | .ThenInclude(p => p.Category)
|
|---|
| 187 | .Include(s => s.SaleItems)
|
|---|
| 188 | .ThenInclude(si => si.Product)
|
|---|
| 189 | .ThenInclude(p => p.Supplier)
|
|---|
| 190 | .Where(s => s.DateTime >= twelveMonthsAgo)
|
|---|
| 191 | .SelectMany(s => s.SaleItems.Select(si => new
|
|---|
| 192 | {
|
|---|
| 193 | SaleId = s.SaleId,
|
|---|
| 194 | Date = s.DateTime,
|
|---|
| 195 | WarehouseName = s.Warehouse.Name,
|
|---|
| 196 | CategoryName = si.Product.Category != null ? si.Product.Category.Name : "Uncategorized",
|
|---|
| 197 | SupplierName = si.Product.Supplier != null ? si.Product.Supplier.Name : "Unknown",
|
|---|
| 198 | Quantity = si.Quantity,
|
|---|
| 199 | Revenue = si.Quantity * si.UnitPriceAtSale
|
|---|
| 200 | }))
|
|---|
| 201 | .ToListAsync();
|
|---|
| 202 |
|
|---|
| 203 | var groupedData = rawData
|
|---|
| 204 | .GroupBy(x => new
|
|---|
| 205 | {
|
|---|
| 206 | Month = x.Date.ToString("yyyy-MM"),
|
|---|
| 207 | x.WarehouseName,
|
|---|
| 208 | x.CategoryName,
|
|---|
| 209 | x.SupplierName
|
|---|
| 210 | })
|
|---|
| 211 | .Select(g => new AnnualSalesReportViewModel
|
|---|
| 212 | {
|
|---|
| 213 | SalesMonth = g.Key.Month,
|
|---|
| 214 | WarehouseName = g.Key.WarehouseName,
|
|---|
| 215 | CategoryName = g.Key.CategoryName,
|
|---|
| 216 | SupplierName = g.Key.SupplierName,
|
|---|
| 217 | TotalOrderCount = g.Select(x => x.SaleId).Distinct().Count(),
|
|---|
| 218 | TotalUnitsSold = g.Sum(x => x.Quantity),
|
|---|
| 219 | TotalGrossRevenue = g.Sum(x => x.Revenue)
|
|---|
| 220 | })
|
|---|
| 221 | .OrderByDescending(x => x.SalesMonth)
|
|---|
| 222 | .ThenByDescending(x => x.TotalGrossRevenue)
|
|---|
| 223 | .ToList();
|
|---|
| 224 |
|
|---|
| 225 | return groupedData;
|
|---|
| 226 | }
|
|---|
| 227 |
|
|---|
| 228 | public async Task<List<DetailedPOViewModel>> GetDetailedPOReportAsync()
|
|---|
| 229 | {
|
|---|
| 230 | var query = await _context.PurchaseOrders
|
|---|
| 231 | .Include(po => po.PurchaseOrderItems)
|
|---|
| 232 | .ThenInclude(poi => poi.Product)
|
|---|
| 233 | .Include(po => po.Supplier)
|
|---|
| 234 | .Include(po => po.Warehouse)
|
|---|
| 235 | .ToListAsync();
|
|---|
| 236 |
|
|---|
| 237 | var result = query.SelectMany(po => po.PurchaseOrderItems.Select(poi => new DetailedPOViewModel
|
|---|
| 238 | {
|
|---|
| 239 | PoId = po.PoId,
|
|---|
| 240 | Status = po.Status,
|
|---|
| 241 | OrderDate = po.OrderDate,
|
|---|
| 242 | ExpectedDeliveryDate = po.ExpectedDeliveryDate,
|
|---|
| 243 | WarehouseName = po.Warehouse.Name,
|
|---|
| 244 | SupplierName = po.Supplier != null ? po.Supplier.Name : "Unknown",
|
|---|
| 245 | ProductName = poi.Product.Name,
|
|---|
| 246 | OrderedQty = poi.Quantity,
|
|---|
| 247 | ReceivedQty = poi.ReceivedQuantity,
|
|---|
| 248 | RemainingToReceive = poi.Quantity - poi.ReceivedQuantity
|
|---|
| 249 | }))
|
|---|
| 250 | .OrderBy(x => x.Status)
|
|---|
| 251 | .ThenBy(x => x.ExpectedDeliveryDate)
|
|---|
| 252 | .ToList();
|
|---|
| 253 |
|
|---|
| 254 | return result;
|
|---|
| 255 | }
|
|---|
| 256 |
|
|---|
| 257 | public async Task<List<StockMaster.Models.VwSalesByDay>> GetSalesByDayAsync()
|
|---|
| 258 | {
|
|---|
| 259 | return await _context.VwSalesByDays.ToListAsync();
|
|---|
| 260 | }
|
|---|
| 261 |
|
|---|
| 262 | public async Task<List<StockMaster.Models.VwEmployeeRanking>> GetEmployeeRankingsAsync()
|
|---|
| 263 | {
|
|---|
| 264 | return await _context.VwEmployeeRankings.ToListAsync();
|
|---|
| 265 | }
|
|---|
| 266 |
|
|---|
| 267 | public async Task<StockMaster.Models.VwTodaysSummary> GetTodaysSummaryAsync()
|
|---|
| 268 | {
|
|---|
| 269 | return await _context.VwTodaysSummaries.FirstOrDefaultAsync() ?? new StockMaster.Models.VwTodaysSummary();
|
|---|
| 270 | }
|
|---|
| 271 |
|
|---|
| 272 | public async Task<List<StockMaster.Models.ProductPriceLog>> GetPriceLogsAsync()
|
|---|
| 273 | {
|
|---|
| 274 | return await _context.ProductPriceLogs.OrderByDescending(l => l.ChangedAt).ToListAsync();
|
|---|
| 275 | }
|
|---|
| 276 |
|
|---|
| 277 | }
|
|---|
| 278 | } |
|---|