26ff8d9b4f
Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
164 lines
5.4 KiB
C#
164 lines
5.4 KiB
C#
using JdeScoping.Api.Hubs;
|
|
using JdeScoping.Core.Interfaces;
|
|
using JdeScoping.Core.Models;
|
|
using JdeScoping.Core.Models.Enums;
|
|
using JdeScoping.Core.Models.Search;
|
|
using JdeScoping.Core.ViewModels;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.SignalR;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace JdeScoping.Api.Controllers;
|
|
|
|
/// <summary>
|
|
/// Search management controller
|
|
/// </summary>
|
|
[Route("api/search")]
|
|
[ApiController]
|
|
[Authorize]
|
|
public class SearchController : ApiControllerBase
|
|
{
|
|
private readonly ILotFinderRepository _repository;
|
|
private readonly IHubContext<StatusHub> _hubContext;
|
|
private readonly ILogger<SearchController> _logger;
|
|
|
|
public SearchController(
|
|
ILotFinderRepository repository,
|
|
IHubContext<StatusHub> hubContext,
|
|
ILogger<SearchController> logger)
|
|
{
|
|
_repository = repository;
|
|
_hubContext = hubContext;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all searches for the current user
|
|
/// </summary>
|
|
[HttpGet]
|
|
[ProducesResponseType(typeof(IEnumerable<SearchViewModel>), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<IEnumerable<SearchViewModel>>> GetSearches(CancellationToken ct)
|
|
{
|
|
var searches = await _repository.GetUserSearchesAsync(CurrentUserName!, ct);
|
|
var viewModels = searches
|
|
.OrderByDescending(s => s.StartDt)
|
|
.Select(s => new SearchViewModel(s));
|
|
return Ok(viewModels);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all queued searches
|
|
/// </summary>
|
|
[HttpGet("queue")]
|
|
[ProducesResponseType(typeof(IEnumerable<SearchViewModel>), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<IEnumerable<SearchViewModel>>> GetQueuedSearches(CancellationToken ct)
|
|
{
|
|
var searches = await _repository.GetQueuedSearchesAsync(ct);
|
|
var viewModels = searches.Select(s => new SearchViewModel(s));
|
|
return Ok(viewModels);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a single search by ID
|
|
/// </summary>
|
|
[HttpGet("{id:int}")]
|
|
[ProducesResponseType(typeof(SearchViewModel), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<ActionResult<SearchViewModel>> GetSearch(int id, CancellationToken ct)
|
|
{
|
|
var search = await _repository.GetSearchAsync(id, ct);
|
|
if (search is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
return Ok(new SearchViewModel(search));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies an existing search for the current user (returns copy without persisting)
|
|
/// </summary>
|
|
[HttpGet("{id:int}/copy")]
|
|
[ProducesResponseType(typeof(SearchViewModel), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<ActionResult<SearchViewModel>> CopySearch(int id, CancellationToken ct)
|
|
{
|
|
var original = await _repository.GetSearchAsync(id, ct);
|
|
if (original is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
// Return a copy with reset status/timestamps (not persisted until user submits)
|
|
var copy = new Search
|
|
{
|
|
Id = 0,
|
|
UserName = CurrentUserName!,
|
|
Name = original.Name,
|
|
Status = SearchStatus.New,
|
|
SubmitDt = null,
|
|
StartDt = null,
|
|
EndDt = null,
|
|
CriteriaJson = original.CriteriaJson
|
|
};
|
|
|
|
return Ok(new SearchViewModel(copy));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new search
|
|
/// </summary>
|
|
[HttpPost]
|
|
[ProducesResponseType(typeof(int), StatusCodes.Status201Created)]
|
|
public async Task<ActionResult<int>> CreateSearch(
|
|
[FromBody] SearchViewModel viewModel,
|
|
CancellationToken ct)
|
|
{
|
|
var search = viewModel.ToEntity();
|
|
search.UserName = CurrentUserName!;
|
|
|
|
var searchId = await _repository.SubmitSearchAsync(search, ct);
|
|
|
|
// Publish to SignalR (best-effort, swallow exceptions)
|
|
try
|
|
{
|
|
var searchUpdate = new SearchUpdate
|
|
{
|
|
Id = searchId,
|
|
UserName = CurrentUserName!,
|
|
Name = search.Name,
|
|
Status = search.Status,
|
|
Timestamp = DateTime.UtcNow
|
|
};
|
|
await _hubContext.Clients.All.SendAsync("searchUpdate", searchUpdate, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to publish search update to SignalR");
|
|
}
|
|
|
|
return CreatedAtAction(nameof(GetSearch), new { id = searchId }, searchId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Downloads search results as an Excel file
|
|
/// </summary>
|
|
[HttpGet("{id:int}/results")]
|
|
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> GetResults(int id, CancellationToken ct)
|
|
{
|
|
var data = await _repository.GetSearchResultsAsync(id, ct);
|
|
if (data is null || data.Length == 0)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
return File(
|
|
data,
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
"search_results.xlsx");
|
|
}
|
|
}
|