using System.Globalization; using System.Text.Json; using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Helpers; using FoodLabeling.Application.Contracts.Dtos.Reports; using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; using FoodLabeling.Application.Contracts.IServices; using FoodLabeling.Application.Services.DbModels; using FoodLabeling.Domain.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using SqlSugar; using Volo.Abp; using Volo.Abp.Application.Services; using Yi.Framework.Rbac.Domain.Entities; using Yi.Framework.SqlSugarCore.Abstractions; namespace FoodLabeling.Application.Services; /// /// Reports(Print Log / Label Report) /// [Authorize] public class ReportsAppService : ApplicationService, IReportsAppService { private const int ExportPdfMaxRows = 5000; private readonly ISqlSugarDbContext _dbContext; private readonly IUsAppLabelingAppService _usAppLabelingAppService; public ReportsAppService(ISqlSugarDbContext dbContext, IUsAppLabelingAppService usAppLabelingAppService) { _dbContext = dbContext; _usAppLabelingAppService = usAppLabelingAppService; } /// public async Task> GetPrintLogListAsync( ReportsPrintLogGetListInputVo input) { if (input is null) { throw new UserFriendlyException("入参不能为空"); } if (!CurrentUser.Id.HasValue) { throw new UserFriendlyException("用户未登录"); } var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); if (locationIds is not null && locationIds.Count == 0) { return EmptyPrintLogPage(input); } var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); var currentUserIdStr = CurrentUser.Id.Value.ToString(); var keyword = input.Keyword?.Trim(); RefAsync total = 0; var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) .LeftJoin((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) .Where((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < rangeEndExcl); if (!string.IsNullOrWhiteSpace(input.Sorting) && input.Sorting.Trim().Equals("PrintedAt asc", StringComparison.OrdinalIgnoreCase)) { query = query.OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Asc); } else { query = query.OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc); } var pageRows = await query .Select((t, l, p, lc, pc, loc, tpl) => new { t.Id, LabelCode = l.LabelCode, ProductName = p.ProductName, LabelCategoryName = lc.CategoryName, ProductCategoryName = pc.CategoryName, tpl.Width, tpl.Height, tpl.Unit, tpl.TemplateName, t.PrintInputJson, PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), t.CreatedBy, t.LocationId, LocName = loc.LocationName, LocCode = loc.LocationCode }) .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); var userMap = await LoadUserNameMapAsync(pageRows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x!).Distinct().ToList()); var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync( _dbContext.SqlSugarClient, pageRows.Select(x => new ReportsPrintLogDailyLabelIdHelper.PrintTaskScopeKey( x.Id, x.LocationId, x.PrintedAt ?? DateTime.MinValue)).ToList()); var items = pageRows.Select(x => { var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) ? x.ProductCategoryName!.Trim() : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim()); var templateText = FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName); var locText = FormatLocationText(x.LocName, x.LocCode); var printedAt = x.PrintedAt ?? DateTime.MinValue; var labelDisplayId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无"; return new ReportsPrintLogListItemDto { TaskId = x.Id, LabelCode = labelDisplayId, ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), ProductCategoryName = string.IsNullOrWhiteSpace(x.ProductCategoryName) ? "无" : x.ProductCategoryName!.Trim(), LabelCategoryName = string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName!.Trim(), CategoryName = string.IsNullOrWhiteSpace(cat) ? "无" : cat, TemplateText = string.IsNullOrWhiteSpace(templateText) ? "无" : templateText, PrintedAt = printedAt, PrintedByName = ResolveUserName(userMap, x.CreatedBy), LocationText = locText, LocationId = x.LocationId?.Trim(), ExpiryDateText = ReportsPrintLogExpiryHelper.ExtractExpiryText(x.PrintInputJson) }; }).ToList(); return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); } /// public async Task ExportPrintLogPdfAsync(ReportsPrintLogGetListInputVo input) { QuestPDF.Settings.License = LicenseType.Community; if (input is null) { throw new UserFriendlyException("入参不能为空"); } if (!CurrentUser.Id.HasValue) { throw new UserFriendlyException("用户未登录"); } var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); if (locationIds is not null && locationIds.Count == 0) { return BuildEmptyPdf("print-log-empty.pdf"); } var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); var currentUserIdStr = CurrentUser.Id.Value.ToString(); var keyword = input.Keyword?.Trim(); var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) .LeftJoin((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) .Where((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < rangeEndExcl) .OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc); var count = await query.CountAsync(); if (count > ExportPdfMaxRows) { throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); } var rows = await query.Take(ExportPdfMaxRows) .Select((t, l, p, lc, pc, loc, tpl) => new { t.Id, LabelCode = l.LabelCode, ProductName = p.ProductName, LabelCategoryName = lc.CategoryName, ProductCategoryName = pc.CategoryName, tpl.Width, tpl.Height, tpl.Unit, tpl.TemplateName, t.PrintInputJson, PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), t.CreatedBy, t.LocationId, LocName = loc.LocationName, LocCode = loc.LocationCode }) .ToListAsync(); var userMap = await LoadUserNameMapAsync(rows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x!).Distinct().ToList()); var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync( _dbContext.SqlSugarClient, rows.Select(x => new ReportsPrintLogDailyLabelIdHelper.PrintTaskScopeKey( x.Id, x.LocationId, x.PrintedAt ?? DateTime.MinValue)).ToList()); var fileName = $"print-log_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; var document = Document.Create(container => { container.Page(page => { page.Margin(22); page.DefaultTextStyle(x => x.FontSize(8.5f)); page.Header().Text("Print Log").SemiBold().FontSize(16); page.Content().PaddingTop(10).Table(table => { table.ColumnsDefinition(c => { c.RelativeColumn(1.1f); c.RelativeColumn(1.2f); c.RelativeColumn(0.9f); c.RelativeColumn(1.1f); c.RelativeColumn(1f); c.RelativeColumn(0.9f); c.RelativeColumn(0.9f); c.RelativeColumn(0.8f); }); static IContainer H(IContainer x) => x.Background(Colors.Grey.Lighten3).Padding(4).DefaultTextStyle(s => s.SemiBold()); table.Cell().Element(H).Text("Label ID"); table.Cell().Element(H).Text("Product"); table.Cell().Element(H).Text("Category"); table.Cell().Element(H).Text("Template"); table.Cell().Element(H).Text("Printed At"); table.Cell().Element(H).Text("Printed By"); table.Cell().Element(H).Text("Location"); table.Cell().Element(H).Text("Expiry"); foreach (var x in rows) { var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) ? x.ProductCategoryName!.Trim() : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim()); var labelDisplayId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无"; table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) .Text(labelDisplayId); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) .Text(string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim()); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3).Text(cat); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) .Text(FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName)); var printedAt = x.PrintedAt ?? DateTime.MinValue; table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) .Text(printedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture)); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) .Text(ResolveUserName(userMap, x.CreatedBy)); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) .Text(FormatLocationText(x.LocName, x.LocCode)); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) .Text(ReportsPrintLogExpiryHelper.ExtractExpiryText(x.PrintInputJson)); } }); }); }); var ms = new MemoryStream(); document.GeneratePdf(ms); ms.Position = 0; return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName }; } /// public async Task ExportPrintLogExcelAsync([FromQuery] ReportsPrintLogGetListInputVo input) { if (input is null) { throw new UserFriendlyException("入参不能为空"); } if (!CurrentUser.Id.HasValue) { throw new UserFriendlyException("用户未登录"); } var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); if (locationIds is not null && locationIds.Count == 0) { var emptyMs = ReportsPrintLogExcelHelper.BuildWorkbook(Array.Empty()); var emptyName = $"print-log_{Clock.Now:yyyyMMdd-HHmmss}.xlsx"; return new FileStreamResult(emptyMs, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = emptyName }; } var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); var currentUserIdStr = CurrentUser.Id.Value.ToString(); var keyword = input.Keyword?.Trim(); var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) .LeftJoin((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) .Where((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < rangeEndExcl); if (!string.IsNullOrWhiteSpace(input.Sorting) && input.Sorting.Trim().Equals("PrintedAt asc", StringComparison.OrdinalIgnoreCase)) { query = query.OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Asc); } else { query = query.OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc); } var count = await query.CountAsync(); if (count > ExportPdfMaxRows) { throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); } var pageRows = await query.Take(ExportPdfMaxRows) .Select((t, l, p, lc, pc, loc, tpl) => new PrintLogExportRow { Id = t.Id, LabelCode = l.LabelCode, ProductName = p.ProductName, LabelCategoryName = lc.CategoryName, ProductCategoryName = pc.CategoryName, Width = tpl.Width, Height = tpl.Height, Unit = tpl.Unit, TemplateName = tpl.TemplateName, PrintInputJson = t.PrintInputJson, PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), CreatedBy = t.CreatedBy, LocationId = t.LocationId, LocName = loc.LocationName, LocCode = loc.LocationCode }) .ToListAsync(); var userMap = await LoadUserNameMapAsync(pageRows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x!).Distinct().ToList()); var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync( _dbContext.SqlSugarClient, pageRows.Select(x => new ReportsPrintLogDailyLabelIdHelper.PrintTaskScopeKey( x.Id, x.LocationId, x.PrintedAt ?? DateTime.MinValue)).ToList()); var items = pageRows.Select(x => MapPrintLogExportRowToListItem(x, userMap, dailyLabelIdMap)).ToList(); var ms = ReportsPrintLogExcelHelper.BuildWorkbook(items); var fileName = $"print-log_{Clock.Now:yyyyMMdd-HHmmss}.xlsx"; return new FileStreamResult(ms, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = fileName }; } /// public Task ReprintPrintLogAsync(UsAppLabelReprintInputVo input) => _usAppLabelingAppService.ReprintAsync(input); /// public async Task> GetTemplatePrintStatListAsync( ReportsTemplatePrintStatGetListInputVo input) { if (input is null) { throw new UserFriendlyException("入参不能为空"); } if (!CurrentUser.Id.HasValue) { throw new UserFriendlyException("用户未登录"); } var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync( CurrentUser, _dbContext.SqlSugarClient, input.PartnerId, input.GroupId, input.LocationId); if (locationIds is not null && locationIds.Count == 0) { return EmptyTemplatePrintStatPage(input); } var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); var currentUserIdStr = CurrentUser.Id.Value.ToString(); var templateKeyword = input.Keyword?.Trim(); var groupedRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword: null, restrictToCreator: false) .LeftJoin((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) .Where((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < rangeEndExcl) .WhereIF(!string.IsNullOrWhiteSpace(templateKeyword), (t, l, p, lc, pc, loc, tpl) => tpl.TemplateName != null && tpl.TemplateName.Contains(templateKeyword!)) .GroupBy((t, l, p, lc, pc, loc, tpl) => new { t.TemplateId, tpl.TemplateName }) .Select((t, l, p, lc, pc, loc, tpl) => new { t.TemplateId, tpl.TemplateName, Cnt = SqlFunc.AggregateCount(t.Id) }) .ToListAsync(); var ordered = groupedRows .Select(x => new ReportsTemplatePrintStatListItemDto { TemplateId = string.IsNullOrWhiteSpace(x.TemplateId) ? null : x.TemplateId.Trim(), TemplateName = string.IsNullOrWhiteSpace(x.TemplateName) ? "无" : x.TemplateName.Trim(), PrintedCount = x.Cnt }) .ToList(); if (!string.IsNullOrWhiteSpace(input.Sorting) && input.Sorting.Trim().Equals("PrintedCount asc", StringComparison.OrdinalIgnoreCase)) { ordered = ordered.OrderBy(x => x.PrintedCount).ThenBy(x => x.TemplateName).ToList(); } else { ordered = ordered.OrderByDescending(x => x.PrintedCount).ThenBy(x => x.TemplateName).ToList(); } var total = ordered.Count; var pageIndex = PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); var pageSize = input.MaxResultCount <= 0 ? total : input.MaxResultCount; var offset = pageSize <= 0 ? 0 : (pageIndex - 1) * pageSize; var pageItems = pageSize <= 0 ? ordered : ordered.Skip(offset).Take(pageSize).ToList(); return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, pageItems); } /// public async Task GetLabelReportAsync(ReportsLabelReportQueryInputVo input) { if (input is null) { throw new UserFriendlyException("入参不能为空"); } if (!CurrentUser.Id.HasValue) { throw new UserFriendlyException("用户未登录"); } var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync( CurrentUser, _dbContext.SqlSugarClient, input.PartnerId, input.GroupId, input.LocationId); if (locationIds is not null && locationIds.Count == 0) { return new ReportsLabelReportOutputDto(); } var (curStart, curEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); var span = curEndExcl - curStart; if (span.TotalDays < 1) { span = TimeSpan.FromDays(1); } var prevEndExcl = curStart; var prevStart = curStart - span; var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); var currentUserIdStr = CurrentUser.Id.Value.ToString(); var keyword = input.Keyword?.Trim(); var totalCur = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) .CountAsync(); var totalPrev = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl) .CountAsync(); var dayCount = Math.Max(1, (int)Math.Ceiling((curEndExcl - curStart).TotalDays)); var prevDayCount = Math.Max(1, (int)Math.Ceiling((prevEndExcl - prevStart).TotalDays)); var avgDaily = Math.Round((decimal)totalCur / dayCount, 2); var avgDailyPrev = Math.Round((decimal)totalPrev / prevDayCount, 2); var categoryRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => l.LabelCategoryId != null && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) .GroupBy((t, l, p, lc, pc, loc) => new { lc.Id, lc.CategoryName }) .Select((t, l, p, lc, pc, loc) => new { lc.Id, lc.CategoryName, Cnt = SqlFunc.AggregateCount(t.Id) }) .ToListAsync(); var topCat = categoryRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); var productRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => !string.IsNullOrEmpty(p.Id) && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) .GroupBy((t, l, p, lc, pc, loc) => new { p.Id, p.ProductName, Cat = pc.CategoryName }) .Select((t, l, p, lc, pc, loc) => new { p.Id, p.ProductName, CategoryName = pc.CategoryName, Cnt = SqlFunc.AggregateCount(t.Id) }) .ToListAsync(); var topProd = productRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); var topList = productRows.OrderByDescending(x => x.Cnt).Take(20).ToList(); var trendEndDay = curEndExcl.Date.AddDays(-1); var trendStartDay = trendEndDay.AddDays(-6); if (trendStartDay < curStart.Date) { trendStartDay = curStart.Date; } var trendEndExcl = trendEndDay.AddDays(1); var trendRaw = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl) .Select((t, l, p, lc, pc, loc) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime)) .ToListAsync(); var trendDict = trendRaw .Where(x => x.HasValue) .GroupBy(x => x!.Value.Date) .ToDictionary(g => g.Key, g => g.Count()); var trend = new List(); for (var d = trendStartDay; d <= trendEndDay; d = d.AddDays(1)) { trend.Add(new ReportsDailyCountDto { Date = d.ToString("yyyy-MM-dd"), Count = trendDict.TryGetValue(d, out var c) ? c : 0 }); } var byCategory = categoryRows .OrderByDescending(x => x.Cnt) .Select(x => new ReportsCategoryCountDto { CategoryId = string.IsNullOrWhiteSpace(x.Id) ? null : x.Id.Trim(), CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? null : x.CategoryName.Trim(), Count = x.Cnt }) .ToList(); var mostUsed = topList.Select(x => { var pct = totalCur <= 0 ? 0m : Math.Round(x.Cnt * 100m / totalCur, 2); return new ReportsTopProductRowDto { ProductId = string.IsNullOrWhiteSpace(x.Id) ? null : x.Id.Trim(), ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? null : x.ProductName.Trim(), CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? null : x.CategoryName!.Trim(), TotalPrinted = x.Cnt, UsagePercent = pct }; }).ToList(); return new ReportsLabelReportOutputDto { Summary = new ReportsLabelReportSummaryDto { TotalLabelsPrinted = totalCur, TotalLabelsPrintedPrevPeriod = totalPrev, TotalLabelsPrintedChangeRate = CalcChangeRate(totalCur, totalPrev), MostPrintedCategoryName = string.IsNullOrWhiteSpace(topCat?.CategoryName) ? null : topCat.CategoryName.Trim(), MostPrintedCategoryCount = topCat?.Cnt ?? 0, TopProductName = string.IsNullOrWhiteSpace(topProd?.ProductName) ? null : topProd.ProductName.Trim(), TopProductCount = topProd?.Cnt ?? 0, AvgDailyPrints = avgDaily, AvgDailyPrintsPrevPeriod = avgDailyPrev, AvgDailyPrintsChangeRate = CalcChangeRate(avgDaily, avgDailyPrev) }, LabelsByCategory = byCategory, PrintVolumeTrend = trend, MostUsedProducts = mostUsed }; } /// public async Task ExportLabelReportPdfAsync(ReportsLabelReportQueryInputVo input) { QuestPDF.Settings.License = LicenseType.Community; var data = await GetLabelReportAsync(input); var fileName = $"label-report_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; var document = Document.Create(container => { container.Page(page => { page.Margin(24); page.DefaultTextStyle(x => x.FontSize(9)); page.Header().Text("Label Report").SemiBold().FontSize(16); page.Content().Column(col => { col.Spacing(10); col.Item().Text( $"Total printed: {data.Summary.TotalLabelsPrinted} (prev: {data.Summary.TotalLabelsPrintedPrevPeriod}, Δ%: {data.Summary.TotalLabelsPrintedChangeRate:0.##}%)"); col.Item().Text( $"Top category: {data.Summary.MostPrintedCategoryName} ({data.Summary.MostPrintedCategoryCount})"); col.Item().Text($"Top product: {data.Summary.TopProductName} ({data.Summary.TopProductCount})"); col.Item().Text( $"Avg daily: {data.Summary.AvgDailyPrints:0.##} (prev: {data.Summary.AvgDailyPrintsPrevPeriod:0.##}, Δ%: {data.Summary.AvgDailyPrintsChangeRate:0.##}%)"); col.Item().Text("By category:").SemiBold(); col.Item().Table(t => { t.ColumnsDefinition(c => { c.RelativeColumn(2); c.RelativeColumn(1); }); t.Cell().Element(HeaderCell).Text("Category"); t.Cell().Element(HeaderCell).Text("Count"); foreach (var r in data.LabelsByCategory) { t.Cell().BorderBottom(0.5f).Padding(3).Text(r.CategoryName); t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Count.ToString()); } }); col.Item().Text("Daily trend:").SemiBold(); col.Item().Table(t => { t.ColumnsDefinition(c => { c.RelativeColumn(1.2f); c.RelativeColumn(1); }); t.Cell().Element(HeaderCell).Text("Date"); t.Cell().Element(HeaderCell).Text("Count"); foreach (var r in data.PrintVolumeTrend) { t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Date); t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Count.ToString()); } }); col.Item().Text("Most used products:").SemiBold(); col.Item().Table(t => { t.ColumnsDefinition(c => { c.RelativeColumn(1.5f); c.RelativeColumn(1f); c.RelativeColumn(0.8f); c.RelativeColumn(0.7f); }); t.Cell().Element(HeaderCell).Text("Product"); t.Cell().Element(HeaderCell).Text("Category"); t.Cell().Element(HeaderCell).Text("Total"); t.Cell().Element(HeaderCell).Text("%"); foreach (var r in data.MostUsedProducts) { t.Cell().BorderBottom(0.5f).Padding(3).Text(r.ProductName); t.Cell().BorderBottom(0.5f).Padding(3).Text(r.CategoryName); t.Cell().BorderBottom(0.5f).Padding(3).Text(r.TotalPrinted.ToString()); t.Cell().BorderBottom(0.5f).Padding(3).Text(r.UsagePercent.ToString("0.##")); } }); }); }); }); static IContainer HeaderCell(IContainer x) => x.Background(Colors.Grey.Lighten3).Padding(4).DefaultTextStyle(s => s.SemiBold()); var ms = new MemoryStream(); document.GeneratePdf(ms); ms.Position = 0; return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName }; } private ISugarQueryable BuildReportTaskCore( List? locationIds, bool isAdmin, string currentUserIdStr, string? keyword, bool restrictToCreator = true) { return _dbContext.SqlSugarClient.Queryable() .LeftJoin((t, l) => t.LabelId == l.Id) .LeftJoin((t, l, p) => t.ProductId == p.Id) .LeftJoin((t, l, p, lc) => l.LabelCategoryId == lc.Id) .LeftJoin((t, l, p, lc, pc) => p.CategoryId == pc.Id) .LeftJoin((t, l, p, lc, pc, loc) => t.LocationId != null && SqlFunc.ToString(loc.Id) == t.LocationId) .Where((t, l, p, lc, pc, loc) => !loc.IsDeleted) .WhereIF(restrictToCreator && !isAdmin, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr) .WhereIF(locationIds is not null, (t, l, p, lc, pc, loc) => locationIds!.Contains(t.LocationId!)) .WhereIF(!string.IsNullOrWhiteSpace(keyword), (t, l, p, lc, pc, loc) => (p.ProductName != null && p.ProductName.Contains(keyword!)) || (lc.CategoryName != null && lc.CategoryName.Contains(keyword!)) || (pc.CategoryName != null && pc.CategoryName.Contains(keyword!))); } private static decimal CalcChangeRate(decimal current, decimal previous) { if (previous == 0) { return current > 0 ? 100m : 0m; } return Math.Round((current - previous) * 100m / previous, 2); } private static decimal CalcChangeRate(int current, int previous) => CalcChangeRate((decimal)current, (decimal)previous); private async Task?> ResolveFilteredLocationIdsAsync(string? partnerId, string? groupId, string? locationId) { var locId = locationId?.Trim(); if (!string.IsNullOrWhiteSpace(locId)) { return new List { locId }; } var gid = groupId?.Trim(); var pid = partnerId?.Trim(); if (string.IsNullOrWhiteSpace(pid) && string.IsNullOrWhiteSpace(gid)) { return null; } var q = _dbContext.SqlSugarClient.Queryable().Where(x => !x.IsDeleted); if (!string.IsNullOrWhiteSpace(gid)) { var g = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == gid); if (g is null) { return new List(); } var gName = g.GroupName?.Trim() ?? string.Empty; var partner = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == g.PartnerId); var pName = partner?.PartnerName?.Trim() ?? string.Empty; q = q.Where(x => x.GroupName == gName && x.Partner == pName); } else if (!string.IsNullOrWhiteSpace(pid)) { var partner = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == pid); if (partner is null) { return new List(); } var pName = partner.PartnerName?.Trim() ?? string.Empty; q = q.Where(x => x.Partner == pName); } var ids = await q.Select(x => SqlFunc.ToString(x.Id)).ToListAsync(); return ids; } private static (DateTime rangeStart, DateTime rangeEndExcl) ResolveDateRange(DateTime? startDate, DateTime? endDate) { var endDay = (endDate ?? DateTime.Today).Date; var endExcl = endDay.AddDays(1); var start = (startDate ?? endDay.AddDays(-29)).Date; if (start >= endExcl) { start = endExcl.AddDays(-1); } return (start, endExcl); } private static PagedResultWithPageDto EmptyPrintLogPage( ReportsPrintLogGetListInputVo input) { var pageSize = input.MaxResultCount <= 0 ? 0 : input.MaxResultCount; var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); return new PagedResultWithPageDto { PageIndex = pageIndex, PageSize = pageSize, TotalCount = 0, TotalPages = 0, Items = new List() }; } private static PagedResultWithPageDto EmptyTemplatePrintStatPage( ReportsTemplatePrintStatGetListInputVo input) { var pageSize = input.MaxResultCount <= 0 ? 0 : input.MaxResultCount; var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); return new PagedResultWithPageDto { PageIndex = pageIndex, PageSize = pageSize, TotalCount = 0, TotalPages = 0, Items = new List() }; } private static PagedResultWithPageDto BuildPagedResult(int skipCount, int maxResultCount, int total, List items) { var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount); var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize); return new PagedResultWithPageDto { PageIndex = pageIndex, PageSize = pageSize, TotalCount = total, TotalPages = totalPages, Items = items }; } private async Task> LoadUserNameMapAsync(List userIdStrings) { var map = new Dictionary(StringComparer.OrdinalIgnoreCase); if (userIdStrings.Count == 0) { return map; } var guids = userIdStrings .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null) .Where(x => x.HasValue) .Select(x => x!.Value) .Distinct() .ToList(); if (guids.Count == 0) { return map; } var users = await _dbContext.SqlSugarClient.Queryable() .Where(u => !u.IsDeleted && guids.Contains(u.Id)) .Select(u => new { u.Id, u.Name, u.UserName }) .ToListAsync(); foreach (var u in users) { var display = !string.IsNullOrWhiteSpace(u.Name) ? u.Name.Trim() : u.UserName.Trim(); map[u.Id.ToString()] = string.IsNullOrWhiteSpace(display) ? "无" : display; } return map; } private static string ResolveUserName(Dictionary map, string? createdBy) { if (string.IsNullOrWhiteSpace(createdBy)) { return "无"; } return map.TryGetValue(createdBy.Trim(), out var n) ? n : "无"; } private static string FormatLocationText(string? locName, string? locCode) { var n = locName?.Trim(); var c = locCode?.Trim(); if (string.IsNullOrWhiteSpace(n) && string.IsNullOrWhiteSpace(c)) { return "无"; } if (string.IsNullOrWhiteSpace(c)) { return n ?? "无"; } if (string.IsNullOrWhiteSpace(n)) { return $"({c})"; } return $"{n} ({c})"; } private static string FormatTemplateDisplay(decimal w, decimal h, string? unit, string? templateName) { var size = FormatLabelSizeWithUnit(w, h, unit ?? "inch"); var tn = templateName?.Trim(); if (string.IsNullOrWhiteSpace(tn)) { return size ?? "无"; } return string.IsNullOrWhiteSpace(size) ? tn : $"{size} {tn}"; } private static string? FormatLabelSizeWithUnit(decimal w, decimal h, string unit) { var u = (unit ?? "inch").Trim().ToLowerInvariant(); var ws = w.ToString(CultureInfo.InvariantCulture); var hs = h.ToString(CultureInfo.InvariantCulture); var normalizedUnit = u is "in" ? "inch" : u; return $"{ws}x{hs}{normalizedUnit}"; } private static IActionResult BuildEmptyPdf(string fileName) { QuestPDF.Settings.License = LicenseType.Community; var document = Document.Create(c => { c.Page(p => { p.Margin(30); p.Content().Text("No data for current filters."); }); }); var ms = new MemoryStream(); document.GeneratePdf(ms); ms.Position = 0; return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName }; } private sealed class PrintLogExportRow { public string Id { get; set; } = string.Empty; public string? LabelCode { get; set; } public string? ProductName { get; set; } public string? LabelCategoryName { get; set; } public string? ProductCategoryName { get; set; } public decimal Width { get; set; } public decimal Height { get; set; } public string? Unit { get; set; } public string? TemplateName { get; set; } public string? PrintInputJson { get; set; } public DateTime? PrintedAt { get; set; } public string? CreatedBy { get; set; } public string? LocationId { get; set; } public string? LocName { get; set; } public string? LocCode { get; set; } } private static ReportsPrintLogListItemDto MapPrintLogExportRowToListItem( PrintLogExportRow x, Dictionary userMap, IReadOnlyDictionary dailyLabelIdMap) { var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) ? x.ProductCategoryName!.Trim() : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim()); var templateText = FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName); var locText = FormatLocationText(x.LocName, x.LocCode); var printedAt = x.PrintedAt ?? DateTime.MinValue; var labelDisplayId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无"; return new ReportsPrintLogListItemDto { TaskId = x.Id, LabelCode = labelDisplayId, ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), ProductCategoryName = string.IsNullOrWhiteSpace(x.ProductCategoryName) ? "无" : x.ProductCategoryName!.Trim(), LabelCategoryName = string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName!.Trim(), CategoryName = string.IsNullOrWhiteSpace(cat) ? "无" : cat, TemplateText = string.IsNullOrWhiteSpace(templateText) ? "无" : templateText, PrintedAt = printedAt, PrintedByName = ResolveUserName(userMap, x.CreatedBy), LocationText = locText, LocationId = x.LocationId?.Trim(), ExpiryDateText = ReportsPrintLogExpiryHelper.ExtractExpiryText(x.PrintInputJson) }; } }