using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using ROLAC.API.DTOs.Giving; using ROLAC.API.DTOs.Members; using ROLAC.API.Hubs; using ROLAC.API.Services; namespace ROLAC.API.Controllers; /// /// Anonymous endpoints powering the mobile Sunday offering-entry page. The page /// has no login yet, so it cannot reach the auth-gated members/categories/ /// offering-sessions APIs — these expose just what it needs (active categories, /// a name-only member typeahead, and append-one-line). /// [ApiController] [Route("api/offering-entry")] [AllowAnonymous] public class OfferingEntryController : ControllerBase { private readonly IOfferingSessionService _sessions; private readonly IGivingCategoryService _categories; private readonly IMemberService _members; private readonly IHubContext _hub; public OfferingEntryController( IOfferingSessionService sessions, IGivingCategoryService categories, IMemberService members, IHubContext hub) { _sessions = sessions; _categories = categories; _members = members; _hub = hub; } // Seed the page in one round-trip: active categories + today's session state. [HttpGet("bootstrap")] public async Task Bootstrap([FromQuery] DateOnly date) => Ok(new OfferingEntryBootstrapDto { SessionDate = date.ToString("yyyy-MM-dd"), Categories = await _categories.GetAllAsync(false), Summary = await _sessions.GetEntrySummaryAsync(date), }); // Name-only member suggestions for the giver typeahead. [HttpGet("members")] public async Task SearchMembers([FromQuery] string? search, [FromQuery] int take = 10) => Ok(await _sessions.SearchMembersForEntryAsync(search, Math.Clamp(take, 1, 25))); // Quick-add a giver who isn't on file yet (created as a Visitor). Reuses the // member service directly — role checks live on MembersController, so this // anonymous path is the intended public entry point for the mobile page. [HttpPost("members")] public async Task QuickAddMember([FromBody] QuickAddMemberRequest request) { var id = await _members.CreateAsync(new CreateMemberRequest { FirstName_en = request.FirstName_en, LastName_en = request.LastName_en, NickName = request.NickName, FirstName_zh = request.FirstName_zh, LastName_zh = request.LastName_zh, PhoneCell = request.PhoneCell, Status = "Visitor", Country = "USA", LanguagePreference = "en", }); return Ok(new MemberTypeaheadDto { Id = id, NickName = request.NickName, FirstName_en = request.FirstName_en, LastName_en = request.LastName_en, }); } // Append one offering line to the date's session (find-or-create), then // broadcast it to everyone viewing that date. [HttpPost("lines")] public async Task AppendLine([FromBody] AppendOfferingLineRequest request) { var result = await _sessions.AppendLineAsync(request.Date, request.Line); await _hub.Clients.Group(result.SessionDate).SendAsync("LineAdded", result); return Ok(result); } // ── Paper-proof PDF for the date's session (merged client-side) ────────── // Date-keyed so the anonymous page (which has no session id) can attach the // count sheet / envelope photos. Mirrors OfferingSessionsController's proof // validation; the desktop session page reviews/deletes the result. [HttpGet("proof")] public async Task GetProof([FromQuery] DateOnly date) { var result = await _sessions.OpenProofForDateAsync(date); if (result is null) return NoContent(); // no session/proof yet — client merges nothing return File(result.Value.stream, result.Value.contentType); } [HttpPost("proof")] [RequestSizeLimit(52_428_800)] // 50 MB — a merged multi-image PDF is larger than one receipt public async Task UploadProof([FromForm] DateOnly date, IFormFile file) { if (file is null || file.Length == 0) return BadRequest(new { message = "No file." }); if (file.ContentType != "application/pdf") return BadRequest(new { message = "Proof must be a PDF." }); await using var stream = file.OpenReadStream(); await _sessions.SaveProofForDateAsync(date, stream, file.FileName); return NoContent(); } }