using Campaign_Tracker.Server.Municipalities; namespace Campaign_Tracker.Server.ElectionCycles; public interface IElectionCycleKanbanReadModel { Task GetAsync( CancellationToken cancellationToken = default); } public sealed class ElectionCycleKanbanReadModel : IElectionCycleKanbanReadModel { public const string UnassignedCycleId = "__unassigned__"; public const string UnassignedCycleName = "Unassigned"; private readonly IMunicipalityProfileRepository _profiles; private readonly IElectionCycleJobRepository _jobs; public ElectionCycleKanbanReadModel( IMunicipalityProfileRepository profiles, IElectionCycleJobRepository jobs) { _profiles = profiles; _jobs = jobs; } public async Task GetAsync( CancellationToken cancellationToken = default) { var profiles = await _profiles.GetAllAsync(cancellationToken); var activeJobs = (await _jobs.GetAllAsync(cancellationToken)) .Where(job => job.IsActive) .ToArray(); var jobsByJCode = activeJobs .GroupBy(job => job.JCode, StringComparer.OrdinalIgnoreCase) .ToDictionary(group => group.Key, group => group.ToArray(), StringComparer.OrdinalIgnoreCase); var cardsByCycle = new Dictionary>( StringComparer.OrdinalIgnoreCase); var lanesByCycle = activeJobs .GroupBy(job => job.CycleId, StringComparer.OrdinalIgnoreCase) .ToDictionary( group => group.Key, group => new ElectionCycleLaneHeader( group.Key, group.Select(job => job.CycleName) .OrderBy(name => name, StringComparer.OrdinalIgnoreCase) .First()), StringComparer.OrdinalIgnoreCase); var unassignedCards = new List(); var matchedJobIds = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var profile in profiles) { if (!jobsByJCode.TryGetValue(profile.Profile.JCode, out var profileJobs)) { unassignedCards.Add(BuildUnassignedCard(profile)); continue; } foreach (var job in profileJobs.OrderBy(job => job.CycleName, StringComparer.OrdinalIgnoreCase)) { if (!cardsByCycle.TryGetValue(job.CycleId, out var cycleCards)) { cycleCards = []; cardsByCycle[job.CycleId] = cycleCards; } cycleCards.Add(BuildAssignedCard(profile, job)); matchedJobIds.Add(job.JobId); } } foreach (var orphan in activeJobs.Where(job => !matchedJobIds.Contains(job.JobId))) { if (!cardsByCycle.TryGetValue(orphan.CycleId, out var cycleCards)) { cycleCards = []; cardsByCycle[orphan.CycleId] = cycleCards; } cycleCards.Add(BuildOrphanedCard(orphan)); } var lanes = cardsByCycle .OrderBy(pair => lanesByCycle[pair.Key].CycleName, StringComparer.OrdinalIgnoreCase) .Select(pair => new ElectionCycleKanbanLane( pair.Key, lanesByCycle[pair.Key].CycleName, SortCards(pair.Value))) .Append(new ElectionCycleKanbanLane( UnassignedCycleId, UnassignedCycleName, SortCards(unassignedCards))) .ToArray(); return new ElectionCycleKanbanBoard(lanes); } private static IReadOnlyList SortCards( IReadOnlyList cards) => cards .OrderBy(card => card.MunicipalityName, StringComparer.OrdinalIgnoreCase) .ThenBy(card => card.JCode, StringComparer.OrdinalIgnoreCase) .ToArray(); private static ElectionCycleKanbanCard BuildAssignedCard( MunicipalityProfileView profile, ElectionCycleJobAssignment job) => new( CardId: job.JobId, MunicipalityName: MunicipalityName(profile), JCode: profile.Profile.JCode, CycleId: job.CycleId, CycleName: job.CycleName, CycleJobStatus: job.Status, LegacyJoinKey: profile.Profile.JCode, QuickOpenHref: $"/election-cycles/jobs/{job.JobId}"); private static ElectionCycleKanbanCard BuildOrphanedCard( ElectionCycleJobAssignment job) => new( CardId: job.JobId, MunicipalityName: $"(unmapped JCode {job.JCode})", JCode: job.JCode, CycleId: job.CycleId, CycleName: job.CycleName, CycleJobStatus: job.Status, LegacyJoinKey: job.JCode, QuickOpenHref: $"/election-cycles/jobs/{job.JobId}"); private static ElectionCycleKanbanCard BuildUnassignedCard( MunicipalityProfileView profile) => new( CardId: $"unassigned-{profile.Profile.JCode}", MunicipalityName: MunicipalityName(profile), JCode: profile.Profile.JCode, CycleId: UnassignedCycleId, CycleName: UnassignedCycleName, CycleJobStatus: UnassignedCycleName, LegacyJoinKey: profile.Profile.JCode, QuickOpenHref: $"/election-cycles/jobs/new?jCode={Uri.EscapeDataString(profile.Profile.JCode)}"); private static string MunicipalityName(MunicipalityProfileView profile) => profile.Profile.DisplayName ?? profile.LegacyName ?? profile.Profile.JCode; private sealed record ElectionCycleLaneHeader(string CycleId, string CycleName); } public sealed record ElectionCycleKanbanBoard( IReadOnlyList Lanes); public sealed record ElectionCycleKanbanLane( string CycleId, string CycleName, IReadOnlyList Cards); public sealed record ElectionCycleKanbanCard( string CardId, string MunicipalityName, string JCode, string CycleId, string CycleName, string CycleJobStatus, string LegacyJoinKey, string QuickOpenHref);