using Microsoft.EntityFrameworkCore; using MigraDocCore.DocumentObjectModel; using MigraDocCore.DocumentObjectModel.Tables; using MigraDocCore.Rendering; using PdfSharpCore.Drawing; using PdfSharpCore.Pdf; using Server.Data; using Server.Models; namespace Server.Services; public class ReportService : IReportService { private readonly ApplicationDbContext _dbContext; public ReportService(ApplicationDbContext dbContext) { _dbContext = dbContext; } public async Task<(bool IsSucceed, string? message, Stream ticketPdf)> GetTicket(int ticketGroupId) { var dbTicketGroup = await _dbContext.TicketGroups .Include(tg => tg.User) .Include(tg => tg.Tickets) .ThenInclude(t => t.VehicleEnrollment) .ThenInclude(ve => ve.Vehicle) .ThenInclude(v => v.Company) .Include(tg => tg.User) .Include(tg => tg.Tickets) .ThenInclude(t => t.VehicleEnrollment) .ThenInclude(ve => ve.Route) .ThenInclude(r => r.RouteAddresses) .ThenInclude(ra => ra.Address) .ThenInclude(a => a.City) .ThenInclude(c => c.State) .ThenInclude(s => s.Country) .Include(tg => tg.User) .Include(tg => tg.Tickets) .ThenInclude(t => t.VehicleEnrollment) .ThenInclude(ve => ve.RouteAddressDetails) .FirstOrDefaultAsync(tg => tg.Id == ticketGroupId); // Define document var document = new PdfDocument(); document.Info.Title = "ticket"; document.Info.Author = "auto.bus"; // Craft document var pdfPage = document.AddPage(); pdfPage.Width = XUnit.FromCentimeter(21.0); pdfPage.Height = XUnit.FromCentimeter(29.7); var gfx = XGraphics.FromPdfPage(pdfPage); // HACK² gfx.MUH = PdfFontEncoding.Unicode; var doc = new Document(); doc.DefaultPageSetup.LeftMargin = Unit.FromCentimeter(1); doc.DefaultPageSetup.RightMargin = Unit.FromCentimeter(1); doc.DefaultPageSetup.MirrorMargins = true; DefineStyles(doc); CreatePage(doc); FillPage(doc, dbTicketGroup); var docRender = new DocumentRenderer(doc); docRender.PrepareDocument(); docRender.RenderPage(gfx, 1); // Save document var memoryStream = new MemoryStream(); document.Save(memoryStream); return (true, null, memoryStream); void DefineStyles(Document doc) { var styles = doc.Styles["Normal"]; styles.Font.Name = "Courier New Cyr"; styles = doc.Styles.AddStyle("Table", "Normal"); styles.Font.Size = 10; styles.ParagraphFormat.SpaceBefore = 2.5; styles.ParagraphFormat.SpaceAfter = 2.5; } void CreatePage(Document doc) { var section = doc.AddSection(); // Create header var paragraph = section.Headers.Primary.AddParagraph(); paragraph.AddText("auto.bus"); paragraph.Style = StyleNames.Header; paragraph.Format.Font.Size = 20; paragraph.Format.Font.Bold = true; paragraph = section.Headers.Primary.AddParagraph(); paragraph.Format.Alignment = ParagraphAlignment.Center; paragraph.Format.Font.Size = 0; paragraph.Format.Borders.Top = new Border { Width = 1, Color = Colors.Black }; paragraph.Format.Borders.Bottom = new Border { Color = Colors.Transparent }; // Add title paragraph = section.AddParagraph(); paragraph.AddText("Посадочний документ"); paragraph.Format.Font.Size = 20; paragraph.Format.Font.Bold = true; // Add break line before table paragraph = section.AddParagraph(); paragraph.Format.Alignment = ParagraphAlignment.Center; paragraph.Format.Font.Size = 0; paragraph.Format.Borders.Top = new Border { Width = 1, Color = Colors.Black }; paragraph.Format.Borders.Bottom = new Border { Color = Colors.Transparent }; paragraph.Format.Borders.Style = BorderStyle.DashLargeGap; paragraph.Format.SpaceBefore = 5; paragraph.Format.SpaceAfter = 5; // Add table and define columns var table = section.AddTable(); table.Style = "Table"; table.Borders.Color = Colors.Black; table.Borders.Width = 0.25; table.Borders.Left.Width = 0.5; table.Borders.Right.Width = 0.5; table.Rows.LeftIndent = 0; table.Rows.Height = Unit.FromPoint(15); table.Rows.VerticalAlignment = VerticalAlignment.Center; for (int i = 0; i < 12; i++) { var column = table.AddColumn(Unit.FromCentimeter(1.583)); column.Format.Alignment = ParagraphAlignment.Center; } // Add break line after table paragraph = section.AddParagraph(); paragraph.Format.Alignment = ParagraphAlignment.Center; paragraph.Format.Font.Size = 0; paragraph.Format.Borders.Top = new Border { Width = 1, Color = Colors.Black }; paragraph.Format.Borders.Bottom = new Border { Color = Colors.Transparent }; paragraph.Format.Borders.Style = BorderStyle.DashLargeGap; paragraph.Format.SpaceBefore = 5; paragraph.Format.SpaceAfter = 5; } void FillPage(Document doc, TicketGroup ticketGroup) { var table = doc.LastSection.LastTable; var row = table.AddRow(); table.AddRow(); row.Cells[0].MergeRight = 2; row.Cells[0].MergeDown = 1; row.Cells[0].AddParagraph("aut.bus – м. Харків, просп. Науки, 14"); row.Cells[0].Format.Alignment = ParagraphAlignment.Left; row.Cells[3].MergeRight = 2; row.Cells[3].MergeDown = 1; row.Cells[3].AddParagraph("ПОСАДОЧНИЙ ДОКУМЕНТ"); row.Cells[3].Format.Font.Bold = true; string ticketNums = ""; foreach (var ticket in ticketGroup.Tickets) { ticketNums += $"{ticket.Id}"; if (!ticketGroup.Tickets.Last().Equals(ticket)) { ticketNums += "; "; continue; } ticketNums += "."; } row.Cells[6].MergeRight = 2; row.Cells[6].MergeDown = 1; row.Cells[6].AddParagraph($"Група: {ticketGroup.Id}.\nКвитки: {ticketNums}"); row.Cells[6].Format.Alignment = ParagraphAlignment.Left; row.Cells[9].MergeRight = 2; row.Cells[9].MergeDown = 1; row.Cells[9].AddParagraph($"{ticketGroup.Tickets.First().PurchaseDateTimeUtc:dd.MM.yyyy HH:mm:ss}"); row = table.AddRow(); row.Cells[0].MergeRight = 2; row.Cells[0].AddParagraph("Прізвище, Ім'я"); row.Cells[0].Format.Alignment = ParagraphAlignment.Left; row.Cells[3].MergeRight = 8; row.Cells[3].AddParagraph($"{ticketGroup.User.LastName} {ticketGroup.User.FirstName}"); row.Cells[3].Format.Alignment = ParagraphAlignment.Left; // Fill stations row = table.AddRow(); row.Cells[0].MergeRight = 11; row.Cells[0].AddParagraph("СТАНЦІЇ"); row.Cells[0].Format.Font.Bold = true; var isFilled = false; for (var i = 0; i < ticketGroup.Tickets.Count; i++) { var ticket = ticketGroup.Tickets[i]; var vehicle = ticket.VehicleEnrollment.Vehicle; var departureDateTimeUtc = GetDepartureTime(ticket); var arrivalDateTimeUtc = GetArrivalTime(ticket); var departureAddress = GetDepartureAddress(ticket); var arrivalAddress = GetArrivalAddress(ticket); row = table.AddRow(); table.AddRow(); row.Shading.Color = isFilled ? Color.FromRgbColor(25, Colors.Black) : Colors.White; isFilled = !isFilled; row.Cells[0].MergeRight = 1; row.Cells[0].MergeDown = 1; if (ticketGroup.Tickets.First().Equals(ticket)) { row.Cells[0].AddParagraph("Відправлення"); } else { row.Cells[0].AddParagraph("Пересадка на"); } row.Cells[2].MergeRight = 1; row.Cells[2].MergeDown = 1; row.Cells[2].AddParagraph($"{departureDateTimeUtc:dd.MM.yyyy HH:mm}"); row.Cells[4].MergeRight = 3; row.Cells[4].MergeDown = 1; row.Cells[4].AddParagraph($"{departureAddress}"); row.Cells[8].MergeRight = 1; row.Cells[8].MergeDown = 1; row.Cells[8].AddParagraph("Тип, номер автобуса"); row.Cells[10].MergeRight = 1; row.Cells[10].MergeDown = 1; row.Cells[10].AddParagraph($"{vehicle.Type}, {vehicle.Number}"); row = table.AddRow(); table.AddRow(); row.Shading.Color = isFilled ? Color.FromRgbColor(25, Colors.Black) : Colors.White; isFilled = !isFilled; row.Cells[0].MergeRight = 1; row.Cells[0].MergeDown = 1; if (!ticketGroup.Tickets.Last().Equals(ticket)) { row.Cells[0].AddParagraph("Пересадка з"); } else { row.Cells[0].AddParagraph("Прибуття"); } row.Cells[2].MergeRight = 1; row.Cells[2].MergeDown = 1; row.Cells[2].AddParagraph($"{arrivalDateTimeUtc:dd.MM.yyyy HH:mm}"); row.Cells[4].MergeRight = 3; row.Cells[4].MergeDown = 1; row.Cells[4].AddParagraph($"{arrivalAddress}"); row.Cells[8].MergeRight = 1; row.Cells[8].MergeDown = 1; row.Cells[8].AddParagraph("Тип, номер автобуса"); row.Cells[10].MergeRight = 1; row.Cells[10].MergeDown = 1; row.Cells[10].AddParagraph($"{vehicle.Type}, {vehicle.Number}"); if (!ticketGroup.Tickets.Last().Equals(ticket)) { var nextDepartureTimeUtc = GetDepartureTime(ticketGroup.Tickets[i + 1]); var freeTime = nextDepartureTimeUtc - arrivalDateTimeUtc; row = table.AddRow(); table.AddRow(); row.Shading.Color = isFilled ? Color.FromRgbColor(25, Colors.Black) : Colors.White; isFilled = !isFilled; row.Cells[0].MergeRight = 2; row.Cells[0].MergeDown = 1; row.Cells[0].AddParagraph("Вільний час"); row.Cells[3].MergeRight = 5; row.Cells[3].MergeDown = 1; row.Cells[3].AddParagraph($"{arrivalDateTimeUtc:dd.MM.yyyy HH:mm} – {nextDepartureTimeUtc:dd.MM.yyyy HH:mm}"); row.Cells[9].MergeRight = 2; row.Cells[9].MergeDown = 1; row.Cells[9].AddParagraph($"{freeTime.ToString(@"dd\.hh\:mm\:ss")}"); } } // Fill value row = table.AddRow(); row.Cells[0].MergeRight = 11; row.Cells[0].AddParagraph("ВАРТІСТЬ"); row.Cells[0].Format.Font.Bold = true; isFilled = false; foreach (var ticket in ticketGroup.Tickets) { var departureAddress = GetDepartureAddress(ticket); var arrivalAddress = GetArrivalAddress(ticket); var cost = GetTicketCost(ticket); row = table.AddRow(); table.AddRow(); row.Shading.Color = isFilled ? Color.FromRgbColor(25, Colors.Black) : Colors.White; isFilled = !isFilled; row.Cells[0].MergeDown = 1; row.Cells[0].AddParagraph("Звідки"); row.Cells[1].MergeRight = 3; row.Cells[1].MergeDown = 1; row.Cells[1].AddParagraph($"{departureAddress}"); row.Cells[5].MergeDown = 1; row.Cells[5].AddParagraph("Куди"); row.Cells[6].MergeRight = 3; row.Cells[6].MergeDown = 1; row.Cells[6].AddParagraph($"{arrivalAddress}"); row.Cells[10].MergeDown = 1; row.Cells[10].AddParagraph("Ціна"); row.Cells[11].MergeDown = 1; row.Cells[11].AddParagraph($"{cost}"); } var totalCost = GetTicketGroupCost(ticketGroup); row = table.AddRow(); row.Shading.Color = isFilled ? Color.FromRgbColor(25, Colors.Black) : Colors.White; isFilled = !isFilled; row.Cells[0].MergeRight = 9; row.Cells[0].AddParagraph("Загальна"); row.Cells[0].Format.Alignment = ParagraphAlignment.Left; row.Cells[10].Borders.Right.Color = Colors.Transparent; row.Cells[11].AddParagraph($"{totalCost}"); } DateTime GetDepartureTime(Ticket ticket) { var departureDateTimeUtc = ticket.VehicleEnrollment.DepartureDateTimeUtc; var routeAddresses = ticket.VehicleEnrollment.Route.RouteAddresses .OrderBy(ra => ra.Order).ToArray(); foreach (var routeAddress in routeAddresses) { var details = routeAddress.RouteAddressDetails .First(rad => rad.RouteAddressId == routeAddress.Id); if (routeAddress.AddressId == ticket.FirstRouteAddressId) { break; } departureDateTimeUtc += details.TimeSpanToNextCity; departureDateTimeUtc += details.WaitTimeSpan; } return departureDateTimeUtc; } DateTime GetArrivalTime(Ticket ticket) { var arrivalDateTimeUtc = ticket.VehicleEnrollment.DepartureDateTimeUtc; var routeAddresses = ticket.VehicleEnrollment.Route.RouteAddresses .OrderBy(ra => ra.Order).ToArray(); foreach (var routeAddress in routeAddresses) { var details = routeAddress.RouteAddressDetails .First(rad => rad.RouteAddressId == routeAddress.Id); if (routeAddress.AddressId == ticket.LastRouteAddressId) { break; } arrivalDateTimeUtc += details.TimeSpanToNextCity; } return arrivalDateTimeUtc; } Address GetDepartureAddress(Ticket ticket) { return ticket.VehicleEnrollment.Route.RouteAddresses .First(ra => ra.AddressId == ticket.FirstRouteAddressId) .Address; } Address GetArrivalAddress(Ticket ticket) { return ticket.VehicleEnrollment.Route.RouteAddresses .First(ra => ra.AddressId == ticket.LastRouteAddressId) .Address; } double GetTicketCost(Ticket ticket) { double cost = 0; var routeAddresses = ticket.VehicleEnrollment.Route.RouteAddresses .OrderBy(ra => ra.Order) .SkipWhile(ra => ra.AddressId != ticket.FirstRouteAddressId) .TakeWhile(ra => ra.AddressId != ticket.LastRouteAddressId) .ToArray(); foreach (var routeAddress in routeAddresses) { var details = routeAddress.RouteAddressDetails .First(rad => rad.RouteAddressId == routeAddress.Id); cost += details.CostToNextCity; } return cost; } double GetTicketGroupCost(TicketGroup ticketGroup) { double cost = 0; foreach (var ticket in ticketGroup.Tickets) { cost += GetTicketCost(ticket); } return cost; } } public async Task<(bool isSucceed, string? message, Stream reportPdf)> GetCompanyReport() { throw new NotImplementedException(); } }