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"]
}