using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; namespace File_Folder_Helper.ADO2024.PI3; internal static partial class Helper20240822 { private record Record(string? Title, ReadOnlyCollection<string> Tags, string? Completed); private record Root([property: JsonPropertyName("headings")] Heading[] Headings, [property: JsonPropertyName("lanes")] Lane[] Lanes); [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Root))] private partial class Helper20240822RootSourceGenerationContext : JsonSerializerContext { } private record Welcome2([property: JsonPropertyName("headings")] Heading[] Headings, [property: JsonPropertyName("lanes")] Lane[] Lanes); private record Heading([property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("heading")] string HeadingHeading); private record Lane([property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("columns")] Column[][] Columns); private record Column([property: JsonPropertyName("id")] string Id, [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("description")] string Description, [property: JsonPropertyName("metadata")] Metadata? Metadata, [property: JsonPropertyName("subTasks")] SubTask[]? SubTasks, [property: JsonPropertyName("relations")] object[] Relations, [property: JsonPropertyName("comments")] Comment[] Comments, [property: JsonPropertyName("column")] string ColumnColumn, [property: JsonPropertyName("workload")] long Workload, [property: JsonPropertyName("progress")] long Progress, [property: JsonPropertyName("remainingWorkload")] long RemainingWorkload, [property: JsonPropertyName("dueData")] DueData DueData); private record Comment([property: JsonPropertyName("text")] string Text, [property: JsonPropertyName("date")] DateTimeOffset Date); private record DueData([property: JsonPropertyName("completed")] bool Completed, [property: JsonPropertyName("completedDate")] object CompletedDate, [property: JsonPropertyName("dueDate")] DateTimeOffset DueDate, [property: JsonPropertyName("overdue")] bool Overdue, [property: JsonPropertyName("dueDelta")] long DueDelta, [property: JsonPropertyName("dueMessage")] string DueMessage); private record Metadata([property: JsonPropertyName("assigned")] string Assigned, [property: JsonPropertyName("created")] DateTimeOffset Created, [property: JsonPropertyName("progress")] long? Progress, [property: JsonPropertyName("started")] DateTimeOffset? Started, [property: JsonPropertyName("status")] string? Status, [property: JsonPropertyName("tags")] string[]? Tags, [property: JsonPropertyName("type")] string? Type, [property: JsonPropertyName("updated")] DateTimeOffset Updated, [property: JsonPropertyName("due")] DateTimeOffset? Due, [property: JsonPropertyName("completed")] DateTimeOffset? Completed); private record SubTask([property: JsonPropertyName("text")] string Text, [property: JsonPropertyName("completed")] bool Completed); private static ReadOnlyCollection<ReadOnlyCollection<Record>> GetRecords(Column[][] columnCollection) { List<ReadOnlyCollection<Record>> results = []; bool check; int subTasks; Column column; int completed; List<Record> row; string? subtasks; List<string> tags; for (int i = 0; i < int.MaxValue; i++) { row = []; check = false; foreach (Column[] columns in columnCollection) { if (columns.Length <= i) row.Add(new(null, new([]), null)); else { tags = []; subTasks = 0; completed = 0; column = columns[i]; if (!check) check = true; if (column.Metadata?.Tags is not null && column.Metadata.Tags.Length != 0) { foreach (string tag in column.Metadata.Tags) tags.Add(tag); } if (column.SubTasks is not null && column.SubTasks.Length != 0) { foreach (SubTask subTask in column.SubTasks) { subTasks += 1; if (subTask.Completed) completed += 1; } } subtasks = subTasks == 0 ? subtasks = null : $"{completed} / {subTasks}"; row.Add(new(column.Name, new(tags), subtasks)); } } if (!check) break; if (results.Count > 0) { if (results[0].Count != row.Count) throw new Exception("Rows must match!"); } results.Add(new(row)); } return new(results); } private static void WriteFile(string destinationFile, Heading[] headings, ReadOnlyCollection<ReadOnlyCollection<Record>> recordCollection) { string title; string completed; List<string> lines = [ "<html>", "<head>", "<style>", ":root {", "color-scheme: light dark;", "}", "body {", "color: light-dark(#333b3c, #efedea);", "background-color: light-dark(#efedea, #333b3c);", "}", "td {", "vertical-align: top;", "}", ".title {", "font-weight: bold;", "}", ".complete {", "font-size: small;", "}", ".speech-bubble {", "position: relative;", "background: darkCyan;", "border-radius: .4em;", "}", ".speech-bubble:after {", "content: '';", "position: absolute;", "bottom: 0;", "left: 50%;", "width: 0;", "height: 0;", "border: 2px solid transparent;", "border-top-color: #00aabb;", "border-bottom: 0;", "margin-left: -2px;", "margin-bottom: -2px;", "}", "</style>", "</head>", "<table border=\"1\">", "<tr>" ]; foreach (Heading heading in headings) lines.Add($"<th>{heading.Name}</th>"); lines.Add("</tr>"); foreach (ReadOnlyCollection<Record> records in recordCollection) { lines.Add("<tr>"); foreach (Record record in records) { lines.Add("<td>"); title = record.Title is null ? " " : record.Title; completed = record.Completed is null ? " " : record.Completed; lines.Add($"<div class=\"title\">{title}</div>"); lines.Add("<div>"); foreach (string tag in record.Tags) lines.Add($"<span class=\"speech-bubble\">{tag}</span>"); lines.Add("</div>"); lines.Add($"<div class=\"completed\">{completed}</div>"); lines.Add("</td>"); } lines.Add("</tr>"); } lines.Add("</table>"); lines.Add("</html>"); File.WriteAllLines(destinationFile, lines); } private static void ParseKanbn(ILogger<Worker> logger, string destinationFile, string json) { Root? root = JsonSerializer.Deserialize(json, Helper20240822RootSourceGenerationContext.Default.Root); if (root is null) logger.LogInformation("<{root}> is null!", root); else if (root.Lanes.Length != 1) logger.LogInformation("{root.Lanes} != 1", root.Lanes.Length); else if (root.Lanes[0].Columns.Length != root.Headings.Length) logger.LogInformation("{root[0].Columns.Lanes} != {root.Headings}", root.Lanes[0].Columns.Length, root.Headings.Length); else { ReadOnlyCollection<ReadOnlyCollection<Record>> recordCollection = GetRecords(root.Lanes[0].Columns); WriteFile(destinationFile, root.Headings, recordCollection); } } internal static void ParseKanbn(ILogger<Worker> logger, List<string> args) { string sourceDirectory = Path.GetFullPath(args[0]); string sourceFile = Path.Combine(sourceDirectory, args[2]); string destinationFile = Path.Combine(sourceDirectory, $"{DateTime.Now.Ticks}-{args[3]}"); if (!File.Exists(sourceFile)) logger.LogInformation("<{sourceFile}> doesn't exist!", sourceFile); else { string json = File.ReadAllText(sourceFile); ParseKanbn(logger, destinationFile, json); } } }