diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetMilestoneEvidenceEndpoint.cs b/PostFundManagement.Api/Endpoints/Evidence/GetMilestoneEvidenceEndpoint.cs new file mode 100644 index 0000000..2381905 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Evidence/GetMilestoneEvidenceEndpoint.cs @@ -0,0 +1,32 @@ +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Evidence; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; + +namespace PostFundManagement.Api.Endpoints.Evidence; + +[ApiVersionTarget(1)] +public class GetMilestoneEvidenceEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/evidence/milestone/{milestoneId}", async (string milestoneId, EvidenceService service, + CancellationToken cancellationToken = default) => + { + var result = await service.GetMilestoneEvidencesAsync(milestoneId, cancellationToken); + + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.NotFound(); + }) + .WithDescription("Get a list of all uploaded evidence metadata for a specific mliestone") + .WithName(typeof(GetMilestoneEvidenceEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Evidence) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetProjectEvidenceEndpoint.cs b/PostFundManagement.Api/Endpoints/Evidence/GetProjectEvidenceEndpoint.cs new file mode 100644 index 0000000..474fe46 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Evidence/GetProjectEvidenceEndpoint.cs @@ -0,0 +1,32 @@ +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Evidence; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; + +namespace PostFundManagement.Api.Endpoints.Evidence; + +[ApiVersionTarget(1)] +public class GetProjectEvidenceEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/evidence/milestone/{projectId}", async (string projectId, EvidenceService service, + CancellationToken cancellationToken = default) => + { + var result = await service.GetProjectEvidencesAsync(projectId, cancellationToken); + + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.NotFound(); + }) + .WithDescription("Get a list of all uploaded evidence metadata for a specific project") + .WithName(typeof(GetProjectEvidenceEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Evidence) + .CacheOutput(); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/GetActivityEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetActivityEndpoint.cs new file mode 100644 index 0000000..ebb83b1 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetActivityEndpoint.cs @@ -0,0 +1,31 @@ +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public class GetActivityEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/activities/{activityId}", async (string activityId, + ProjectService service, CancellationToken cancellationToken = default) => + { + var result = await service.GetActivityAsync(activityId, cancellationToken); + + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.NotFound(); + }) + .WithDescription("Gets a specific activity by it's id") + .WithName(typeof(GetActivityEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetMilestoneEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetMilestoneEndpoint.cs similarity index 78% rename from PostFundManagement.Api/Endpoints/Evidence/GetMilestoneEndpoint.cs rename to PostFundManagement.Api/Endpoints/Projects/GetMilestoneEndpoint.cs index c1cdcd3..3cc0672 100644 --- a/PostFundManagement.Api/Endpoints/Evidence/GetMilestoneEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Projects/GetMilestoneEndpoint.cs @@ -1,18 +1,18 @@ using PostFundManagement.Application.Extensions; -using PostFundManagement.Application.Services.Evidence; +using PostFundManagement.Application.Services.Projects; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Security; -namespace PostFundManagement.Api.Endpoints.Evidence; +namespace PostFundManagement.Api.Endpoints.Projects; [ApiVersionTarget(1)] public class GetMilestoneEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/evidence/milestones/{milestoneId}", async (string milestoneId, EvidenceService service, + builder.MapGet("api/projects/milestones/{milestoneId}", async (string milestoneId, ProjectService service, CancellationToken cancellationToken = default) => { var result = await service.GetMilestoneAsync(milestoneId, cancellationToken); @@ -27,7 +27,7 @@ public class GetMilestoneEndpoint : IEndpoint .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized) - .WithTags(EndpointTags.Evidence) + .WithTags(EndpointTags.Projects) .CacheOutput(); } } diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetMilestonesEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetMilestonesEndpoint.cs similarity index 75% rename from PostFundManagement.Api/Endpoints/Evidence/GetMilestonesEndpoint.cs rename to PostFundManagement.Api/Endpoints/Projects/GetMilestonesEndpoint.cs index cd5bf92..d4ee1af 100644 --- a/PostFundManagement.Api/Endpoints/Evidence/GetMilestonesEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Projects/GetMilestonesEndpoint.cs @@ -1,20 +1,20 @@ using PostFundManagement.Application.Extensions; -using PostFundManagement.Application.Services.Evidence; +using PostFundManagement.Application.Services.Projects; using PostFundManagement.Domain; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Security; -namespace PostFundManagement.Api.Endpoints.Evidence; +namespace PostFundManagement.Api.Endpoints.Projects; [ApiVersionTarget(1)] public class GetMilestonesEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/evidence/milestones/{page:int}/{pageSize:int}", async (EvidenceService service, - int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) => + builder.MapGet("api/projects/milestones/{page:int}/{pageSize:int}", async (ProjectService service, + int page = 1, int pageSize = 100, CancellationToken cancellationToken = default) => { var result = await service.GetMilestonesAsync(Pagination.Create(page, pageSize), cancellationToken); @@ -29,6 +29,6 @@ public class GetMilestonesEndpoint : IEndpoint .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) - .WithTags(EndpointTags.Evidence); + .WithTags(EndpointTags.Projects); } } diff --git a/PostFundManagement.Api/Endpoints/Projects/GetProjectByIdEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetProjectEndpoint.cs similarity index 83% rename from PostFundManagement.Api/Endpoints/Projects/GetProjectByIdEndpoint.cs rename to PostFundManagement.Api/Endpoints/Projects/GetProjectEndpoint.cs index b2eccb2..e9327d1 100644 --- a/PostFundManagement.Api/Endpoints/Projects/GetProjectByIdEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Projects/GetProjectEndpoint.cs @@ -8,21 +8,21 @@ using PostFundManagement.Domain.Security; namespace PostFundManagement.Api.Endpoints.Projects; [ApiVersionTarget(1)] -public class GetProjectByIdEndpoint : IEndpoint +public class GetProjectEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { builder.MapGet("api/projects/{projectId}", async (string projectId, ProjectService service, CancellationToken cancellationToken = default) => { - var result = await service.GetProjectByIdAsync(projectId, cancellationToken); + var result = await service.GetProjectAsync(projectId, cancellationToken); return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(); }) .WithDescription("Get project by ID") - .WithName(typeof(GetProjectByIdEndpoint).ToEndpointName()) + .WithName(typeof(GetProjectEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound) diff --git a/PostFundManagement.Api/Endpoints/Projects/GetProjectIndicatorsEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetProjectIndicatorsEndpoint.cs new file mode 100644 index 0000000..6a2dd56 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/GetProjectIndicatorsEndpoint.cs @@ -0,0 +1,32 @@ +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Security; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public class GetProjectIndicatorsEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/projects/indicators/project/{projectId}", async (string projectId, ProjectService service, + CancellationToken cancellationToken = default) => + { + var result = await service.GetProjectIndicatorsAsync(projectIdHash: projectId, cancellationToken); + + return result.IsSuccess + ? Results.Ok(result.Value) + : Results.NotFound(); + }) + .WithDescription("Get get a detailed list of project indicators") + .WithName(typeof(GetProjectIndicatorsEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Evidence/GetProjectMilestonesEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/GetProjectMilestonesEndpoint.cs similarity index 66% rename from PostFundManagement.Api/Endpoints/Evidence/GetProjectMilestonesEndpoint.cs rename to PostFundManagement.Api/Endpoints/Projects/GetProjectMilestonesEndpoint.cs index 8af68df..3a48bcc 100644 --- a/PostFundManagement.Api/Endpoints/Evidence/GetProjectMilestonesEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Projects/GetProjectMilestonesEndpoint.cs @@ -1,21 +1,21 @@ using PostFundManagement.Application.Extensions; -using PostFundManagement.Application.Services.Evidence; +using PostFundManagement.Application.Services.Projects; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models; using PostFundManagement.Domain.Security; -namespace PostFundManagement.Api.Endpoints.Evidence; +namespace PostFundManagement.Api.Endpoints.Projects; [ApiVersionTarget(1)] public class GetProjectMilestonesEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapGet("api/evidence/milestones/project/{projectId}", async (string projectId, - EvidenceService service, CancellationToken cancellationToken = default) => + builder.MapGet("api/projects/{projectId}/milestones", async (string projectId, + ProjectService service, CancellationToken cancellationToken = default) => { - var result = await service.GetMilestonesByProjectIdAsync(projectId, cancellationToken); + var result = await service.GetProjectMilestonesAsync(projectId, cancellationToken); return result.IsSuccess ? Results.Ok(result.Value) @@ -27,6 +27,6 @@ public class GetProjectMilestonesEndpoint : IEndpoint .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized) - .WithTags(EndpointTags.Evidence); + .WithTags(EndpointTags.Projects); } } diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateMilestoneEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateMilestoneEndpoint.cs new file mode 100644 index 0000000..cf5712e --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateMilestoneEndpoint.cs @@ -0,0 +1,31 @@ +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public sealed class UpdateMilestoneEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPut("api/projects/milestone", async (UpdateMilestoneRequest request, ProjectService service, + HttpContext context, CancellationToken cancellationToken = default) => + { + var result = await service.UpdateMilestoneAsync(request, context.GetUserId(), cancellationToken); + + return result.IsSuccess + ? Results.Ok() + : Results.BadRequest(result.Errors.ToProblems()); + }) + .WithDescription("Update project milestone details") + .WithName(typeof(UpdateMilestoneEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateProjectEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateProjectEndpoint.cs index a2cfbcb..d5a5157 100644 --- a/PostFundManagement.Api/Endpoints/Projects/UpdateProjectEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateProjectEndpoint.cs @@ -1,4 +1,4 @@ -using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Services.Projects; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Extensions; @@ -7,20 +7,20 @@ using PostFundManagement.Domain.Security; namespace PostFundManagement.Api.Endpoints.Projects; [ApiVersionTarget(1)] -public class UpdateProjectEndpoint : IEndpoint +public sealed class UpdateProjectEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - builder.MapPost("api/projects/status", async (UpdateProjectStatusRequest request, HttpContext context, - ProjectService service, CancellationToken cancellationToken = default) => + builder.MapPut("api/projects", async (UpdateProjectRequest request, + ProjectService service, HttpContext context, CancellationToken cancellationToken = default) => { - var result = await service.UpdateProjectStatusAsync(request, context.GetUserId(), cancellationToken); + var result = await service.UpdateProjectAsync(request, userId: context.GetUserId(), cancellationToken); return result.IsSuccess ? Results.Ok() : Results.BadRequest(result.Errors.ToProblems()); }) - .WithDescription("Update project status (M04)") + .WithDescription("Update project details") .WithName(typeof(UpdateProjectEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK) diff --git a/PostFundManagement.Api/Endpoints/Projects/UpdateProjectStatusEndpoint.cs b/PostFundManagement.Api/Endpoints/Projects/UpdateProjectStatusEndpoint.cs new file mode 100644 index 0000000..bfaefd6 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Projects/UpdateProjectStatusEndpoint.cs @@ -0,0 +1,31 @@ +using PostFundManagement.Application.Extensions; +using PostFundManagement.Application.Services.Projects; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Security; + +namespace PostFundManagement.Api.Endpoints.Projects; + +[ApiVersionTarget(1)] +public class UpdateProjectStatusEndpoint : IEndpoint +{ + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPut("api/projects/status", async (UpdateProjectStatusRequest request, HttpContext context, + ProjectService service, CancellationToken cancellationToken = default) => + { + var result = await service.UpdateProjectStatusAsync(request, context.GetUserId(), cancellationToken); + + return result.IsSuccess + ? Results.Ok() + : Results.BadRequest(result.Errors.ToProblems()); + }) + .WithDescription("Update project status (M04)") + .WithName(typeof(UpdateProjectStatusEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized) + .WithTags(EndpointTags.Projects); + } +} diff --git a/PostFundManagement.Application/Services/Evidence/EvidenceService.cs b/PostFundManagement.Application/Services/Evidence/EvidenceService.cs index a070297..e084ebc 100644 --- a/PostFundManagement.Application/Services/Evidence/EvidenceService.cs +++ b/PostFundManagement.Application/Services/Evidence/EvidenceService.cs @@ -82,51 +82,7 @@ public sealed class EvidenceService(IDbContextFactory cont return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } - - public async ValueTask> GetMilestonesByProjectIdAsync(string projectIdHash, CancellationToken cancellationToken = default) - { - try - { - var decodeResult = hashService.DecodeLongIdHash(projectIdHash); - - if (decodeResult.IsFailed) - return Result.Fail(decodeResult.Errors); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var milestones = await context.Milestones.AsNoTracking() - .Where(m => m.ProjectId == decodeResult.Value) - .OrderBy(m => m.DueAt) - .ToListAsync(cancellationToken); - - return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } - - public async ValueTask> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) - { - try - { - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var milestones = await context.Milestones.AsNoTracking() - .OrderByDescending(m => m.CreatedAt) - .Skip((pagination.Page - 1) * pagination.PageSize) - .Take(pagination.PageSize) - .ToListAsync(cancellationToken); - - return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []); - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } - + public async ValueTask> GetIndicatorsAsync(Pagination pagination, CancellationToken cancellationToken = default) { try @@ -253,31 +209,7 @@ public sealed class EvidenceService(IDbContextFactory cont return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } - - public async Task> GetMilestoneAsync(string milestoneId, CancellationToken cancellationToken) - { - try - { - var decodeResult = hashService.DecodeLongIdHash(milestoneId); - - if (decodeResult.IsFailed) - return Result.Fail(decodeResult.Errors); - - using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - - var milestone = await context.Milestones.AsNoTracking().FirstOrDefaultAsync(m => m.Id == decodeResult.Value, cancellationToken); - - return milestone is not null - ? Result.Ok(milestone.Map()) - : Result.Fail("Failed to get specific milestone"); - - } - catch (Exception ex) - { - return Result.Fail(new Error(ex.Message).CausedBy(ex)); - } - } - + public async ValueTask> GetEvidenceAsync(string evidenceId, CancellationToken cancellationToken = default) { try @@ -300,4 +232,54 @@ public sealed class EvidenceService(IDbContextFactory cont return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } + + public async ValueTask> GetMilestoneEvidencesAsync(string milestoneIdHash, CancellationToken cancellationToken = default) + { + try + { + var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(decodeResult.Errors); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var evidences = await context.Evidences.AsNoTracking() + .Where(e => e.MilestoneId == decodeResult.Value) + .OrderByDescending(o => o.CreatedAt) + .ThenByDescending(o => o.UpdatedAt) + .ToListAsync(cancellationToken); + + return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task> GetProjectEvidencesAsync(string projectIdHash, CancellationToken cancellationToken = default) + { + try + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(decodeResult.Errors); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var evidences = await context.Evidences.AsNoTracking() + .Where(e => e.ProjectId == decodeResult.Value) + .OrderByDescending(o => o.CreatedAt) + .ThenBy(o => o.UpdatedAt) + .ToListAsync(cancellationToken); + + return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } } diff --git a/PostFundManagement.Application/Services/Projects/ProjectService.cs b/PostFundManagement.Application/Services/Projects/ProjectService.cs index 1e5ccac..07c138d 100644 --- a/PostFundManagement.Application/Services/Projects/ProjectService.cs +++ b/PostFundManagement.Application/Services/Projects/ProjectService.cs @@ -62,7 +62,7 @@ public sealed class ProjectService(IDbContextFactory conte } } - public async ValueTask> GetProjectByIdAsync(string projectIdHash, CancellationToken cancellationToken = default) + public async ValueTask> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) { try { @@ -179,4 +179,171 @@ public sealed class ProjectService(IDbContextFactory conte return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } + + public async ValueTask> GetProjectMilestonesAsync(string projectIdHash, CancellationToken cancellationToken = default) + { + try + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(decodeResult.Errors); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var milestones = await context.Milestones.AsNoTracking() + .Where(m => m.ProjectId == decodeResult.Value) + .OrderBy(m => m.DueAt) + .ToListAsync(cancellationToken); + + return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task> GetProjectIndicatorsAsync(string projectIdHash, CancellationToken cancellationToken) + { + try + { + var decodeResult = hashService.DecodeLongIdHash(projectIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(decodeResult.Errors); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var evidences = await context.Indicators.AsNoTracking() + .Where(e => e.ProjectId == decodeResult.Value) + .OrderByDescending(o => o.Id) + .ToListAsync(cancellationToken); + + return Result.Ok(evidences?.Select(e => e.Map()).ToArray() ?? []); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task> GetMilestoneAsync(string milestoneIdHash, CancellationToken cancellationToken) + { + try + { + var decodeResult = hashService.DecodeLongIdHash(milestoneIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(decodeResult.Errors); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var milestone = await context.Milestones.AsNoTracking().FirstOrDefaultAsync(m => m.Id == decodeResult.Value, cancellationToken); + + return milestone is not null + ? Result.Ok(milestone.Map()) + : Result.Fail("Failed to get specific milestone"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetMilestonesAsync(Pagination pagination, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var milestones = await context.Milestones.AsNoTracking() + .OrderByDescending(m => m.CreatedAt) + .Skip((pagination.Page - 1) * pagination.PageSize) + .Take(pagination.PageSize) + .ToListAsync(cancellationToken); + + return Result.Ok(milestones?.Select(i => i.Map()).ToArray() ?? []); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task UpdateMilestoneAsync(UpdateMilestoneRequest request, Guid userId, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var changedTotal = await context.Milestones.Where(m => m.Id == request.MilestoneId) + .ExecuteUpdateAsync(f => f + .SetProperty(u => u.Name, request.Name) + .SetProperty(u => u.DueAt, request.DueAt) + .SetProperty(u => u.Priority, request.Priority) + .SetProperty(u => u.Status, request.Status) + .SetProperty(u => u.UpdatedBy, userId) + .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); + + return changedTotal > 0 + ? Result.Ok() + : Result.Fail("Failed to update milestone"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async Task UpdateProjectAsync(UpdateProjectRequest request, Guid userId, CancellationToken cancellationToken) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var changedTotal = await context.Projects.Where(p => p.Id == request.ProjectId) + .ExecuteUpdateAsync(f => f + .SetProperty(u => u.Name, request.Name) + .SetProperty(u => u.Description, request.Description) + .SetProperty(u => u.StartedAt, request.StartedAt) + .SetProperty(u => u.EndedAt, request.EndedAt) + .SetProperty(u => u.UpdatedBy, userId) + .SetProperty(u => u.UpdatedAt, DateTime.UtcNow) + .SetProperty(u => u.Sector, request.Sector) + .SetProperty(u => u.ProjectType, request.ProjectType), cancellationToken); + + return changedTotal > 0 + ? Result.Ok() + : Result.Fail("Failed to update project"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetActivityAsync(string activityIdHash, CancellationToken cancellationToken) + { + try + { + var decodeResult = hashService.DecodeLongIdHash(activityIdHash); + + if (decodeResult.IsFailed) + return Result.Fail(decodeResult.Errors); + + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var activity = await context.Activities.AsNoTracking().FirstOrDefaultAsync(a => a.Id == decodeResult.Value, cancellationToken); + + return activity is not null + ? Result.Ok(activity.Map()) + : Result.Fail("Failed to get specific activity."); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } } diff --git a/PostFundManagement.Application/Services/Projects/Records.cs b/PostFundManagement.Application/Services/Projects/Records.cs index 0f3f897..10a9b09 100644 --- a/PostFundManagement.Application/Services/Projects/Records.cs +++ b/PostFundManagement.Application/Services/Projects/Records.cs @@ -2,6 +2,10 @@ namespace PostFundManagement.Application.Services.Projects; +public record UpdateProjectRequest(long ProjectId, string Name, string Description, DateTime StartedAt, DateTime? EndedAt, IndustrySector Sector, ProjectType ProjectType); + +public record UpdateMilestoneRequest(long MilestoneId, DateTime DueAt, string Name, Priority Priority, ApprovalStatus Status); + public record UpdateProjectStatusRequest(long ProjectId, ApprovalStatus Status); public record CreateProjectRequest(long ProgrammeId, string Name, string Description, IndustrySector Sector, ProjectType ProjectType);