using CPRNIMS.Domain.Contracts.Inventory; using CPRNIMS.Infrastructure.Database; using CPRNIMS.Infrastructure.Dto.Inventory.Reports; using Microsoft.EntityFrameworkCore; namespace CPRNIMS.Domain.Services.Inventory { public class InventoryReports : IInventoryReports { private readonly NonInventoryDbContext _db; public InventoryReports(NonInventoryDbContext db) => _db = db; public async Task GetRISReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, int? departmentId = null, string? userName = "") { var endDate = dateTo.Date.AddDays(1); var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" }; bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName) && allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase); var dateToInclusive = dateTo.AddDays(1); var query = _db.RIS .Include(r => r.Discipline) .Include(r => r.Inventory) .Include(r => r.MaterialReturns) .Where(r => r.CreatedDate >= dateFrom && r.CreatedDate < dateToInclusive); if (departmentId.HasValue && !seeAllDepartments) { query = query.Where(m => m.Inventory.User != null && m.Inventory.User.DepartmentId == departmentId.Value); } var rows = await query .OrderByDescending(r => r.CreatedDate) .Select(r => new RISReportRow { RISNo = r.RISNo, CreatedDate = r.CreatedDate, ItemName = r.PRDetail != null ? r.PRDetail.ItemName : "—", ItemNo = r.Inventory.ItemNo, DisciplineName = r.Discipline.DisciplineName, IssuedTo = r.IssuedTo, QtyIssued = r.QtyIssued, TotalReturned = r.MaterialReturns .Where(m => m.Status != 2) .Sum(m => (int?)m.QtyReturned) ?? 0, Status = r.Status, StatusLabel = r.Status == 0 ? "Draft" : r.Status == 1 ? "Approved" : "Cancelled" }) .ToListAsync(ct); foreach (var row in rows) row.NetIssued = row.QtyIssued - row.TotalReturned; var summary = new RISReportSummary { TotalRIS = rows.Count, TotalApproved = rows.Count(r => r.Status == 1), TotalPending = rows.Count(r => r.Status == 0), TotalCancelled = rows.Count(r => r.Status == 2), TotalQtyIssued = rows.Sum(r => r.QtyIssued), TotalQtyReturned = rows.Sum(r => r.TotalReturned), TotalNetIssued = rows.Sum(r => r.NetIssued), ApprovalRatePct = rows.Count > 0 ? Math.Round(rows.Count(r => r.Status == 1) * 100m / rows.Count, 1) : 0 }; var byDiscipline = rows .GroupBy(r => r.DisciplineName) .Select(g => new DisciplineCount { DisciplineName = g.Key, Count = g.Count() }) .OrderByDescending(d => d.Count) .ToList(); var topRecipients = rows .GroupBy(r => r.IssuedTo) .Select(g => new TopRecipient { IssuedTo = g.Key, SlipCount = g.Count(), TotalQty = g.Sum(r => r.QtyIssued) }) .OrderByDescending(t => t.TotalQty) .Take(5) .ToList(); return new RISReportDto { ReportNo = $"RPT-RIS-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}", DateFrom = dateFrom, DateTo = dateTo, Summary = summary, Rows = rows, ByDiscipline = byDiscipline, TopRecipients = topRecipients }; } public async Task GetMRSReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, int? departmentId = null, string? userName = "") { var endDate = dateTo.Date.AddDays(1); var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25", "LHRIOCAS24" }; bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName) && allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase); var query = _db.MRS .Include(m => m.RIS) .Include(m => m.Inventory) .ThenInclude(i => i.User) .Where(m => m.CreatedDate >= dateFrom && m.CreatedDate < endDate); if (departmentId.HasValue && !seeAllDepartments) { query = query.Where(m => m.Inventory.User != null && m.Inventory.User.DepartmentId == departmentId.Value); } var rows = await query .OrderByDescending(m => m.CreatedDate) .Select(m => new MRSReportRow { MRSNo = m.MRSNo, CreatedDate = m.CreatedDate, RISNo = m.RIS.RISNo, ItemName = m.RIS.PRDetail != null ? m.RIS.PRDetail.ItemName : "—", ReturnedBy = m.ReturnedBy, QtyReturned = m.QtyReturned, Condition = m.Condition ?? "Good", Status = m.Status, StatusLabel = m.Status == 0 ? "Draft" : m.Status == 1 ? "Approved" : "Cancelled" }) .ToListAsync(ct); // Total RIS qty issued in the same period (for the comparison panel) var totalRISQty = await _db.RIS .Where(r => r.CreatedDate >= dateFrom && r.CreatedDate < endDate && r.Status != 2) .SumAsync(r => (int?)r.QtyIssued, ct) ?? 0; var totalReturned = rows.Where(r => r.Status != 2).Sum(r => r.QtyReturned); var goodCount = rows.Count(r => r.Condition == "Good"); var summary = new MRSReportSummary { TotalMRS = rows.Count, TotalQtyReturned = totalReturned, TotalQtyIssuedRIS = totalRISQty, NetQtyConsumed = totalRISQty - totalReturned, ReturnRatePct = totalRISQty > 0 ? Math.Round(totalReturned * 100m / totalRISQty, 1) : 0, GoodConditionPct = rows.Count > 0 ? Math.Round(goodCount * 100m / rows.Count, 1) : 0 }; var byCondition = rows .Where(r => r.Status != 2) .GroupBy(r => r.Condition) .Select(g => new ConditionTotal { Condition = g.Key, TotalQty = g.Sum(r => r.QtyReturned) }) .OrderByDescending(c => c.TotalQty) .ToList(); return new MRSReportDto { ReportNo = $"RPT-MRS-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}", DateFrom = dateFrom, DateTo = dateTo, Summary = summary, Rows = rows, ByCondition = byCondition }; } public async Task GetInventoryReportAsync(DateTime dateFrom, DateTime dateTo, CancellationToken ct, int? departmentId = null, string? userName = "") { var endDate = dateTo.Date.AddDays(1); var allowedAllDeptUsers = new[] { "LSKRISUR24", "LSCYNDIZ25", "LSJONTAN25" , "LHRIOCAS24" }; bool seeAllDepartments = !string.IsNullOrWhiteSpace(userName) && allowedAllDeptUsers.Contains(userName, StringComparer.OrdinalIgnoreCase); var query = _db.InventTransDetails .Where(itd => itd.IsActive && itd.InventTrans.IsActive && itd.InventTrans.Inventory.IsActive && itd.CreatedDate >= dateFrom && itd.CreatedDate < endDate); if (departmentId.HasValue && !seeAllDepartments) { query = query.Where(itd => itd.InventTrans.Inventory.User.DepartmentId == departmentId.Value); } var rawRows = await query .Select(itd => new { itd.InventTrans.Inventory.InventoryId, itd.InventTrans.Inventory.ItemNo, itd.InventTrans.Inventory.QtyIn, itd.InventTrans.Inventory.QtyOut, itd.InventTrans.Inventory.QtyOnHand, LotNo = itd.InventTrans.Inventory.Lot != null ? itd.InventTrans.Inventory.Lot.LotName : null, ItemName = itd.InventTrans.Inventory.Item.ItemCode.ItemName ?? "None", ItemCategoryName = itd.InventTrans.Inventory.Item.ItemCode.ItemCategory.ItemCategoryName ?? "None", itd.CreatedDate }) .ToListAsync(ct); // De-duplicate: one row per Inventory (latest trans detail wins for the date shown) var rows = rawRows .GroupBy(r => r.InventoryId) .Select(g => { var inv = g.OrderByDescending(x => x.CreatedDate).First(); return new InventoryReportRow { ItemName = inv.ItemName, ItemNo = inv.ItemNo, ItemCategoryName = inv.ItemCategoryName, LotNo = inv.LotNo, QtyIn = inv.QtyIn, QtyOut = inv.QtyOut, QtyOnHand = inv.QtyOnHand, StockPct = inv.QtyIn > 0 ? (int)Math.Round(Math.Max(0, Math.Min(100, (inv.QtyOnHand / inv.QtyIn) * 100))) : 0 }; }) .OrderBy(r => r.ItemName) .ToList(); var summary = new InventoryReportSummary { TotalSKUs = rows.Count, TotalOnHand = rows.Sum(r => r.QtyOnHand), TotalQtyIn = rows.Sum(r => r.QtyIn), TotalQtyOut = rows.Sum(r => r.QtyOut), LowStockCount = rows.Count(r => r.StockPct < 20 && r.QtyOnHand > 0), OutOfStockCount = rows.Count(r => r.QtyOnHand <= 0) }; var byCategory = rows .GroupBy(r => r.ItemCategoryName) .Select(g => new CategoryStockLevel { CategoryName = g.Key, AvgStockPct = (int)Math.Round(g.Average(r => r.StockPct)) }) .OrderByDescending(c => c.AvgStockPct) .ToList(); var alerts = rows .Where(r => r.StockPct < 20) .OrderBy(r => r.StockPct) .Take(10) .Select(r => new InventoryAlert { ItemName = r.ItemName, QtyOnHand = r.QtyOnHand, Severity = r.QtyOnHand <= 0 ? "Critical" : "Low" }) .ToList(); return new InventoryReportDto { ReportNo = $"RPT-INV-{DateTime.Now:yyyyMM}-{Random.Shared.Next(1, 999):D3}", AsOf = dateTo.Date, // reflect the requested period end, not always today Summary = summary, Rows = rows, ByCategory = byCategory, Alerts = alerts }; } } }