feature: Draft Data Entry

This commit is contained in:
voltsrage
2026-06-26 05:04:39 +08:00
parent f74f8f471f
commit 7121520926
20 changed files with 2033 additions and 2 deletions
@@ -0,0 +1,151 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
/// <summary>
/// Draft data entry endpoints for digitization batches. A data entry clerk
/// uses these endpoints to transcribe scanned paper charts into structured data.
/// </summary>
[ApiController]
[Route("api/v1/digitization-batches/{batchId:guid}/draft")]
[Produces("application/json")]
[Authorize]
public class DraftController : ControllerBase
{
private readonly IDraftService _draft;
public DraftController(IDraftService draft) => _draft = draft;
/// <summary>
/// Returns the full draft payload for a batch, including patient demographics,
/// encounter context, and all observation rows entered so far.
/// </summary>
/// <param name="batchId">Digitization batch id.</param>
/// <returns>Complete draft data for the batch.</returns>
[HttpGet]
[ProducesResponseType(typeof(ApiResponse<DraftPayloadResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetDraft(Guid batchId)
{
var result = await _draft.GetDraftAsync(batchId);
return Ok(ApiResponse<DraftPayloadResponse>.Ok(result));
}
/// <summary>
/// Upserts draft patient demographics for a batch. Creates the patient record
/// on the first call; updates it on subsequent calls. Automatically transitions
/// the batch to IN_ENTRY on first save.
/// </summary>
/// <param name="batchId">Digitization batch id.</param>
/// <param name="req">Patient demographic fields.</param>
/// <returns>The upserted patient record.</returns>
[HttpPut("patient")]
[ProducesResponseType(typeof(ApiResponse<DraftPatientDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
public async Task<IActionResult> UpsertPatient(
Guid batchId, [FromBody] UpsertDraftPatientRequest req)
{
var actorUserId = GetCurrentUserId();
var result = await _draft.UpsertPatientAsync(batchId, req, actorUserId);
return Ok(ApiResponse<DraftPatientDto>.Ok(result));
}
/// <summary>
/// Upserts draft encounter fields for a batch. Creates the encounter record
/// on the first call; updates it on subsequent calls.
/// </summary>
/// <param name="batchId">Digitization batch id.</param>
/// <param name="req">Encounter context fields.</param>
/// <returns>The upserted encounter record.</returns>
[HttpPut("encounter")]
[ProducesResponseType(typeof(ApiResponse<DraftEncounterDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
public async Task<IActionResult> UpsertEncounter(
Guid batchId, [FromBody] UpsertDraftEncounterRequest req)
{
var actorUserId = GetCurrentUserId();
var result = await _draft.UpsertEncounterAsync(batchId, req, actorUserId);
return Ok(ApiResponse<DraftEncounterDto>.Ok(result));
}
/// <summary>
/// Adds a new observation row to the batch draft. Validates the value against
/// plausibility ranges before saving.
/// </summary>
/// <param name="batchId">Digitization batch id.</param>
/// <param name="req">Observation data to add.</param>
/// <returns>The created observation record.</returns>
[HttpPost("observations")]
[ProducesResponseType(typeof(ApiResponse<DraftObservationDto>), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> AddObservation(
Guid batchId, [FromBody] CreateDraftObservationRequest req)
{
var actorUserId = GetCurrentUserId();
var result = await _draft.AddObservationAsync(batchId, req, actorUserId);
return StatusCode(201, ApiResponse<DraftObservationDto>.Created(result));
}
/// <summary>
/// Updates an existing observation row in the batch draft. Re-validates the
/// new value against plausibility ranges.
/// </summary>
/// <param name="batchId">Digitization batch id.</param>
/// <param name="obsId">Observation id to update.</param>
/// <param name="req">Updated observation data.</param>
/// <returns>The updated observation record.</returns>
[HttpPut("observations/{obsId:guid}")]
[ProducesResponseType(typeof(ApiResponse<DraftObservationDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> UpdateObservation(
Guid batchId, Guid obsId, [FromBody] UpdateDraftObservationRequest req)
{
var actorUserId = GetCurrentUserId();
var result = await _draft.UpdateObservationAsync(batchId, obsId, req, actorUserId);
return Ok(ApiResponse<DraftObservationDto>.Ok(result));
}
/// <summary>
/// Removes an observation row from the batch draft.
/// </summary>
/// <param name="batchId">Digitization batch id.</param>
/// <param name="obsId">Observation id to delete.</param>
[HttpDelete("observations/{obsId:guid}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
public async Task<IActionResult> DeleteObservation(Guid batchId, Guid obsId)
{
var actorUserId = GetCurrentUserId();
await _draft.DeleteObservationAsync(batchId, obsId, actorUserId);
return NoContent();
}
/// <summary>
/// Validates completeness per batch type and transitions the batch from
/// IN_ENTRY to PENDING_VERIFICATION. Returns 422 if required fields are missing.
/// </summary>
/// <param name="batchId">Digitization batch id.</param>
/// <returns>The updated batch record.</returns>
[HttpPost("~/api/v1/digitization-batches/{batchId:guid}/submit-for-verification")]
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> SubmitForVerification(Guid batchId)
{
var actorUserId = GetCurrentUserId();
var batch = await _draft.SubmitForVerificationAsync(batchId, actorUserId);
return Ok(ApiResponse<BatchDetailResponse>.Ok(BatchDetailResponse.FromEntity(batch)));
}
private Guid GetCurrentUserId() =>
Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
}