Files
jobtrackingapp/JobTrackerApi/Controllers/OperationsController.cs
T

133 lines
5.5 KiB
C#

using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace JobTrackerApi.Controllers;
[ApiController]
[Route("api/operations")]
[Authorize(AuthenticationSchemes = "local")]
public sealed class OperationsController(UserOperationStore operations) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<IReadOnlyList<OperationDto>>> List([FromQuery] int limit = 25, CancellationToken cancellationToken = default)
{
if (limit is < 1 or > 100) return BadRequest(new { code = "invalid_limit", message = "Limit must be between 1 and 100." });
return Ok((await operations.ListAsync(limit, cancellationToken)).Select(OperationDto.From).ToList());
}
[HttpGet("{id:guid}")]
public async Task<ActionResult<OperationDto>> Get(Guid id, CancellationToken cancellationToken)
{
var operation = await operations.GetAsync(id, cancellationToken);
return operation is null ? NotFound() : Ok(OperationDto.From(operation));
}
[HttpPost("{id:guid}/cancel")]
public async Task<ActionResult<OperationDto>> Cancel(Guid id, CancellationToken cancellationToken)
{
if (await operations.GetAsync(id, cancellationToken) is null) return NotFound();
if (!await operations.RequestCancellationAsync(id, cancellationToken))
return Conflict(new { code = "operation_not_cancellable", message = "This operation can no longer be cancelled." });
return Ok(OperationDto.From((await operations.GetAsync(id, cancellationToken))!));
}
[HttpPost("{id:guid}/retry")]
public async Task<ActionResult<OperationDto>> Retry(Guid id, CancellationToken cancellationToken)
{
if (await operations.GetAsync(id, cancellationToken) is null) return NotFound();
if (!await operations.RetryAsync(id, cancellationToken))
return Conflict(new { code = "operation_not_retryable", message = "Only failed or cancelled operations can be retried." });
return Ok(OperationDto.From((await operations.GetAsync(id, cancellationToken))!));
}
}
public sealed record OperationDto(
Guid Id,
string TaskType,
string Status,
string? SubjectType,
DateTime CreatedAtUtc,
DateTime? StartedAtUtc,
DateTime? CompletedAtUtc,
DateTime? DeadlineAtUtc,
DateTime? CancellationRequestedAtUtc,
string? ProgressStage,
int? ProgressPercent,
string? FailureCategory,
bool CanCancel,
bool CanRetry)
{
// SQLite drops DateTime.Kind; these fields retain their stored UTC ticks.
public static OperationDto From(UserOperation operation) => new(
operation.Id,
operation.TaskType,
operation.Status,
operation.SubjectType,
DateTime.SpecifyKind(operation.CreatedAtUtc, DateTimeKind.Utc),
operation.StartedAtUtc is { } started ? DateTime.SpecifyKind(started, DateTimeKind.Utc) : null,
operation.CompletedAtUtc is { } completed ? DateTime.SpecifyKind(completed, DateTimeKind.Utc) : null,
operation.DeadlineAtUtc is { } deadline ? DateTime.SpecifyKind(deadline, DateTimeKind.Utc) : null,
operation.CancellationRequestedAtUtc is { } cancellation ? DateTime.SpecifyKind(cancellation, DateTimeKind.Utc) : null,
operation.ProgressStage,
operation.ProgressPercent,
operation.FailureCategory,
!OperationStatuses.IsTerminal(operation.Status) && operation.CancellationRequestedAtUtc is null,
operation.Status is OperationStatuses.Failed or OperationStatuses.Cancelled);
}
[ApiController]
[Route("api/notifications")]
[Authorize(AuthenticationSchemes = "local")]
public sealed class NotificationsController(UserNotificationStore notifications) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<IReadOnlyList<NotificationDto>>> List([FromQuery] int limit = 25, CancellationToken cancellationToken = default)
{
if (limit is < 1 or > 100) return BadRequest(new { code = "invalid_limit", message = "Limit must be between 1 and 100." });
return Ok((await notifications.ListAsync(limit, cancellationToken)).Select(ToDto).ToList());
}
[HttpGet("unread-count")]
public async Task<IActionResult> UnreadCount(CancellationToken cancellationToken) =>
Ok(new { count = await notifications.UnreadCountAsync(cancellationToken) });
[HttpPost("{id:guid}/read")]
public async Task<IActionResult> MarkRead(Guid id, CancellationToken cancellationToken)
{
if (await notifications.GetAsync(id, cancellationToken) is null) return NotFound();
await notifications.MarkReadAsync(id, cancellationToken);
return NoContent();
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Dismiss(Guid id, CancellationToken cancellationToken)
{
if (await notifications.GetAsync(id, cancellationToken) is null) return NotFound();
await notifications.DismissAsync(id, cancellationToken);
return NoContent();
}
private static NotificationDto ToDto(UserNotification notification) => new(
notification.Id,
notification.OperationId,
notification.Kind,
notification.Title,
notification.Message,
notification.LinkPath,
DateTime.SpecifyKind(notification.CreatedAtUtc, DateTimeKind.Utc),
notification.ReadAtUtc is { } read ? DateTime.SpecifyKind(read, DateTimeKind.Utc) : null);
}
public sealed record NotificationDto(
Guid Id,
Guid? OperationId,
string Kind,
string Title,
string Message,
string? LinkPath,
DateTime CreatedAtUtc,
DateTime? ReadAtUtc);