backend phase 14 & frontend phase 7

This commit is contained in:
hamid
2026-07-09 15:30:03 +03:30
parent de53f9d8a6
commit 93cc5ecb98
101 changed files with 12930 additions and 39 deletions
@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using Asp.Versioning;
using Baya.Application.Features.Reviews.Queries.GetReviewModerationQueue;
using Baya.Application.Models.Common;
using Baya.Application.Models.Reviews;
using Baya.Infrastructure.Identity.Identity.PermissionManager;
using Baya.WebFramework.Attributes;
using Baya.WebFramework.BaseController;
using Mediator;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Baya.Web.Api.Controllers.V1;
/// <summary>Admin review console: the moderation queue (defaults to <c>pending_moderation</c>), with any linked
/// low-rating alert id. Support alerts themselves stay internal — only their id is surfaced here for triage.</summary>
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/admin/reviews")]
[Authorize(ConstantPolicies.DynamicPermission)]
[Display(Description = "Admin review moderation queue")]
public sealed class AdminReviewsController(ISender sender) : BaseController
{
[HttpGet("moderation_queue")]
[ProducesOkApiResponseType<PagedResult<ModerationQueueItemDto>>]
public async Task<IActionResult> ModerationQueue([FromQuery] GetReviewModerationQueueQuery query, CancellationToken cancellationToken)
=> OperationResult(await sender.Send(query, cancellationToken));
}
@@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using Asp.Versioning;
using Baya.Application.Features.Reviews.Commands.SubmitReview;
using Baya.Application.Models.Reviews;
using Baya.WebFramework.Attributes;
using Baya.WebFramework.BaseController;
using Mediator;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Baya.Web.Api.Controllers.V1;
/// <summary>
/// The customer submits the one allowed review for a completed booking. Ownership, the completed/closed
/// eligibility gate, and the 1:1 rule are enforced in the handler; the booking id comes from the route.
/// </summary>
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/bookings")]
[Authorize]
[Display(Description = "Submit the one review for a completed booking (customer)")]
public sealed class BookingReviewsController(ISender sender) : BaseController
{
[HttpPost("{bookingId}/review")]
[ProducesOkApiResponseType<SubmitReviewResult>]
public async Task<IActionResult> Review(long bookingId, SubmitReviewBody body, CancellationToken cancellationToken)
=> OperationResult(await sender.Send(
new SubmitReviewCommand(bookingId, body.Rating, body.Body, body.TagCodes), cancellationToken));
/// <summary>The review body (the booking id comes from the route).</summary>
public record SubmitReviewBody(int Rating, string? Body, IReadOnlyList<string>? TagCodes);
}
@@ -1,6 +1,9 @@
using System.ComponentModel.DataAnnotations;
using Asp.Versioning;
using Baya.Application.Features.Reviews.Queries.GetTagAggregates;
using Baya.Application.Features.Reviews.Queries.ListReviewsForNurse;
using Baya.Application.Features.Verification.Queries.GetTrustBadge;
using Baya.Application.Models.Reviews;
using Baya.Application.Models.Verification;
using Baya.WebFramework.Attributes;
using Baya.WebFramework.BaseController;
@@ -14,7 +17,7 @@ namespace Baya.Web.Api.Controllers.V1;
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[AllowAnonymous]
[Display(Description = "Public nurse read surface (the verified trust badge)")]
[Display(Description = "Public nurse read surface (the verified trust badge + published reviews)")]
public sealed class NursesController(ISender sender) : BaseController
{
// Public: the verified badge exposes credential *types* held, never the encrypted numbers.
@@ -22,4 +25,16 @@ public sealed class NursesController(ISender sender) : BaseController
[ProducesOkApiResponseType<TrustBadgeDto>]
public async Task<IActionResult> TrustBadge(long nurseId, CancellationToken cancellationToken)
=> OperationResult(await sender.Send(new GetVerifiedTrustBadgeQuery(nurseId), cancellationToken));
// Public: published reviews only (the publish gate is enforced in the query) + the cached rating aggregate.
[HttpGet("{nurseProfileId}/reviews")]
[ProducesOkApiResponseType<NurseReviewsResult>]
public async Task<IActionResult> Reviews(long nurseProfileId, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken cancellationToken = default)
=> OperationResult(await sender.Send(new ListReviewsForNurseQuery(nurseProfileId, page, pageSize), cancellationToken));
// Public: the per-nurse tag rollup ("% punctual", …) over published reviews.
[HttpGet("{nurseProfileId}/review_tags")]
[ProducesOkApiResponseType<NurseTagAggregatesResult>]
public async Task<IActionResult> ReviewTags(long nurseProfileId, CancellationToken cancellationToken)
=> OperationResult(await sender.Send(new GetTagAggregatesQuery(nurseProfileId), cancellationToken));
}
@@ -0,0 +1,41 @@
using System.ComponentModel.DataAnnotations;
using Asp.Versioning;
using Baya.Application.Features.PatientCareRecords.Commands.WritePatientCareRecord;
using Baya.Application.Features.PatientCareRecords.Queries.GetPatientHistory;
using Baya.Application.Models.Common;
using Baya.Application.Models.Reviews;
using Baya.WebFramework.Attributes;
using Baya.WebFramework.BaseController;
using Mediator;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Baya.Web.Api.Controllers.V1;
/// <summary>
/// Patient-scoped clinical care records. Writing is nurse-only (with a confirmed booking for that patient);
/// reading is restricted to the owning customer, a nurse with a confirmed booking, or admin — the strict
/// clinical access rule is enforced in the handler (not just this route policy). Clinical bodies are encrypted
/// at rest and decrypted only after the access check passes.
/// </summary>
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/patients")]
[Authorize]
[Display(Description = "Patient-scoped, encrypted clinical care records (write: nurse; read: owner/nurse/admin)")]
public sealed class PatientCareRecordsController(ISender sender) : BaseController
{
[HttpPost("{patientId}/care_records")]
[ProducesOkApiResponseType<WriteCareRecordResult>]
public async Task<IActionResult> Write(long patientId, WriteCareRecordBody body, CancellationToken cancellationToken)
=> OperationResult(await sender.Send(
new WritePatientCareRecordCommand(patientId, body.BookingId, body.Body), cancellationToken));
[HttpGet("{patientId}/care_records")]
[ProducesOkApiResponseType<PagedResult<CareRecordDto>>]
public async Task<IActionResult> History(long patientId, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken cancellationToken = default)
=> OperationResult(await sender.Send(new GetPatientHistoryQuery(patientId, page, pageSize), cancellationToken));
/// <summary>The care-record body (the patient id comes from the route).</summary>
public record WriteCareRecordBody(long? BookingId, string Body);
}
@@ -0,0 +1,43 @@
using System.ComponentModel.DataAnnotations;
using Asp.Versioning;
using Baya.Application.Features.Reviews.Commands.AttachReviewTags;
using Baya.Application.Features.Reviews.Commands.ModerateReview;
using Baya.Application.Models.Reviews;
using Baya.Infrastructure.Identity.Identity.PermissionManager;
using Baya.WebFramework.Attributes;
using Baya.WebFramework.BaseController;
using Mediator;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Baya.Web.Api.Controllers.V1;
/// <summary>
/// Review write surface. Tagging a review is for its author (or a moderator, enforced in the handler); the
/// moderation transition is admin/moderator-only (the narrower <see cref="ConstantPolicies.DynamicPermission"/>
/// policy overrides the controller-level authorize).
/// </summary>
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/reviews")]
[Authorize]
[Display(Description = "Review tagging (owner/moderator) and moderation transitions (admin)")]
public sealed class ReviewsController(ISender sender) : BaseController
{
[HttpPost("{reviewId}/tags")]
[ProducesOkApiResponseType<ReviewTagsResult>]
public async Task<IActionResult> Tags(long reviewId, AttachReviewTagsBody body, CancellationToken cancellationToken)
=> OperationResult(await sender.Send(new AttachReviewTagsCommand(reviewId, body.TagCodes), cancellationToken));
[HttpPatch("{reviewId}/status")]
[Authorize(ConstantPolicies.DynamicPermission)]
[ProducesOkApiResponseType<ModerateReviewResult>]
public async Task<IActionResult> Status(long reviewId, ModerateReviewBody body, CancellationToken cancellationToken)
=> OperationResult(await sender.Send(new ModerateReviewCommand(reviewId, body.Action, body.Reason), cancellationToken));
/// <summary>Tag-attach body (the review id comes from the route).</summary>
public record AttachReviewTagsBody(IReadOnlyList<string> TagCodes);
/// <summary>Moderation body (the review id comes from the route): <c>publish</c>|<c>hide</c>|<c>reject</c>|<c>unpublish</c>.</summary>
public record ModerateReviewBody(string Action, string? Reason);
}