@using TaskTrackingSystem.WebApp.Components.Shared.Charts
@if (!Items.Any()) {
@EmptyText
} else {
@foreach (var month in Months) { @month.ToString("MMM") }
@foreach (var item in Bars) {
@item.Label
}
}
@code { [Parameter] public IReadOnlyList Items { get; set; } = Array.Empty(); [Parameter] public string EmptyText { get; set; } = "No timeline data available."; [Parameter] public string WrapperClass { get; set; } = "overflow-auto"; private IReadOnlyList Months => BuildMonths(); private string GridStyle => $"grid-template-columns: 180px repeat({Math.Max(Months.Count, 1)}, minmax(56px, 1fr));"; private IReadOnlyList Bars => BuildBars(); private IReadOnlyList BuildMonths() { if (!Items.Any()) { return Array.Empty(); } var start = new DateTime(Items.Min(item => item.StartDate).Year, Items.Min(item => item.StartDate).Month, 1); var end = new DateTime(Items.Max(item => item.EndDate).Year, Items.Max(item => item.EndDate).Month, 1); var months = new List(); for (var current = start; current <= end; current = current.AddMonths(1)) { months.Add(current); } return months; } private IReadOnlyList BuildBars() { if (!Months.Any()) { return Array.Empty(); } var first = Months.First(); var slots = Months.Count; return Items.Select(item => { var startIndex = ((item.StartDate.Year - first.Year) * 12) + item.StartDate.Month - first.Month; var endIndex = ((item.EndDate.Year - first.Year) * 12) + item.EndDate.Month - first.Month; startIndex = Math.Clamp(startIndex, 0, slots - 1); endIndex = Math.Clamp(endIndex, startIndex, slots - 1); return new TimelineBar( item.Label, startIndex / (double)slots * 100, Math.Max(2, (endIndex - startIndex + 1) / (double)slots * 100), Math.Clamp(item.CompletionPercentage, 0, 100), item.Color); }).ToList(); } private sealed record TimelineBar(string Label, double Left, double Width, double CompletionPercentage, string Color); }