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>> 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> 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> 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> 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) { public static OperationDto From(UserOperation operation) => new( operation.Id, operation.TaskType, operation.Status, operation.SubjectType, operation.CreatedAtUtc, operation.StartedAtUtc, operation.CompletedAtUtc, operation.DeadlineAtUtc, operation.CancellationRequestedAtUtc, 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>> 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 UnreadCount(CancellationToken cancellationToken) => Ok(new { count = await notifications.UnreadCountAsync(cancellationToken) }); [HttpPost("{id:guid}/read")] public async Task 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 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, notification.CreatedAtUtc, notification.ReadAtUtc); } public sealed record NotificationDto( Guid Id, Guid? OperationId, string Kind, string Title, string Message, string? LinkPath, DateTime CreatedAtUtc, DateTime? ReadAtUtc);