Initial commit: JDE Scoping Tool migration project
Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user