feat(backend): add communication, tester, admin and middleman modules
Implement step 17 modules: threads/messages, notifications with automation (submission.created, idea.created, grant_call.opened), innovation test feedback, admin panel (submissions, verify, metrics, grant call status), Middleman adaptation and partnerships. Split multi-type files into one type per file and replace object-valued dictionaries with string-valued maps in data contracts (migrations 0006/0007).
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IAdminService
|
||||
{
|
||||
Task<IReadOnlyList<SubmissionResponse>> ListSubmissionsAsync(
|
||||
string? status,
|
||||
string? challengeId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<KnowledgeItemResponse?> VerifyKnowledgeItemAsync(
|
||||
string knowledgeItemId,
|
||||
VerifyKnowledgeItemRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<AdminMetricsResponse> GetMetricsAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public enum CreateApplicationOutcome
|
||||
{
|
||||
Created,
|
||||
CallNotFound,
|
||||
CallNotOpen,
|
||||
IdeaNotFound,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public sealed record CreateApplicationResult(
|
||||
CreateApplicationOutcome Outcome,
|
||||
ApplicationResponse? Application);
|
||||
@@ -0,0 +1,18 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface IApplicationService
|
||||
{
|
||||
Task<CreateApplicationResult> CreateAsync(
|
||||
string userId,
|
||||
CreateApplicationRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<ApplicationResponse?> GetAsync(string applicationId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<ApplicationResponse>> ListForAuthorAsync(
|
||||
string authorId,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -6,6 +6,6 @@ public interface IAuditLogService
|
||||
string? actorId,
|
||||
string action,
|
||||
string target,
|
||||
IReadOnlyDictionary<string, object?> context,
|
||||
IReadOnlyDictionary<string, string?> context,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface IGrantCallService
|
||||
{
|
||||
Task<IReadOnlyList<GrantCallResponse>> ListAsync(string? status, CancellationToken cancellationToken);
|
||||
|
||||
Task<GrantCallResponse?> GetAsync(string grantCallId, CancellationToken cancellationToken);
|
||||
|
||||
Task<GrantCallResponse?> UpdateStatusAsync(
|
||||
string grantCallId,
|
||||
string status,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface IIdeaAssistant
|
||||
{
|
||||
Task<IdeaAssistResponse?> AssistAsync(
|
||||
string ideaId,
|
||||
IdeaAssistRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface IIdeaService
|
||||
{
|
||||
Task<IdeaResponse> CreateAsync(string authorId, CreateIdeaRequest request, CancellationToken cancellationToken);
|
||||
|
||||
Task<IdeaResponse?> GetAsync(string ideaId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<IdeaResponse>> ListForAuthorAsync(string authorId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<string>> ListDistinctAuthorIdsAsync(CancellationToken cancellationToken);
|
||||
|
||||
Task<IdeaResponse?> UpdateAsync(string ideaId, UpdateIdeaRequest request, CancellationToken cancellationToken);
|
||||
|
||||
Task<IdeaCardResponse?> GetCardAsync(string ideaId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IdeaCardResponse?> UpdateCardAsync(string ideaId, IdeaCardRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface INotificationService
|
||||
{
|
||||
Task NotifyStaffAsync(
|
||||
string type,
|
||||
IReadOnlyDictionary<string, string?> payload,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task NotifyUserAsync(
|
||||
string userId,
|
||||
string type,
|
||||
IReadOnlyDictionary<string, string?> payload,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<NotificationResponse>> ListForUserAsync(
|
||||
string userId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<NotificationResponse?> MarkReadAsync(
|
||||
string notificationId,
|
||||
string userId,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public sealed record SubmissionEvent(
|
||||
string Type,
|
||||
string SubmissionId,
|
||||
string? ActorId,
|
||||
string Status,
|
||||
DateTimeOffset At);
|
||||
|
||||
public interface ISubmissionEventPublisher
|
||||
{
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface IThreadService
|
||||
{
|
||||
Task<IReadOnlyList<ThreadResponse>> ListForUserAsync(
|
||||
string userId,
|
||||
bool isStaff,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<ThreadResponse?> GetAsync(
|
||||
string threadId,
|
||||
string userId,
|
||||
bool isStaff,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<ThreadResponse> CreateAsync(
|
||||
string creatorId,
|
||||
CreateThreadRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<MessageResponse?> AddMessageAsync(
|
||||
string threadId,
|
||||
string authorId,
|
||||
bool isStaff,
|
||||
CreateMessageRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -2,9 +2,8 @@ using TarninaHub.Application.Models;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface ITokenService
|
||||
{
|
||||
TokenResult CreateToken(UserAccount user);
|
||||
}
|
||||
|
||||
public sealed record TokenResult(string AccessToken, DateTimeOffset ExpiresAt);
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IIdeaService
|
||||
{
|
||||
Task<IdeaResponse> CreateAsync(string authorId, CreateIdeaRequest request, CancellationToken cancellationToken);
|
||||
|
||||
Task<IdeaResponse?> GetAsync(string ideaId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<IdeaResponse>> ListForAuthorAsync(string authorId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IdeaResponse?> UpdateAsync(string ideaId, UpdateIdeaRequest request, CancellationToken cancellationToken);
|
||||
|
||||
Task<IdeaCardResponse?> GetCardAsync(string ideaId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IdeaCardResponse?> UpdateCardAsync(string ideaId, IdeaCardRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface IIdeaAssistant
|
||||
{
|
||||
Task<IdeaAssistResponse?> AssistAsync(
|
||||
string ideaId,
|
||||
IdeaAssistRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface IGrantCallService
|
||||
{
|
||||
Task<IReadOnlyList<GrantCallResponse>> ListAsync(string? status, CancellationToken cancellationToken);
|
||||
|
||||
Task<GrantCallResponse?> GetAsync(string grantCallId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public enum CreateApplicationOutcome
|
||||
{
|
||||
Created,
|
||||
CallNotFound,
|
||||
CallNotOpen,
|
||||
IdeaNotFound,
|
||||
}
|
||||
|
||||
public sealed record CreateApplicationResult(
|
||||
CreateApplicationOutcome Outcome,
|
||||
ApplicationResponse? Application);
|
||||
|
||||
public interface IApplicationService
|
||||
{
|
||||
Task<CreateApplicationResult> CreateAsync(
|
||||
string userId,
|
||||
CreateApplicationRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<ApplicationResponse?> GetAsync(string applicationId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<ApplicationResponse>> ListForAuthorAsync(
|
||||
string authorId,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IMiddlemanService
|
||||
{
|
||||
Task<MiddlemanResponse?> AdaptAsync(MiddlemanRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IPartnershipService
|
||||
{
|
||||
Task<IReadOnlyList<PartnershipResponse>> ListForUserAsync(
|
||||
string userId,
|
||||
bool isStaff,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<PartnershipResponse> CreateAsync(
|
||||
string initiatorId,
|
||||
CreatePartnershipRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public sealed record SubmissionEvent(
|
||||
string Type,
|
||||
string SubmissionId,
|
||||
string? ActorId,
|
||||
string Status,
|
||||
DateTimeOffset At);
|
||||
@@ -0,0 +1,16 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface ITestFeedbackService
|
||||
{
|
||||
Task<TestFeedbackResponse?> CreateAsync(
|
||||
string innovationId,
|
||||
string authorId,
|
||||
CreateTestFeedbackRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<TestFeedbackResponse>> ListForInnovationAsync(
|
||||
string innovationId,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using TarninaHub.Application.Models;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public sealed record TokenResult(string AccessToken, DateTimeOffset ExpiresAt);
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record AdminMetricsResponse(
|
||||
int Submissions,
|
||||
int Problems,
|
||||
int Ideas,
|
||||
int Innovations,
|
||||
int KnowledgeItems,
|
||||
int Users,
|
||||
int OpenGrantCalls,
|
||||
int Applications);
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed record GrantCallResponse(
|
||||
string Id,
|
||||
string Name,
|
||||
DateTimeOffset OpensAt,
|
||||
DateTimeOffset ClosesAt,
|
||||
string Status,
|
||||
string? ExternalId);
|
||||
|
||||
public sealed record CreateApplicationRequest(
|
||||
[Required] string GrantCallId,
|
||||
[Required] string IdeaId,
|
||||
IReadOnlyDictionary<string, object?>? Content);
|
||||
|
||||
public sealed record UpdateApplicationRequest(
|
||||
IReadOnlyDictionary<string, object?>? Content,
|
||||
string? Status);
|
||||
|
||||
public sealed record ApplicationResponse(
|
||||
string Id,
|
||||
string GrantCallId,
|
||||
string IdeaId,
|
||||
IReadOnlyDictionary<string, object?> Content,
|
||||
string Status,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record ApplicationResponse(
|
||||
string Id,
|
||||
string GrantCallId,
|
||||
string IdeaId,
|
||||
IReadOnlyDictionary<string, string?> Content,
|
||||
string Status,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed record RegisterRequest(string Phone, string? Email, string Password);
|
||||
|
||||
public sealed record LoginRequest(string Phone, string Password);
|
||||
|
||||
public sealed record UserResponse(
|
||||
string Id,
|
||||
string Phone,
|
||||
string? Email,
|
||||
string Role,
|
||||
string? OrganizationId);
|
||||
|
||||
public sealed record TokenResponse(
|
||||
string AccessToken,
|
||||
string TokenType,
|
||||
DateTimeOffset ExpiresAt,
|
||||
UserResponse User);
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed record GrantConsentRequest(string Scope, DateTimeOffset? ExpiresAt);
|
||||
|
||||
public sealed record ConsentResponse(
|
||||
string Id,
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record CreateApplicationRequest(
|
||||
[Required] string GrantCallId,
|
||||
[Required] string IdeaId,
|
||||
IReadOnlyDictionary<string, string?>? Content);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record CreateIdeaRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[Required, StringLength(4000, MinimumLength = 10)] string Description,
|
||||
IdeaCardRequest? Card);
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record CreateMessageRequest(
|
||||
[Required, StringLength(4000, MinimumLength = 1)] string Body);
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record CreatePartnershipRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Subject,
|
||||
IReadOnlyList<string>? Partners);
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record CreateSubmissionRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[Required, StringLength(4000, MinimumLength = 10)] string Description,
|
||||
[Required] string ChallengeId,
|
||||
[Required] string LocationId,
|
||||
IReadOnlyList<string>? Keywords);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record CreateTestFeedbackRequest(
|
||||
[Range(1, 5)] int Rating,
|
||||
string? Comment,
|
||||
string? Improvements);
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record CreateThreadRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Subject,
|
||||
IReadOnlyList<string>? Participants);
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record GrantCallResponse(
|
||||
string Id,
|
||||
string Name,
|
||||
DateTimeOffset OpensAt,
|
||||
DateTimeOffset ClosesAt,
|
||||
string Status,
|
||||
string? ExternalId);
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record GrantConsentRequest(string Scope, DateTimeOffset? ExpiresAt);
|
||||
@@ -0,0 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record IdeaAssistRequest(string? Focus);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record IdeaAssistResponse(
|
||||
IReadOnlyList<string> Suggestions,
|
||||
string ImprovedDescription,
|
||||
bool FromLlm);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record IdeaCardRequest(
|
||||
string? Essence,
|
||||
string? Audience,
|
||||
string? Stage);
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record IdeaCardResponse(
|
||||
string Id,
|
||||
string IdeaId,
|
||||
string Essence,
|
||||
string Audience,
|
||||
string Stage);
|
||||
@@ -1,41 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed record IdeaCardRequest(
|
||||
string? Essence,
|
||||
string? Audience,
|
||||
string? Stage);
|
||||
|
||||
public sealed record CreateIdeaRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[Required, StringLength(4000, MinimumLength = 10)] string Description,
|
||||
IdeaCardRequest? Card);
|
||||
|
||||
public sealed record UpdateIdeaRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[Required, StringLength(4000, MinimumLength = 10)] string Description,
|
||||
string? Status);
|
||||
|
||||
public sealed record IdeaCardResponse(
|
||||
string Id,
|
||||
string IdeaId,
|
||||
string Essence,
|
||||
string Audience,
|
||||
string Stage);
|
||||
|
||||
public sealed record IdeaResponse(
|
||||
string Id,
|
||||
string Title,
|
||||
string Description,
|
||||
string AuthorId,
|
||||
string Status,
|
||||
IdeaCardResponse? Card,
|
||||
DateTimeOffset? UpdatedAt);
|
||||
|
||||
public sealed record IdeaAssistRequest(string? Focus);
|
||||
|
||||
public sealed record IdeaAssistResponse(
|
||||
IReadOnlyList<string> Suggestions,
|
||||
string ImprovedDescription,
|
||||
bool FromLlm);
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record IdeaResponse(
|
||||
string Id,
|
||||
string Title,
|
||||
string Description,
|
||||
string AuthorId,
|
||||
string Status,
|
||||
IdeaCardResponse? Card,
|
||||
DateTimeOffset? UpdatedAt);
|
||||
@@ -1,27 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed record InnovationRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[StringLength(2000)] string? Summary,
|
||||
[StringLength(8000)] string? Description,
|
||||
[Required] string ChallengeId,
|
||||
[Required] string Stage,
|
||||
string? Cost,
|
||||
IReadOnlyDictionary<string, object?>? Contact,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, object?>>? Media,
|
||||
bool Published);
|
||||
|
||||
public sealed record InnovationResponse(
|
||||
string Id,
|
||||
string Title,
|
||||
string Summary,
|
||||
string Description,
|
||||
string ChallengeId,
|
||||
string Stage,
|
||||
string? Cost,
|
||||
IReadOnlyDictionary<string, object?>? Contact,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, object?>> Media,
|
||||
bool Published,
|
||||
DateTimeOffset? UpdatedAt);
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record InnovationRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[StringLength(2000)] string? Summary,
|
||||
[StringLength(8000)] string? Description,
|
||||
[Required] string ChallengeId,
|
||||
[Required] string Stage,
|
||||
string? Cost,
|
||||
IReadOnlyDictionary<string, string?>? Contact,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, string?>>? Media,
|
||||
bool Published);
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record InnovationResponse(
|
||||
string Id,
|
||||
string Title,
|
||||
string Summary,
|
||||
string Description,
|
||||
string ChallengeId,
|
||||
string Stage,
|
||||
string? Cost,
|
||||
IReadOnlyDictionary<string, string?>? Contact,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, string?>> Media,
|
||||
bool Published,
|
||||
DateTimeOffset? UpdatedAt);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record InstitutionProfile(
|
||||
string? Type,
|
||||
string? Area,
|
||||
IReadOnlyList<string>? Audiences);
|
||||
@@ -1,27 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed record KnowledgeItemRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[Required] string Kind,
|
||||
string? ChallengeId,
|
||||
string? SourceUrl,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, object?>>? Attachments,
|
||||
bool Published);
|
||||
|
||||
public sealed record KnowledgeItemResponse(
|
||||
string Id,
|
||||
string Title,
|
||||
string Kind,
|
||||
string? ChallengeId,
|
||||
string? SourceUrl,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, object?>> Attachments,
|
||||
bool Published,
|
||||
DateTimeOffset? UpdatedAt);
|
||||
|
||||
public sealed record SocialChallengeResponse(
|
||||
string Id,
|
||||
string Name,
|
||||
string Description,
|
||||
int Order);
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record KnowledgeItemRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[Required] string Kind,
|
||||
string? ChallengeId,
|
||||
string? SourceUrl,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, string?>>? Attachments,
|
||||
bool Published);
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record KnowledgeItemResponse(
|
||||
string Id,
|
||||
string Title,
|
||||
string Kind,
|
||||
string? ChallengeId,
|
||||
string? SourceUrl,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, string?>> Attachments,
|
||||
bool Published,
|
||||
DateTimeOffset? UpdatedAt);
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record LoginRequest(string Phone, string Password);
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record MatchRequest(
|
||||
int? MaxResults,
|
||||
string? ChallengeId);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record MatchResponse(
|
||||
IReadOnlyList<SimilarCaseItem> SimilarCases,
|
||||
IReadOnlyList<SolutionItem> Solutions,
|
||||
bool Fallback);
|
||||
@@ -1,23 +0,0 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed record MatchRequest(
|
||||
int? MaxResults,
|
||||
string? ChallengeId);
|
||||
|
||||
public sealed record SimilarCaseItem(
|
||||
string Id,
|
||||
string Title,
|
||||
double Score);
|
||||
|
||||
public sealed record SolutionItem(
|
||||
string InnovationId,
|
||||
string Title,
|
||||
string Stage,
|
||||
string ChallengeId,
|
||||
double Score,
|
||||
string Rationale);
|
||||
|
||||
public sealed record MatchResponse(
|
||||
IReadOnlyList<SimilarCaseItem> SimilarCases,
|
||||
IReadOnlyList<SolutionItem> Solutions,
|
||||
bool Fallback);
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record MessageResponse(
|
||||
string Id,
|
||||
string ThreadId,
|
||||
string AuthorId,
|
||||
string Body,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record MiddlemanRequest(
|
||||
[Required] string InnovationId,
|
||||
InstitutionProfile? InstitutionProfile,
|
||||
string? Needs);
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record MiddlemanResponse(
|
||||
string ServiceForm,
|
||||
IReadOnlyList<string> RequiredResources,
|
||||
string Rationale,
|
||||
bool FromLlm);
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record NotificationResponse(
|
||||
string Id,
|
||||
string Type,
|
||||
IReadOnlyDictionary<string, string?> Payload,
|
||||
DateTimeOffset? ReadAt,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record PartnershipResponse(
|
||||
string Id,
|
||||
string InitiatorId,
|
||||
string Subject,
|
||||
IReadOnlyList<string> Partners,
|
||||
string Status);
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record RegisterRequest(string Phone, string? Email, string Password);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record SimilarCaseItem(
|
||||
string Id,
|
||||
string Title,
|
||||
double Score);
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record SocialChallengeResponse(
|
||||
string Id,
|
||||
string Name,
|
||||
string Description,
|
||||
int Order);
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record SolutionItem(
|
||||
string InnovationId,
|
||||
string Title,
|
||||
string Stage,
|
||||
string ChallengeId,
|
||||
double Score,
|
||||
string Rationale);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record StorageInfo(
|
||||
string Engine,
|
||||
string DataDirectory,
|
||||
string Namespace,
|
||||
string Database);
|
||||
@@ -1,25 +0,0 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed class StorageProbe
|
||||
{
|
||||
public string Label { get; set; } = "probe";
|
||||
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed record StorageInfo(
|
||||
string Engine,
|
||||
string DataDirectory,
|
||||
string Namespace,
|
||||
string Database);
|
||||
|
||||
public sealed record StorageStatusResponse(
|
||||
string Engine,
|
||||
string DataDirectory,
|
||||
string Namespace,
|
||||
string Database,
|
||||
int ProbeCount);
|
||||
|
||||
public sealed record StorageProbeResponse(
|
||||
string Label,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed class StorageProbe
|
||||
{
|
||||
public string Label { get; set; } = "probe";
|
||||
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record StorageProbeResponse(
|
||||
string Label,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record StorageStatusResponse(
|
||||
string Engine,
|
||||
string DataDirectory,
|
||||
string Namespace,
|
||||
string Database,
|
||||
int ProbeCount);
|
||||
-9
@@ -2,15 +2,6 @@ using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed record CreateSubmissionRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[Required, StringLength(4000, MinimumLength = 10)] string Description,
|
||||
[Required] string ChallengeId,
|
||||
[Required] string LocationId,
|
||||
IReadOnlyList<string>? Keywords);
|
||||
|
||||
public sealed record UpdateSubmissionStatusRequest(
|
||||
[Required] string Status);
|
||||
|
||||
public sealed record SubmissionResponse(
|
||||
string Id,
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record TestFeedbackResponse(
|
||||
string Id,
|
||||
string InnovationId,
|
||||
string AuthorId,
|
||||
int Rating,
|
||||
string Comment,
|
||||
string Improvements,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record ThreadResponse(
|
||||
string Id,
|
||||
string Subject,
|
||||
IReadOnlyList<string> Participants,
|
||||
DateTimeOffset CreatedAt,
|
||||
IReadOnlyList<MessageResponse> Messages);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record TokenResponse(
|
||||
string AccessToken,
|
||||
string TokenType,
|
||||
DateTimeOffset ExpiresAt,
|
||||
UserResponse User);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record TrendPoint(
|
||||
string ChallengeId,
|
||||
string ChallengeName,
|
||||
int SubmissionCount);
|
||||
@@ -1,9 +1,5 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed record TrendPoint(
|
||||
string ChallengeId,
|
||||
string ChallengeName,
|
||||
int SubmissionCount);
|
||||
|
||||
public sealed record TrendsResponse(
|
||||
DateTimeOffset From,
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record UpdateApplicationRequest(
|
||||
IReadOnlyDictionary<string, string?>? Content,
|
||||
string? Status);
|
||||
@@ -0,0 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record UpdateGrantCallStatusRequest([Required] string Status);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record UpdateIdeaRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[Required, StringLength(4000, MinimumLength = 10)] string Description,
|
||||
string? Status);
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record UpdateSubmissionStatusRequest(
|
||||
[Required] string Status);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record UserResponse(
|
||||
string Id,
|
||||
string Phone,
|
||||
string? Email,
|
||||
string Role,
|
||||
string? OrganizationId);
|
||||
@@ -0,0 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record VerifyKnowledgeItemRequest(bool Verified);
|
||||
@@ -0,0 +1,22 @@
|
||||
using TarninaHub.Domain.Enums;
|
||||
|
||||
namespace TarninaHub.Application.Mappings;
|
||||
|
||||
public static class PartnershipStatusWire
|
||||
{
|
||||
public static string ToWire(this PartnershipStatus status) => status switch
|
||||
{
|
||||
PartnershipStatus.Proposed => "proposed",
|
||||
PartnershipStatus.Active => "active",
|
||||
PartnershipStatus.Closed => "closed",
|
||||
_ => "proposed",
|
||||
};
|
||||
|
||||
public static PartnershipStatus? Parse(string? value) => value?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"proposed" => PartnershipStatus.Proposed,
|
||||
"active" => PartnershipStatus.Active,
|
||||
"closed" => PartnershipStatus.Closed,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
|
||||
public sealed record LlmPrompt(string System, string User);
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
public sealed record LlmPrompt(string System, string User);
|
||||
|
||||
public static class MatchPromptBuilder
|
||||
{
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
|
||||
public sealed record RankedItem<T>(T Item, double Score);
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
public sealed record RankedItem<T>(T Item, double Score);
|
||||
|
||||
public static class SemanticRanker
|
||||
{
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace TarninaHub.Application.Security;
|
||||
|
||||
|
||||
public static class ConsentScopes
|
||||
{
|
||||
/// <summary>Access granted by the data subject's consent.</summary>
|
||||
public const string ByConsent = "by-consent";
|
||||
|
||||
/// <summary>Time-limited access (uses consent.expires_at).</summary>
|
||||
public const string Temporary = "temporary";
|
||||
|
||||
/// <summary>Fully anonymized data (no personal identifiers).</summary>
|
||||
public const string Anonymized = "anonymized";
|
||||
|
||||
public static readonly IReadOnlyList<string> All = [ByConsent, Temporary, Anonymized];
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace TarninaHub.Application.Security;
|
||||
|
||||
|
||||
public static class Policies
|
||||
{
|
||||
/// <summary>operator, manager, admin — ROPS/expert staff.</summary>
|
||||
public const string Staff = "Staff";
|
||||
|
||||
/// <summary>operator, admin.</summary>
|
||||
public const string OperatorOrAdmin = "OperatorOrAdmin";
|
||||
|
||||
/// <summary>manager, operator, admin.</summary>
|
||||
public const string ManagerOrAbove = "ManagerOrAbove";
|
||||
|
||||
/// <summary>admin only.</summary>
|
||||
public const string AdminOnly = "AdminOnly";
|
||||
|
||||
/// <summary>jst and above (jst, operator, manager, admin).</summary>
|
||||
public const string JstOrAbove = "JstOrAbove";
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace TarninaHub.Application.Security;
|
||||
|
||||
|
||||
public static class RoleNames
|
||||
{
|
||||
public const string User = "user";
|
||||
public const string Jst = "jst";
|
||||
public const string Operator = "operator";
|
||||
public const string Manager = "manager";
|
||||
public const string Admin = "admin";
|
||||
|
||||
public static readonly IReadOnlyList<string> All =
|
||||
[User, Jst, Operator, Manager, Admin];
|
||||
|
||||
public static bool IsValid(string role) => All.Contains(role);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
namespace TarninaHub.Application.Security;
|
||||
|
||||
public static class RoleNames
|
||||
{
|
||||
public const string User = "user";
|
||||
public const string Jst = "jst";
|
||||
public const string Operator = "operator";
|
||||
public const string Manager = "manager";
|
||||
public const string Admin = "admin";
|
||||
|
||||
public static readonly IReadOnlyList<string> All =
|
||||
[User, Jst, Operator, Manager, Admin];
|
||||
|
||||
public static bool IsValid(string role) => All.Contains(role);
|
||||
}
|
||||
|
||||
public static class Policies
|
||||
{
|
||||
/// <summary>operator, manager, admin — ROPS/expert staff.</summary>
|
||||
public const string Staff = "Staff";
|
||||
|
||||
/// <summary>operator, admin.</summary>
|
||||
public const string OperatorOrAdmin = "OperatorOrAdmin";
|
||||
|
||||
/// <summary>manager, operator, admin.</summary>
|
||||
public const string ManagerOrAbove = "ManagerOrAbove";
|
||||
|
||||
/// <summary>admin only.</summary>
|
||||
public const string AdminOnly = "AdminOnly";
|
||||
|
||||
/// <summary>jst and above (jst, operator, manager, admin).</summary>
|
||||
public const string JstOrAbove = "JstOrAbove";
|
||||
}
|
||||
|
||||
public static class ConsentScopes
|
||||
{
|
||||
/// <summary>Access granted by the data subject's consent.</summary>
|
||||
public const string ByConsent = "by-consent";
|
||||
|
||||
/// <summary>Time-limited access (uses consent.expires_at).</summary>
|
||||
public const string Temporary = "temporary";
|
||||
|
||||
/// <summary>Fully anonymized data (no personal identifiers).</summary>
|
||||
public const string Anonymized = "anonymized";
|
||||
|
||||
public static readonly IReadOnlyList<string> All = [ByConsent, Temporary, Anonymized];
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
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/admin")]
|
||||
[Authorize(Policy = Policies.OperatorOrAdmin)]
|
||||
public class AdminController : ControllerBase
|
||||
{
|
||||
private readonly IAdminService _admin;
|
||||
private readonly IGrantCallService _grantCalls;
|
||||
private readonly IIdeaService _ideas;
|
||||
private readonly INotificationService _notifications;
|
||||
|
||||
public AdminController(
|
||||
IAdminService admin,
|
||||
IGrantCallService grantCalls,
|
||||
IIdeaService ideas,
|
||||
INotificationService notifications)
|
||||
{
|
||||
_admin = admin;
|
||||
_grantCalls = grantCalls;
|
||||
_ideas = ideas;
|
||||
_notifications = notifications;
|
||||
}
|
||||
|
||||
[HttpGet("submissions")]
|
||||
public async Task<ActionResult<IReadOnlyList<SubmissionResponse>>> ListSubmissions(
|
||||
[FromQuery] string? status,
|
||||
[FromQuery] string? challengeId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var submissions = await _admin.ListSubmissionsAsync(status, challengeId, cancellationToken);
|
||||
|
||||
return Ok(submissions);
|
||||
}
|
||||
|
||||
[HttpPut("knowledge-items/{knowledgeItemId}/verify")]
|
||||
public async Task<ActionResult<KnowledgeItemResponse>> VerifyKnowledgeItem(
|
||||
string knowledgeItemId,
|
||||
VerifyKnowledgeItemRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var item = await _admin.VerifyKnowledgeItemAsync(knowledgeItemId, request, cancellationToken);
|
||||
|
||||
return item is null ? NotFound() : Ok(item);
|
||||
}
|
||||
|
||||
[HttpGet("metrics")]
|
||||
public async Task<ActionResult<AdminMetricsResponse>> Metrics(CancellationToken cancellationToken)
|
||||
{
|
||||
var metrics = await _admin.GetMetricsAsync(cancellationToken);
|
||||
|
||||
return Ok(metrics);
|
||||
}
|
||||
|
||||
[HttpPut("grant-calls/{grantCallId}/status")]
|
||||
public async Task<ActionResult<GrantCallResponse>> UpdateGrantCallStatus(
|
||||
string grantCallId,
|
||||
UpdateGrantCallStatusRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var updated = await _grantCalls.UpdateStatusAsync(grantCallId, request.Status, cancellationToken);
|
||||
|
||||
if (updated is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (string.Equals(request.Status, "open", StringComparison.Ordinal))
|
||||
{
|
||||
await NotifyIdeaAuthorsAsync(updated, cancellationToken);
|
||||
}
|
||||
|
||||
return Ok(updated);
|
||||
}
|
||||
|
||||
private async Task NotifyIdeaAuthorsAsync(GrantCallResponse call, CancellationToken cancellationToken)
|
||||
{
|
||||
var authorIds = await _ideas.ListDistinctAuthorIdsAsync(cancellationToken);
|
||||
var payload = new Dictionary<string, string?>
|
||||
{
|
||||
["grantCallId"] = call.Id,
|
||||
["name"] = call.Name,
|
||||
};
|
||||
|
||||
foreach (var authorId in authorIds)
|
||||
{
|
||||
await _notifications.NotifyUserAsync(authorId, "grant_call.opened", payload, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public class ConsentsController : ControllerBase
|
||||
userId,
|
||||
"consent.grant",
|
||||
consent.Id,
|
||||
new Dictionary<string, object?> { ["scope"] = request.Scope },
|
||||
new Dictionary<string, string?> { ["scope"] = request.Scope },
|
||||
cancellationToken);
|
||||
|
||||
return StatusCode(StatusCodes.Status201Created, consent);
|
||||
@@ -90,7 +90,7 @@ public class ConsentsController : ControllerBase
|
||||
userId,
|
||||
"consent.revoke",
|
||||
consentId,
|
||||
new Dictionary<string, object?>(),
|
||||
new Dictionary<string, string?>(),
|
||||
cancellationToken);
|
||||
|
||||
return NoContent();
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace TarninaHub.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/health")]
|
||||
|
||||
public class HealthController : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
@@ -12,5 +13,3 @@ public class HealthController : ControllerBase
|
||||
return Ok(new HealthResponse("ok"));
|
||||
}
|
||||
}
|
||||
|
||||
public record HealthResponse(string Status);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace TarninaHub.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/health")]
|
||||
|
||||
public record HealthResponse(string Status);
|
||||
@@ -17,11 +17,16 @@ public class IdeasController : ControllerBase
|
||||
{
|
||||
private readonly IIdeaService _ideaService;
|
||||
private readonly IIdeaAssistant _ideaAssistant;
|
||||
private readonly INotificationService _notifications;
|
||||
|
||||
public IdeasController(IIdeaService ideaService, IIdeaAssistant ideaAssistant)
|
||||
public IdeasController(
|
||||
IIdeaService ideaService,
|
||||
IIdeaAssistant ideaAssistant,
|
||||
INotificationService notifications)
|
||||
{
|
||||
_ideaService = ideaService;
|
||||
_ideaAssistant = ideaAssistant;
|
||||
_notifications = notifications;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@@ -38,6 +43,16 @@ public class IdeasController : ControllerBase
|
||||
|
||||
var created = await _ideaService.CreateAsync(userId, request, cancellationToken);
|
||||
|
||||
await _notifications.NotifyStaffAsync(
|
||||
"idea.created",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
["ideaId"] = created.Id,
|
||||
["title"] = created.Title,
|
||||
["authorId"] = created.AuthorId,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
return CreatedAtAction(nameof(GetById), new { ideaId = created.Id }, created);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,16 @@ public class InnovationsController : ControllerBase
|
||||
{
|
||||
private readonly IInnovationService _innovations;
|
||||
private readonly IMatchmakingService _matchmakingService;
|
||||
private readonly ITestFeedbackService _testFeedback;
|
||||
|
||||
public InnovationsController(IInnovationService innovations, IMatchmakingService matchmakingService)
|
||||
public InnovationsController(
|
||||
IInnovationService innovations,
|
||||
IMatchmakingService matchmakingService,
|
||||
ITestFeedbackService testFeedback)
|
||||
{
|
||||
_innovations = innovations;
|
||||
_matchmakingService = matchmakingService;
|
||||
_testFeedback = testFeedback;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -85,6 +90,35 @@ public class InnovationsController : ControllerBase
|
||||
return updated is null ? NotFound() : Ok(updated);
|
||||
}
|
||||
|
||||
[HttpPost("{innovationId}/test-feedback")]
|
||||
public async Task<ActionResult<TestFeedbackResponse>> CreateTestFeedback(
|
||||
string innovationId,
|
||||
CreateTestFeedbackRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var feedback = await _testFeedback.CreateAsync(innovationId, userId, request, cancellationToken);
|
||||
|
||||
return feedback is null ? NotFound() : StatusCode(StatusCodes.Status201Created, feedback);
|
||||
}
|
||||
|
||||
[HttpGet("{innovationId}/test-feedback")]
|
||||
[Authorize(Policy = Policies.ManagerOrAbove)]
|
||||
public async Task<ActionResult<IReadOnlyList<TestFeedbackResponse>>> ListTestFeedback(
|
||||
string innovationId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var feedback = await _testFeedback.ListForInnovationAsync(innovationId, cancellationToken);
|
||||
|
||||
return Ok(feedback);
|
||||
}
|
||||
|
||||
private bool ValidateStage(string? stage) => InnovationStageWire.IsValid(stage);
|
||||
|
||||
private ActionResult InvalidStage()
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace TarninaHub.Controllers;
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/matchmaking")]
|
||||
[Authorize]
|
||||
|
||||
public class MatchmakingController : ControllerBase
|
||||
{
|
||||
private readonly IMatchmakingService _matchmakingService;
|
||||
@@ -28,5 +29,3 @@ public class MatchmakingController : ControllerBase
|
||||
return Ok(new MatchmakingReindexResponse(indexed));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record MatchmakingReindexResponse(int Indexed);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Security;
|
||||
|
||||
namespace TarninaHub.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/matchmaking")]
|
||||
[Authorize]
|
||||
|
||||
public sealed record MatchmakingReindexResponse(int Indexed);
|
||||
@@ -0,0 +1,32 @@
|
||||
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/middleman")]
|
||||
[Authorize(Policy = Policies.JstOrAbove)]
|
||||
public class MiddlemanController : ControllerBase
|
||||
{
|
||||
private readonly IMiddlemanService _middleman;
|
||||
|
||||
public MiddlemanController(IMiddlemanService middleman)
|
||||
{
|
||||
_middleman = middleman;
|
||||
}
|
||||
|
||||
[HttpPost("adapt")]
|
||||
public async Task<ActionResult<MiddlemanResponse>> Adapt(
|
||||
MiddlemanRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _middleman.AdaptAsync(request, cancellationToken);
|
||||
|
||||
return response is null ? NotFound() : Ok(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
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/notifications")]
|
||||
[Authorize]
|
||||
public class NotificationsController : ControllerBase
|
||||
{
|
||||
private readonly INotificationService _notifications;
|
||||
|
||||
public NotificationsController(INotificationService notifications)
|
||||
{
|
||||
_notifications = notifications;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IReadOnlyList<NotificationResponse>>> List(CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var notifications = await _notifications.ListForUserAsync(userId, cancellationToken);
|
||||
|
||||
return Ok(notifications);
|
||||
}
|
||||
|
||||
[HttpPost("{notificationId}/read")]
|
||||
public async Task<ActionResult<NotificationResponse>> MarkRead(
|
||||
string notificationId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var notification = await _notifications.MarkReadAsync(notificationId, userId, cancellationToken);
|
||||
|
||||
return notification is null ? NotFound() : Ok(notification);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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/partnerships")]
|
||||
[Authorize]
|
||||
public class PartnershipsController : ControllerBase
|
||||
{
|
||||
private readonly IPartnershipService _partnerships;
|
||||
|
||||
public PartnershipsController(IPartnershipService partnerships)
|
||||
{
|
||||
_partnerships = partnerships;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IReadOnlyList<PartnershipResponse>>> List(CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var partnerships = await _partnerships.ListForUserAsync(userId, IsStaff(), cancellationToken);
|
||||
|
||||
return Ok(partnerships);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<PartnershipResponse>> Create(
|
||||
CreatePartnershipRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var created = await _partnerships.CreateAsync(userId, request, cancellationToken);
|
||||
|
||||
return StatusCode(StatusCodes.Status201Created, created);
|
||||
}
|
||||
|
||||
private bool IsStaff() =>
|
||||
User.IsInRole(RoleNames.Operator) || User.IsInRole(RoleNames.Manager) || User.IsInRole(RoleNames.Admin);
|
||||
}
|
||||
@@ -17,13 +17,16 @@ public class SubmissionsController : ControllerBase
|
||||
{
|
||||
private readonly ISubmissionService _submissionService;
|
||||
private readonly IMatchmakingService _matchmakingService;
|
||||
private readonly INotificationService _notifications;
|
||||
|
||||
public SubmissionsController(
|
||||
ISubmissionService submissionService,
|
||||
IMatchmakingService matchmakingService)
|
||||
IMatchmakingService matchmakingService,
|
||||
INotificationService notifications)
|
||||
{
|
||||
_submissionService = submissionService;
|
||||
_matchmakingService = matchmakingService;
|
||||
_notifications = notifications;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@@ -42,6 +45,16 @@ public class SubmissionsController : ControllerBase
|
||||
|
||||
await _matchmakingService.IndexSubmissionAsync(created.Id, cancellationToken);
|
||||
|
||||
await _notifications.NotifyStaffAsync(
|
||||
"submission.created",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
["submissionId"] = created.Id,
|
||||
["title"] = created.Title,
|
||||
["authorId"] = created.AuthorId,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
return CreatedAtAction(nameof(GetById), new { submissionId = created.Id }, created);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
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/threads")]
|
||||
[Authorize]
|
||||
public class ThreadsController : ControllerBase
|
||||
{
|
||||
private readonly IThreadService _threads;
|
||||
|
||||
public ThreadsController(IThreadService threads)
|
||||
{
|
||||
_threads = threads;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IReadOnlyList<ThreadResponse>>> List(CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = RequireUserId(out var unauthorized);
|
||||
|
||||
if (unauthorized is not null)
|
||||
{
|
||||
return unauthorized;
|
||||
}
|
||||
|
||||
var threads = await _threads.ListForUserAsync(userId, IsStaff(), cancellationToken);
|
||||
|
||||
return Ok(threads);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<ThreadResponse>> Create(
|
||||
CreateThreadRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = RequireUserId(out var unauthorized);
|
||||
|
||||
if (unauthorized is not null)
|
||||
{
|
||||
return unauthorized;
|
||||
}
|
||||
|
||||
var created = await _threads.CreateAsync(userId, request, cancellationToken);
|
||||
|
||||
return CreatedAtAction(nameof(GetById), new { threadId = created.Id }, created);
|
||||
}
|
||||
|
||||
[HttpGet("{threadId}")]
|
||||
public async Task<ActionResult<ThreadResponse>> GetById(
|
||||
string threadId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = RequireUserId(out var unauthorized);
|
||||
|
||||
if (unauthorized is not null)
|
||||
{
|
||||
return unauthorized;
|
||||
}
|
||||
|
||||
var thread = await _threads.GetAsync(threadId, userId, IsStaff(), cancellationToken);
|
||||
|
||||
return thread is null ? NotFound() : Ok(thread);
|
||||
}
|
||||
|
||||
[HttpPost("{threadId}/messages")]
|
||||
public async Task<ActionResult<MessageResponse>> AddMessage(
|
||||
string threadId,
|
||||
CreateMessageRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = RequireUserId(out var unauthorized);
|
||||
|
||||
if (unauthorized is not null)
|
||||
{
|
||||
return unauthorized;
|
||||
}
|
||||
|
||||
var message = await _threads.AddMessageAsync(threadId, userId, IsStaff(), request, cancellationToken);
|
||||
|
||||
return message is null ? NotFound() : StatusCode(StatusCodes.Status201Created, message);
|
||||
}
|
||||
|
||||
private string RequireUserId(out ActionResult? unauthorized)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
unauthorized = string.IsNullOrWhiteSpace(userId) ? Unauthorized() : null;
|
||||
|
||||
return userId ?? string.Empty;
|
||||
}
|
||||
|
||||
private bool IsStaff() =>
|
||||
User.IsInRole(RoleNames.Operator) || User.IsInRole(RoleNames.Manager) || User.IsInRole(RoleNames.Admin);
|
||||
}
|
||||
@@ -64,7 +64,7 @@ public class UsersController : ControllerBase
|
||||
currentUserId,
|
||||
"user.read",
|
||||
userId,
|
||||
new Dictionary<string, object?> { ["reason"] = "staff-lookup" },
|
||||
new Dictionary<string, string?> { ["reason"] = "staff-lookup" },
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using TarninaHub.Domain.Enums;
|
||||
|
||||
namespace TarninaHub.Domain.Rules;
|
||||
|
||||
public static class PartnershipWorkflow
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<PartnershipStatus, PartnershipStatus[]> Transitions =
|
||||
new Dictionary<PartnershipStatus, PartnershipStatus[]>
|
||||
{
|
||||
[PartnershipStatus.Proposed] = [PartnershipStatus.Active, PartnershipStatus.Closed],
|
||||
[PartnershipStatus.Active] = [PartnershipStatus.Closed],
|
||||
[PartnershipStatus.Closed] = [],
|
||||
};
|
||||
|
||||
public static bool CanTransition(PartnershipStatus from, PartnershipStatus to) =>
|
||||
from == to || Transitions[from].Contains(to);
|
||||
|
||||
public static bool IsTerminal(PartnershipStatus status) =>
|
||||
Transitions[status].Length == 0;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Dtos;
|
||||
using TarninaHub.Application.Matchmaking;
|
||||
|
||||
namespace TarninaHub.Infrastructure.Assist;
|
||||
|
||||
/// <summary>
|
||||
/// Middleman Innowacji (F-8). Adapts an innovation to a service form for an
|
||||
/// institution. Uses the configured LLM when available and a deterministic,
|
||||
/// template-based rationale otherwise.
|
||||
/// </summary>
|
||||
public sealed class MiddlemanAssistant : IMiddlemanService
|
||||
{
|
||||
private const string SystemPrompt =
|
||||
"Jesteś Middlemanem Innowacji w Małopolskim Hubie Innowacji Społecznych. " +
|
||||
"Dostosowujesz sprawdzoną innowację do formy usługi dla instytucji (JST/NGO). " +
|
||||
"Odpowiadaj prostym językiem, konkretnie (co/komu/jak) i nie wymyślaj faktów.";
|
||||
|
||||
private readonly IInnovationService _innovations;
|
||||
private readonly ILlmProvider _llmProvider;
|
||||
private readonly ILlmRequestLimiter _llmRequestLimiter;
|
||||
|
||||
public MiddlemanAssistant(
|
||||
IInnovationService innovations,
|
||||
ILlmProvider llmProvider,
|
||||
ILlmRequestLimiter llmRequestLimiter)
|
||||
{
|
||||
_innovations = innovations;
|
||||
_llmProvider = llmProvider;
|
||||
_llmRequestLimiter = llmRequestLimiter;
|
||||
}
|
||||
|
||||
public async Task<MiddlemanResponse?> AdaptAsync(
|
||||
MiddlemanRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var innovation = await _innovations.GetAsync(request.InnovationId, cancellationToken);
|
||||
|
||||
if (innovation is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var serviceForm = BuildServiceForm(innovation, request);
|
||||
var rationale = BuildRationale(innovation, request);
|
||||
var fromLlm = false;
|
||||
|
||||
if (_llmProvider.IsConfigured && _llmRequestLimiter.TryAcquire())
|
||||
{
|
||||
var profile = request.InstitutionProfile;
|
||||
var userPrompt =
|
||||
$"Innowacja: {innovation.Title}\n" +
|
||||
$"Streszczenie: {innovation.Summary}\n" +
|
||||
$"Opis: {innovation.Description}\n" +
|
||||
$"Etap: {innovation.Stage}\n" +
|
||||
$"Instytucja: {profile?.Type ?? "nieokreślona"} (obszar: {profile?.Area ?? "ogólny"})\n" +
|
||||
$"Odbiorcy: {(profile?.Audiences is { Count: > 0 } audiences ? string.Join(", ", audiences) : "nieokreśleni")}\n" +
|
||||
$"Potrzeby: {request.Needs ?? "ogólne"}\n" +
|
||||
"Zaproponuj formę usługi (co/komu/jak) i uzasadnienie.";
|
||||
|
||||
var completion = await _llmProvider.CompleteAsync(SystemPrompt, userPrompt, cancellationToken);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(completion))
|
||||
{
|
||||
serviceForm = completion.Trim();
|
||||
fromLlm = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new MiddlemanResponse(serviceForm, BuildResources(innovation), rationale, fromLlm);
|
||||
}
|
||||
|
||||
private static string BuildServiceForm(InnovationResponse innovation, MiddlemanRequest request)
|
||||
{
|
||||
var audiences = request.InstitutionProfile?.Audiences is { Count: > 0 } values
|
||||
? string.Join(", ", values)
|
||||
: "odbiorcy wskazani przez instytucję";
|
||||
|
||||
var area = request.InstitutionProfile?.Area ?? innovation.ChallengeId;
|
||||
|
||||
return $"Co: {innovation.Title} — {innovation.Summary}. " +
|
||||
$"Komu: {audiences}. " +
|
||||
$"Jak: wdrożyć jako usługę w obszarze '{area}', etap '{innovation.Stage}', " +
|
||||
$"z potrzeby: {request.Needs ?? "zdiagnozowanej lokalnie"}.";
|
||||
}
|
||||
|
||||
private static string BuildRationale(InnovationResponse innovation, MiddlemanRequest request)
|
||||
{
|
||||
var institution = request.InstitutionProfile?.Type ?? "instytucja";
|
||||
|
||||
return $"Innowacja na etapie '{innovation.Stage}' odpowiada profilowi '{institution}' i obszarowi " +
|
||||
$"'{request.InstitutionProfile?.Area ?? innovation.ChallengeId}', dlatego nadaje się do " +
|
||||
"przekształcenia w powtarzalną usługę społeczną.";
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> BuildResources(InnovationResponse innovation)
|
||||
{
|
||||
var resources = new List<string>
|
||||
{
|
||||
"Koordynator usługi i zespół realizacyjny",
|
||||
"Budżet na pilotaż oraz pomiar efektów",
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(innovation.Cost))
|
||||
{
|
||||
resources.Add($"Szacowany koszt: {innovation.Cost}");
|
||||
}
|
||||
|
||||
resources.Add("Partnerzy lokalni (NGO/JST/firma) do wdrożenia");
|
||||
|
||||
return resources;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user