diff --git a/Bezalu.ProjectReporting.API/Bezalu.ProjectReporting.API.csproj b/Bezalu.ProjectReporting.API/Bezalu.ProjectReporting.API.csproj index e16d01f..5e52cc9 100644 --- a/Bezalu.ProjectReporting.API/Bezalu.ProjectReporting.API.csproj +++ b/Bezalu.ProjectReporting.API/Bezalu.ProjectReporting.API.csproj @@ -19,6 +19,7 @@ + diff --git a/Bezalu.ProjectReporting.API/Functions/ReportFunction.cs b/Bezalu.ProjectReporting.API/Functions/ReportFunction.cs index 68112c4..6dc3cdf 100644 --- a/Bezalu.ProjectReporting.API/Functions/ReportFunction.cs +++ b/Bezalu.ProjectReporting.API/Functions/ReportFunction.cs @@ -7,6 +7,7 @@ using QuestPDF; using QuestPDF.Fluent; using QuestPDF.Infrastructure; +using QuestPDF.Markdown; namespace Bezalu.ProjectReporting.API.Functions; @@ -172,7 +173,7 @@ private static Action BudgetSection(ProjectCompletionReportResponse c.Column(col => { col.Item().Text("Budget Analysis").FontSize(14).Bold(); - col.Item().Text($"Estimated Hours: {b?.EstimatedHours}").FontSize(10); + col.Item().Text($"Budget Hours: {b?.BudgetHours}").FontSize(10); col.Item().Text($"Actual Hours: {b?.ActualHours}").FontSize(10); col.Item().Text($"Variance Hours: {b?.VarianceHours}").FontSize(10); col.Item().Text($"Budget Adherence: {b?.BudgetAdherence}").FontSize(10); @@ -188,7 +189,7 @@ private static Action AISummarySection(ProjectCompletionReportRespon c.Column(col => { col.Item().Text("AI Generated Summary").FontSize(14).Bold(); - col.Item().Text(report.AiGeneratedSummary ?? string.Empty).FontSize(10); + col.Item().DefaultTextStyle(x => x.FontSize(10)).Markdown(report.AiGeneratedSummary ?? string.Empty); }); }; } @@ -207,7 +208,7 @@ private static Action PhasesSection(ProjectCompletionReportResponse if (phase is { ActualStart: not null, ActualEnd: not null }) inner.Item().Text($"Actual: {phase.ActualStart:yyyy-MM-dd} > {phase.ActualEnd:yyyy-MM-dd}") .FontSize(9); - inner.Item().Text($"Hours est/actual: {phase.EstimatedHours}/{phase.ActualHours}").FontSize(9); + inner.Item().Text($"Hours budget/actual: {phase.BudgetHours}/{phase.ActualHours}").FontSize(9); }); }); }; @@ -226,7 +227,7 @@ private static Action TicketsSection(ProjectCompletionReportResponse { inner.Item().Text($"#{ticket.TicketNumber} {ticket.Summary} ({ticket.Status})").SemiBold(); inner.Item().Text($"Type: {ticket.Type}/{ticket.SubType}").FontSize(9); - inner.Item().Text($"Hours est/actual: {ticket.EstimatedHours}/{ticket.ActualHours}") + inner.Item().Text($"Hours budget/actual: {ticket.BudgetHours}/{ticket.ActualHours}") .FontSize(9); if (ticket.ClosedDate != null) inner.Item().Text($"Closed: {ticket.ClosedDate:yyyy-MM-dd}").FontSize(9); diff --git a/Bezalu.ProjectReporting.API/Models/ConnectWiseModels.cs b/Bezalu.ProjectReporting.API/Models/ConnectWiseModels.cs index deabc0f..73cb1c0 100644 --- a/Bezalu.ProjectReporting.API/Models/ConnectWiseModels.cs +++ b/Bezalu.ProjectReporting.API/Models/ConnectWiseModels.cs @@ -10,6 +10,7 @@ public class CWProject public DateTime? EstimatedStart { get; set; } public DateTime? EstimatedEnd { get; set; } public decimal? EstimatedHours { get; set; } + public decimal? BudgetHours { get; set; } public decimal? ActualHours { get; set; } public CWReference? Manager { get; set; } public CWReference? Company { get; set; } @@ -32,6 +33,7 @@ public class CWTicket public CWReference? Type { get; set; } public CWReference? SubType { get; set; } public decimal? EstimatedHours { get; set; } + public decimal? BudgetHours { get; set; } public decimal? ActualHours { get; set; } public DateTime? ClosedDate { get; set; } public CWReference? AssignedTo { get; set; } @@ -53,6 +55,7 @@ public class CWPhase public DateTime? ActualStart { get; set; } public DateTime? ActualEnd { get; set; } public decimal? EstimatedHours { get; set; } + public decimal? BudgetHours { get; set; } public decimal? ActualHours { get; set; } } diff --git a/Bezalu.ProjectReporting.API/Services/ProjectReportingService.cs b/Bezalu.ProjectReporting.API/Services/ProjectReportingService.cs index 7c424f1..a5d268f 100644 --- a/Bezalu.ProjectReporting.API/Services/ProjectReportingService.cs +++ b/Bezalu.ProjectReporting.API/Services/ProjectReportingService.cs @@ -131,20 +131,20 @@ private ProjectCompletionReportResponse BuildReport( }; } - var estimatedHours = project.EstimatedHours ?? 0; + var budgetHours = project.BudgetHours ?? 0; var actualHours = project.ActualHours ?? 0; - var hoursVariance = actualHours - estimatedHours; + var hoursVariance = actualHours - budgetHours; report.Budget = new BudgetAnalysis { - EstimatedHours = estimatedHours, + BudgetHours = budgetHours, ActualHours = actualHours, VarianceHours = hoursVariance, EstimatedCost = 0, ActualCost = 0, VarianceCost = 0, - BudgetAdherence = hoursVariance <= 0 ? "Under Budget" : hoursVariance <= estimatedHours * 0.1m ? "Slightly Over" : "Over Budget", - CostPerformance = estimatedHours > 0 ? $"{(double)(actualHours / estimatedHours) * 100:F1}%" : "N/A" + BudgetAdherence = hoursVariance <= 0 ? "Under Budget" : hoursVariance <= budgetHours * 0.1m ? "Slightly Over" : "Over Budget", + CostPerformance = budgetHours > 0 ? $"{(double)(actualHours / budgetHours) * 100:F1}%" : "N/A" }; report.Phases = phases.Select(phase => new PhaseDetail @@ -154,7 +154,7 @@ private ProjectCompletionReportResponse BuildReport( Status = phase.Status?.Name, ActualStart = phase.ActualStart, ActualEnd = phase.ActualEnd, - EstimatedHours = phase.EstimatedHours ?? 0, + BudgetHours = phase.BudgetHours ?? 0, ActualHours = phase.ActualHours ?? 0, Notes = new List() }).ToList(); @@ -167,7 +167,7 @@ private ProjectCompletionReportResponse BuildReport( Status = ticket.Status?.Name, Type = ticket.Type?.Name, SubType = ticket.SubType?.Name, - EstimatedHours = ticket.EstimatedHours ?? 0, + BudgetHours = ticket.BudgetHours ?? 0, ActualHours = ticket.ActualHours ?? 0, Notes = ticketNotes.TryGetValue(ticket.Id ?? 0, out var notes) ? notes.OrderBy(n => n.DateCreated).Select(n => n.Text ?? "").ToList() @@ -205,7 +205,7 @@ private string PrepareDataForAI(ProjectCompletionReportResponse report, List()) { - sb.AppendLine($"- {phase.PhaseName}: {phase.Status}; Hours est/actual {phase.EstimatedHours}/{phase.ActualHours}"); + sb.AppendLine($"- {phase.PhaseName}: {phase.Status}; Hours budget/actual {phase.BudgetHours}/{phase.ActualHours}"); } sb.AppendLine(); @@ -231,7 +231,7 @@ private string PrepareDataForAI(ProjectCompletionReportResponse report, List()) { - sb.AppendLine($"- Ticket #{ticket.TicketNumber} {ticket.Summary} (Status: {ticket.Status}, Type: {ticket.Type}/{ticket.SubType}, Hours est/actual {ticket.EstimatedHours}/{ticket.ActualHours})"); + sb.AppendLine($"- Ticket #{ticket.TicketNumber} {ticket.Summary} (Status: {ticket.Status}, Type: {ticket.Type}/{ticket.SubType}, Hours budget/actual {ticket.BudgetHours}/{ticket.ActualHours})"); if (ticketNotes.TryGetValue(ticket.TicketId, out var notes) && notes.Any()) { var limited = notes.OrderBy(n => n.DateCreated).ToList(); // cap to 20 per ticket diff --git a/Bezalu.ProjectReporting.Shared/DTOs/ProjectCompletionReportModels.cs b/Bezalu.ProjectReporting.Shared/DTOs/ProjectCompletionReportModels.cs index 2169846..7d967b8 100644 --- a/Bezalu.ProjectReporting.Shared/DTOs/ProjectCompletionReportModels.cs +++ b/Bezalu.ProjectReporting.Shared/DTOs/ProjectCompletionReportModels.cs @@ -49,7 +49,7 @@ public class TimelineAnalysis public class BudgetAnalysis { - public decimal EstimatedHours { get; set; } + public decimal BudgetHours { get; set; } public decimal ActualHours { get; set; } public decimal VarianceHours { get; set; } public decimal EstimatedCost { get; set; } @@ -66,7 +66,7 @@ public class PhaseDetail public string? Status { get; set; } public DateTime? ActualStart { get; set; } public DateTime? ActualEnd { get; set; } - public decimal EstimatedHours { get; set; } + public decimal BudgetHours { get; set; } public decimal ActualHours { get; set; } public List? Notes { get; set; } public string? Summary { get; set; } @@ -80,7 +80,7 @@ public class TicketSummary public string? Status { get; set; } public string? Type { get; set; } public string? SubType { get; set; } - public decimal EstimatedHours { get; set; } + public decimal BudgetHours { get; set; } public decimal ActualHours { get; set; } public List? Notes { get; set; } public DateTime? ClosedDate { get; set; } diff --git a/Bezalu.ProjectReporting.Web/Pages/Home.razor b/Bezalu.ProjectReporting.Web/Pages/Home.razor index 11519e4..8450caa 100644 --- a/Bezalu.ProjectReporting.Web/Pages/Home.razor +++ b/Bezalu.ProjectReporting.Web/Pages/Home.razor @@ -76,14 +76,14 @@ else

Manager: @Report.Summary?.Manager | Company: @Report.Summary?.Company

Timeline: @Report.Timeline?.PlannedDays d planned / @Report.Timeline?.TotalDays d actual (Var: @Report.Timeline?.VarianceDays)

-

Budget: @Report.Budget?.EstimatedHours h est / @Report.Budget?.ActualHours h actual (Var: @Report.Budget?.VarianceHours)

+

Budget: @Report.Budget?.BudgetHours h budget / @Report.Budget?.ActualHours h actual (Var: @Report.Budget?.VarianceHours)

@if (!string.IsNullOrWhiteSpace(Report.AiGeneratedSummary)) { -
@(new MarkupString(AiSummaryHtml))
+
@(new MarkupString(AiSummaryHtml))
} @@ -99,7 +99,7 @@ else - + } @@ -115,7 +115,7 @@ else - + } @@ -125,6 +125,11 @@ else } @code { + static readonly MarkdownPipeline MdPipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() // enables tables, pipe tables + .UseSoftlineBreakAsHardlineBreak() + .Build(); + List? Projects; ProjectCompletionReportResponse? Report; bool IsLoadingProjects; @@ -134,7 +139,7 @@ else bool ShowLoginPrompt; string LoginPromptText = "Please sign in to continue."; string? ErrorMessage; - string AiSummaryHtml => Report?.AiGeneratedSummary is null ? string.Empty : Markdown.ToHtml(Report.AiGeneratedSummary); + string AiSummaryHtml => Report?.AiGeneratedSummary is null ? string.Empty : Markdown.ToHtml(Report.AiGeneratedSummary, MdPipeline); protected override async Task OnInitializedAsync() { diff --git a/Bezalu.ProjectReporting.Web/staticwebapp.config.json b/Bezalu.ProjectReporting.Web/staticwebapp.config.json index aed1fd3..877446d 100644 --- a/Bezalu.ProjectReporting.Web/staticwebapp.config.json +++ b/Bezalu.ProjectReporting.Web/staticwebapp.config.json @@ -9,7 +9,7 @@ "allowedRoles": [ "anonymous" ] }, { - "route": "/*", + "route": "*", "allowedRoles": [ "editor" ] } ], diff --git a/Bezalu.ProjectReporting.Web/wwwroot/css/app.css b/Bezalu.ProjectReporting.Web/wwwroot/css/app.css index 2d7b409..5dba895 100644 --- a/Bezalu.ProjectReporting.Web/wwwroot/css/app.css +++ b/Bezalu.ProjectReporting.Web/wwwroot/css/app.css @@ -185,3 +185,29 @@ code { right: unset; } } + +.markdown-content table { + border-collapse: collapse; + width: 100%; + margin: 0.5rem 0 1rem 0; +} + +.markdown-content th, +.markdown-content td { + border: 1px solid #d2d6dc; + padding: 4px 6px; + text-align: left; +} + +.markdown-content th { + background: #f6f7f9; + font-weight: 600; +} + +.markdown-content tbody tr:nth-child(odd) { + background: #fafafa; +} + +.markdown-content p { + margin: 0.25rem 0 0.5rem; +} diff --git a/docs/contract.md b/docs/contract.md index 0363a7c..5a52844 100644 --- a/docs/contract.md +++ b/docs/contract.md @@ -29,7 +29,7 @@ "schedulePerformance": "121.6%" }, "budget": { - "estimatedHours":500.0, + "budgetHours":500.0, "actualHours":550.0, "varianceHours":50.0, "estimatedCost":0, @@ -45,7 +45,7 @@ "status": "Complete", "actualStart": "2024-01-01T00:00:00Z", "actualEnd": "2024-01-05T00:00:00Z", - "estimatedHours":40.0, + "budgetHours":40.0, "actualHours":42.5, "notes": ["Initial kickoff done"], "summary": null @@ -59,7 +59,7 @@ "status": "Closed", "type": "Development", "subType": "Enhancement", - "estimatedHours":16.0, + "budgetHours":16.0, "actualHours":18.25, "notes": ["Reviewed by QA", "Minor fixes"] }