using CouponReport.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using ClosedXML.Excel; using iText.Kernel.Pdf; using iText.Layout; using iText.Layout.Element; using iText.Layout.Properties; using iText.Kernel.Font; using iText.IO.Font; using iText.IO.Font.Constants; using CouponReport.Models.CouponMiddleware; using CouponReport.Models.Parkingeyes; using CouponReport.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authentication.Cookies; namespace CouponReport.Controllers; [Authorize] public class ReportController : Controller { private readonly CouponMiddlewareContext _couponContext; private readonly ParkingEyesContext _parkingEyesContext; private readonly ReportService _reportService; public ReportController(CouponMiddlewareContext context, ParkingEyesContext parkingEyesContext, ReportService reportService) { _couponContext = context; _parkingEyesContext = parkingEyesContext; _reportService = reportService; } public async Task Index(DateTime? startDate, DateTime? endDate) { // 如果沒有提供日期,預設為當月第一天到最後一天 if (!startDate.HasValue && !endDate.HasValue) { var today = DateTime.Today; startDate = new DateTime(today.Year, today.Month, 1); endDate = startDate.Value.AddMonths(1).AddDays(-1); } return await LoadReportData(startDate, endDate); } [HttpPost] public async Task Index(DateTime? startDate, DateTime? endDate, string action) { return await LoadReportData(startDate, endDate); } private async Task LoadReportData(DateTime? startDate, DateTime? endDate) { var query = _couponContext.Logs.AsQueryable().Where(x => x.LogType == "Consume" && x.LogInfo.Contains("耗用成功")); var serialNo = query.GroupBy(x => x.SerialNo).Select(g => g.Key).ToList(); var carEnter = _parkingEyesContext.CarEnters.Where(x => serialNo.Contains(x.SerialNo)).ToList(); if (startDate.HasValue) { query = query.Where(x => x.LogTime >= startDate.Value); } if (endDate.HasValue) { query = query.Where(x => x.LogTime <= endDate.Value.AddDays(1).AddSeconds(-1)); } var logs = await query.OrderBy(x => x.LogTime).ToListAsync(); // 建立 SerialNo 到編號的映射 var serialNoToRowNumber = new Dictionary(); int currentRowNumber = 1; // 建立 SerialNo 到 CarEnter 的映射,方便快速查找 var carEnterDict = carEnter.GroupBy(x => x.SerialNo) .ToDictionary(g => g.Key, g => g.FirstOrDefault()); var viewModel = new CouponReportViewModel { StartDate = startDate, EndDate = endDate, ReportItems = logs.Select(log => { // 如果這個 SerialNo 還沒出現過,給它一個新編號 if (!string.IsNullOrEmpty(log.SerialNo) && !serialNoToRowNumber.ContainsKey(log.SerialNo)) { serialNoToRowNumber[log.SerialNo] = currentRowNumber++; } // 從 CarEnter 取得出場時間 DateTime? exitTime = null; var tenantCode = string.Empty; DateTime? invoiceDate = null; var invoiceNo = string.Empty; var invoiceAmount = 0m; if (!string.IsNullOrEmpty(log.SerialNo) && carEnterDict.ContainsKey(log.SerialNo)) { exitTime = carEnterDict[log.SerialNo]?.DepartureDateTime; } invoiceDate = _reportService.GetInvoiceDateTime(log.ExternalSystemKey); invoiceNo = _reportService.GetInvoiceNo(log.ExternalSystemKey); invoiceAmount = _reportService.GetInvoiceMoney(log.ExternalSystemKey); tenantCode = _reportService.GetTenantCode(log.ExternalSystemKey); return new CouponReportItem { RowNumber = !string.IsNullOrEmpty(log.SerialNo) ? serialNoToRowNumber[log.SerialNo] : 0, TenantCode = tenantCode, CarNumber = log.PlateNo, InvoiceDate = invoiceDate, InvoiceNumber = invoiceNo, InvoiceAmount = invoiceAmount, DiscountUnit = "新台幣", DiscountAmount = log.DiscountAmount, DiscountTime = log.LogTime, EnterTime = log.EnterTime, ExitTime = exitTime, ParkingAmount = log.TotalAmount, ClaimAmount = log.DiscountAmount }; }).ToList() }; return View(viewModel); } public async Task ExportToExcel(DateTime? startDate, DateTime? endDate) { var query = _couponContext.Logs.AsQueryable().Where(x => x.LogType == "Consume" && x.LogInfo.Contains("耗用成功")); var serialNo = query.GroupBy(x => x.SerialNo).Select(g => g.Key).ToList(); var carEnter = _parkingEyesContext.CarEnters.Where(x => serialNo.Contains(x.SerialNo)).ToList(); if (startDate.HasValue) { query = query.Where(x => x.LogTime >= startDate.Value); } if (endDate.HasValue) { query = query.Where(x => x.LogTime <= endDate.Value.AddDays(1).AddSeconds(-1)); } var logs = await query.OrderBy(x => x.LogTime).ToListAsync(); // 建立 SerialNo 到編號的映射 var serialNoToRowNumber = new Dictionary(); int currentRowNumber = 1; // 建立 SerialNo 到 CarEnter 的映射,方便快速查找 var carEnterDict = carEnter.GroupBy(x => x.SerialNo) .ToDictionary(g => g.Key, g => g.FirstOrDefault()); // 建立報表資料列表 var reportItems = new List<(int rowNumber, string tenantCode, string carNumber, DateTime? invoiceDate, string invoiceNo, decimal invoiceAmount, string discountUnit, decimal? discountAmount, DateTime discountTime, DateTime? enterTime, DateTime? exitTime, decimal? parkingAmount, decimal? claimAmount)>(); foreach (var log in logs) { if (!string.IsNullOrEmpty(log.SerialNo) && !serialNoToRowNumber.ContainsKey(log.SerialNo)) { serialNoToRowNumber[log.SerialNo] = currentRowNumber++; } DateTime? exitTime = null; if (!string.IsNullOrEmpty(log.SerialNo) && carEnterDict.ContainsKey(log.SerialNo)) { exitTime = carEnterDict[log.SerialNo]?.DepartureDateTime; } var invoiceDate = _reportService.GetInvoiceDateTime(log.ExternalSystemKey); var invoiceNo = _reportService.GetInvoiceNo(log.ExternalSystemKey); var invoiceAmount = _reportService.GetInvoiceMoney(log.ExternalSystemKey); var tenantCode = _reportService.GetTenantCode(log.ExternalSystemKey); int rowNumber = !string.IsNullOrEmpty(log.SerialNo) ? serialNoToRowNumber[log.SerialNo] : 0; reportItems.Add((rowNumber, tenantCode, log.PlateNo, invoiceDate, invoiceNo, invoiceAmount, "新台幣", log.DiscountAmount, log.LogTime, log.EnterTime, exitTime, log.TotalAmount, log.DiscountAmount)); } using var workbook = new XLWorkbook(); var worksheet = workbook.Worksheets.Add("折扣報表"); // 標題列 worksheet.Cell(1, 1).Value = "編號"; worksheet.Cell(1, 2).Value = "店別(統編)"; worksheet.Cell(1, 3).Value = "車號"; worksheet.Cell(1, 4).Value = "發票日期"; worksheet.Cell(1, 5).Value = "發票號碼"; worksheet.Cell(1, 6).Value = "發票金額"; worksheet.Cell(1, 7).Value = "折扣單位"; worksheet.Cell(1, 8).Value = "折扣金額"; worksheet.Cell(1, 9).Value = "折扣時間"; worksheet.Cell(1, 10).Value = "入場時間"; worksheet.Cell(1, 11).Value = "出場時間"; worksheet.Cell(1, 12).Value = "停車金額"; worksheet.Cell(1, 13).Value = "請款金額"; // 設定標題列樣式 var headerRange = worksheet.Range(1, 1, 1, 13); headerRange.Style.Font.Bold = true; headerRange.Style.Fill.BackgroundColor = XLColor.LightGray; headerRange.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; headerRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; // 填入資料並記錄合併範圍 int row = 2; var mergedCells = new Dictionary(); foreach (var item in reportItems) { // 填入資料 worksheet.Cell(row, 1).Value = item.rowNumber; worksheet.Cell(row, 2).Value = item.tenantCode; worksheet.Cell(row, 3).Value = item.carNumber; worksheet.Cell(row, 4).Value = item.invoiceDate?.ToString("yyyy-MM-dd HH:mm:ss"); worksheet.Cell(row, 5).Value = item.invoiceNo; worksheet.Cell(row, 6).Value = item.invoiceAmount; worksheet.Cell(row, 7).Value = item.discountUnit; worksheet.Cell(row, 8).Value = item.discountAmount; worksheet.Cell(row, 9).Value = item.discountTime.ToString("yyyy-MM-dd HH:mm:ss"); worksheet.Cell(row, 10).Value = item.enterTime?.ToString("yyyy-MM-dd HH:mm:ss"); worksheet.Cell(row, 11).Value = item.exitTime?.ToString("yyyy-MM-dd HH:mm:ss"); worksheet.Cell(row, 12).Value = item.parkingAmount; worksheet.Cell(row, 13).Value = item.claimAmount; // 記錄需要合併的儲存格範圍(按 rowNumber 分組) if (item.rowNumber > 0) { if (!mergedCells.ContainsKey(item.rowNumber)) { mergedCells[item.rowNumber] = (row, row); } else { mergedCells[item.rowNumber] = (mergedCells[item.rowNumber].startRow, row); } } row++; } // 執行合併儲存格 foreach (var (rowNumber, (startRow, endRow)) in mergedCells) { if (startRow < endRow) { // 合併:編號(1)、車號(3)、折扣單位(7)、折扣金額(8)、折扣時間(9)、入場時間(10)、出場時間(11)、停車金額(12)、請款金額(13) int[] mergeCols = { 1, 3, 7, 8, 9, 10, 11, 12, 13 }; foreach (int col in mergeCols) { var range = worksheet.Range(startRow, col, endRow, col); range.Merge(); range.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; range.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; } } } // 自動調整欄寬,並設定最小寬度以顯示完整表頭 worksheet.Columns().AdjustToContents(); for (int col = 1; col <= 13; col++) { if (worksheet.Column(col).Width < 12) { worksheet.Column(col).Width = 12; } } // 將所有資料儲存格置中 var dataRange = worksheet.Range(2, 1, row - 1, 13); dataRange.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; dataRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; var stream = new MemoryStream(); workbook.SaveAs(stream); stream.Position = 0; var fileName = $"折扣報表_{DateTime.Now:yyyyMMddHHmmss}.xlsx"; return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName); } public async Task ExportToPdf(DateTime? startDate, DateTime? endDate) { var query = _couponContext.Logs.AsQueryable().Where(x => x.LogType == "Consume" && x.LogInfo.Contains("耗用成功")); var serialNo = query.GroupBy(x => x.SerialNo).Select(g => g.Key).ToList(); var carEnter = _parkingEyesContext.CarEnters.Where(x => serialNo.Contains(x.SerialNo)).ToList(); if (startDate.HasValue) { query = query.Where(x => x.LogTime >= startDate.Value); } if (endDate.HasValue) { query = query.Where(x => x.LogTime <= endDate.Value.AddDays(1).AddSeconds(-1)); } var logs = await query.OrderBy(x => x.LogTime).ToListAsync(); // 建立 SerialNo 到編號的映射 var serialNoToRowNumber = new Dictionary(); int currentRowNumber = 1; // 建立 SerialNo 到 CarEnter 的映射,方便快速查找 var carEnterDict = carEnter.GroupBy(x => x.SerialNo) .ToDictionary(g => g.Key, g => g.FirstOrDefault()); // 建立報表資料並分組 var reportGroups = new Dictionary>(); var groupData = new Dictionary(); foreach (var log in logs) { if (!string.IsNullOrEmpty(log.SerialNo) && !serialNoToRowNumber.ContainsKey(log.SerialNo)) { serialNoToRowNumber[log.SerialNo] = currentRowNumber++; } DateTime? exitTime = null; if (!string.IsNullOrEmpty(log.SerialNo) && carEnterDict.ContainsKey(log.SerialNo)) { exitTime = carEnterDict[log.SerialNo]?.DepartureDateTime; } var invoiceDate = _reportService.GetInvoiceDateTime(log.ExternalSystemKey); var invoiceNo = _reportService.GetInvoiceNo(log.ExternalSystemKey); var invoiceAmount = _reportService.GetInvoiceMoney(log.ExternalSystemKey); var tenantCode = _reportService.GetTenantCode(log.ExternalSystemKey); int rowNumber = !string.IsNullOrEmpty(log.SerialNo) ? serialNoToRowNumber[log.SerialNo] : 0; // 記錄群組資料 if (!groupData.ContainsKey(rowNumber)) { groupData[rowNumber] = (rowNumber, log.PlateNo, "新台幣", log.DiscountAmount, log.LogTime, log.EnterTime, exitTime, log.TotalAmount, log.DiscountAmount); reportGroups[rowNumber] = new List<(string, DateTime?, string, decimal)>(); } reportGroups[rowNumber].Add((tenantCode, invoiceDate, invoiceNo, invoiceAmount)); } var stream = new MemoryStream(); var writer = new PdfWriter(stream); writer.SetCloseStream(false); // 防止關閉 MemoryStream var pdf = new PdfDocument(writer); var document = new Document(pdf, iText.Kernel.Geom.PageSize.A4.Rotate()); // 設定中文字型 var fontPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "kaiu.ttf"); if (!System.IO.File.Exists(fontPath)) { fontPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "msjh.ttc,0"); } PdfFont font; try { font = PdfFontFactory.CreateFont(fontPath, PdfEncodings.IDENTITY_H); } catch { font = PdfFontFactory.CreateFont(StandardFonts.HELVETICA); } document.SetFont(font); // 標題 var title = new Paragraph("折扣報表") .SetTextAlignment(TextAlignment.CENTER) .SetFontSize(18); document.Add(title); // 建立表格 var table = new Table(13).UseAllAvailableWidth(); table.SetFontSize(8); // 標題列 table.AddHeaderCell(new Cell().Add(new Paragraph("編號")).SetTextAlignment(TextAlignment.CENTER)); table.AddHeaderCell(new Cell().Add(new Paragraph("店別")).SetTextAlignment(TextAlignment.CENTER)); table.AddHeaderCell(new Cell().Add(new Paragraph("車號")).SetTextAlignment(TextAlignment.CENTER)); table.AddHeaderCell(new Cell().Add(new Paragraph("發票日期")).SetTextAlignment(TextAlignment.CENTER)); table.AddHeaderCell(new Cell().Add(new Paragraph("發票號碼")).SetTextAlignment(TextAlignment.CENTER)); table.AddHeaderCell(new Cell().Add(new Paragraph("發票金額")).SetTextAlignment(TextAlignment.CENTER)); table.AddHeaderCell(new Cell().Add(new Paragraph("折扣單位")).SetTextAlignment(TextAlignment.CENTER)); table.AddHeaderCell(new Cell().Add(new Paragraph("折扣金額")).SetTextAlignment(TextAlignment.CENTER)); table.AddHeaderCell(new Cell().Add(new Paragraph("折扣時間")).SetTextAlignment(TextAlignment.CENTER)); table.AddHeaderCell(new Cell().Add(new Paragraph("入場時間")).SetTextAlignment(TextAlignment.CENTER)); table.AddHeaderCell(new Cell().Add(new Paragraph("出場時間")).SetTextAlignment(TextAlignment.CENTER)); table.AddHeaderCell(new Cell().Add(new Paragraph("停車金額")).SetTextAlignment(TextAlignment.CENTER)); table.AddHeaderCell(new Cell().Add(new Paragraph("請款金額")).SetTextAlignment(TextAlignment.CENTER)); // 資料列(使用合併儲存格) foreach (var (rowNum, group) in groupData.OrderBy(x => x.Key)) { var invoices = reportGroups[rowNum]; int rowSpan = invoices.Count; for (int i = 0; i < invoices.Count; i++) { var invoice = invoices[i]; bool isFirstRow = i == 0; if (isFirstRow) { // 第一行:顯示合併的欄位 - 編號 table.AddCell(new Cell(rowSpan, 1).Add(new Paragraph(group.rowNumber.ToString())).SetTextAlignment(TextAlignment.CENTER).SetVerticalAlignment(VerticalAlignment.MIDDLE)); } // 每一行:店別 table.AddCell(new Cell().Add(new Paragraph(invoice.tenantCode ?? "")).SetTextAlignment(TextAlignment.CENTER)); if (isFirstRow) { // 車號 table.AddCell(new Cell(rowSpan, 1).Add(new Paragraph(group.carNumber ?? "")).SetTextAlignment(TextAlignment.CENTER).SetVerticalAlignment(VerticalAlignment.MIDDLE)); } // 每一行:發票日期、發票號碼、發票金額 table.AddCell(new Cell().Add(new Paragraph(invoice.invoiceDate?.ToString("yyyy-MM-dd") ?? "")).SetTextAlignment(TextAlignment.CENTER)); table.AddCell(new Cell().Add(new Paragraph(invoice.invoiceNo ?? "")).SetTextAlignment(TextAlignment.CENTER)); table.AddCell(new Cell().Add(new Paragraph(invoice.invoiceAmount.ToString("N0"))).SetTextAlignment(TextAlignment.CENTER)); if (isFirstRow) { // 合併欄位:折扣單位、折扣金額、折扣時間、入場時間、出場時間、停車金額、請款金額 table.AddCell(new Cell(rowSpan, 1).Add(new Paragraph(group.discountUnit ?? "")).SetTextAlignment(TextAlignment.CENTER).SetVerticalAlignment(VerticalAlignment.MIDDLE)); table.AddCell(new Cell(rowSpan, 1).Add(new Paragraph(group.discountAmount?.ToString("N0") ?? "")).SetTextAlignment(TextAlignment.CENTER).SetVerticalAlignment(VerticalAlignment.MIDDLE)); table.AddCell(new Cell(rowSpan, 1).Add(new Paragraph(group.discountTime.ToString("yyyy-MM-dd HH:mm"))).SetTextAlignment(TextAlignment.CENTER).SetVerticalAlignment(VerticalAlignment.MIDDLE)); table.AddCell(new Cell(rowSpan, 1).Add(new Paragraph(group.enterTime?.ToString("yyyy-MM-dd HH:mm") ?? "")).SetTextAlignment(TextAlignment.CENTER).SetVerticalAlignment(VerticalAlignment.MIDDLE)); table.AddCell(new Cell(rowSpan, 1).Add(new Paragraph(group.exitTime?.ToString("yyyy-MM-dd HH:mm") ?? "")).SetTextAlignment(TextAlignment.CENTER).SetVerticalAlignment(VerticalAlignment.MIDDLE)); table.AddCell(new Cell(rowSpan, 1).Add(new Paragraph(group.parkingAmount?.ToString("N0") ?? "")).SetTextAlignment(TextAlignment.CENTER).SetVerticalAlignment(VerticalAlignment.MIDDLE)); table.AddCell(new Cell(rowSpan, 1).Add(new Paragraph(group.claimAmount?.ToString("N0") ?? "")).SetTextAlignment(TextAlignment.CENTER).SetVerticalAlignment(VerticalAlignment.MIDDLE)); } } } document.Add(table); document.Close(); stream.Position = 0; var fileName = $"折扣報表_{DateTime.Now:yyyyMMddHHmmss}.pdf"; return File(stream, "application/pdf", fileName); } }