using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
///
/// Draft data entry endpoints for digitization batches. A data entry clerk
/// uses these endpoints to transcribe scanned paper charts into structured data.
///
[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;
///
/// Returns the full draft payload for a batch, including patient demographics,
/// encounter context, and all observation rows entered so far.
///
/// Digitization batch id.
/// Complete draft data for the batch.
[HttpGet]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
public async Task GetDraft(Guid batchId)
{
var result = await _draft.GetDraftAsync(batchId);
return Ok(ApiResponse.Ok(result));
}
///
/// 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.
///
/// Digitization batch id.
/// Patient demographic fields.
/// The upserted patient record.
[HttpPut("patient")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)]
public async Task UpsertPatient(
Guid batchId, [FromBody] UpsertDraftPatientRequest req)
{
var actorUserId = GetCurrentUserId();
var result = await _draft.UpsertPatientAsync(batchId, req, actorUserId);
return Ok(ApiResponse.Ok(result));
}
///
/// Upserts draft encounter fields for a batch. Creates the encounter record
/// on the first call; updates it on subsequent calls.
///
/// Digitization batch id.
/// Encounter context fields.
/// The upserted encounter record.
[HttpPut("encounter")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)]
public async Task UpsertEncounter(
Guid batchId, [FromBody] UpsertDraftEncounterRequest req)
{
var actorUserId = GetCurrentUserId();
var result = await _draft.UpsertEncounterAsync(batchId, req, actorUserId);
return Ok(ApiResponse.Ok(result));
}
///
/// Adds a new observation row to the batch draft. Validates the value against
/// plausibility ranges before saving.
///
/// Digitization batch id.
/// Observation data to add.
/// The created observation record.
[HttpPost("observations")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status422UnprocessableEntity)]
public async Task AddObservation(
Guid batchId, [FromBody] CreateDraftObservationRequest req)
{
var actorUserId = GetCurrentUserId();
var result = await _draft.AddObservationAsync(batchId, req, actorUserId);
return StatusCode(201, ApiResponse.Created(result));
}
///
/// Updates an existing observation row in the batch draft. Re-validates the
/// new value against plausibility ranges.
///
/// Digitization batch id.
/// Observation id to update.
/// Updated observation data.
/// The updated observation record.
[HttpPut("observations/{obsId:guid}")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status422UnprocessableEntity)]
public async Task UpdateObservation(
Guid batchId, Guid obsId, [FromBody] UpdateDraftObservationRequest req)
{
var actorUserId = GetCurrentUserId();
var result = await _draft.UpdateObservationAsync(batchId, obsId, req, actorUserId);
return Ok(ApiResponse.Ok(result));
}
///
/// Removes an observation row from the batch draft.
///
/// Digitization batch id.
/// Observation id to delete.
[HttpDelete("observations/{obsId:guid}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)]
public async Task DeleteObservation(Guid batchId, Guid obsId)
{
var actorUserId = GetCurrentUserId();
await _draft.DeleteObservationAsync(batchId, obsId, actorUserId);
return NoContent();
}
///
/// Validates completeness per batch type and transitions the batch from
/// IN_ENTRY to PENDING_VERIFICATION. Returns 422 if required fields are missing.
///
/// Digitization batch id.
/// The updated batch record.
[HttpPost("~/api/v1/digitization-batches/{batchId:guid}/submit-for-verification")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status422UnprocessableEntity)]
public async Task SubmitForVerification(Guid batchId)
{
var actorUserId = GetCurrentUserId();
var batch = await _draft.SubmitForVerificationAsync(batchId, actorUserId);
return Ok(ApiResponse.Ok(BatchDetailResponse.FromEntity(batch)));
}
private Guid GetCurrentUserId() =>
Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
}