using Microsoft.EntityFrameworkCore; using Socialize.Api.Data; namespace Socialize.Api.Modules.CalendarIntegrations.Services; public class CalendarExportFeedService(AppDbContext dbContext, CalendarExportFeedBuilder feedBuilder) { public async Task BuildUserFeedAsync(Guid userId, string? userEmail, string appBaseUrl, CancellationToken ct) { string normalizedEmail = userEmail?.Trim().ToUpperInvariant() ?? string.Empty; Guid[] workspaceIds = await dbContext.Workspaces .Where(workspace => workspace.OwnerUserId == userId || dbContext.OrganizationMemberships.Any(membership => membership.OrganizationId == workspace.OrganizationId && membership.UserId == userId)) .Select(workspace => workspace.Id) .ToArrayAsync(ct); List events = []; events.AddRange(await dbContext.ContentItems .Where(item => workspaceIds.Contains(item.WorkspaceId) && item.DueDate.HasValue) .Join( dbContext.Workspaces, item => item.WorkspaceId, workspace => workspace.Id, (item, workspace) => new { item, workspace }) .Join( dbContext.Clients, itemWorkspace => itemWorkspace.item.ClientId, client => client.Id, (itemWorkspace, client) => new { itemWorkspace.item, itemWorkspace.workspace, client }) .Join( dbContext.Campaigns, itemWorkspaceClient => itemWorkspaceClient.item.CampaignId, campaign => campaign.Id, (itemWorkspaceClient, campaign) => new { itemWorkspaceClient.item, itemWorkspaceClient.workspace, itemWorkspaceClient.client, campaign }) .Select(candidate => ToContentFeedEvent( candidate.item.Id, candidate.item.Title, candidate.item.Status, candidate.item.DueDate!.Value, candidate.workspace.Name, candidate.client.Name, candidate.campaign.Name, appBaseUrl)) .ToListAsync(ct)); events.AddRange(await dbContext.ApprovalRequests .Where(approval => approval.DueAt.HasValue && (approval.RequestedByUserId == userId || (!string.IsNullOrEmpty(normalizedEmail) && approval.ReviewerEmail.ToUpper() == normalizedEmail))) .Join( dbContext.ContentItems, approval => approval.ContentItemId, item => item.Id, (approval, item) => new { approval, item }) .Where(candidate => workspaceIds.Contains(candidate.approval.WorkspaceId)) .Join( dbContext.Workspaces, approvalItem => approvalItem.approval.WorkspaceId, workspace => workspace.Id, (approvalItem, workspace) => new { approvalItem.approval, approvalItem.item, workspace }) .Select(candidate => ToApprovalFeedEvent( candidate.approval.Id, candidate.item.Id, candidate.item.Title, candidate.approval.Stage, candidate.approval.State, candidate.approval.DueAt!.Value, candidate.workspace.Name, appBaseUrl)) .ToListAsync(ct)); events.AddRange(await dbContext.Campaigns .Where(campaign => workspaceIds.Contains(campaign.WorkspaceId)) .Join( dbContext.Workspaces, campaign => campaign.WorkspaceId, workspace => workspace.Id, (campaign, workspace) => new { campaign, workspace }) .Select(candidate => ToCampaignFeedEvent( candidate.campaign.Id, candidate.campaign.Name, candidate.campaign.Status, candidate.campaign.StartDate, candidate.campaign.EndDate, candidate.workspace.Name, appBaseUrl)) .ToListAsync(ct)); return feedBuilder.Build("Socialize my work", events); } private static CalendarExportFeedEvent ToContentFeedEvent( Guid contentItemId, string title, string status, DateTimeOffset dueDate, string workspaceName, string clientName, string campaignName, string appBaseUrl) { (DateTimeOffset start, DateTimeOffset end, bool isAllDay) = NormalizeEventTime(dueDate); return new CalendarExportFeedEvent( $"content-{contentItemId}@socialize", title, start, end, isAllDay, $"Status: {status}\nWorkspace: {workspaceName}\nClient: {clientName}\nCampaign: {campaignName}", $"{appBaseUrl.TrimEnd('/')}/app/content/{contentItemId}"); } private static CalendarExportFeedEvent ToApprovalFeedEvent( Guid approvalId, Guid contentItemId, string contentTitle, string stage, string state, DateTimeOffset dueAt, string workspaceName, string appBaseUrl) { (DateTimeOffset start, DateTimeOffset end, bool isAllDay) = NormalizeEventTime(dueAt); return new CalendarExportFeedEvent( $"approval-{approvalId}@socialize", $"Approval due: {contentTitle}", start, end, isAllDay, $"Stage: {stage}\nState: {state}\nWorkspace: {workspaceName}", $"{appBaseUrl.TrimEnd('/')}/app/content/{contentItemId}"); } private static CalendarExportFeedEvent ToCampaignFeedEvent( Guid campaignId, string name, string status, DateTimeOffset startDate, DateTimeOffset endDate, string workspaceName, string appBaseUrl) { DateTimeOffset start = new(startDate.Date, startDate.Offset); DateTimeOffset end = new(endDate.Date.AddDays(1), endDate.Offset); return new CalendarExportFeedEvent( $"campaign-{campaignId}@socialize", $"Campaign: {name}", start, end <= start ? start.AddDays(1) : end, true, $"Status: {status}\nWorkspace: {workspaceName}", $"{appBaseUrl.TrimEnd('/')}/app/campaigns/{campaignId}"); } private static (DateTimeOffset Start, DateTimeOffset End, bool IsAllDay) NormalizeEventTime(DateTimeOffset value) { if (value.TimeOfDay == TimeSpan.Zero) { DateTimeOffset start = new(value.Date, value.Offset); return (start, start.AddDays(1), true); } return (value, value.AddMinutes(30), false); } }