diff --git a/colaflow-api/src/ColaFlow.API/EventHandlers/SprintEventHandlers.cs b/colaflow-api/src/ColaFlow.API/EventHandlers/SprintEventHandlers.cs
new file mode 100644
index 0000000..5654249
--- /dev/null
+++ b/colaflow-api/src/ColaFlow.API/EventHandlers/SprintEventHandlers.cs
@@ -0,0 +1,139 @@
+using MediatR;
+using ColaFlow.API.Services;
+using ColaFlow.Modules.ProjectManagement.Domain.Events;
+using Microsoft.Extensions.Logging;
+
+namespace ColaFlow.API.EventHandlers;
+
+///
+/// Handles Sprint domain events and sends SignalR notifications
+///
+public class SprintEventHandlers :
+ INotificationHandler,
+ INotificationHandler,
+ INotificationHandler,
+ INotificationHandler,
+ INotificationHandler
+{
+ private readonly IRealtimeNotificationService _notificationService;
+ private readonly ILogger _logger;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ public SprintEventHandlers(
+ IRealtimeNotificationService notificationService,
+ ILogger logger,
+ IHttpContextAccessor httpContextAccessor)
+ {
+ _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
+ }
+
+ public async Task Handle(SprintCreatedEvent notification, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var tenantId = GetCurrentTenantId();
+ await _notificationService.NotifySprintCreated(
+ tenantId,
+ notification.ProjectId,
+ notification.SprintId,
+ notification.SprintName
+ );
+ _logger.LogInformation("Sprint created notification sent: {SprintId}", notification.SprintId);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to send Sprint created notification: {SprintId}", notification.SprintId);
+ }
+ }
+
+ public async Task Handle(SprintUpdatedEvent notification, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var tenantId = GetCurrentTenantId();
+ await _notificationService.NotifySprintUpdated(
+ tenantId,
+ notification.ProjectId,
+ notification.SprintId,
+ notification.SprintName
+ );
+ _logger.LogInformation("Sprint updated notification sent: {SprintId}", notification.SprintId);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to send Sprint updated notification: {SprintId}", notification.SprintId);
+ }
+ }
+
+ public async Task Handle(SprintStartedEvent notification, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var tenantId = GetCurrentTenantId();
+ await _notificationService.NotifySprintStarted(
+ tenantId,
+ notification.ProjectId,
+ notification.SprintId,
+ notification.SprintName
+ );
+ _logger.LogInformation("Sprint started notification sent: {SprintId}", notification.SprintId);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to send Sprint started notification: {SprintId}", notification.SprintId);
+ }
+ }
+
+ public async Task Handle(SprintCompletedEvent notification, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var tenantId = GetCurrentTenantId();
+ await _notificationService.NotifySprintCompleted(
+ tenantId,
+ notification.ProjectId,
+ notification.SprintId,
+ notification.SprintName
+ );
+ _logger.LogInformation("Sprint completed notification sent: {SprintId}", notification.SprintId);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to send Sprint completed notification: {SprintId}", notification.SprintId);
+ }
+ }
+
+ public async Task Handle(SprintDeletedEvent notification, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var tenantId = GetCurrentTenantId();
+ await _notificationService.NotifySprintDeleted(
+ tenantId,
+ notification.ProjectId,
+ notification.SprintId,
+ notification.SprintName
+ );
+ _logger.LogInformation("Sprint deleted notification sent: {SprintId}", notification.SprintId);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to send Sprint deleted notification: {SprintId}", notification.SprintId);
+ }
+ }
+
+ private Guid GetCurrentTenantId()
+ {
+ var tenantIdClaim = _httpContextAccessor?.HttpContext?.User
+ .FindFirst("tenant_id")?.Value;
+
+ if (Guid.TryParse(tenantIdClaim, out var tenantId) && tenantId != Guid.Empty)
+ {
+ return tenantId;
+ }
+
+ return Guid.Empty; // Default for non-HTTP contexts
+ }
+}
diff --git a/colaflow-api/src/ColaFlow.API/Services/IRealtimeNotificationService.cs b/colaflow-api/src/ColaFlow.API/Services/IRealtimeNotificationService.cs
index c016422..b41f24d 100644
--- a/colaflow-api/src/ColaFlow.API/Services/IRealtimeNotificationService.cs
+++ b/colaflow-api/src/ColaFlow.API/Services/IRealtimeNotificationService.cs
@@ -30,6 +30,13 @@ public interface IRealtimeNotificationService
Task NotifyIssueDeleted(Guid tenantId, Guid projectId, Guid issueId);
Task NotifyIssueStatusChanged(Guid tenantId, Guid projectId, Guid issueId, string oldStatus, string newStatus);
+ // Sprint notifications
+ Task NotifySprintCreated(Guid tenantId, Guid projectId, Guid sprintId, string sprintName);
+ Task NotifySprintUpdated(Guid tenantId, Guid projectId, Guid sprintId, string sprintName);
+ Task NotifySprintStarted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName);
+ Task NotifySprintCompleted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName);
+ Task NotifySprintDeleted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName);
+
// User-level notifications
Task NotifyUser(Guid userId, string message, string type = "info");
Task NotifyUsersInTenant(Guid tenantId, string message, string type = "info");
diff --git a/colaflow-api/src/ColaFlow.API/Services/RealtimeNotificationService.cs b/colaflow-api/src/ColaFlow.API/Services/RealtimeNotificationService.cs
index 9a72106..0b652bf 100644
--- a/colaflow-api/src/ColaFlow.API/Services/RealtimeNotificationService.cs
+++ b/colaflow-api/src/ColaFlow.API/Services/RealtimeNotificationService.cs
@@ -202,6 +202,118 @@ public class RealtimeNotificationService : IRealtimeNotificationService
});
}
+ // Sprint notifications
+ public async Task NotifySprintCreated(Guid tenantId, Guid projectId, Guid sprintId, string sprintName)
+ {
+ var projectGroupName = $"project-{projectId}";
+ var tenantGroupName = $"tenant-{tenantId}";
+
+ _logger.LogInformation("Notifying sprint {SprintId} created in project {ProjectId}", sprintId, projectId);
+
+ await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintCreated", new
+ {
+ SprintId = sprintId,
+ SprintName = sprintName,
+ ProjectId = projectId,
+ Timestamp = DateTime.UtcNow
+ });
+
+ await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintCreated", new
+ {
+ SprintId = sprintId,
+ SprintName = sprintName,
+ ProjectId = projectId,
+ Timestamp = DateTime.UtcNow
+ });
+ }
+
+ public async Task NotifySprintUpdated(Guid tenantId, Guid projectId, Guid sprintId, string sprintName)
+ {
+ var projectGroupName = $"project-{projectId}";
+
+ _logger.LogInformation("Notifying sprint {SprintId} updated", sprintId);
+
+ await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintUpdated", new
+ {
+ SprintId = sprintId,
+ SprintName = sprintName,
+ ProjectId = projectId,
+ Timestamp = DateTime.UtcNow
+ });
+ }
+
+ public async Task NotifySprintStarted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName)
+ {
+ var projectGroupName = $"project-{projectId}";
+ var tenantGroupName = $"tenant-{tenantId}";
+
+ _logger.LogInformation("Notifying sprint {SprintId} started", sprintId);
+
+ await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintStarted", new
+ {
+ SprintId = sprintId,
+ SprintName = sprintName,
+ ProjectId = projectId,
+ Timestamp = DateTime.UtcNow
+ });
+
+ await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintStarted", new
+ {
+ SprintId = sprintId,
+ SprintName = sprintName,
+ ProjectId = projectId,
+ Timestamp = DateTime.UtcNow
+ });
+ }
+
+ public async Task NotifySprintCompleted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName)
+ {
+ var projectGroupName = $"project-{projectId}";
+ var tenantGroupName = $"tenant-{tenantId}";
+
+ _logger.LogInformation("Notifying sprint {SprintId} completed", sprintId);
+
+ await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintCompleted", new
+ {
+ SprintId = sprintId,
+ SprintName = sprintName,
+ ProjectId = projectId,
+ Timestamp = DateTime.UtcNow
+ });
+
+ await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintCompleted", new
+ {
+ SprintId = sprintId,
+ SprintName = sprintName,
+ ProjectId = projectId,
+ Timestamp = DateTime.UtcNow
+ });
+ }
+
+ public async Task NotifySprintDeleted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName)
+ {
+ var projectGroupName = $"project-{projectId}";
+ var tenantGroupName = $"tenant-{tenantId}";
+
+ _logger.LogInformation("Notifying sprint {SprintId} deleted", sprintId);
+
+ await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintDeleted", new
+ {
+ SprintId = sprintId,
+ SprintName = sprintName,
+ ProjectId = projectId,
+ Timestamp = DateTime.UtcNow
+ });
+
+ await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintDeleted", new
+ {
+ SprintId = sprintId,
+ SprintName = sprintName,
+ ProjectId = projectId,
+ Timestamp = DateTime.UtcNow
+ });
+ }
+
public async Task NotifyUser(Guid userId, string message, string type = "info")
{
var userConnectionId = $"user-{userId}";
diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandHandler.cs
index a5bf17a..cd7e44e 100644
--- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandHandler.cs
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Commands/UpdateSprint/UpdateSprintCommandHandler.cs
@@ -4,6 +4,7 @@ using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
+using ColaFlow.Modules.ProjectManagement.Domain.Events;
namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateSprint;
@@ -12,11 +13,13 @@ namespace ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateSprint;
///
public sealed class UpdateSprintCommandHandler(
IApplicationDbContext context,
- IUnitOfWork unitOfWork)
+ IUnitOfWork unitOfWork,
+ IMediator mediator)
: IRequestHandler
{
private readonly IApplicationDbContext _context = context ?? throw new ArgumentNullException(nameof(context));
private readonly IUnitOfWork _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
+ private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
public async Task Handle(UpdateSprintCommand request, CancellationToken cancellationToken)
{
@@ -39,6 +42,9 @@ public sealed class UpdateSprintCommandHandler(
// Save changes
await _unitOfWork.SaveChangesAsync(cancellationToken);
+ // Publish domain event
+ await _mediator.Publish(new SprintUpdatedEvent(sprint.Id.Value, sprint.Name, sprint.ProjectId.Value), cancellationToken);
+
return Unit.Value;
}
}