using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tickets; public sealed record TicketImportResult(int Examined, int Created); /// /// Holt das Board des verknüpften Ticket-Projekts und legt für jedes offene, dem Token-Inhaber /// zugewiesene Ticket einen Idle-Task an. Bereits importierte Tickets werden übersprungen und /// NICHT aktualisiert — ein erneuter Import würde sonst Titel/Beschreibung überschreiben und /// damit die am Task gemachten Notizen zerstören. /// public sealed class TicketImportService { private const int StatusOffen = 1; private readonly TicketClientFactory _clients; private readonly IDbContextFactory _dbFactory; private readonly ILogger _logger; public TicketImportService( TicketClientFactory clients, IDbContextFactory dbFactory, ILogger logger) { _clients = clients; _dbFactory = dbFactory; _logger = logger; } public async Task ImportAsync(string listId, CancellationToken ct) { var client = _clients.Create() ?? throw new TicketApiException("Ticketsystem ist nicht eingerichtet (Base-URL oder Token fehlt)."); await using var ctx = await _dbFactory.CreateDbContextAsync(ct); var config = await ctx.ListConfigs.AsNoTracking() .FirstOrDefaultAsync(c => c.ListId == listId, ct); var projectId = config?.TicketProjectId ?? throw new TicketApiException("Diese Liste ist mit keinem Ticket-Projekt verknüpft."); var identity = await client.GetIdentityAsync(ct); var board = await client.GetProjectBoardAsync(projectId, ct); var mine = board .Where(t => t.StatusId == StatusOffen) .Where(t => string.Equals(t.AssigneeName, identity.UserName, StringComparison.OrdinalIgnoreCase)) .ToList(); var known = await ctx.Tasks.AsNoTracking() .Where(t => t.ListId == listId && t.TicketRef != null) .Select(t => t.TicketRef!) .ToListAsync(ct); var knownSet = known.ToHashSet(StringComparer.OrdinalIgnoreCase); var repo = new TaskRepository(ctx); var created = 0; foreach (var ticket in mine) { var reference = $"bandel:{ticket.Id}"; if (!knownSet.Add(reference)) continue; await repo.AddAsync(new TaskEntity { Id = Guid.NewGuid().ToString(), ListId = listId, Title = ticket.Title, Description = BuildDescription(ticket), Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, TicketRef = reference, CreatedBy = "ticketsystem", }, ct); created++; } _logger.LogInformation( "Ticket-Import für Liste {list_id}: {examined} geprüft, {created} neue Tasks", listId, board.Count, created); return new TicketImportResult(board.Count, created); } private static string BuildDescription(TicketBoardItem ticket) { var body = string.IsNullOrWhiteSpace(ticket.Description) ? "" : ticket.Description.Trim() + "\n\n"; return body + $"Ticket #{ticket.Id}"; } }