Files
ROLAC/API/ROLAC.API/Controllers/OfferingEntryController.cs
2026-06-24 11:35:34 -07:00

115 lines
4.8 KiB
C#

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;
/// <summary>
/// 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).
/// </summary>
[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<OfferingEntryHub> _hub;
public OfferingEntryController(
IOfferingSessionService sessions,
IGivingCategoryService categories,
IMemberService members,
IHubContext<OfferingEntryHub> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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,
Entity = request.Entity,
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,
Entity = request.Entity,
});
}
// 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}