@model PowderCoating.Application.DTOs.Dashboard.DashboardViewModel @using Microsoft.AspNetCore.Html @using PowderCoating.Application.DTOs.Health @using PowderCoating.Web.ViewModels.Dashboard @{ ViewData["Title"] = "Dashboard"; var today = DateTime.Today; var currentMonth = DateTime.Now.ToString("MMMM yyyy"); var configHealth = ViewBag.ConfigHealth as CompanyConfigHealth; var guidedActivationBanner = ViewBag.GuidedActivationBanner as PowderCoating.Web.ViewModels.GuidedActivation.GuidedActivationBannerViewModel; var shopProgressWidget = ViewBag.ShopProgressWidget as ShopProgressWidgetViewModel; } @{ var _attnCount = Model.OverdueJobsCount + Model.OverdueInvoicesCount; }
@DateTime.Now.ToString("dddd").ToUpper() · @DateTime.Now.ToString("MMMM d").ToUpper()

@if (_attnCount > 0) { Shop is running hot — @_attnCount item@(_attnCount == 1 ? "" : "s") need attention. } else { Everything's on track. @Model.TodaysJobsCount job@(Model.TodaysJobsCount == 1 ? "" : "s") scheduled for today. }

Open Jobs Board @if (!string.IsNullOrEmpty(Model.TipOfTheDay)) { @Model.TipOfTheDay }
@await Html.PartialAsync("_Metric", (Label: "OUTSTANDING A/R", Value: Model.OutstandingAr.ToString("C0"), Delta: (string?)null, DeltaDir: (string?)null))
@await Html.PartialAsync("_Metric", (Label: "COLLECTED " + DateTime.Now.ToString("MMM").ToUpper(), Value: Model.CollectedThisMonth.ToString("C0"), Delta: (string?)null, DeltaDir: (string?)null))
@await Html.PartialAsync("_Metric", (Label: "TODAY'S JOBS", Value: Model.TodaysJobsCount.ToString(), Delta: (string?)null, DeltaDir: (string?)null))
@await Html.PartialAsync("_Metric", (Label: "ACTIVE CUSTOMERS", Value: Model.ActiveCustomersCount.ToString(), Delta: (string?)null, DeltaDir: (string?)null))
@* PWA install banner — rendered by JS only on mobile, hidden once dismissed or already installed *@
Add to Home Screen
@if (guidedActivationBanner?.Show == true) {
@guidedActivationBanner.Title
@guidedActivationBanner.Message
} @if (shopProgressWidget != null) { @await Html.PartialAsync("_ShopProgressWidget", shopProgressWidget) } @* Config health alert — only shown when there are setup gaps *@ @if (configHealth != null && !configHealth.IsHealthy) {
@if (configHealth.CriticalCount > 0) { Setup issues need attention } else { Setup incomplete } @configHealth.Issues.Count item@(configHealth.Issues.Count == 1 ? "" : "s") found
@foreach (var issue in configHealth.Issues.OrderByDescending(i => i.Severity)) { var badgeCss = issue.Severity == ConfigIssueSeverity.Critical ? "bg-danger" : issue.Severity == ConfigIssueSeverity.Warning ? "bg-warning text-dark" : "bg-secondary"; if (!string.IsNullOrEmpty(issue.FixPath)) { @issue.Title } else { @issue.Title } }
}
Today's Schedule @(Model.TodaysJobsCount + Model.TodaysAppointmentsCount) Item@(Model.TodaysJobsCount + Model.TodaysAppointmentsCount == 1 ? "" : "s")
@if (Model.TodaysJobs.Any() || Model.TodaysAppointments.Any()) {
@* Combine jobs and appointments, sorted by time *@ @{ var todaysSchedule = Model.TodaysJobs .Select(j => new { Type = "Job", Time = j.ScheduledDate ?? j.DueDate ?? DateTime.MinValue, JobData = (PowderCoating.Application.DTOs.Dashboard.DashboardJobDto?)j, ApptData = (PowderCoating.Application.DTOs.Dashboard.DashboardAppointmentDto?)null }) .Concat(Model.TodaysAppointments.Select(a => new { Type = "Appointment", Time = a.ScheduledStartTime, JobData = (PowderCoating.Application.DTOs.Dashboard.DashboardJobDto?)null, ApptData = (PowderCoating.Application.DTOs.Dashboard.DashboardAppointmentDto?)a })) .OrderBy(x => x.Time) .ToList(); } @foreach (var item in todaysSchedule) { @if (item.Type == "Job" && item.JobData != null) { var job = item.JobData;
Job @job.JobNumber @PriorityBadge(job.PriorityCode, job.PriorityDisplayName, job.PriorityColorClass) @StatusBadge(job.StatusCode, job.StatusDisplayName, job.StatusColorClass)
@job.CustomerName
@if (job.ScheduledDate.HasValue) {
@job.ScheduledDate.Value.ToString("h:mm tt")
}
} else if (item.Type == "Appointment" && item.ApptData != null) { var appt = item.ApptData;
@appt.TypeDisplayName @appt.Title
@appt.CustomerName
@if (!appt.IsAllDay) {
@appt.ScheduledStartTime.ToString("h:mm tt")
} else {
All Day
}
} }
} else {

No jobs or appointments scheduled for today

}
Bills Due @if (Model.BillsDueCount > 0) { @Model.BillsDueAmount.ToString("C0") outstanding }
View All
Financial Snapshot @currentMonth
View All
@* AR Aging mini-chart *@ @if (Model.OutstandingAr > 0) {
A/R Aging @Model.OutstandingAr.ToString("C0") total
@{ var agingTotal = Model.OutstandingAr > 0 ? Model.OutstandingAr : 1m; double PctOf(decimal v) => (double)(v / agingTotal * 100); }
@if (Model.AgingCurrent > 0) {
} @if (Model.AgingDays1To30 > 0) {
} @if (Model.AgingDays31To60 > 0) {
} @if (Model.AgingDays61To90 > 0) {
} @if (Model.AgingDaysOver90 > 0) {
}
@if (Model.AgingCurrent > 0) { Current @Model.AgingCurrent.ToString("C0") } @if (Model.AgingDays1To30 > 0) { 1–30d @Model.AgingDays1To30.ToString("C0") } @if (Model.AgingDays31To60 > 0) { 31–60d @Model.AgingDays31To60.ToString("C0") } @if (Model.AgingDays61To90 > 0) { 61–90d @Model.AgingDays61To90.ToString("C0") } @if (Model.AgingDaysOver90 > 0) { 90+d @Model.AgingDaysOver90.ToString("C0") }

} @* Recent Payments *@ @if (Model.RecentPayments.Any()) {
Recent Payments
} else if (Model.OutstandingAr == 0) {

No invoices yet this month

}
@if (Model.PowderOrdersNeeded.Any()) {
Powder in Queue to be Ordered @Model.PowderOrdersNeededCount item@(Model.PowderOrdersNeededCount == 1 ? "" : "s")
Grouped by vendor · Mark lines as ordered to remove them
@foreach (var vendorGroup in Model.PowderOrdersNeeded) {
@vendorGroup.VendorName @if (!string.IsNullOrEmpty(vendorGroup.VendorPhone)) { @vendorGroup.VendorPhone } @if (!string.IsNullOrEmpty(vendorGroup.VendorEmail)) { @vendorGroup.VendorEmail } @vendorGroup.Lines.Count line@(vendorGroup.Lines.Count == 1 ? "" : "s") @vendorGroup.TotalLbsNeeded.ToString("N1") lbs @if (vendorGroup.TotalEstCost > 0) { ~@vendorGroup.TotalEstCost.ToString("C0") }
@foreach (var line in vendorGroup.Lines) { }
Customer Color Lbs to Order Est. Cost
@line.CustomerName (@line.JobNumber) @if (!string.IsNullOrEmpty(line.ColorName)) {@line.ColorName} @if (!string.IsNullOrEmpty(line.ColorCode)) {(@line.ColorCode)} @if (!string.IsNullOrEmpty(line.Finish)) {@line.Finish} @line.LbsToOrder.ToString("N2") lbs @if (line.EstCost.HasValue) {@line.EstCost.Value.ToString("C")} else {}
Vendor Total @vendorGroup.TotalLbsNeeded.ToString("N2") lbs @(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "—")
}
}
Powder Ordered — Awaiting Receipt @Model.PowderOrdersPlacedCount item@(Model.PowderOrdersPlacedCount == 1 ? "" : "s")
Grouped by vendor · Enter lbs received to update inventory
@if (Model.PowderOrdersPlaced.Any()) { @foreach (var vendorGroup in Model.PowderOrdersPlaced) {
@vendorGroup.VendorName @if (!string.IsNullOrEmpty(vendorGroup.VendorPhone)) { @vendorGroup.VendorPhone } @vendorGroup.Lines.Count line@(vendorGroup.Lines.Count == 1 ? "" : "s") @vendorGroup.TotalLbsNeeded.ToString("N1") lbs @if (vendorGroup.TotalEstCost > 0) { ~@vendorGroup.TotalEstCost.ToString("C0") }
@foreach (var line in vendorGroup.Lines) { }
Customer Color Lbs Ordered Est. Cost Ordered Receive
@line.CustomerName (@line.JobNumber) @if (!string.IsNullOrEmpty(line.ColorName)) {@line.ColorName} @if (!string.IsNullOrEmpty(line.ColorCode)) {(@line.ColorCode)} @if (!string.IsNullOrEmpty(line.Finish)) {@line.Finish} @line.LbsToOrder.ToString("N2") lbs @if (line.EstCost.HasValue) {@line.EstCost.Value.ToString("C")} else {} @if (line.OrderedAt.HasValue) {@line.OrderedAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMM d")} else {}
Vendor Total @vendorGroup.TotalLbsNeeded.ToString("N2") lbs @(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "—")
} }
@section Scripts { } @functions { IHtmlContent PriorityBadge(string priorityCode, string displayName, string colorClass) { if (string.IsNullOrEmpty(priorityCode)) return HtmlString.Empty; var kind = colorClass switch { "danger" => "bad", "warning" => "warn", "info" => "cool", "secondary" => "neutral", _ => "ember" }; return new HtmlString($"{displayName}"); } IHtmlContent StatusBadge(string statusCode, string displayName, string colorClass) { if (string.IsNullOrEmpty(statusCode)) return HtmlString.Empty; var kind = colorClass switch { "success" => "ok", "danger" => "bad", "warning" => "warn", "info" => "cool", "primary" => "cool", _ => "neutral" }; return new HtmlString($"{displayName}"); } string GetAppointmentTypeChipKind(string colorClass) { return colorClass switch { "primary" => "cool", "success" => "ok", "info" => "cool", "warning" => "warn", "danger" => "bad", _ => "neutral" }; } }