From 39432ac588ea264b7b756246765e5e96840c7e30 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Tue, 23 Jun 2026 19:20:28 -0700 Subject: [PATCH] Add admin NotificationsController for binding, groups, history, and send --- .../Controllers/NotificationsController.cs | 95 +++++++++++++++++++ .../Notifications/NotificationRequests.cs | 7 ++ 2 files changed, 102 insertions(+) create mode 100644 API/ROLAC.API/Controllers/NotificationsController.cs create mode 100644 API/ROLAC.API/DTOs/Notifications/NotificationRequests.cs diff --git a/API/ROLAC.API/Controllers/NotificationsController.cs b/API/ROLAC.API/Controllers/NotificationsController.cs new file mode 100644 index 0000000..972d686 --- /dev/null +++ b/API/ROLAC.API/Controllers/NotificationsController.cs @@ -0,0 +1,95 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using ROLAC.API.Data; +using ROLAC.API.DTOs.Notifications; +using ROLAC.API.Services.Logging; +using ROLAC.API.Services.Notifications; + +namespace ROLAC.API.Controllers; + +/// +/// Admin endpoints for the notification module (API-only phase). Binding-code generation, group +/// management, send history, and manual send — the manual send endpoints are the only way to fire +/// a message before a UI exists; programmatic callers use the services directly. +/// +[ApiController] +[Route("api/notifications")] +[Authorize] +public sealed class NotificationsController : ControllerBase +{ + private readonly IEmailService _email; + private readonly ILineNotificationService _line; + private readonly AppDbContext _db; + private readonly CurrentUserAccessor _currentUser; + + public NotificationsController( + IEmailService email, ILineNotificationService line, + AppDbContext db, CurrentUserAccessor currentUser) + { + _email = email; + _line = line; + _db = db; + _currentUser = currentUser; + } + + [HttpPost("members/{id:int}/line-binding-code")] + public async Task GenerateBindingCode(int id, CancellationToken ct) + => Ok(new { code = await _line.GenerateLineBindingCodeAsync(id, ct) }); + + [HttpGet("groups")] + public async Task Groups(CancellationToken ct) + => Ok(await _db.MessagingGroups + .OrderBy(g => g.Id) + .Select(g => new { g.Id, g.Name, g.IsActive, g.RegisteredAt }) + .ToListAsync(ct)); + + [HttpPut("groups/{id:int}")] + public async Task UpdateGroup(int id, [FromBody] UpdateGroupRequest request, CancellationToken ct) + { + var group = await _db.MessagingGroups.FirstOrDefaultAsync(g => g.Id == id, ct); + if (group is null) return NotFound(); + + group.Name = request.Name; + group.IsActive = request.IsActive; + await _db.SaveChangesAsync(ct); + return NoContent(); + } + + [HttpGet("history")] + public async Task History( + [FromQuery] int page = 1, [FromQuery] int pageSize = 50, CancellationToken ct = default) + { + var size = Math.Clamp(pageSize, 1, 200); + var skip = (Math.Max(page, 1) - 1) * size; + + var query = _db.NotificationLogs.OrderByDescending(l => l.SentAt); + var total = await query.CountAsync(ct); + var items = await query + .Skip(skip).Take(size) + .Select(l => new + { + l.Id, l.Channel, l.TargetType, l.TargetExternalId, l.Subject, + l.Status, l.Error, l.SentByUserId, l.SentAt, + }) + .ToListAsync(ct); + + return Ok(new { total, items }); + } + + [HttpPost("send-line")] + public async Task SendLine([FromBody] SendLineRequest request, CancellationToken ct) + => Ok(await _line.SendLineAsync( + request.Body, request.MemberIds ?? [], request.GroupIds ?? [], + _currentUser.UserIdOrSystem, ct)); + + [HttpPost("send-email")] + public async Task SendEmail([FromBody] SendEmailRequest request, CancellationToken ct) + => Ok(await _email.SendAsync(new EmailMessage( + MemberIds: request.MemberIds ?? [], + Addresses: request.Addresses ?? [], + Subject: request.Subject, + HtmlBody: request.HtmlBody, + Attachments: null, + SentByUserId: _currentUser.UserIdOrSystem), ct)); +} diff --git a/API/ROLAC.API/DTOs/Notifications/NotificationRequests.cs b/API/ROLAC.API/DTOs/Notifications/NotificationRequests.cs new file mode 100644 index 0000000..903954f --- /dev/null +++ b/API/ROLAC.API/DTOs/Notifications/NotificationRequests.cs @@ -0,0 +1,7 @@ +namespace ROLAC.API.DTOs.Notifications; + +public sealed record UpdateGroupRequest(string? Name, bool IsActive); + +public sealed record SendLineRequest(string Body, int[]? MemberIds, int[]? GroupIds); + +public sealed record SendEmailRequest(string Subject, string HtmlBody, int[]? MemberIds, string[]? Addresses);