feat(backend): add knowledge base, innovation library and trends modules
Add knowledge item and innovation CRUD with search/filtering, published-only visibility for non-staff, admin publish/update, challenge dictionary, problem aggregation with trends (admin only) and canonical embedding text preparation for matchmaking.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Dtos;
|
||||
using TarninaHub.Application.Mappings;
|
||||
using TarninaHub.Application.Security;
|
||||
|
||||
namespace TarninaHub.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/innovations")]
|
||||
[Authorize]
|
||||
public class InnovationsController : ControllerBase
|
||||
{
|
||||
private readonly IInnovationService _innovations;
|
||||
|
||||
public InnovationsController(IInnovationService innovations)
|
||||
{
|
||||
_innovations = innovations;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IReadOnlyList<InnovationResponse>>> List(
|
||||
[FromQuery] string? challengeId,
|
||||
[FromQuery] string? stage,
|
||||
[FromQuery] string? q,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var innovations = await _innovations.ListAsync(challengeId, stage, q, !IsStaff(), cancellationToken);
|
||||
|
||||
return Ok(innovations);
|
||||
}
|
||||
|
||||
[HttpGet("{innovationId}")]
|
||||
public async Task<ActionResult<InnovationResponse>> GetById(
|
||||
string innovationId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var innovation = await _innovations.GetAsync(innovationId, cancellationToken);
|
||||
|
||||
return innovation is null ? NotFound() : Ok(innovation);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Policy = Policies.OperatorOrAdmin)]
|
||||
public async Task<ActionResult<InnovationResponse>> Create(
|
||||
InnovationRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!ValidateStage(request.Stage))
|
||||
{
|
||||
return InvalidStage();
|
||||
}
|
||||
|
||||
var created = await _innovations.CreateAsync(request, cancellationToken);
|
||||
|
||||
return CreatedAtAction(nameof(GetById), new { innovationId = created.Id }, created);
|
||||
}
|
||||
|
||||
[HttpPut("{innovationId}")]
|
||||
[Authorize(Policy = Policies.OperatorOrAdmin)]
|
||||
public async Task<ActionResult<InnovationResponse>> Update(
|
||||
string innovationId,
|
||||
InnovationRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!ValidateStage(request.Stage))
|
||||
{
|
||||
return InvalidStage();
|
||||
}
|
||||
|
||||
var updated = await _innovations.UpdateAsync(innovationId, request, cancellationToken);
|
||||
|
||||
return updated is null ? NotFound() : Ok(updated);
|
||||
}
|
||||
|
||||
private bool ValidateStage(string? stage) => InnovationStageWire.IsValid(stage);
|
||||
|
||||
private ActionResult InvalidStage()
|
||||
{
|
||||
ModelState.AddModelError(nameof(InnovationRequest.Stage),
|
||||
"Stage must be one of: idea, pilot, scaling, service.");
|
||||
|
||||
return ValidationProblem(ModelState);
|
||||
}
|
||||
|
||||
private bool IsStaff() =>
|
||||
User.IsInRole(RoleNames.Operator) || User.IsInRole(RoleNames.Manager) || User.IsInRole(RoleNames.Admin);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Dtos;
|
||||
using TarninaHub.Application.Security;
|
||||
|
||||
namespace TarninaHub.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/knowledge-items")]
|
||||
[Authorize]
|
||||
public class KnowledgeItemsController : ControllerBase
|
||||
{
|
||||
private readonly IKnowledgeItemService _knowledgeItems;
|
||||
|
||||
public KnowledgeItemsController(IKnowledgeItemService knowledgeItems)
|
||||
{
|
||||
_knowledgeItems = knowledgeItems;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IReadOnlyList<KnowledgeItemResponse>>> List(
|
||||
[FromQuery] string? kind,
|
||||
[FromQuery] string? challengeId,
|
||||
[FromQuery] string? q,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var items = await _knowledgeItems.ListAsync(kind, challengeId, q, !IsStaff(), cancellationToken);
|
||||
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
[HttpGet("{knowledgeItemId}")]
|
||||
public async Task<ActionResult<KnowledgeItemResponse>> GetById(
|
||||
string knowledgeItemId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var item = await _knowledgeItems.GetAsync(knowledgeItemId, cancellationToken);
|
||||
|
||||
return item is null ? NotFound() : Ok(item);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Policy = Policies.OperatorOrAdmin)]
|
||||
public async Task<ActionResult<KnowledgeItemResponse>> Create(
|
||||
KnowledgeItemRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var created = await _knowledgeItems.CreateAsync(request, cancellationToken);
|
||||
|
||||
return CreatedAtAction(nameof(GetById), new { knowledgeItemId = created.Id }, created);
|
||||
}
|
||||
|
||||
[HttpPut("{knowledgeItemId}")]
|
||||
[Authorize(Policy = Policies.OperatorOrAdmin)]
|
||||
public async Task<ActionResult<KnowledgeItemResponse>> Update(
|
||||
string knowledgeItemId,
|
||||
KnowledgeItemRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var updated = await _knowledgeItems.UpdateAsync(knowledgeItemId, request, cancellationToken);
|
||||
|
||||
return updated is null ? NotFound() : Ok(updated);
|
||||
}
|
||||
|
||||
[HttpPost("{knowledgeItemId}/publish")]
|
||||
[Authorize(Policy = Policies.OperatorOrAdmin)]
|
||||
public async Task<ActionResult<KnowledgeItemResponse>> Publish(
|
||||
string knowledgeItemId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var published = await _knowledgeItems.PublishAsync(knowledgeItemId, cancellationToken);
|
||||
|
||||
return published is null ? NotFound() : Ok(published);
|
||||
}
|
||||
|
||||
private bool IsStaff() =>
|
||||
User.IsInRole(RoleNames.Operator) || User.IsInRole(RoleNames.Manager) || User.IsInRole(RoleNames.Admin);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Dtos;
|
||||
using TarninaHub.Application.Security;
|
||||
|
||||
namespace TarninaHub.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/social-challenges")]
|
||||
[Authorize]
|
||||
public class SocialChallengesController : ControllerBase
|
||||
{
|
||||
private readonly IChallengeService _challenges;
|
||||
|
||||
public SocialChallengesController(IChallengeService challenges)
|
||||
{
|
||||
_challenges = challenges;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IReadOnlyList<SocialChallengeResponse>>> List(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var challenges = await _challenges.ListAsync(cancellationToken);
|
||||
|
||||
return Ok(challenges);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Dtos;
|
||||
using TarninaHub.Application.Security;
|
||||
|
||||
namespace TarninaHub.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/trends")]
|
||||
[Authorize(Policy = Policies.OperatorOrAdmin)]
|
||||
public class TrendsController : ControllerBase
|
||||
{
|
||||
private readonly ITrendService _trends;
|
||||
|
||||
public TrendsController(ITrendService trends)
|
||||
{
|
||||
_trends = trends;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<TrendsResponse>> Get(
|
||||
[FromQuery] string? challengeId,
|
||||
[FromQuery] DateTimeOffset? from,
|
||||
[FromQuery] DateTimeOffset? to,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var rangeEnd = to ?? DateTimeOffset.UtcNow;
|
||||
var rangeStart = from ?? rangeEnd.AddDays(-30);
|
||||
|
||||
var trends = await _trends.GetAsync(challengeId, rangeStart, rangeEnd, cancellationToken);
|
||||
|
||||
return Ok(trends);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user