Skip to content

Commit d94f859

Browse files
committed
Add admin Communications page for sending emails to users
1 parent a21f7e6 commit d94f859

9 files changed

Lines changed: 576 additions & 82 deletions

File tree

cloud/src/LrmCloud.Api/Controllers/AdminController.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,4 +368,45 @@ public async Task<ActionResult<ApiResponse<AdminOrganizationDetailDto>>> Transfe
368368
var org = await _adminService.GetOrganizationAsync(id);
369369
return Success(org!);
370370
}
371+
372+
// ===== Communications Endpoints =====
373+
374+
/// <summary>
375+
/// Get count of potential email recipients based on filters.
376+
/// </summary>
377+
[HttpGet("email/recipient-count")]
378+
[ProducesResponseType(typeof(ApiResponse<int>), StatusCodes.Status200OK)]
379+
public async Task<ActionResult<ApiResponse<int>>> GetEmailRecipientCount(
380+
[FromQuery] AdminEmailRecipientType recipientType,
381+
[FromQuery] string? planFilter,
382+
[FromQuery] bool? emailVerifiedFilter)
383+
{
384+
var count = await _adminService.GetEmailRecipientCountAsync(recipientType, planFilter, emailVerifiedFilter);
385+
return Success(count);
386+
}
387+
388+
/// <summary>
389+
/// Send email to users.
390+
/// </summary>
391+
[HttpPost("email/send")]
392+
[ProducesResponseType(typeof(ApiResponse<AdminSendEmailResultDto>), StatusCodes.Status200OK)]
393+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
394+
public async Task<ActionResult<ApiResponse<AdminSendEmailResultDto>>> SendEmail([FromBody] AdminSendEmailDto dto)
395+
{
396+
if (string.IsNullOrWhiteSpace(dto.Subject))
397+
return BadRequest(ErrorCodes.VAL_INVALID_INPUT, "Subject is required");
398+
399+
if (string.IsNullOrWhiteSpace(dto.HtmlBody))
400+
return BadRequest(ErrorCodes.VAL_INVALID_INPUT, "Message body is required");
401+
402+
if (dto.RecipientType == AdminEmailRecipientType.SingleUser && !dto.UserId.HasValue)
403+
return BadRequest(ErrorCodes.VAL_INVALID_INPUT, "User ID is required for single user emails");
404+
405+
var result = await _adminService.SendEmailAsync(dto);
406+
407+
if (!result.Success)
408+
return BadRequest(ErrorCodes.VAL_INVALID_INPUT, result.Error ?? "Failed to send email");
409+
410+
return Success(result);
411+
}
371412
}

cloud/src/LrmCloud.Api/Services/AdminService.cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class AdminService : IAdminService
1919
private readonly CloudConfiguration _config;
2020
private readonly IDistributedCache _cache;
2121
private readonly IMinioClient _minio;
22+
private readonly IMailService _mailService;
2223
private readonly ILogger<AdminService> _logger;
2324
private static readonly DateTime _startTime = DateTime.UtcNow;
2425

@@ -27,12 +28,14 @@ public AdminService(
2728
CloudConfiguration config,
2829
IDistributedCache cache,
2930
IMinioClient minio,
31+
IMailService mailService,
3032
ILogger<AdminService> logger)
3133
{
3234
_db = db;
3335
_config = config;
3436
_cache = cache;
3537
_minio = minio;
38+
_mailService = mailService;
3639
_logger = logger;
3740
}
3841

@@ -1132,4 +1135,110 @@ public async Task<BulkActionResult> BulkResetUsageAsync(BulkActionRequest reques
11321135

11331136
return (true, null);
11341137
}
1138+
1139+
// ===== Communications Methods =====
1140+
1141+
public async Task<int> GetEmailRecipientCountAsync(AdminEmailRecipientType recipientType, string? planFilter, bool? emailVerifiedFilter)
1142+
{
1143+
return recipientType switch
1144+
{
1145+
AdminEmailRecipientType.SingleUser => 1,
1146+
AdminEmailRecipientType.FilteredUsers => await GetFilteredUserCountAsync(planFilter, emailVerifiedFilter),
1147+
AdminEmailRecipientType.AllUsers => await _db.Users.CountAsync(u => u.DeletedAt == null && u.Email != null),
1148+
_ => 0
1149+
};
1150+
}
1151+
1152+
private async Task<int> GetFilteredUserCountAsync(string? planFilter, bool? emailVerifiedFilter)
1153+
{
1154+
var query = _db.Users.Where(u => u.DeletedAt == null && u.Email != null);
1155+
1156+
if (!string.IsNullOrWhiteSpace(planFilter))
1157+
query = query.Where(u => u.Plan == planFilter.ToLowerInvariant());
1158+
1159+
if (emailVerifiedFilter.HasValue)
1160+
query = query.Where(u => u.EmailVerified == emailVerifiedFilter.Value);
1161+
1162+
return await query.CountAsync();
1163+
}
1164+
1165+
public async Task<AdminSendEmailResultDto> SendEmailAsync(AdminSendEmailDto dto)
1166+
{
1167+
try
1168+
{
1169+
var recipients = await GetRecipientsAsync(dto);
1170+
1171+
if (recipients.Count == 0)
1172+
{
1173+
return new AdminSendEmailResultDto
1174+
{
1175+
Success = false,
1176+
RecipientCount = 0,
1177+
Error = "No recipients found matching the criteria"
1178+
};
1179+
}
1180+
1181+
// Send emails (fire and forget - no tracking)
1182+
var sendTasks = recipients.Select(email =>
1183+
_mailService.SendEmailAsync(email, dto.Subject, dto.HtmlBody));
1184+
1185+
await Task.WhenAll(sendTasks);
1186+
1187+
_logger.LogInformation("Admin sent email to {RecipientCount} recipients. Subject: {Subject}",
1188+
recipients.Count, dto.Subject);
1189+
1190+
return new AdminSendEmailResultDto
1191+
{
1192+
Success = true,
1193+
RecipientCount = recipients.Count
1194+
};
1195+
}
1196+
catch (Exception ex)
1197+
{
1198+
_logger.LogError(ex, "Failed to send admin email. Subject: {Subject}", dto.Subject);
1199+
return new AdminSendEmailResultDto
1200+
{
1201+
Success = false,
1202+
RecipientCount = 0,
1203+
Error = ex.Message
1204+
};
1205+
}
1206+
}
1207+
1208+
private async Task<List<string>> GetRecipientsAsync(AdminSendEmailDto dto)
1209+
{
1210+
switch (dto.RecipientType)
1211+
{
1212+
case AdminEmailRecipientType.SingleUser:
1213+
if (!dto.UserId.HasValue)
1214+
return new List<string>();
1215+
1216+
var user = await _db.Users
1217+
.Where(u => u.Id == dto.UserId.Value && u.DeletedAt == null && u.Email != null)
1218+
.Select(u => u.Email)
1219+
.FirstOrDefaultAsync();
1220+
1221+
return user != null ? new List<string> { user } : new List<string>();
1222+
1223+
case AdminEmailRecipientType.FilteredUsers:
1224+
var filteredQuery = _db.Users.Where(u => u.DeletedAt == null && u.Email != null);
1225+
1226+
if (!string.IsNullOrWhiteSpace(dto.PlanFilter))
1227+
filteredQuery = filteredQuery.Where(u => u.Plan == dto.PlanFilter.ToLowerInvariant());
1228+
1229+
if (dto.EmailVerifiedFilter.HasValue)
1230+
filteredQuery = filteredQuery.Where(u => u.EmailVerified == dto.EmailVerifiedFilter.Value);
1231+
1232+
return await filteredQuery.Select(u => u.Email!).ToListAsync();
1233+
1234+
case AdminEmailRecipientType.AllUsers:
1235+
return await _db.Users
1236+
.Where(u => u.DeletedAt == null && u.Email != null)
1237+
.Select(u => u.Email!)
1238+
.ToListAsync();
1239+
1240+
default:
1241+
return new List<string>();
1242+
}
1243+
}
11351244
}

cloud/src/LrmCloud.Api/Services/IAdminService.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,16 @@ public interface IAdminService
107107
/// Transfer organization ownership.
108108
/// </summary>
109109
Task<(bool Success, string? ErrorMessage)> TransferOrganizationOwnershipAsync(int orgId, AdminTransferOwnershipRequest request);
110+
111+
// ===== Communications Methods =====
112+
113+
/// <summary>
114+
/// Get count of email recipients based on filters.
115+
/// </summary>
116+
Task<int> GetEmailRecipientCountAsync(AdminEmailRecipientType recipientType, string? planFilter, bool? emailVerifiedFilter);
117+
118+
/// <summary>
119+
/// Send email to users.
120+
/// </summary>
121+
Task<AdminSendEmailResultDto> SendEmailAsync(AdminSendEmailDto dto);
110122
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace LrmCloud.Shared.DTOs.Admin;
2+
3+
public class AdminSendEmailDto
4+
{
5+
public AdminEmailRecipientType RecipientType { get; set; }
6+
public int? UserId { get; set; }
7+
public string? PlanFilter { get; set; }
8+
public bool? EmailVerifiedFilter { get; set; }
9+
public required string Subject { get; set; }
10+
public required string HtmlBody { get; set; }
11+
}
12+
13+
public enum AdminEmailRecipientType
14+
{
15+
SingleUser,
16+
FilteredUsers,
17+
AllUsers
18+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace LrmCloud.Shared.DTOs.Admin;
2+
3+
public class AdminSendEmailResultDto
4+
{
5+
public bool Success { get; set; }
6+
public int RecipientCount { get; set; }
7+
public string? Error { get; set; }
8+
}

cloud/src/LrmCloud.Web/Layout/MainLayout.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
<RadzenPanelMenuItem Text="Organizations" Path="admin/organizations" Icon="business" />
8585
<RadzenPanelMenuItem Text="System Health" Path="admin/health" Icon="health_and_safety" />
8686
<RadzenPanelMenuItem Text="Logs" Path="admin/logs" Icon="article" />
87+
<RadzenPanelMenuItem Text="Communications" Path="admin/communications" Icon="mail" />
8788
</RadzenPanelMenuItem>
8889
}
8990
</Authorized>

0 commit comments

Comments
 (0)