Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion backend/src/CCE.Api.Common/Results/EnvelopeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ public static class EnvelopeWriter
internal static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

??

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

دا علشان ب ignore اي فيلد null

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need if name=""
need

Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static IEndpointRouteBuilder MapInteractiveMapPublicEndpoints(this IEndpo

// GET /api/interactive-maps/nodes/{nodeId}/details
// Returns the side-panel details when a user clicks a map node:
// node info + linked topic + top-5 news, events, posts, and resources.
// node info + top-5 news, events, and resources.
maps.MapGet("/nodes/{nodeId:guid}/details", async (
System.Guid nodeId,
IMediator mediator, CancellationToken cancellationToken) =>
Expand Down
16 changes: 12 additions & 4 deletions backend/src/CCE.Api.Internal/Endpoints/InteractiveMapEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ public static IEndpointRouteBuilder MapInteractiveMapEndpoints(this IEndpointRou
{
var cmd = new CreateInteractiveMapNodeCommand(
mapId, body.NameAr, body.NameEn, body.IconKey, body.Category,
body.CategoryNameAr, body.CategoryNameEn, body.Level,
body.CategoryNameAr, body.CategoryNameEn,
body.TitleAr, body.TitleEn, body.DescriptionAr, body.DescriptionEn,
body.ParentId, body.TopicId);
var response = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false);
return response.ToCreatedHttpResult();
Expand All @@ -79,7 +80,8 @@ public static IEndpointRouteBuilder MapInteractiveMapEndpoints(this IEndpointRou
{
var cmd = new UpdateInteractiveMapNodeCommand(
mapId, id, body.NameAr, body.NameEn, body.IconKey, body.Category,
body.CategoryNameAr, body.CategoryNameEn, body.Level,
body.CategoryNameAr, body.CategoryNameEn,
body.TitleAr, body.TitleEn, body.DescriptionAr, body.DescriptionEn,
body.ParentId, body.TopicId, body.IsActive);
var response = await mediator.Send(cmd, cancellationToken).ConfigureAwait(false);
return response.ToHttpResult();
Expand Down Expand Up @@ -114,7 +116,10 @@ public sealed record CreateInteractiveMapNodeRequest(
int? Category,
string? CategoryNameAr,
string? CategoryNameEn,
int Level,
string? TitleAr,
string? TitleEn,
string? DescriptionAr,
string? DescriptionEn,
System.Guid? ParentId,
System.Guid TopicId);

Expand All @@ -125,7 +130,10 @@ public sealed record UpdateInteractiveMapNodeRequest(
int? Category,
string? CategoryNameAr,
string? CategoryNameEn,
int Level,
string? TitleAr,
string? TitleEn,
string? DescriptionAr,
string? DescriptionEn,
System.Guid? ParentId,
System.Guid TopicId,
bool IsActive);
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ public sealed record CreateInteractiveMapNodeCommand(
int? Category,
string? CategoryNameAr,
string? CategoryNameEn,
int Level,
string? TitleAr,
string? TitleEn,
string? DescriptionAr,
string? DescriptionEn,
System.Guid? ParentId,
System.Guid TopicId) : IRequest<Response<VoidData>>;
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,19 @@ public async Task<Response<VoidData>> Handle(
CancellationToken cancellationToken)
{
var entity = InteractiveMapNode.Create(
request.InteractiveMapId,
request.NameAr,
request.NameEn,
request.IconKey,
request.Category,
request.CategoryNameAr,
request.CategoryNameEn,
request.Level,
request.ParentId,
request.TopicId);
interactiveMapId: request.InteractiveMapId,
nameAr: request.NameAr,
nameEn: request.NameEn,
iconKey: request.IconKey,
category: request.Category,
categoryNameAr: request.CategoryNameAr,
categoryNameEn: request.CategoryNameEn,
titleAr: request.TitleAr,
titleEn: request.TitleEn,
descriptionAr: request.DescriptionAr,
descriptionEn: request.DescriptionEn,
parentId: request.ParentId,
topicId: request.TopicId);

await _repo.AddAsync(entity, cancellationToken).ConfigureAwait(false);
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ public CreateInteractiveMapNodeCommandValidator()
RuleFor(x => x.IconKey)
.NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD)
.MaximumLength(128).WithErrorCode(MessageKeys.Validation.MAX_LENGTH);
RuleFor(x => x.Level)
.GreaterThanOrEqualTo(0).WithErrorCode(MessageKeys.Validation.INVALID_FORMAT);
RuleFor(x => x.TopicId)
.NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD);

RuleFor(x => x.TitleAr)
.MaximumLength(512).WithErrorCode(MessageKeys.Validation.MAX_LENGTH);

RuleFor(x => x.TitleEn)
.MaximumLength(512).WithErrorCode(MessageKeys.Validation.MAX_LENGTH);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ public sealed record UpdateInteractiveMapNodeCommand(
int? Category,
string? CategoryNameAr,
string? CategoryNameEn,
int Level,
string? TitleAr,
string? TitleEn,
string? DescriptionAr,
string? DescriptionEn,
System.Guid? ParentId,
System.Guid TopicId,
bool IsActive) : IRequest<Response<VoidData>>;
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,18 @@ public async Task<Response<VoidData>> Handle(
return _msg.NotFound<VoidData>(MessageKeys.InteractiveMaps.NODE_NOT_FOUND);

entity.UpdateDetails(
request.NameAr,
request.NameEn,
request.IconKey,
request.Category,
request.CategoryNameAr,
request.CategoryNameEn,
request.Level,
request.ParentId,
request.TopicId);
nameAr: request.NameAr,
nameEn: request.NameEn,
iconKey: request.IconKey,
category: request.Category,
categoryNameAr: request.CategoryNameAr,
categoryNameEn: request.CategoryNameEn,
titleAr: request.TitleAr,
titleEn: request.TitleEn,
descriptionAr: request.DescriptionAr,
descriptionEn: request.DescriptionEn,
parentId: request.ParentId,
topicId: request.TopicId);

if (request.IsActive)
entity.Activate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ public UpdateInteractiveMapNodeCommandValidator()
RuleFor(x => x.IconKey)
.NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD)
.MaximumLength(128).WithErrorCode(MessageKeys.Validation.MAX_LENGTH);
RuleFor(x => x.Level)
.GreaterThanOrEqualTo(0).WithErrorCode(MessageKeys.Validation.INVALID_FORMAT);
RuleFor(x => x.TopicId)
.NotEmpty().WithErrorCode(MessageKeys.Validation.REQUIRED_FIELD);

RuleFor(x => x.TitleAr)
.MaximumLength(512).WithErrorCode(MessageKeys.Validation.MAX_LENGTH);

RuleFor(x => x.TitleEn)
.MaximumLength(512).WithErrorCode(MessageKeys.Validation.MAX_LENGTH);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ public sealed record InteractiveMapNodeDto(
int? Category,
string? CategoryNameAr,
string? CategoryNameEn,
int Level,
string? TitleAr,
string? TitleEn,
string? DescriptionAr,
string? DescriptionEn,
System.Guid? ParentId,
System.Guid TopicId,
bool IsActive,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using CCE.Domain.Community;
using CCE.Domain.Content;

namespace CCE.Application.InteractiveMaps.Public.Dtos;
Expand All @@ -8,30 +7,22 @@ namespace CCE.Application.InteractiveMaps.Public.Dtos;
/// </summary>
public sealed record MapNodeDetailsDto(
MapNodeSummaryDto Node,
MapNodeTopicDto Topic,
IReadOnlyList<MapNodeResourceDto> Resources,
IReadOnlyList<MapNodeNewsDto> News,
IReadOnlyList<MapNodeEventDto> Events,
IReadOnlyList<MapNodePostDto> Posts);
IReadOnlyList<MapNodeEventDto> Events);

/// <summary>Core fields of the clicked node.</summary>
public sealed record MapNodeSummaryDto(
System.Guid Id,
string NameAr,
string NameEn,
string IconKey,
string? TitleAr,
string? TitleEn,
string? DescriptionAr,
string? DescriptionEn,
System.Guid TopicId);

/// <summary>Topic linked to the node.</summary>
public sealed record MapNodeTopicDto(
System.Guid Id,
string NameAr,
string NameEn,
string DescriptionAr,
string DescriptionEn,
string Slug,
string? IconUrl);

/// <summary>Slim resource card — top N recently published.</summary>
public sealed record MapNodeResourceDto(
System.Guid Id,
Expand All @@ -58,12 +49,3 @@ public sealed record MapNodeEventDto(
System.DateTimeOffset StartsOn,
System.DateTimeOffset EndsOn,
string? FeaturedImageUrl);

/// <summary>Slim post card — published posts filtered by the node's topic.</summary>
public sealed record MapNodePostDto(
System.Guid Id,
PostType Type,
string? Title,
string? Content,
int CommentsCount,
System.DateTimeOffset CreatedOn);
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ public sealed record PublicInteractiveMapNodeDto(
int? Category,
string? CategoryNameAr,
string? CategoryNameEn,
int Level,
System.Guid? ParentId,
System.Guid TopicId,
System.Collections.Generic.IReadOnlyList<InteractiveMapTagDto> Tags)
{
internal static PublicInteractiveMapNodeDto FromEntity(InteractiveMapNode n) => new(
n.Id, n.NameAr, n.NameEn, n.IconKey,
n.Category, n.CategoryNameAr, n.CategoryNameEn,
n.Level, n.ParentId, n.TopicId,
n.ParentId, n.TopicId,
n.Tags.Select(t => new InteractiveMapTagDto(t.Id, t.NameAr, t.NameEn)).ToList());
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using CCE.Application.Common;
using CCE.Application.Common.Interfaces;
using CCE.Application.InteractiveMaps.Dtos;
using CCE.Application.InteractiveMaps.Public.Dtos;
using CCE.Application.Messages;
using CCE.Domain.InteractiveMaps;
using MediatR;
using Microsoft.EntityFrameworkCore;

Expand Down Expand Up @@ -32,17 +34,20 @@ public async Task<Response<PublicInteractiveMapDto>> Handle(
return _msg.NotFound<PublicInteractiveMapDto>(MessageKeys.InteractiveMaps.MAP_NOT_FOUND);

var nodes = await _db.InteractiveMapNodes
.Include(n => n.Tags)
.AsNoTracking()
.Where(n => n.InteractiveMapId == map.Id && n.IsActive)
.OrderBy(n => n.Category)
.ThenBy(n => n.Level)
.Select(n => new PublicInteractiveMapNodeDto(
n.Id, n.NameAr, n.NameEn, n.IconKey,
n.Category, n.CategoryNameAr, n.CategoryNameEn,
n.ParentId, n.TopicId,
n.Tags.Select(t => new InteractiveMapTagDto(t.Id, t.NameAr, t.NameEn)).ToList()))
.ToListAsync(cancellationToken)
.ConfigureAwait(false);

return _msg.Ok(
PublicInteractiveMapDto.FromEntity(
map,
nodes.Select(PublicInteractiveMapNodeDto.FromEntity).ToList()),
new PublicInteractiveMapDto(
map.Id, map.NameAr, map.NameEn, map.DescriptionAr, map.DescriptionEn, nodes),
MessageKeys.General.ITEMS_LISTED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using CCE.Application.Common.Interfaces;
using CCE.Application.InteractiveMaps.Public.Dtos;
using CCE.Application.Messages;
using CCE.Domain.Community;
using MediatR;
using Microsoft.EntityFrameworkCore;

Expand Down Expand Up @@ -47,18 +46,7 @@ public async Task<Response<MapNodeDetailsDto>> Handle(

var hasTags = nodeTagIds.Count > 0;

// ─── 2. Resolve the linked topic ───
var topic = await _db.Topics
.AsNoTracking()
.Where(t => t.Id == node.TopicId && t.IsActive)
.Select(t => new { t.Id, t.NameAr, t.NameEn, t.DescriptionAr, t.DescriptionEn, t.Slug, t.IconUrl })
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);

if (topic is null)
return _msg.NotFound<MapNodeDetailsDto>(MessageKeys.InteractiveMaps.MAP_NOT_FOUND);

// ─── 3. News — top N by topic or tags, newest first ───
// ─── 2. News — top N by topic or tags, newest first ───
var news = await _db.News
.AsNoTracking()
.Where(n => n.PublishedOn != null)
Expand All @@ -69,7 +57,7 @@ public async Task<Response<MapNodeDetailsDto>> Handle(
.ToListAsync(cancellationToken)
.ConfigureAwait(false);

// ─── 4. Events — upcoming by topic or tags, soonest first ───
// ─── 3. Events — upcoming by topic or tags, soonest first ───
var now = DateTimeOffset.UtcNow;
var events = await _db.Events
.AsNoTracking()
Expand All @@ -81,18 +69,7 @@ public async Task<Response<MapNodeDetailsDto>> Handle(
.ToListAsync(cancellationToken)
.ConfigureAwait(false);

// ─── 5. Posts — published by topic or tags, hottest first ───
var posts = await _db.Posts
.AsNoTracking()
.Where(p => p.Status == PostStatus.Published)
.Where(p => p.TopicId == node.TopicId || (hasTags && p.Tags.Any(t => nodeTagIds.Contains(t.Id))))
.OrderByDescending(p => p.Score)
.Take(SliceSize)
.Select(p => new MapNodePostDto(p.Id, p.Type, p.Title, p.Content, p.CommentsCount, p.CreatedOn))
.ToListAsync(cancellationToken)
.ConfigureAwait(false);

// ─── 6. Resources — top N recently published (Resource has no TopicId FK) ───
// ─── 4. Resources — top N recently published ───
var categoryIds = await _db.Resources
.AsNoTracking()
.Where(r => r.PublishedOn != null)
Expand Down Expand Up @@ -123,14 +100,12 @@ public async Task<Response<MapNodeDetailsDto>> Handle(
})
.ToList();

// ─── 7. Assemble ───
// ─── 5. Assemble ───
var dto = new MapNodeDetailsDto(
Node: new MapNodeSummaryDto(node.Id, node.NameAr, node.NameEn, node.IconKey, node.TopicId),
Topic: new MapNodeTopicDto(topic.Id, topic.NameAr, topic.NameEn, topic.DescriptionAr, topic.DescriptionEn, topic.Slug, topic.IconUrl),
Node: new MapNodeSummaryDto(node.Id, node.NameAr, node.NameEn, node.IconKey, null, null, null, null, node.TopicId),
Resources: resources,
News: news,
Events: events,
Posts: posts);
Events: events);

return _msg.Ok(dto, MessageKeys.General.ITEMS_LISTED);
}
Expand Down
Loading