using System.Globalization; using System.IO.Compression; using System.Security; using System.Text; using System.Xml; namespace TaskTrackingSystem.Shared; public static class SpreadsheetReportBuilder { private const string DateFormat = "dd-MM-yyyy"; private const string ContentTypeWorkbook = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"; private const string ContentTypeWorksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"; private const string ContentTypeStyles = "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"; public static byte[] BuildTableWorkbook( string worksheetName, string title, IReadOnlyList headers, IEnumerable> rows, IEnumerable? summaryLines = null) { var rowData = rows.Select(row => row.ToArray()).ToList(); var summary = summaryLines? .Where(line => !string.IsNullOrWhiteSpace(line)) .Select(line => line.Trim()) .ToList() ?? new List(); var totalRows = 0; if (!string.IsNullOrWhiteSpace(title)) { totalRows++; } totalRows += summary.Count; totalRows++; // blank row before header totalRows++; // header row totalRows += rowData.Count; var columnCount = Math.Max(1, headers.Count); var lastColumnName = ToColumnName(columnCount); var lastRowNumber = Math.Max(1, totalRows); var dimension = $"A1:{lastColumnName}{lastRowNumber}"; using var stream = new MemoryStream(); using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true)) { AddEntry(archive, "[Content_Types].xml", BuildContentTypesXml()); AddEntry(archive, "_rels/.rels", BuildRootRelsXml()); AddEntry(archive, "xl/workbook.xml", BuildWorkbookXml(worksheetName)); AddEntry(archive, "xl/_rels/workbook.xml.rels", BuildWorkbookRelsXml()); AddEntry(archive, "xl/styles.xml", BuildStylesXml()); AddEntry(archive, "xl/worksheets/sheet1.xml", BuildWorksheetXml(title, headers, rowData, summary, dimension)); } return stream.ToArray(); } private static void AddEntry(ZipArchive archive, string path, string content) { var entry = archive.CreateEntry(path, CompressionLevel.Optimal); using var writer = new StreamWriter(entry.Open(), new UTF8Encoding(false)); writer.Write(content); } private static string BuildContentTypesXml() => """ """; private static string BuildRootRelsXml() => """ """; private static string BuildWorkbookXml(string worksheetName) { var safeName = EscapeXml(SanitizeWorksheetName(worksheetName)); return $""" """; } private static string BuildWorkbookRelsXml() => """ """; private static string BuildStylesXml() => """ """; private static string BuildWorksheetXml( string title, IReadOnlyList headers, IReadOnlyList rows, IReadOnlyList summaryLines, string dimension) { var rowXml = new StringBuilder(); var rowIndex = 1; var columnCount = Math.Max(1, headers.Count); if (!string.IsNullOrWhiteSpace(title)) { rowXml.Append(BuildSingleCellRow(rowIndex++, 0, title.Trim(), styleIndex: 1)); } foreach (var line in summaryLines) { rowXml.Append(BuildSingleCellRow(rowIndex++, 0, line, styleIndex: 2)); } rowIndex++; // blank row rowXml.Append(""); for (var col = 1; col <= columnCount; col++) { var header = col <= headers.Count ? headers[col - 1] : string.Empty; rowXml.Append(BuildInlineCell(rowIndex, col, header, styleIndex: 1)); } rowXml.AppendLine(""); rowIndex++; foreach (var row in rows) { rowXml.Append(""); for (var col = 1; col <= columnCount; col++) { var value = col <= row.Length ? row[col - 1] : null; rowXml.Append(BuildCell(rowIndex, col, value)); } rowXml.AppendLine(""); rowIndex++; } var autoFilterRow = 1 + (string.IsNullOrWhiteSpace(title) ? 0 : 1) + summaryLines.Count + 1; var worksheetXml = new StringBuilder(); worksheetXml.AppendLine(""); worksheetXml.AppendLine(""); worksheetXml.AppendLine($" "); worksheetXml.AppendLine(" "); worksheetXml.AppendLine(" "); worksheetXml.AppendLine($" "); worksheetXml.AppendLine($" "); worksheetXml.AppendLine(" "); worksheetXml.AppendLine(" "); worksheetXml.AppendLine(" "); worksheetXml.AppendLine(" "); worksheetXml.Append(BuildColumnDefinitions(columnCount)); worksheetXml.AppendLine(" "); worksheetXml.AppendLine(" "); worksheetXml.Append(rowXml); worksheetXml.AppendLine(" "); worksheetXml.AppendLine($" "); worksheetXml.AppendLine(""); return worksheetXml.ToString(); } private static string BuildSingleCellRow(int rowIndex, int colIndex, string value, int styleIndex) { return $"{BuildInlineCell(rowIndex, colIndex + 1, value, styleIndex)}\n"; } private static string BuildColumnDefinitions(int columnCount) { var builder = new StringBuilder(); for (var col = 1; col <= columnCount; col++) { builder.Append($" \n"); } return builder.ToString(); } private static string BuildCell(int rowIndex, int columnIndex, object? value) { if (value == null) { return string.Empty; } return value switch { DateTime dateTime => BuildNumericCell(rowIndex, columnIndex, dateTime.ToOADate().ToString(CultureInfo.InvariantCulture), styleIndex: 3), DateTimeOffset dateTimeOffset => BuildNumericCell(rowIndex, columnIndex, dateTimeOffset.DateTime.ToOADate().ToString(CultureInfo.InvariantCulture), styleIndex: 3), DateOnly dateOnly => BuildNumericCell(rowIndex, columnIndex, dateOnly.ToDateTime(TimeOnly.MinValue).ToOADate().ToString(CultureInfo.InvariantCulture), styleIndex: 3), bool boolean => BuildBooleanCell(rowIndex, columnIndex, boolean), sbyte or byte or short or ushort or int or uint or long or ulong or float or double or decimal => BuildNumericCell(rowIndex, columnIndex, Convert.ToString(value, CultureInfo.InvariantCulture) ?? "0"), _ => BuildInlineCell(rowIndex, columnIndex, Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty, styleIndex: 0) }; } private static string BuildInlineCell(int rowIndex, int columnIndex, string value, int styleIndex) { var cellRef = $"{ToColumnName(columnIndex)}{rowIndex}"; return $""" {EscapeXml(value)} """; } private static string BuildNumericCell(int rowIndex, int columnIndex, string value, int styleIndex = 0) { var cellRef = $"{ToColumnName(columnIndex)}{rowIndex}"; return $"""{value}"""; } private static string BuildBooleanCell(int rowIndex, int columnIndex, bool value) { var cellRef = $"{ToColumnName(columnIndex)}{rowIndex}"; return $"""{(value ? "1" : "0")}"""; } private static string ToColumnName(int columnNumber) { var dividend = columnNumber; var columnName = string.Empty; while (dividend > 0) { var modulo = (dividend - 1) % 26; columnName = Convert.ToChar('A' + modulo) + columnName; dividend = (dividend - modulo) / 26; } return columnName; } private static string SanitizeWorksheetName(string name) { var invalid = new[] { '[', ']', ':', '*', '?', '/', '\\' }; var cleaned = new string(name.Select(ch => invalid.Contains(ch) ? ' ' : ch).ToArray()).Trim(); if (string.IsNullOrWhiteSpace(cleaned)) { cleaned = "Report"; } return cleaned.Length <= 31 ? cleaned : cleaned[..31]; } private static string EscapeXml(string value) => SecurityElement.Escape(value) ?? string.Empty; }