feature: Draft Data Entry
This commit is contained in:
@@ -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)!);
|
||||
}
|
||||
Reference in New Issue
Block a user