From 36508b0284f5d1acca37c694a1999fa3be8bce6a Mon Sep 17 00:00:00 2001 From: NhanNPSE184696 Date: Thu, 29 Jan 2026 09:34:10 +0700 Subject: [PATCH 01/12] refactor: remove unnecessary comment in RaiseSessionRangeAsync method --- SnakeAid.Service/Implements/SnakebiteIncidentService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SnakeAid.Service/Implements/SnakebiteIncidentService.cs b/SnakeAid.Service/Implements/SnakebiteIncidentService.cs index 9b0209b4..163d3eca 100644 --- a/SnakeAid.Service/Implements/SnakebiteIncidentService.cs +++ b/SnakeAid.Service/Implements/SnakebiteIncidentService.cs @@ -187,7 +187,7 @@ public async Task> RaiseSessionRangeAsync(Ra await _unitOfWork.GetRepository().InsertAsync(newSession); - // No need to call Update() - entity is already tracked and EF Core will auto-detect changes + await _unitOfWork.CommitAsync(); // Reload sessions collection from DB to ensure consistency and proper order From af0aaf70868ee84267d640be977f3e259b893cd7 Mon Sep 17 00:00:00 2001 From: NhanNPSE184696 Date: Fri, 30 Jan 2026 18:28:14 +0700 Subject: [PATCH 02/12] feat: implement First Aid Guideline management with CRUD operations --- .../FirstAidGuidelineController.cs | 105 +++++++++ .../CreateFirstAidGuidelineRequest.cs | 33 +++ .../GetFirstAidGuidelineRequest.cs | 18 ++ .../UpdateFirstAidGuidelineRequest.cs | 30 +++ .../FirstAidGuidelineResponse.cs | 15 ++ .../Implements/FirstAidGuidelineService.cs | 213 ++++++++++++++++++ .../Interfaces/IFirstAidGuidelineService.cs | 34 +++ 7 files changed, 448 insertions(+) create mode 100644 SnakeAid.Api/Controllers/FirstAidGuidelineController.cs create mode 100644 SnakeAid.Core/Requests/FirstAidGuideline/CreateFirstAidGuidelineRequest.cs create mode 100644 SnakeAid.Core/Requests/FirstAidGuideline/GetFirstAidGuidelineRequest.cs create mode 100644 SnakeAid.Core/Requests/FirstAidGuideline/UpdateFirstAidGuidelineRequest.cs create mode 100644 SnakeAid.Core/Responses/FirstAidGuideline/FirstAidGuidelineResponse.cs create mode 100644 SnakeAid.Service/Implements/FirstAidGuidelineService.cs create mode 100644 SnakeAid.Service/Interfaces/IFirstAidGuidelineService.cs diff --git a/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs b/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs new file mode 100644 index 00000000..c9160c6b --- /dev/null +++ b/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs @@ -0,0 +1,105 @@ +using MapsterMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SnakeAid.Core.Meta; +using SnakeAid.Core.Requests.FirstAidGuideline; +using SnakeAid.Core.Responses.FirstAidGuideline; +using SnakeAid.Core.Validators; +using SnakeAid.Service.Interfaces; +using Swashbuckle.AspNetCore.Annotations; + +namespace SnakeAid.Api.Controllers +{ + [Route("api/first-aid-guidelines")] + [ApiController] + public class FirstAidGuidelineController : BaseController + { + private readonly IFirstAidGuidelineService _guidelineService; + + public FirstAidGuidelineController( + ILogger logger, + IHttpContextAccessor httpContextAccessor, + IMapper mapper, + IFirstAidGuidelineService guidelineService) + : base(logger, httpContextAccessor, mapper) + { + _guidelineService = guidelineService; + } + + /// + /// Create a new first aid guideline + /// + [HttpPost] + [Authorize(Roles = "Admin")] + [ValidateModel] + [SwaggerOperation(Summary = "Create First Aid Guideline", Description = "Create a new first aid guideline (Admin only)")] + [SwaggerResponse(200, "Created successfully", typeof(ApiResponse))] + [SwaggerResponse(400, "Validation error")] + [SwaggerResponse(401, "Unauthorized")] + [SwaggerResponse(403, "Forbidden")] + public async Task CreateFirstAidGuideline([FromBody] CreateFirstAidGuidelineRequest request) + { + var result = await _guidelineService.CreateFirstAidGuidelineAsync(request); + return StatusCode(result.StatusCode, result); + } + + /// + /// Get first aid guideline by ID + /// + [HttpGet("{id}")] + [SwaggerOperation(Summary = "Get First Aid Guideline by ID", Description = "Get detailed information of a first aid guideline")] + [SwaggerResponse(200, "Success", typeof(ApiResponse))] + [SwaggerResponse(404, "Guideline not found")] + public async Task GetFirstAidGuidelineById(int id) + { + var result = await _guidelineService.GetFirstAidGuidelineByIdAsync(id); + return StatusCode(result.StatusCode, result); + } + + /// + /// Get list of first aid guidelines with pagination and filters + /// + [HttpGet] + [SwaggerOperation(Summary = "Get First Aid Guidelines", Description = "Get paginated list of first aid guidelines with optional filters")] + [SwaggerResponse(200, "Success", typeof(ApiResponse>))] + public async Task GetFirstAidGuidelines([FromQuery] GetFirstAidGuidelineRequest request) + { + var result = await _guidelineService.GetFirstAidGuidelinesAsync(request); + return StatusCode(result.StatusCode, result); + } + + /// + /// Update an existing first aid guideline + /// + [HttpPut("{id}")] + [Authorize(Roles = "Admin")] + [ValidateModel] + [SwaggerOperation(Summary = "Update First Aid Guideline", Description = "Update an existing first aid guideline (Admin only)")] + [SwaggerResponse(200, "Updated successfully", typeof(ApiResponse))] + [SwaggerResponse(400, "Validation error")] + [SwaggerResponse(401, "Unauthorized")] + [SwaggerResponse(403, "Forbidden")] + [SwaggerResponse(404, "Guideline not found")] + public async Task UpdateFirstAidGuideline(int id, [FromBody] UpdateFirstAidGuidelineRequest request) + { + var result = await _guidelineService.UpdateFirstAidGuidelineAsync(id, request); + return StatusCode(result.StatusCode, result); + } + + /// + /// Delete a first aid guideline + /// + [HttpDelete("{id}")] + [Authorize(Roles = "Admin")] + [SwaggerOperation(Summary = "Delete First Aid Guideline", Description = "Delete a first aid guideline (Admin only)")] + [SwaggerResponse(200, "Deleted successfully", typeof(ApiResponse))] + [SwaggerResponse(401, "Unauthorized")] + [SwaggerResponse(403, "Forbidden")] + [SwaggerResponse(404, "Guideline not found")] + public async Task DeleteFirstAidGuideline(int id) + { + var result = await _guidelineService.DeleteFirstAidGuidelineAsync(id); + return StatusCode(result.StatusCode, result); + } + } +} diff --git a/SnakeAid.Core/Requests/FirstAidGuideline/CreateFirstAidGuidelineRequest.cs b/SnakeAid.Core/Requests/FirstAidGuideline/CreateFirstAidGuidelineRequest.cs new file mode 100644 index 00000000..620b3b25 --- /dev/null +++ b/SnakeAid.Core/Requests/FirstAidGuideline/CreateFirstAidGuidelineRequest.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using SnakeAid.Core.Domains; + +namespace SnakeAid.Core.Requests.FirstAidGuideline +{ + public class CreateFirstAidGuidelineRequest + { + /// + /// Tên hướng dẫn sơ cứu + /// + [Required(ErrorMessage = "Name is required")] + [MaxLength(255, ErrorMessage = "Name must not exceed 255 characters")] + public string Name { get; set; } + + /// + /// Nội dung chi tiết (JSON format) + /// + [Required(ErrorMessage = "Content is required")] + public string Content { get; set; } + + /// + /// Loại hướng dẫn: General (0) hoặc SpeciesSpecific (1) + /// + [Required(ErrorMessage = "Type is required")] + public GuidelineType Type { get; set; } + + /// + /// Tóm tắt ngắn + /// + [MaxLength(500, ErrorMessage = "Summary must not exceed 500 characters")] + public string? Summary { get; set; } + } +} diff --git a/SnakeAid.Core/Requests/FirstAidGuideline/GetFirstAidGuidelineRequest.cs b/SnakeAid.Core/Requests/FirstAidGuideline/GetFirstAidGuidelineRequest.cs new file mode 100644 index 00000000..3d384fca --- /dev/null +++ b/SnakeAid.Core/Requests/FirstAidGuideline/GetFirstAidGuidelineRequest.cs @@ -0,0 +1,18 @@ +using SnakeAid.Core.Domains; +using SnakeAid.Core.Meta; + +namespace SnakeAid.Core.Requests.FirstAidGuideline +{ + public class GetFirstAidGuidelineRequest : PaginationRequest + { + /// + /// Tìm kiếm theo tên + /// + public string? Name { get; set; } + + /// + /// Lọc theo loại hướng dẫn + /// + public GuidelineType? Type { get; set; } + } +} diff --git a/SnakeAid.Core/Requests/FirstAidGuideline/UpdateFirstAidGuidelineRequest.cs b/SnakeAid.Core/Requests/FirstAidGuideline/UpdateFirstAidGuidelineRequest.cs new file mode 100644 index 00000000..50e65d81 --- /dev/null +++ b/SnakeAid.Core/Requests/FirstAidGuideline/UpdateFirstAidGuidelineRequest.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using SnakeAid.Core.Domains; + +namespace SnakeAid.Core.Requests.FirstAidGuideline +{ + public class UpdateFirstAidGuidelineRequest + { + /// + /// Tên hướng dẫn sơ cứu + /// + [MaxLength(255, ErrorMessage = "Name must not exceed 255 characters")] + public string? Name { get; set; } + + /// + /// Nội dung chi tiết (JSON format) + /// + public string? Content { get; set; } + + /// + /// Loại hướng dẫn: General (0) hoặc SpeciesSpecific (1) + /// + public GuidelineType? Type { get; set; } + + /// + /// Tóm tắt ngắn + /// + [MaxLength(500, ErrorMessage = "Summary must not exceed 500 characters")] + public string? Summary { get; set; } + } +} diff --git a/SnakeAid.Core/Responses/FirstAidGuideline/FirstAidGuidelineResponse.cs b/SnakeAid.Core/Responses/FirstAidGuideline/FirstAidGuidelineResponse.cs new file mode 100644 index 00000000..b80e499b --- /dev/null +++ b/SnakeAid.Core/Responses/FirstAidGuideline/FirstAidGuidelineResponse.cs @@ -0,0 +1,15 @@ +using SnakeAid.Core.Domains; + +namespace SnakeAid.Core.Responses.FirstAidGuideline +{ + public class FirstAidGuidelineResponse + { + public int Id { get; set; } + public string Name { get; set; } + public string Content { get; set; } + public GuidelineType Type { get; set; } + public string? Summary { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/SnakeAid.Service/Implements/FirstAidGuidelineService.cs b/SnakeAid.Service/Implements/FirstAidGuidelineService.cs new file mode 100644 index 00000000..4127a7b8 --- /dev/null +++ b/SnakeAid.Service/Implements/FirstAidGuidelineService.cs @@ -0,0 +1,213 @@ +using Mapster; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SnakeAid.Core.Domains; +using SnakeAid.Core.Exceptions; +using SnakeAid.Core.Meta; +using SnakeAid.Core.Requests.FirstAidGuideline; +using SnakeAid.Core.Responses.FirstAidGuideline; +using SnakeAid.Repository.Data; +using SnakeAid.Repository.Interfaces; +using SnakeAid.Service.Interfaces; + +namespace SnakeAid.Service.Implements +{ + public class FirstAidGuidelineService : IFirstAidGuidelineService + { + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public FirstAidGuidelineService( + IUnitOfWork unitOfWork, + ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + } + + public async Task> CreateFirstAidGuidelineAsync(CreateFirstAidGuidelineRequest request) + { + try + { + if (request == null) + { + throw new ArgumentNullException(nameof(request), "Request data cannot be null."); + } + + return await _unitOfWork.ExecuteInTransactionAsync(async () => + { + var guideline = new FirstAidGuideline + { + Name = request.Name, + Content = request.Content, + Type = request.Type, + Summary = request.Summary, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + await _unitOfWork.GetRepository().InsertAsync(guideline); + await _unitOfWork.CommitAsync(); + + var response = guideline.Adapt(); + return ApiResponseBuilder.BuildSuccessResponse(response, "First aid guideline created successfully!"); + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating first aid guideline"); + return ApiResponseBuilder.BuildFailureResponse(ex.Message); + } + } + + public async Task> GetFirstAidGuidelineByIdAsync(int id) + { + try + { + var guideline = await _unitOfWork.GetRepository() + .GetByIdAsync(id); + + if (guideline == null) + { + throw new NotFoundException($"First aid guideline with ID {id} not found."); + } + + var response = guideline.Adapt(); + return ApiResponseBuilder.BuildSuccessResponse(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting first aid guideline by ID {Id}", id); + return ApiResponseBuilder.BuildFailureResponse(ex.Message); + } + } + + public async Task>> GetFirstAidGuidelinesAsync(GetFirstAidGuidelineRequest request) + { + try + { + var query = _unitOfWork.GetRepository() + .GetQueryable(); + + // Apply filters + if (!string.IsNullOrWhiteSpace(request.Name)) + { + query = query.Where(g => g.Name.Contains(request.Name)); + } + + if (request.Type.HasValue) + { + query = query.Where(g => g.Type == request.Type.Value); + } + + // Get paginated data + var pagedData = await _unitOfWork.GetRepository() + .GetPagingListAsync( + predicate: query.Expression, + orderBy: o => o.OrderByDescending(g => g.CreatedAt), + page: request.Page, + size: request.Size + ); + + var response = new PagedData + { + Items = pagedData.Items.Adapt>(), + TotalCount = pagedData.TotalCount, + Page = pagedData.Page, + Size = pagedData.Size, + TotalPages = pagedData.TotalPages + }; + + return ApiResponseBuilder.BuildSuccessResponse(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting first aid guidelines"); + return ApiResponseBuilder.BuildFailureResponse>(ex.Message); + } + } + + public async Task> UpdateFirstAidGuidelineAsync(int id, UpdateFirstAidGuidelineRequest request) + { + try + { + if (request == null) + { + throw new ArgumentNullException(nameof(request), "Request data cannot be null."); + } + + return await _unitOfWork.ExecuteInTransactionAsync(async () => + { + var guideline = await _unitOfWork.GetRepository() + .GetByIdAsync(id); + + if (guideline == null) + { + throw new NotFoundException($"First aid guideline with ID {id} not found."); + } + + // Update only provided fields + if (!string.IsNullOrWhiteSpace(request.Name)) + { + guideline.Name = request.Name; + } + + if (!string.IsNullOrWhiteSpace(request.Content)) + { + guideline.Content = request.Content; + } + + if (request.Type.HasValue) + { + guideline.Type = request.Type.Value; + } + + if (request.Summary != null) + { + guideline.Summary = request.Summary; + } + + guideline.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.GetRepository().Update(guideline); + await _unitOfWork.CommitAsync(); + + var response = guideline.Adapt(); + return ApiResponseBuilder.BuildSuccessResponse(response, "First aid guideline updated successfully!"); + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating first aid guideline with ID {Id}", id); + return ApiResponseBuilder.BuildFailureResponse(ex.Message); + } + } + + public async Task> DeleteFirstAidGuidelineAsync(int id) + { + try + { + return await _unitOfWork.ExecuteInTransactionAsync(async () => + { + var guideline = await _unitOfWork.GetRepository() + .GetByIdAsync(id); + + if (guideline == null) + { + throw new NotFoundException($"First aid guideline with ID {id} not found."); + } + + _unitOfWork.GetRepository().Delete(guideline); + await _unitOfWork.CommitAsync(); + + return ApiResponseBuilder.BuildSuccessResponse(true, "First aid guideline deleted successfully!"); + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting first aid guideline with ID {Id}", id); + return ApiResponseBuilder.BuildFailureResponse(ex.Message); + } + } + } +} diff --git a/SnakeAid.Service/Interfaces/IFirstAidGuidelineService.cs b/SnakeAid.Service/Interfaces/IFirstAidGuidelineService.cs new file mode 100644 index 00000000..5702aa98 --- /dev/null +++ b/SnakeAid.Service/Interfaces/IFirstAidGuidelineService.cs @@ -0,0 +1,34 @@ +using SnakeAid.Core.Meta; +using SnakeAid.Core.Requests.FirstAidGuideline; +using SnakeAid.Core.Responses.FirstAidGuideline; + +namespace SnakeAid.Service.Interfaces +{ + public interface IFirstAidGuidelineService + { + /// + /// Create a new first aid guideline + /// + Task> CreateFirstAidGuidelineAsync(CreateFirstAidGuidelineRequest request); + + /// + /// Get first aid guideline by ID + /// + Task> GetFirstAidGuidelineByIdAsync(int id); + + /// + /// Get list of first aid guidelines with pagination and filters + /// + Task>> GetFirstAidGuidelinesAsync(GetFirstAidGuidelineRequest request); + + /// + /// Update an existing first aid guideline + /// + Task> UpdateFirstAidGuidelineAsync(int id, UpdateFirstAidGuidelineRequest request); + + /// + /// Delete a first aid guideline + /// + Task> DeleteFirstAidGuidelineAsync(int id); + } +} From 67832132227c456977c22424d69ccaa13fce53fe Mon Sep 17 00:00:00 2001 From: NhanNPSE184696 Date: Fri, 30 Jan 2026 18:35:49 +0700 Subject: [PATCH 03/12] feat: implement Symptom Configuration management with CRUD operations --- .../Controllers/SymptomConfigController.cs | 118 ++++++ .../CreateSymptomConfigRequest.cs | 69 ++++ .../SymptomConfig/GetSymptomConfigRequest.cs | 38 ++ .../UpdateSymptomConfigRequest.cs | 63 +++ .../SymptomConfig/SymptomConfigResponse.cs | 30 ++ .../Implements/SymptomConfigService.cs | 391 ++++++++++++++++++ .../Interfaces/ISymptomConfigService.cs | 39 ++ 7 files changed, 748 insertions(+) create mode 100644 SnakeAid.Api/Controllers/SymptomConfigController.cs create mode 100644 SnakeAid.Core/Requests/SymptomConfig/CreateSymptomConfigRequest.cs create mode 100644 SnakeAid.Core/Requests/SymptomConfig/GetSymptomConfigRequest.cs create mode 100644 SnakeAid.Core/Requests/SymptomConfig/UpdateSymptomConfigRequest.cs create mode 100644 SnakeAid.Core/Responses/SymptomConfig/SymptomConfigResponse.cs create mode 100644 SnakeAid.Service/Implements/SymptomConfigService.cs create mode 100644 SnakeAid.Service/Interfaces/ISymptomConfigService.cs diff --git a/SnakeAid.Api/Controllers/SymptomConfigController.cs b/SnakeAid.Api/Controllers/SymptomConfigController.cs new file mode 100644 index 00000000..affab166 --- /dev/null +++ b/SnakeAid.Api/Controllers/SymptomConfigController.cs @@ -0,0 +1,118 @@ +using MapsterMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SnakeAid.Core.Meta; +using SnakeAid.Core.Requests.SymptomConfig; +using SnakeAid.Core.Responses.SymptomConfig; +using SnakeAid.Core.Validators; +using SnakeAid.Service.Interfaces; +using Swashbuckle.AspNetCore.Annotations; + +namespace SnakeAid.Api.Controllers +{ + [Route("api/symptom-configs")] + [ApiController] + public class SymptomConfigController : BaseController + { + private readonly ISymptomConfigService _symptomConfigService; + + public SymptomConfigController( + ILogger logger, + IHttpContextAccessor httpContextAccessor, + IMapper mapper, + ISymptomConfigService symptomConfigService) + : base(logger, httpContextAccessor, mapper) + { + _symptomConfigService = symptomConfigService; + } + + /// + /// Create a new symptom configuration + /// + [HttpPost] + [Authorize(Roles = "Admin")] + [ValidateModel] + [SwaggerOperation(Summary = "Create Symptom Configuration", Description = "Create a new symptom configuration (Admin only)")] + [SwaggerResponse(200, "Created successfully", typeof(ApiResponse))] + [SwaggerResponse(400, "Validation error")] + [SwaggerResponse(401, "Unauthorized")] + [SwaggerResponse(403, "Forbidden")] + [SwaggerResponse(404, "VenomType not found")] + public async Task CreateSymptomConfig([FromBody] CreateSymptomConfigRequest request) + { + var result = await _symptomConfigService.CreateSymptomConfigAsync(request); + return StatusCode(result.StatusCode, result); + } + + /// + /// Get symptom configuration by ID + /// + [HttpGet("{id}")] + [SwaggerOperation(Summary = "Get Symptom Configuration by ID", Description = "Get detailed information of a symptom configuration")] + [SwaggerResponse(200, "Success", typeof(ApiResponse))] + [SwaggerResponse(404, "Configuration not found")] + public async Task GetSymptomConfigById(int id) + { + var result = await _symptomConfigService.GetSymptomConfigByIdAsync(id); + return StatusCode(result.StatusCode, result); + } + + /// + /// Get list of symptom configurations with pagination and filters + /// + [HttpGet] + [SwaggerOperation(Summary = "Get Symptom Configurations", Description = "Get paginated list of symptom configurations with optional filters")] + [SwaggerResponse(200, "Success", typeof(ApiResponse>))] + public async Task GetSymptomConfigs([FromQuery] GetSymptomConfigRequest request) + { + var result = await _symptomConfigService.GetSymptomConfigsAsync(request); + return StatusCode(result.StatusCode, result); + } + + /// + /// Get symptom configurations grouped by AttributeKey + /// + [HttpGet("grouped")] + [SwaggerOperation(Summary = "Get Symptom Configurations Grouped", Description = "Get active symptom configurations grouped by AttributeKey")] + [SwaggerResponse(200, "Success", typeof(ApiResponse>>))] + public async Task GetSymptomConfigsGrouped() + { + var result = await _symptomConfigService.GetSymptomConfigsGroupedByKeyAsync(); + return StatusCode(result.StatusCode, result); + } + + /// + /// Update an existing symptom configuration + /// + [HttpPut("{id}")] + [Authorize(Roles = "Admin")] + [ValidateModel] + [SwaggerOperation(Summary = "Update Symptom Configuration", Description = "Update an existing symptom configuration (Admin only)")] + [SwaggerResponse(200, "Updated successfully", typeof(ApiResponse))] + [SwaggerResponse(400, "Validation error")] + [SwaggerResponse(401, "Unauthorized")] + [SwaggerResponse(403, "Forbidden")] + [SwaggerResponse(404, "Configuration or VenomType not found")] + public async Task UpdateSymptomConfig(int id, [FromBody] UpdateSymptomConfigRequest request) + { + var result = await _symptomConfigService.UpdateSymptomConfigAsync(id, request); + return StatusCode(result.StatusCode, result); + } + + /// + /// Delete a symptom configuration + /// + [HttpDelete("{id}")] + [Authorize(Roles = "Admin")] + [SwaggerOperation(Summary = "Delete Symptom Configuration", Description = "Delete a symptom configuration (Admin only)")] + [SwaggerResponse(200, "Deleted successfully", typeof(ApiResponse))] + [SwaggerResponse(401, "Unauthorized")] + [SwaggerResponse(403, "Forbidden")] + [SwaggerResponse(404, "Configuration not found")] + public async Task DeleteSymptomConfig(int id) + { + var result = await _symptomConfigService.DeleteSymptomConfigAsync(id); + return StatusCode(result.StatusCode, result); + } + } +} diff --git a/SnakeAid.Core/Requests/SymptomConfig/CreateSymptomConfigRequest.cs b/SnakeAid.Core/Requests/SymptomConfig/CreateSymptomConfigRequest.cs new file mode 100644 index 00000000..4c00d19d --- /dev/null +++ b/SnakeAid.Core/Requests/SymptomConfig/CreateSymptomConfigRequest.cs @@ -0,0 +1,69 @@ +using System.ComponentModel.DataAnnotations; +using SnakeAid.Core.Domains; + +namespace SnakeAid.Core.Requests.SymptomConfig +{ + public class CreateSymptomConfigRequest + { + /// + /// Khóa thuộc tính (VD: "BITE_LOCATION", "AGE_GROUP", "SYMPTOMS") + /// + [Required(ErrorMessage = "AttributeKey is required")] + [MaxLength(100, ErrorMessage = "AttributeKey must not exceed 100 characters")] + public string AttributeKey { get; set; } + + /// + /// Nhãn hiển thị của thuộc tính + /// + [Required(ErrorMessage = "AttributeLabel is required")] + [MaxLength(500, ErrorMessage = "AttributeLabel must not exceed 500 characters")] + public string AttributeLabel { get; set; } + + /// + /// Loại input trên UI: SingleChoice (1), MultiChoice (2), Boolean (3), Numeric (4), Text (5) + /// + [Required(ErrorMessage = "UIHint is required")] + public InputType UIHint { get; set; } + + /// + /// Thứ tự hiển thị (1-999) + /// + [Required(ErrorMessage = "DisplayOrder is required")] + [Range(1, 999, ErrorMessage = "DisplayOrder must be between 1 and 999")] + public int DisplayOrder { get; set; } + + /// + /// Tên của option cụ thể + /// + [Required(ErrorMessage = "Name is required")] + [MaxLength(300, ErrorMessage = "Name must not exceed 300 characters")] + public string Name { get; set; } + + /// + /// Mô tả chi tiết + /// + [MaxLength(1000, ErrorMessage = "Description must not exceed 1000 characters")] + public string? Description { get; set; } + + /// + /// Trạng thái kích hoạt + /// + public bool IsActive { get; set; } = true; + + /// + /// Danh mục triệu chứng: Core (1) - Max điểm, Modifier (2) - Cộng dồn + /// + [Required(ErrorMessage = "Category is required")] + public SymptomCategory Category { get; set; } + + /// + /// Danh sách điểm theo thời gian + /// + public List? TimeScoreList { get; set; } + + /// + /// ID loại nọc độc (optional) + /// + public int? VenomTypeId { get; set; } + } +} diff --git a/SnakeAid.Core/Requests/SymptomConfig/GetSymptomConfigRequest.cs b/SnakeAid.Core/Requests/SymptomConfig/GetSymptomConfigRequest.cs new file mode 100644 index 00000000..3fe0c85b --- /dev/null +++ b/SnakeAid.Core/Requests/SymptomConfig/GetSymptomConfigRequest.cs @@ -0,0 +1,38 @@ +using SnakeAid.Core.Domains; +using SnakeAid.Core.Meta; + +namespace SnakeAid.Core.Requests.SymptomConfig +{ + public class GetSymptomConfigRequest : PaginationRequest + { + /// + /// Tìm kiếm theo AttributeKey + /// + public string? AttributeKey { get; set; } + + /// + /// Tìm kiếm theo tên + /// + public string? Name { get; set; } + + /// + /// Lọc theo loại UI + /// + public InputType? UIHint { get; set; } + + /// + /// Lọc theo category + /// + public SymptomCategory? Category { get; set; } + + /// + /// Lọc theo trạng thái + /// + public bool? IsActive { get; set; } + + /// + /// Lọc theo VenomTypeId + /// + public int? VenomTypeId { get; set; } + } +} diff --git a/SnakeAid.Core/Requests/SymptomConfig/UpdateSymptomConfigRequest.cs b/SnakeAid.Core/Requests/SymptomConfig/UpdateSymptomConfigRequest.cs new file mode 100644 index 00000000..8b1999b3 --- /dev/null +++ b/SnakeAid.Core/Requests/SymptomConfig/UpdateSymptomConfigRequest.cs @@ -0,0 +1,63 @@ +using System.ComponentModel.DataAnnotations; +using SnakeAid.Core.Domains; + +namespace SnakeAid.Core.Requests.SymptomConfig +{ + public class UpdateSymptomConfigRequest + { + /// + /// Khóa thuộc tính (VD: "BITE_LOCATION", "AGE_GROUP", "SYMPTOMS") + /// + [MaxLength(100, ErrorMessage = "AttributeKey must not exceed 100 characters")] + public string? AttributeKey { get; set; } + + /// + /// Nhãn hiển thị của thuộc tính + /// + [MaxLength(500, ErrorMessage = "AttributeLabel must not exceed 500 characters")] + public string? AttributeLabel { get; set; } + + /// + /// Loại input trên UI + /// + public InputType? UIHint { get; set; } + + /// + /// Thứ tự hiển thị (1-999) + /// + [Range(1, 999, ErrorMessage = "DisplayOrder must be between 1 and 999")] + public int? DisplayOrder { get; set; } + + /// + /// Tên của option cụ thể + /// + [MaxLength(300, ErrorMessage = "Name must not exceed 300 characters")] + public string? Name { get; set; } + + /// + /// Mô tả chi tiết + /// + [MaxLength(1000, ErrorMessage = "Description must not exceed 1000 characters")] + public string? Description { get; set; } + + /// + /// Trạng thái kích hoạt + /// + public bool? IsActive { get; set; } + + /// + /// Danh mục triệu chứng + /// + public SymptomCategory? Category { get; set; } + + /// + /// Danh sách điểm theo thời gian + /// + public List? TimeScoreList { get; set; } + + /// + /// ID loại nọc độc (optional) + /// + public int? VenomTypeId { get; set; } + } +} diff --git a/SnakeAid.Core/Responses/SymptomConfig/SymptomConfigResponse.cs b/SnakeAid.Core/Responses/SymptomConfig/SymptomConfigResponse.cs new file mode 100644 index 00000000..f7ab706e --- /dev/null +++ b/SnakeAid.Core/Responses/SymptomConfig/SymptomConfigResponse.cs @@ -0,0 +1,30 @@ +using SnakeAid.Core.Domains; + +namespace SnakeAid.Core.Responses.SymptomConfig +{ + public class SymptomConfigResponse + { + public int Id { get; set; } + public string AttributeKey { get; set; } + public string AttributeLabel { get; set; } + public InputType UIHint { get; set; } + public string UIHintDisplay { get; set; } // Friendly display name + public int DisplayOrder { get; set; } + public string Name { get; set; } + public string? Description { get; set; } + public bool IsActive { get; set; } + public SymptomCategory Category { get; set; } + public string CategoryDisplay { get; set; } // Friendly display name + public List TimeScoreList { get; set; } = new List(); + public int? VenomTypeId { get; set; } + public VenomTypeInfo? VenomType { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } + + public class VenomTypeInfo + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/SnakeAid.Service/Implements/SymptomConfigService.cs b/SnakeAid.Service/Implements/SymptomConfigService.cs new file mode 100644 index 00000000..803bb522 --- /dev/null +++ b/SnakeAid.Service/Implements/SymptomConfigService.cs @@ -0,0 +1,391 @@ +using Mapster; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SnakeAid.Core.Domains; +using SnakeAid.Core.Exceptions; +using SnakeAid.Core.Meta; +using SnakeAid.Core.Requests.SymptomConfig; +using SnakeAid.Core.Responses.SymptomConfig; +using SnakeAid.Repository.Data; +using SnakeAid.Repository.Interfaces; +using SnakeAid.Service.Interfaces; +using System.Text.Json; + +namespace SnakeAid.Service.Implements +{ + public class SymptomConfigService : ISymptomConfigService + { + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public SymptomConfigService( + IUnitOfWork unitOfWork, + ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + } + + public async Task> CreateSymptomConfigAsync(CreateSymptomConfigRequest request) + { + try + { + if (request == null) + { + throw new ArgumentNullException(nameof(request), "Request data cannot be null."); + } + + return await _unitOfWork.ExecuteInTransactionAsync(async () => + { + // Validate VenomTypeId if provided + if (request.VenomTypeId.HasValue) + { + var venomTypeExists = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: vt => vt.Id == request.VenomTypeId.Value); + + if (venomTypeExists == null) + { + throw new NotFoundException($"VenomType with ID {request.VenomTypeId.Value} not found."); + } + } + + var symptomConfig = new Core.Domains.SymptomConfig + { + AttributeKey = request.AttributeKey, + AttributeLabel = request.AttributeLabel, + UIHint = request.UIHint, + DisplayOrder = request.DisplayOrder, + Name = request.Name, + Description = request.Description, + IsActive = request.IsActive, + Category = request.Category, + TimeScoresJson = request.TimeScoreList != null && request.TimeScoreList.Any() + ? JsonSerializer.Serialize(request.TimeScoreList) + : null, + VenomTypeId = request.VenomTypeId, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + await _unitOfWork.GetRepository().InsertAsync(symptomConfig); + await _unitOfWork.CommitAsync(); + + // Load VenomType if exists + if (symptomConfig.VenomTypeId.HasValue) + { + symptomConfig = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + predicate: sc => sc.Id == symptomConfig.Id, + include: q => q.Include(sc => sc.VenomType), + asNoTracking: false + ); + } + + var response = MapToResponse(symptomConfig); + return ApiResponseBuilder.BuildSuccessResponse(response, "Symptom configuration created successfully!"); + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating symptom configuration"); + return ApiResponseBuilder.BuildFailureResponse(ex.Message); + } + } + + public async Task> GetSymptomConfigByIdAsync(int id) + { + try + { + var symptomConfig = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + predicate: sc => sc.Id == id, + include: q => q.Include(sc => sc.VenomType) + ); + + if (symptomConfig == null) + { + throw new NotFoundException($"Symptom configuration with ID {id} not found."); + } + + var response = MapToResponse(symptomConfig); + return ApiResponseBuilder.BuildSuccessResponse(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting symptom configuration by ID {Id}", id); + return ApiResponseBuilder.BuildFailureResponse(ex.Message); + } + } + + public async Task>> GetSymptomConfigsAsync(GetSymptomConfigRequest request) + { + try + { + var query = _unitOfWork.GetRepository() + .GetQueryable() + .Include(sc => sc.VenomType); + + // Apply filters + if (!string.IsNullOrWhiteSpace(request.AttributeKey)) + { + query = query.Where(sc => sc.AttributeKey.Contains(request.AttributeKey)); + } + + if (!string.IsNullOrWhiteSpace(request.Name)) + { + query = query.Where(sc => sc.Name.Contains(request.Name)); + } + + if (request.UIHint.HasValue) + { + query = query.Where(sc => sc.UIHint == request.UIHint.Value); + } + + if (request.Category.HasValue) + { + query = query.Where(sc => sc.Category == request.Category.Value); + } + + if (request.IsActive.HasValue) + { + query = query.Where(sc => sc.IsActive == request.IsActive.Value); + } + + if (request.VenomTypeId.HasValue) + { + query = query.Where(sc => sc.VenomTypeId == request.VenomTypeId.Value); + } + + // Get total count + var totalCount = await query.CountAsync(); + + // Apply ordering + query = query.OrderBy(sc => sc.DisplayOrder) + .ThenBy(sc => sc.AttributeKey) + .ThenBy(sc => sc.Name); + + // Apply pagination + var items = await query + .Skip((request.Page - 1) * request.Size) + .Take(request.Size) + .ToListAsync(); + + var responseItems = items.Select(MapToResponse).ToList(); + + var pagedData = new PagedData + { + Items = responseItems, + TotalCount = totalCount, + Page = request.Page, + Size = request.Size, + TotalPages = (int)Math.Ceiling(totalCount / (double)request.Size) + }; + + return ApiResponseBuilder.BuildSuccessResponse(pagedData); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting symptom configurations"); + return ApiResponseBuilder.BuildFailureResponse>(ex.Message); + } + } + + public async Task> UpdateSymptomConfigAsync(int id, UpdateSymptomConfigRequest request) + { + try + { + if (request == null) + { + throw new ArgumentNullException(nameof(request), "Request data cannot be null."); + } + + return await _unitOfWork.ExecuteInTransactionAsync(async () => + { + var symptomConfig = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + predicate: sc => sc.Id == id, + include: q => q.Include(sc => sc.VenomType), + asNoTracking: false + ); + + if (symptomConfig == null) + { + throw new NotFoundException($"Symptom configuration with ID {id} not found."); + } + + // Validate VenomTypeId if provided + if (request.VenomTypeId.HasValue) + { + var venomTypeExists = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: vt => vt.Id == request.VenomTypeId.Value); + + if (venomTypeExists == null) + { + throw new NotFoundException($"VenomType with ID {request.VenomTypeId.Value} not found."); + } + } + + // Update only provided fields + if (!string.IsNullOrWhiteSpace(request.AttributeKey)) + { + symptomConfig.AttributeKey = request.AttributeKey; + } + + if (!string.IsNullOrWhiteSpace(request.AttributeLabel)) + { + symptomConfig.AttributeLabel = request.AttributeLabel; + } + + if (request.UIHint.HasValue) + { + symptomConfig.UIHint = request.UIHint.Value; + } + + if (request.DisplayOrder.HasValue) + { + symptomConfig.DisplayOrder = request.DisplayOrder.Value; + } + + if (!string.IsNullOrWhiteSpace(request.Name)) + { + symptomConfig.Name = request.Name; + } + + if (request.Description != null) + { + symptomConfig.Description = request.Description; + } + + if (request.IsActive.HasValue) + { + symptomConfig.IsActive = request.IsActive.Value; + } + + if (request.Category.HasValue) + { + symptomConfig.Category = request.Category.Value; + } + + if (request.TimeScoreList != null) + { + symptomConfig.TimeScoresJson = request.TimeScoreList.Any() + ? JsonSerializer.Serialize(request.TimeScoreList) + : null; + } + + if (request.VenomTypeId != null) + { + symptomConfig.VenomTypeId = request.VenomTypeId; + } + + symptomConfig.UpdatedAt = DateTime.UtcNow; + + _unitOfWork.GetRepository().Update(symptomConfig); + await _unitOfWork.CommitAsync(); + + // Reload to get updated VenomType + if (symptomConfig.VenomTypeId.HasValue) + { + symptomConfig = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + predicate: sc => sc.Id == id, + include: q => q.Include(sc => sc.VenomType), + asNoTracking: false + ); + } + + var response = MapToResponse(symptomConfig); + return ApiResponseBuilder.BuildSuccessResponse(response, "Symptom configuration updated successfully!"); + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating symptom configuration with ID {Id}", id); + return ApiResponseBuilder.BuildFailureResponse(ex.Message); + } + } + + public async Task> DeleteSymptomConfigAsync(int id) + { + try + { + return await _unitOfWork.ExecuteInTransactionAsync(async () => + { + var symptomConfig = await _unitOfWork.GetRepository() + .GetByIdAsync(id); + + if (symptomConfig == null) + { + throw new NotFoundException($"Symptom configuration with ID {id} not found."); + } + + _unitOfWork.GetRepository().Delete(symptomConfig); + await _unitOfWork.CommitAsync(); + + return ApiResponseBuilder.BuildSuccessResponse(true, "Symptom configuration deleted successfully!"); + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting symptom configuration with ID {Id}", id); + return ApiResponseBuilder.BuildFailureResponse(ex.Message); + } + } + + public async Task>>> GetSymptomConfigsGroupedByKeyAsync() + { + try + { + var symptomConfigs = await _unitOfWork.GetRepository() + .GetListAsync( + predicate: sc => sc.IsActive, + orderBy: o => o.OrderBy(sc => sc.DisplayOrder).ThenBy(sc => sc.Name), + include: q => q.Include(sc => sc.VenomType) + ); + + var grouped = symptomConfigs + .GroupBy(sc => sc.AttributeKey) + .ToDictionary( + g => g.Key, + g => g.Select(MapToResponse).ToList() + ); + + return ApiResponseBuilder.BuildSuccessResponse(grouped); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting symptom configurations grouped by key"); + return ApiResponseBuilder.BuildFailureResponse>>(ex.Message); + } + } + + private SymptomConfigResponse MapToResponse(Core.Domains.SymptomConfig symptomConfig) + { + return new SymptomConfigResponse + { + Id = symptomConfig.Id, + AttributeKey = symptomConfig.AttributeKey, + AttributeLabel = symptomConfig.AttributeLabel, + UIHint = symptomConfig.UIHint, + UIHintDisplay = symptomConfig.UIHint.ToString(), + DisplayOrder = symptomConfig.DisplayOrder, + Name = symptomConfig.Name, + Description = symptomConfig.Description, + IsActive = symptomConfig.IsActive, + Category = symptomConfig.Category, + CategoryDisplay = symptomConfig.Category.ToString(), + TimeScoreList = symptomConfig.TimeScoreList, + VenomTypeId = symptomConfig.VenomTypeId, + VenomType = symptomConfig.VenomType != null + ? new VenomTypeInfo + { + Id = symptomConfig.VenomType.Id, + Name = symptomConfig.VenomType.Name + } + : null, + CreatedAt = symptomConfig.CreatedAt, + UpdatedAt = symptomConfig.UpdatedAt + }; + } + } +} diff --git a/SnakeAid.Service/Interfaces/ISymptomConfigService.cs b/SnakeAid.Service/Interfaces/ISymptomConfigService.cs new file mode 100644 index 00000000..7d993caa --- /dev/null +++ b/SnakeAid.Service/Interfaces/ISymptomConfigService.cs @@ -0,0 +1,39 @@ +using SnakeAid.Core.Meta; +using SnakeAid.Core.Requests.SymptomConfig; +using SnakeAid.Core.Responses.SymptomConfig; + +namespace SnakeAid.Service.Interfaces +{ + public interface ISymptomConfigService + { + /// + /// Create a new symptom configuration + /// + Task> CreateSymptomConfigAsync(CreateSymptomConfigRequest request); + + /// + /// Get symptom configuration by ID + /// + Task> GetSymptomConfigByIdAsync(int id); + + /// + /// Get list of symptom configurations with pagination and filters + /// + Task>> GetSymptomConfigsAsync(GetSymptomConfigRequest request); + + /// + /// Update an existing symptom configuration + /// + Task> UpdateSymptomConfigAsync(int id, UpdateSymptomConfigRequest request); + + /// + /// Delete a symptom configuration + /// + Task> DeleteSymptomConfigAsync(int id); + + /// + /// Get symptom configurations grouped by AttributeKey + /// + Task>>> GetSymptomConfigsGroupedByKeyAsync(); + } +} From 6ebf746d9ec75b183ce7d7b1c2a085fe3c8937d0 Mon Sep 17 00:00:00 2001 From: NhanNPSE184696 Date: Sat, 31 Jan 2026 02:26:28 +0700 Subject: [PATCH 04/12] feat: enhance First Aid and Symptom Configuration services with filtering and retrieval of all entries --- .../FirstAidGuidelineController.cs | 18 ++++++++++++--- .../Controllers/SymptomConfigController.cs | 18 ++++++++++++--- .../Implements/FirstAidGuidelineService.cs | 19 +++++++++++++++- .../Implements/SymptomConfigService.cs | 22 ++++++++++++++++++- .../Interfaces/IFirstAidGuidelineService.cs | 7 +++++- .../Interfaces/ISymptomConfigService.cs | 7 +++++- 6 files changed, 81 insertions(+), 10 deletions(-) diff --git a/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs b/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs index c9160c6b..7d59df21 100644 --- a/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs +++ b/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs @@ -60,11 +60,23 @@ public async Task GetFirstAidGuidelineById(int id) /// Get list of first aid guidelines with pagination and filters /// [HttpGet] - [SwaggerOperation(Summary = "Get First Aid Guidelines", Description = "Get paginated list of first aid guidelines with optional filters")] + [SwaggerOperation(Summary = "Filter First Aid Guidelines", Description = "Get paginated list of first aid guidelines with optional filters")] [SwaggerResponse(200, "Success", typeof(ApiResponse>))] - public async Task GetFirstAidGuidelines([FromQuery] GetFirstAidGuidelineRequest request) + public async Task FilterFirstAidGuidelines([FromQuery] GetFirstAidGuidelineRequest request) { - var result = await _guidelineService.GetFirstAidGuidelinesAsync(request); + var result = await _guidelineService.FilterFirstAidGuidelinesAsync(request); + return StatusCode(result.StatusCode, result); + } + + /// + /// Get all first aid guidelines without pagination + /// + [HttpGet("all")] + [SwaggerOperation(Summary = "Get All First Aid Guidelines", Description = "Get all first aid guidelines without pagination")] + [SwaggerResponse(200, "Success", typeof(ApiResponse>))] + public async Task GetAllFirstAidGuideline() + { + var result = await _guidelineService.GetAllFirstAidGuidelineAsync(); return StatusCode(result.StatusCode, result); } diff --git a/SnakeAid.Api/Controllers/SymptomConfigController.cs b/SnakeAid.Api/Controllers/SymptomConfigController.cs index affab166..868eb913 100644 --- a/SnakeAid.Api/Controllers/SymptomConfigController.cs +++ b/SnakeAid.Api/Controllers/SymptomConfigController.cs @@ -61,11 +61,23 @@ public async Task GetSymptomConfigById(int id) /// Get list of symptom configurations with pagination and filters /// [HttpGet] - [SwaggerOperation(Summary = "Get Symptom Configurations", Description = "Get paginated list of symptom configurations with optional filters")] + [SwaggerOperation(Summary = "Filter Symptom Configurations", Description = "Get paginated list of symptom configurations with optional filters")] [SwaggerResponse(200, "Success", typeof(ApiResponse>))] - public async Task GetSymptomConfigs([FromQuery] GetSymptomConfigRequest request) + public async Task FilterSymptomConfigs([FromQuery] GetSymptomConfigRequest request) { - var result = await _symptomConfigService.GetSymptomConfigsAsync(request); + var result = await _symptomConfigService.FilterSymptomConfigsAsync(request); + return StatusCode(result.StatusCode, result); + } + + /// + /// Get all symptom configurations without pagination + /// + [HttpGet("all")] + [SwaggerOperation(Summary = "Get All Symptom Configurations", Description = "Get all symptom configurations without pagination")] + [SwaggerResponse(200, "Success", typeof(ApiResponse>))] + public async Task GetAllSymptomConfig() + { + var result = await _symptomConfigService.GetAllSymptomConfigAsync(); return StatusCode(result.StatusCode, result); } diff --git a/SnakeAid.Service/Implements/FirstAidGuidelineService.cs b/SnakeAid.Service/Implements/FirstAidGuidelineService.cs index 4127a7b8..69b3ebec 100644 --- a/SnakeAid.Service/Implements/FirstAidGuidelineService.cs +++ b/SnakeAid.Service/Implements/FirstAidGuidelineService.cs @@ -82,7 +82,7 @@ public async Task> GetFirstAidGuidelineBy } } - public async Task>> GetFirstAidGuidelinesAsync(GetFirstAidGuidelineRequest request) + public async Task>> FilterFirstAidGuidelinesAsync(GetFirstAidGuidelineRequest request) { try { @@ -209,5 +209,22 @@ public async Task> DeleteFirstAidGuidelineAsync(int id) return ApiResponseBuilder.BuildFailureResponse(ex.Message); } } + + public async Task>> GetAllFirstAidGuidelineAsync() + { + try + { + var guidelines = await _unitOfWork.GetRepository() + .GetListAsync(orderBy: o => o.OrderBy(g => g.Name)); + + var response = guidelines.Adapt>(); + return ApiResponseBuilder.BuildSuccessResponse(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting all first aid guidelines"); + return ApiResponseBuilder.BuildFailureResponse>(ex.Message); + } + } } } diff --git a/SnakeAid.Service/Implements/SymptomConfigService.cs b/SnakeAid.Service/Implements/SymptomConfigService.cs index 803bb522..61773d8f 100644 --- a/SnakeAid.Service/Implements/SymptomConfigService.cs +++ b/SnakeAid.Service/Implements/SymptomConfigService.cs @@ -117,7 +117,7 @@ public async Task> GetSymptomConfigByIdAsync( } } - public async Task>> GetSymptomConfigsAsync(GetSymptomConfigRequest request) + public async Task>> FilterSymptomConfigsAsync(GetSymptomConfigRequest request) { try { @@ -359,6 +359,26 @@ public async Task>>> } } + public async Task>> GetAllSymptomConfigAsync() + { + try + { + var symptomConfigs = await _unitOfWork.GetRepository() + .GetListAsync( + orderBy: o => o.OrderBy(sc => sc.DisplayOrder).ThenBy(sc => sc.Name), + include: q => q.Include(sc => sc.VenomType) + ); + + var response = symptomConfigs.Select(MapToResponse).ToList(); + return ApiResponseBuilder.BuildSuccessResponse(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting all symptom configurations"); + return ApiResponseBuilder.BuildFailureResponse>(ex.Message); + } + } + private SymptomConfigResponse MapToResponse(Core.Domains.SymptomConfig symptomConfig) { return new SymptomConfigResponse diff --git a/SnakeAid.Service/Interfaces/IFirstAidGuidelineService.cs b/SnakeAid.Service/Interfaces/IFirstAidGuidelineService.cs index 5702aa98..e19f4d84 100644 --- a/SnakeAid.Service/Interfaces/IFirstAidGuidelineService.cs +++ b/SnakeAid.Service/Interfaces/IFirstAidGuidelineService.cs @@ -19,7 +19,7 @@ public interface IFirstAidGuidelineService /// /// Get list of first aid guidelines with pagination and filters /// - Task>> GetFirstAidGuidelinesAsync(GetFirstAidGuidelineRequest request); + Task>> FilterFirstAidGuidelinesAsync(GetFirstAidGuidelineRequest request); /// /// Update an existing first aid guideline @@ -30,5 +30,10 @@ public interface IFirstAidGuidelineService /// Delete a first aid guideline /// Task> DeleteFirstAidGuidelineAsync(int id); + + /// + /// Get all first aid guidelines without pagination + /// + Task>> GetAllFirstAidGuidelineAsync(); } } diff --git a/SnakeAid.Service/Interfaces/ISymptomConfigService.cs b/SnakeAid.Service/Interfaces/ISymptomConfigService.cs index 7d993caa..9a9ed34f 100644 --- a/SnakeAid.Service/Interfaces/ISymptomConfigService.cs +++ b/SnakeAid.Service/Interfaces/ISymptomConfigService.cs @@ -19,7 +19,7 @@ public interface ISymptomConfigService /// /// Get list of symptom configurations with pagination and filters /// - Task>> GetSymptomConfigsAsync(GetSymptomConfigRequest request); + Task>> FilterSymptomConfigsAsync(GetSymptomConfigRequest request); /// /// Update an existing symptom configuration @@ -35,5 +35,10 @@ public interface ISymptomConfigService /// Get symptom configurations grouped by AttributeKey /// Task>>> GetSymptomConfigsGroupedByKeyAsync(); + + /// + /// Get all symptom configurations without pagination + /// + Task>> GetAllSymptomConfigAsync(); } } From 20a27a12759a7ca30bf240f3b38c33d44e90ea68 Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Sat, 31 Jan 2026 07:55:55 +0700 Subject: [PATCH 05/12] feat: add GitHub Actions workflow for deploying Docsify to GitHub Pages --- .github/workflows/deploy-docsify.yml | 44 ++++++++++++++++++++++++++++ SnakeAid.Docs | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/deploy-docsify.yml diff --git a/.github/workflows/deploy-docsify.yml b/.github/workflows/deploy-docsify.yml new file mode 100644 index 00000000..57569641 --- /dev/null +++ b/.github/workflows/deploy-docsify.yml @@ -0,0 +1,44 @@ +name: Deploy Docsify to GitHub Pages + +on: + push: + branches: [dev] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload Docs artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deploy + uses: actions/deploy-pages@v4 + + - name: Show deployed link + run: echo "Docs available at ${{ steps.deploy.outputs.page_url }}" diff --git a/SnakeAid.Docs b/SnakeAid.Docs index c183b129..d93f0855 160000 --- a/SnakeAid.Docs +++ b/SnakeAid.Docs @@ -1 +1 @@ -Subproject commit c183b129fb673e88086d95fca94889c87e11ba81 +Subproject commit d93f0855c096942a1f4c90633446ac8a0bfbec0d From 060bcabc831349ffdf28128decb1a128c9814c29 Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Sat, 31 Jan 2026 07:57:21 +0700 Subject: [PATCH 06/12] fix: update deployment branch from dev to main in GitHub Actions workflow --- .github/workflows/deploy-docsify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-docsify.yml b/.github/workflows/deploy-docsify.yml index 57569641..4ab10e1a 100644 --- a/.github/workflows/deploy-docsify.yml +++ b/.github/workflows/deploy-docsify.yml @@ -2,7 +2,7 @@ name: Deploy Docsify to GitHub Pages on: push: - branches: [dev] + branches: [main] workflow_dispatch: permissions: From df9234eda6d7279ea2eed0c56bb32ed0c0abc669 Mon Sep 17 00:00:00 2001 From: Duong Nguyen Manh <152079455+DuongNManh@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:51:35 +0700 Subject: [PATCH 07/12] Feat/tracking location (#10) * feat: test signalr tracking * feat: add GitHub Actions workflow for deploying Docsify to GitHub Pages --------- Co-authored-by: the-khiem7 --- Directory.Packages.props | 3 +- .../Controllers/SignalRTestController.cs | 149 + SnakeAid.Api/Hubs/TestChatHub.cs | 285 ++ SnakeAid.Api/Program.cs | 39 +- SnakeAid.Api/SnakeAid.Api.csproj | 1 + SnakeAid.Core/Domains/Account.cs | 8 +- SnakeAid.Core/Domains/ExpertProfile.cs | 5 - SnakeAid.Core/Domains/RescueRequestSession.cs | 3 - SnakeAid.Core/Domains/RescuerProfile.cs | 5 - SnakeAid.Core/Domains/RescuerRequest.cs | 3 - SnakeAid.Core/Domains/SnakebiteIncident.cs | 5 - SnakeAid.Core/Mappings/MapsterConfig.cs | 5 - .../Mappings/SnakebiteIncidentMapper.cs | 21 + .../CreateIncidentResponse.cs | 2 - .../Data/DesignTimeDbContextFactory.cs | 2 +- ...0131094719_ChangeProfileSchema.Designer.cs | 3479 +++++++++++++++++ .../20260131094719_ChangeProfileSchema.cs | 118 + .../SnakeAidDbContextModelSnapshot.cs | 73 +- 18 files changed, 4143 insertions(+), 63 deletions(-) create mode 100644 SnakeAid.Api/Controllers/SignalRTestController.cs create mode 100644 SnakeAid.Api/Hubs/TestChatHub.cs create mode 100644 SnakeAid.Core/Mappings/SnakebiteIncidentMapper.cs create mode 100644 SnakeAid.Repository/Migrations/20260131094719_ChangeProfileSchema.Designer.cs create mode 100644 SnakeAid.Repository/Migrations/20260131094719_ChangeProfileSchema.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 06a6bc55..f3e4765f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,7 @@ - + @@ -48,5 +48,6 @@ + diff --git a/SnakeAid.Api/Controllers/SignalRTestController.cs b/SnakeAid.Api/Controllers/SignalRTestController.cs new file mode 100644 index 00000000..5cf6f060 --- /dev/null +++ b/SnakeAid.Api/Controllers/SignalRTestController.cs @@ -0,0 +1,149 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using SnakeAid.Api.Hubs; +using SnakeAid.Core.Meta; + +namespace SnakeAid.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class SignalRTestController : ControllerBase +{ + private readonly IHubContext _hubContext; + + public SignalRTestController(IHubContext hubContext) + { + _hubContext = hubContext; + } + + [HttpGet("test-connection")] + public async Task TestConnection() + { + try + { + return Ok(new ApiResponse + { + StatusCode = 200, + Message = "SignalR hub is working correctly. Connect to /chat-hub for WebSocket connection.", + IsSuccess = true, + Data = new { HubEndpoint = "/chat-hub", Status = "Available" } + }); + } + catch (Exception ex) + { + return StatusCode(500, new ApiResponse + { + StatusCode = 500, + Message = $"SignalR test failed: {ex.Message}", + IsSuccess = false, + Data = null + }); + } + } + + [HttpPost("send-test-message")] + public async Task SendTestMessage([FromBody] TestMessageRequest request) + { + try + { + var messageData = new + { + UserId = "server", + UserName = "Server", + Message = request.Message, + Timestamp = DateTime.UtcNow, + MessageId = Guid.NewGuid().ToString() + }; + + await _hubContext.Clients.All.SendAsync("ReceiveMessage", messageData); + + return Ok(new ApiResponse + { + StatusCode = 200, + Message = "Message sent successfully", + IsSuccess = true, + Data = null + }); + } + catch (Exception ex) + { + return StatusCode(500, new ApiResponse + { + StatusCode = 500, + Message = $"Failed to send message: {ex.Message}", + IsSuccess = false, + Data = null + }); + } + } + + [HttpGet("connected-users")] + public async Task GetConnectedUsers() + { + try + { + // This is a simple endpoint to trigger user list update + await _hubContext.Clients.All.SendAsync("RequestUserListUpdate"); + return Ok(new ApiResponse + { + StatusCode = 200, + Message = "User list update requested", + IsSuccess = true, + Data = null + }); + } + catch (Exception ex) + { + return StatusCode(500, new ApiResponse + { + StatusCode = 500, + Message = $"Failed to request user list: {ex.Message}", + IsSuccess = false, + Data = null + }); + } + } + + [HttpPost("broadcast")] + public async Task BroadcastMessage([FromBody] BroadcastRequest request) + { + try + { + await _hubContext.Clients.All.SendAsync("ServerBroadcast", new + { + Message = request.Message, + Timestamp = DateTime.UtcNow, + Type = request.Type ?? "info" + }); + + return Ok(new ApiResponse + { + StatusCode = 200, + Message = "Broadcast sent successfully", + IsSuccess = true, + Data = null + }); + } + catch (Exception ex) + { + return StatusCode(500, new ApiResponse + { + StatusCode = 500, + Message = $"Failed to broadcast: {ex.Message}", + IsSuccess = false, + Data = null + }); + } + } +} + +public class TestMessageRequest +{ + public string Message { get; set; } = string.Empty; +} + +public class BroadcastRequest +{ + public string Message { get; set; } = string.Empty; + public string? Type { get; set; } // info, warning, error, success +} \ No newline at end of file diff --git a/SnakeAid.Api/Hubs/TestChatHub.cs b/SnakeAid.Api/Hubs/TestChatHub.cs new file mode 100644 index 00000000..862becf2 --- /dev/null +++ b/SnakeAid.Api/Hubs/TestChatHub.cs @@ -0,0 +1,285 @@ +using Microsoft.AspNetCore.SignalR; + +namespace SnakeAid.Api.Hubs; + +public class TestChatHub : Hub +{ + private static readonly Dictionary ConnectedUsers = new(); + private static readonly Dictionary UserLocations = new(); + + public override async Task OnConnectedAsync() + { + await base.OnConnectedAsync(); + Console.WriteLine($"Client connected: {Context.ConnectionId}"); + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + // Remove user from connected users when they disconnect + if (ConnectedUsers.ContainsValue(Context.ConnectionId)) + { + var userToRemove = ConnectedUsers.FirstOrDefault(x => x.Value == Context.ConnectionId); + if (!string.IsNullOrEmpty(userToRemove.Key)) + { + ConnectedUsers.Remove(userToRemove.Key); + UserLocations.Remove(userToRemove.Key); // Remove location data + + // Notify others that user left + await Clients.Others.SendAsync("UserLeft", userToRemove.Key); + await Clients.Others.SendAsync("UpdateUserList", ConnectedUsers.Keys.ToList()); + } + } + + Console.WriteLine($"Client disconnected: {Context.ConnectionId}"); + await base.OnDisconnectedAsync(exception); + } + + // Client joins with their UUID + public async Task JoinChat(string userId, string userName) + { + try + { + // Store user mapping + ConnectedUsers[userId] = Context.ConnectionId; + + // Join a group for easier management (optional) + await Groups.AddToGroupAsync(Context.ConnectionId, "TestChatGroup"); + + // Notify all clients about new user + await Clients.Others.SendAsync("UserJoined", userId, userName); + + // Send current user list to the new user + await Clients.Caller.SendAsync("UpdateUserList", ConnectedUsers.Keys.ToList()); + + // Send latest locations of other users to the new user + await SendLatestLocationsToUser(userId); + + // Send success confirmation to the joining user + await Clients.Caller.SendAsync("JoinedSuccessfully", userId); + + Console.WriteLine($"User {userName} ({userId}) joined the chat"); + } + catch (Exception ex) + { + await Clients.Caller.SendAsync("Error", $"Failed to join chat: {ex.Message}"); + } + } + + // Send message to all connected clients + public async Task SendMessageToAll(string userId, string userName, string message) + { + try + { + var messageData = new + { + UserId = userId, + UserName = userName, + Message = message, + Timestamp = DateTime.UtcNow, + MessageId = Guid.NewGuid().ToString() + }; + + // Send to all clients in the test chat group + await Clients.Group("TestChatGroup").SendAsync("ReceiveMessage", messageData); + + Console.WriteLine($"Message from {userName}: {message}"); + } + catch (Exception ex) + { + await Clients.Caller.SendAsync("Error", $"Failed to send message: {ex.Message}"); + } + } + + // Send private message to specific user + public async Task SendPrivateMessage(string fromUserId, string fromUserName, string toUserId, string message) + { + try + { + if (ConnectedUsers.TryGetValue(toUserId, out var toConnectionId)) + { + var messageData = new + { + FromUserId = fromUserId, + FromUserName = fromUserName, + ToUserId = toUserId, + Message = message, + Timestamp = DateTime.UtcNow, + MessageId = Guid.NewGuid().ToString(), + IsPrivate = true + }; + + // Send to specific user + await Clients.Client(toConnectionId).SendAsync("ReceivePrivateMessage", messageData); + + // Send confirmation to sender + await Clients.Caller.SendAsync("MessageSent", messageData); + + Console.WriteLine($"Private message from {fromUserName} to {toUserId}: {message}"); + } + else + { + await Clients.Caller.SendAsync("Error", $"User {toUserId} not found or not connected"); + } + } + catch (Exception ex) + { + await Clients.Caller.SendAsync("Error", $"Failed to send private message: {ex.Message}"); + } + } + + // Get list of connected users + public async Task GetConnectedUsers() + { + try + { + await Clients.Caller.SendAsync("UpdateUserList", ConnectedUsers.Keys.ToList()); + } + catch (Exception ex) + { + await Clients.Caller.SendAsync("Error", $"Failed to get user list: {ex.Message}"); + } + } + + // Test ping method + public async Task Ping() + { + await Clients.Caller.SendAsync("Pong", DateTime.UtcNow); + } + + // Typing indicator + public async Task UserTyping(string userId, string userName, bool isTyping) + { + try + { + await Clients.Others.SendAsync("UserTyping", userId, userName, isTyping); + } + catch (Exception ex) + { + await Clients.Caller.SendAsync("Error", $"Failed to send typing indicator: {ex.Message}"); + } + } + + // Location sharing methods + public async Task UpdateLocation(string userId, string userName, double latitude, double longitude) + { + try + { + var locationData = new UserLocationData + { + UserId = userId, + UserName = userName, + Latitude = latitude, + Longitude = longitude, + Timestamp = DateTime.UtcNow + }; + + // Update stored location + UserLocations[userId] = locationData; + + // Broadcast location to all other users + await Clients.All.SendAsync("LocationUpdated", locationData); + + Console.WriteLine($"Location updated for {userName}: {latitude}, {longitude}"); + } + catch (Exception ex) + { + await Clients.Caller.SendAsync("Error", $"Failed to update location: {ex.Message}"); + } + } + + public async Task GetAllLocations() + { + try + { + await Clients.Caller.SendAsync("AllLocations", UserLocations.Values.ToList()); + } + catch (Exception ex) + { + await Clients.Caller.SendAsync("Error", $"Failed to get locations: {ex.Message}"); + } + } + + public async Task RequestLocationUpdate(string userId) + { + try + { + if (ConnectedUsers.TryGetValue(userId, out var connectionId)) + { + await Clients.Client(connectionId).SendAsync("LocationUpdateRequested"); + } + else + { + await Clients.Caller.SendAsync("Error", $"User {userId} not found"); + } + } + catch (Exception ex) + { + await Clients.Caller.SendAsync("Error", $"Failed to request location update: {ex.Message}"); + } + } + + // Gửi thông tin vị trí mới nhất của tất cả users + public async Task BroadcastLatestLocations() + { + try + { + // Lọc chỉ lấy location của users đang connected + var activeUserLocations = UserLocations + .Where(loc => ConnectedUsers.ContainsKey(loc.Key)) + .Select(loc => loc.Value) + .OrderByDescending(loc => loc.Timestamp) + .ToList(); + + // Gửi tới tất cả clients + await Clients.All.SendAsync("LatestUserLocations", new + { + Locations = activeUserLocations, + TotalUsers = activeUserLocations.Count, + Timestamp = DateTime.UtcNow + }); + + Console.WriteLine($"Broadcasted latest locations for {activeUserLocations.Count} users"); + } + catch (Exception ex) + { + await Clients.Caller.SendAsync("Error", $"Failed to broadcast latest locations: {ex.Message}"); + } + } + + // Gửi vị trí mới nhất cho user cụ thể khi họ join + public async Task SendLatestLocationsToUser(string requestingUserId) + { + try + { + // Lấy vị trí của các user khác (không bao gồm user đang request) + var otherUserLocations = UserLocations + .Where(loc => ConnectedUsers.ContainsKey(loc.Key) && loc.Key != requestingUserId) + .Select(loc => loc.Value) + .OrderByDescending(loc => loc.Timestamp) + .ToList(); + + // Gửi chỉ cho user đang request + await Clients.Caller.SendAsync("OtherUserLocations", new + { + Locations = otherUserLocations, + TotalOtherUsers = otherUserLocations.Count, + Timestamp = DateTime.UtcNow + }); + + Console.WriteLine($"Sent latest locations of {otherUserLocations.Count} other users to {requestingUserId}"); + } + catch (Exception ex) + { + await Clients.Caller.SendAsync("Error", $"Failed to send latest locations: {ex.Message}"); + } + } +} + +public class UserLocationData +{ + public string UserId { get; set; } = string.Empty; + public string UserName { get; set; } = string.Empty; + public double Latitude { get; set; } + public double Longitude { get; set; } + public DateTime Timestamp { get; set; } +} \ No newline at end of file diff --git a/SnakeAid.Api/Program.cs b/SnakeAid.Api/Program.cs index 995a7838..49dd0465 100644 --- a/SnakeAid.Api/Program.cs +++ b/SnakeAid.Api/Program.cs @@ -11,6 +11,7 @@ using SnakeAid.Core.Mappings; using SnakeAid.Core.Middlewares; using SnakeAid.Api.DI; +using SnakeAid.Api.Hubs; using SnakeAid.Repository.Data; using SQLitePCL; using Swashbuckle.AspNetCore.SwaggerUI; @@ -117,9 +118,20 @@ public static void Main(string[] args) { options.AddPolicy("AllowAll", policy => { - policy.AllowAnyOrigin() + policy.SetIsOriginAllowed(_ => true) .AllowAnyMethod() - .AllowAnyHeader(); + .AllowAnyHeader() + .AllowCredentials(); // Required for SignalR + }); + + // Specific policy for SignalR + options.AddPolicy("SignalRCorsPolicy", policy => + { + policy.SetIsOriginAllowed(_ => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials() + .WithExposedHeaders("Access-Control-Allow-Origin"); }); }); @@ -133,6 +145,20 @@ public static void Main(string[] args) builder.Services.AddControllers(); + // Add SignalR + builder.Services.AddSignalR(options => + { + options.EnableDetailedErrors = builder.Environment.IsDevelopment(); + options.KeepAliveInterval = TimeSpan.FromMinutes(1); + options.ClientTimeoutInterval = TimeSpan.FromMinutes(2); + options.HandshakeTimeout = TimeSpan.FromSeconds(30); + options.MaximumReceiveMessageSize = 64 * 1024; // 64KB + options.StreamBufferCapacity = 10; + }).AddJsonProtocol(options => + { + options.PayloadSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase; + }); + builder.Services.AddAuthenticateAuthor(builder.Configuration); // Add Swagger with JWT support @@ -252,13 +278,20 @@ public static void Main(string[] args) app.UseSerilogRequestLogging(); - app.UseHttpsRedirection(); + // Only use HTTPS redirection in production + if (!app.Environment.IsDevelopment()) + { + app.UseHttpsRedirection(); + } app.UseAuthentication(); app.UseAuthorization(); app.UseSerilogUi(options => options.WithRoutePrefix("logs")); + // Map SignalR Hub with specific CORS policy + app.MapHub("/chat-hub").RequireCors("SignalRCorsPolicy"); + app.MapControllers(); app.Run(); diff --git a/SnakeAid.Api/SnakeAid.Api.csproj b/SnakeAid.Api/SnakeAid.Api.csproj index 07bcdc72..44cfa683 100644 --- a/SnakeAid.Api/SnakeAid.Api.csproj +++ b/SnakeAid.Api/SnakeAid.Api.csproj @@ -10,6 +10,7 @@ + diff --git a/SnakeAid.Core/Domains/Account.cs b/SnakeAid.Core/Domains/Account.cs index 2f1a25a6..40904d0b 100644 --- a/SnakeAid.Core/Domains/Account.cs +++ b/SnakeAid.Core/Domains/Account.cs @@ -36,12 +36,12 @@ public class Account : IdentityUser public ReputationStatus ReputationStatus { get; set; } = ReputationStatus.Good; public DateTime? SuspendedUntil { get; set; } // Nếu bị tạm khóa - + public string? SuspensionReason { get; set; } - public MemberProfile MemberProfile { get; set; } - public ExpertProfile ExpertProfile { get; set; } - public RescuerProfile RescuerProfile { get; set; } + public MemberProfile? MemberProfile { get; set; } + public ExpertProfile? ExpertProfile { get; set; } + public RescuerProfile? RescuerProfile { get; set; } public ICollection Otps { get; set; } = new List(); } diff --git a/SnakeAid.Core/Domains/ExpertProfile.cs b/SnakeAid.Core/Domains/ExpertProfile.cs index 7a507a69..f6f67694 100644 --- a/SnakeAid.Core/Domains/ExpertProfile.cs +++ b/SnakeAid.Core/Domains/ExpertProfile.cs @@ -20,9 +20,6 @@ public class ExpertProfile : BaseEntity [Required] public bool IsOnline { get; set; } = false; - [Required] - public bool IsAvailable { get; set; } = true; - [Range(0, 999999.99)] [Column(TypeName = "numeric(18,2)")] public decimal ConsultationFee { get; set; } @@ -34,8 +31,6 @@ public class ExpertProfile : BaseEntity [Range(0, int.MaxValue)] public int RatingCount { get; set; } = 0; - [MaxLength(500)] - public string? UnavailableReason { get; set; } // Navigation properties public Account Account { get; set; } diff --git a/SnakeAid.Core/Domains/RescueRequestSession.cs b/SnakeAid.Core/Domains/RescueRequestSession.cs index d60a351d..b1b9ddff 100644 --- a/SnakeAid.Core/Domains/RescueRequestSession.cs +++ b/SnakeAid.Core/Domains/RescueRequestSession.cs @@ -25,9 +25,6 @@ public class RescueRequestSession : BaseEntity [Required] public SessionStatus Status { get; set; } = SessionStatus.Active; - [Required] - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public DateTime? CompletedAt { get; set; } // Tracking fields diff --git a/SnakeAid.Core/Domains/RescuerProfile.cs b/SnakeAid.Core/Domains/RescuerProfile.cs index 8bf2cfcd..ede9d608 100644 --- a/SnakeAid.Core/Domains/RescuerProfile.cs +++ b/SnakeAid.Core/Domains/RescuerProfile.cs @@ -33,11 +33,6 @@ public class RescuerProfile : BaseEntity public DateTime? LastLocationUpdate { get; set; } - - [Required] - public bool IsAvailable { get; set; } = true; // sẵn sàng nhận mission - - // Statistics [Required] public int TotalMissions { get; set; } = 0; diff --git a/SnakeAid.Core/Domains/RescuerRequest.cs b/SnakeAid.Core/Domains/RescuerRequest.cs index 205b452b..ebb5d7cf 100644 --- a/SnakeAid.Core/Domains/RescuerRequest.cs +++ b/SnakeAid.Core/Domains/RescuerRequest.cs @@ -35,9 +35,6 @@ public class RescuerRequest : BaseEntity [Required] public DateTime ExpiredAt { get; set; } // Auto-expire after X minutes - [MaxLength(500)] - public string? RejectionReason { get; set; } - // Navigation properties public RescueRequestSession Session { get; set; } diff --git a/SnakeAid.Core/Domains/SnakebiteIncident.cs b/SnakeAid.Core/Domains/SnakebiteIncident.cs index 35d8f2ea..fbc6f7b2 100644 --- a/SnakeAid.Core/Domains/SnakebiteIncident.cs +++ b/SnakeAid.Core/Domains/SnakebiteIncident.cs @@ -17,10 +17,6 @@ public class SnakebiteIncident : BaseEntity [ForeignKey(nameof(User))] public Guid UserId { get; set; } // FK to MemberProfile - [Required] - [MaxLength(1000)] - public string Location { get; set; } = "location not set"; - [Required] [Column(TypeName = "geometry(Point, 4326)")] public Point LocationCoordinates { get; set; } @@ -50,7 +46,6 @@ public class SnakebiteIncident : BaseEntity [MaxLength(500)] public string? CancellationReason { get; set; } - [Range(1, 5)] public int? SeverityLevel { get; set; } = 1; // 1-5 emergency level public DateTime? IncidentOccurredAt { get; set; } // Khi nào bị cắn diff --git a/SnakeAid.Core/Mappings/MapsterConfig.cs b/SnakeAid.Core/Mappings/MapsterConfig.cs index 31453932..2861a69e 100644 --- a/SnakeAid.Core/Mappings/MapsterConfig.cs +++ b/SnakeAid.Core/Mappings/MapsterConfig.cs @@ -11,11 +11,6 @@ public static class MapsterConfig { public static void RegisterMappings() { - // Configure mapping for NetTopologySuite Point (geometry) - TypeAdapterConfig - .NewConfig() - .Map(dest => dest.LocationCoordinates, src => src.LocationCoordinates) - .Map(dest => dest.Location, src => src.Location); // Scan and register all mapping configurations in the assembly // This will automatically find all classes implementing IRegister diff --git a/SnakeAid.Core/Mappings/SnakebiteIncidentMapper.cs b/SnakeAid.Core/Mappings/SnakebiteIncidentMapper.cs new file mode 100644 index 00000000..4e8cbcb2 --- /dev/null +++ b/SnakeAid.Core/Mappings/SnakebiteIncidentMapper.cs @@ -0,0 +1,21 @@ +using Mapster; +using SnakeAid.Core.Domains; +using SnakeAid.Core.Responses.SnakebiteIncident; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SnakeAid.Core.Mappings +{ + public class SnakebiteIncidentMapper : IRegister + { + public void Register(TypeAdapterConfig config) + { + TypeAdapterConfig + .NewConfig() + .Map(dest => dest.LocationCoordinates, src => src.LocationCoordinates); + } + } +} diff --git a/SnakeAid.Core/Responses/SnakebiteIncident/CreateIncidentResponse.cs b/SnakeAid.Core/Responses/SnakebiteIncident/CreateIncidentResponse.cs index d97cbc3a..57769b48 100644 --- a/SnakeAid.Core/Responses/SnakebiteIncident/CreateIncidentResponse.cs +++ b/SnakeAid.Core/Responses/SnakebiteIncident/CreateIncidentResponse.cs @@ -17,8 +17,6 @@ public class CreateIncidentResponse public Guid UserId { get; set; } // FK to MemberProfile - public string Location { get; set; } - public Point LocationCoordinates { get; set; } public SnakebiteIncidentStatus Status { get; set; } = SnakebiteIncidentStatus.Pending; diff --git a/SnakeAid.Repository/Data/DesignTimeDbContextFactory.cs b/SnakeAid.Repository/Data/DesignTimeDbContextFactory.cs index b5bf35a3..5189afe4 100644 --- a/SnakeAid.Repository/Data/DesignTimeDbContextFactory.cs +++ b/SnakeAid.Repository/Data/DesignTimeDbContextFactory.cs @@ -10,7 +10,7 @@ public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SnakeAid.Repository.Data; + +#nullable disable + +namespace SnakeAid.Repository.Migrations +{ + [DbContext(typeof(SnakeAidDbContext))] + [Migration("20260131094719_ChangeProfileSchema")] + partial class ChangeProfileSchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("SnakeAid") + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", "AspNetIdentity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", "AspNetIdentity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", "AspNetIdentity"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.AIModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeployedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("EndpointUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("ModelParameters") + .HasColumnType("jsonb"); + + b.Property("RetiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_AIModels_IsActive"); + + b.HasIndex("IsDefault") + .HasDatabaseName("IX_AIModels_IsDefault"); + + b.HasIndex("Version") + .IsUnique() + .HasDatabaseName("IX_AIModels_Version"); + + b.ToTable("AIModels", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.AISnakeClassMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AIModelId") + .HasColumnType("integer"); + + b.Property("Confidence") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("SnakeSpeciesId") + .HasColumnType("integer"); + + b.Property("YoloClassId") + .HasColumnType("integer"); + + b.Property("YoloClassName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_AISnakeClassMappings_IsActive"); + + b.HasIndex("SnakeSpeciesId"); + + b.HasIndex("AIModelId", "YoloClassId") + .IsUnique() + .HasDatabaseName("IX_AISnakeClassMappings_AIModelId_YoloClassId"); + + b.ToTable("AISnakeClassMappings", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("AvatarUrl") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("ReputationPoints") + .HasColumnType("integer"); + + b.Property("ReputationStatus") + .HasColumnType("integer"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("SuspendedUntil") + .HasColumnType("timestamp with time zone"); + + b.Property("SuspensionReason") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_Accounts_IsActive"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("Role") + .HasDatabaseName("IX_Accounts_Role"); + + b.ToTable("Accounts", "AspNetIdentity"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Antivenom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Manufacturer") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TreatmentFacilityId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TreatmentFacilityId"); + + b.ToTable("Antivenoms", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.AppNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("IsRead") + .HasDatabaseName("IX_AppNotifications_IsRead"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_AppNotifications_UserId"); + + b.HasIndex("UserId", "IsRead") + .HasDatabaseName("IX_AppNotifications_UserId_IsRead"); + + b.ToTable("AppNotifications", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Blog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorId") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RejectionReason") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId") + .HasDatabaseName("IX_Blogs_AuthorId"); + + b.HasIndex("Status") + .HasDatabaseName("IX_Blogs_Status"); + + b.ToTable("Blogs", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.CatchingEnvironment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("character varying(5)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("CatchingEnvironments", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.CatchingMissionDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("SnakeCatchingMissionId") + .HasColumnType("uuid"); + + b.Property("SnakeSpeciesId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SnakeCatchingMissionId") + .HasDatabaseName("IX_CatchingMissionDetails_SnakeCatchingMissionId"); + + b.HasIndex("SnakeSpeciesId") + .HasDatabaseName("IX_CatchingMissionDetails_SnakeSpeciesId"); + + b.ToTable("CatchingMissionDetails", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.CatchingRequestDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("SnakeCatchingRequestId") + .HasColumnType("uuid"); + + b.Property("SnakeSpeciesId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SnakeCatchingRequestId") + .HasDatabaseName("IX_CatchingRequestDetails_SnakeCatchingRequestId"); + + b.HasIndex("SnakeSpeciesId") + .HasDatabaseName("IX_CatchingRequestDetails_SnakeSpeciesId"); + + b.ToTable("CatchingRequestDetails", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttachmentUrl") + .HasColumnType("text"); + + b.Property("ConsultationId") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ConsultationId") + .HasDatabaseName("IX_ChatMessages_ConsultationId"); + + b.HasIndex("SenderId") + .HasDatabaseName("IX_ChatMessages_SenderId"); + + b.HasIndex("SentAt") + .HasDatabaseName("IX_ChatMessages_SentAt"); + + b.ToTable("ChatMessages", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.CommunityReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdditionalDetails") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_CommunityReports_UserId"); + + b.ToTable("CommunityReports", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Consultation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CalleeId") + .HasColumnType("uuid"); + + b.Property("CallerId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpertTimeSlotId") + .HasColumnType("uuid"); + + b.Property("RoomId") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CalleeId") + .HasDatabaseName("IX_Consultations_CalleeId"); + + b.HasIndex("CallerId") + .HasDatabaseName("IX_Consultations_CallerId"); + + b.HasIndex("ExpertTimeSlotId"); + + b.HasIndex("RoomId") + .HasDatabaseName("IX_Consultations_RoomId"); + + b.HasIndex("Status") + .HasDatabaseName("IX_Consultations_Status"); + + b.ToTable("Consultations", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ConsultationBooking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BookedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CancellationReason") + .HasColumnType("text"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsultationId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpertId") + .HasColumnType("uuid"); + + b.Property("PaymentDeadline") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TimeSlotId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ConsultationId"); + + b.HasIndex("ExpertId") + .HasDatabaseName("IX_ConsultationBookings_ExpertId"); + + b.HasIndex("Status") + .HasDatabaseName("IX_ConsultationBookings_Status"); + + b.HasIndex("TimeSlotId") + .HasDatabaseName("IX_ConsultationBookings_TimeSlotId"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_ConsultationBookings_UserId"); + + b.ToTable("ConsultationBookings", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ConsultationPingRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConsultationId") + .HasColumnType("uuid"); + + b.Property("ExpertId") + .HasColumnType("uuid"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RescueMissionId") + .HasColumnType("uuid"); + + b.Property("RescuerId") + .HasColumnType("uuid"); + + b.Property("RespondedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ConsultationId"); + + b.HasIndex("ExpertId") + .HasDatabaseName("IX_ConsultationPingRequests_ExpertId"); + + b.HasIndex("ExpiresAt") + .HasDatabaseName("IX_ConsultationPingRequests_ExpiresAt"); + + b.HasIndex("RescueMissionId"); + + b.HasIndex("RescuerId") + .HasDatabaseName("IX_ConsultationPingRequests_RescuerId"); + + b.HasIndex("Status") + .HasDatabaseName("IX_ConsultationPingRequests_Status"); + + b.ToTable("ConsultationPingRequests", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ExpertCertificate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CertificateName") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("CertificateUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpertId") + .HasColumnType("uuid"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IssueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuingOrganization") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("RejectionReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VerificationStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ExpertCertificates", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ExpertProfile", b => + { + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("Biography") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ConsultationFee") + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsOnline") + .HasColumnType("boolean"); + + b.Property("Rating") + .HasColumnType("numeric(3,2)"); + + b.Property("RatingCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("AccountId"); + + b.ToTable("ExpertProfiles", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ExpertSpecialization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpertId") + .HasColumnType("uuid"); + + b.Property("SpecializationId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SpecializationId"); + + b.HasIndex("ExpertId", "SpecializationId") + .IsUnique() + .HasDatabaseName("IX_ExpertSpecializations_ExpertId_SpecializationId"); + + b.ToTable("ExpertSpecializations", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ExpertTimeSlot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpertId") + .HasColumnType("uuid"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ExpertId") + .HasDatabaseName("IX_ExpertTimeSlots_ExpertId"); + + b.HasIndex("Status") + .HasDatabaseName("IX_ExpertTimeSlots_Status"); + + b.HasIndex("ExpertId", "StartTime") + .HasDatabaseName("IX_ExpertTimeSlots_ExpertId_StartTime"); + + b.ToTable("ExpertTimeSlots", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.FilterOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("OptionImageUrl") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("OptionText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("QuestionId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_FilterOptions_IsActive"); + + b.HasIndex("QuestionId") + .HasDatabaseName("IX_FilterOptions_QuestionId"); + + b.ToTable("FilterOptions", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.FilterQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Question") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_FilterQuestions_IsActive"); + + b.ToTable("FilterQuestions", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.FilterSnakeMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FilterOptionId") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("SnakeSpeciesId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_FilterSnakeMappings_IsActive"); + + b.HasIndex("SnakeSpeciesId"); + + b.HasIndex("FilterOptionId", "SnakeSpeciesId") + .IsUnique() + .HasDatabaseName("IX_FilterSnakeMappings_FilterOptionId_SnakeSpeciesId"); + + b.ToTable("FilterSnakeMappings", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.FirstAidGuideline", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Type") + .HasDatabaseName("IX_FirstAidGuidelines_Type"); + + b.ToTable("FirstAidGuidelines", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Lesson", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Lessons", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.LibraryMedia", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayOrder") + .HasColumnType("integer"); + + b.Property("FileName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsPublic") + .HasColumnType("boolean"); + + b.Property("MediaType") + .HasColumnType("integer"); + + b.Property("MediaUrl") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("SnakeSpeciesId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UploadedById") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_LibraryMedias_IsActive"); + + b.HasIndex("IsPublic") + .HasDatabaseName("IX_LibraryMedias_IsPublic"); + + b.HasIndex("MediaType") + .HasDatabaseName("IX_LibraryMedias_MediaType"); + + b.HasIndex("SnakeSpeciesId") + .HasDatabaseName("IX_LibraryMedias_SnakeSpeciesId"); + + b.HasIndex("UploadedById"); + + b.ToTable("LibraryMedias", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.LocationEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("Heading") + .HasColumnType("real"); + + b.Property("RecordedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("SessionId") + .HasColumnType("uuid"); + + b.Property("SessionType") + .HasColumnType("integer"); + + b.Property("Speed") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("AccountId") + .HasDatabaseName("IX_LocationEvents_AccountId"); + + b.HasIndex("RecordedAt") + .HasDatabaseName("IX_LocationEvents_RecordedAt"); + + b.HasIndex("SessionId") + .HasDatabaseName("IX_LocationEvents_SessionId"); + + b.HasIndex("SessionId", "RecordedAt") + .HasDatabaseName("IX_LocationEvents_SessionId_RecordedAt"); + + b.ToTable("LocationEvents", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.MemberProfile", b => + { + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmergencyContacts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("HasUnderlyingDisease") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Rating") + .HasColumnType("real"); + + b.Property("RatingCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("AccountId"); + + b.ToTable("MemberProfiles", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Otp", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttemptLeft") + .HasColumnType("integer"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("OtpCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Otp", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.PaymentCard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CardHolderName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Cvv") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("PaymentCards", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ReportMedia", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsProcessed") + .HasColumnType("boolean"); + + b.Property("MediaUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Purpose") + .HasColumnType("integer"); + + b.Property("ReferenceId") + .HasColumnType("uuid"); + + b.Property("ReferenceType") + .HasColumnType("integer"); + + b.Property("RequiresAIProcessing") + .HasColumnType("boolean"); + + b.Property("SequenceOrder") + .HasColumnType("integer"); + + b.Property("SnakeCatchingRequestId") + .HasColumnType("uuid"); + + b.Property("SnakebiteIncidentId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UploadBatchId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ReferenceId") + .HasDatabaseName("IX_ReportMedias_ReferenceId"); + + b.HasIndex("ReferenceType") + .HasDatabaseName("IX_ReportMedias_ReferenceType"); + + b.HasIndex("RequiresAIProcessing") + .HasDatabaseName("IX_ReportMedias_RequiresAIProcessing"); + + b.HasIndex("SnakeCatchingRequestId"); + + b.HasIndex("SnakebiteIncidentId"); + + b.HasIndex("ReferenceId", "ReferenceType") + .HasDatabaseName("IX_ReportMedias_ReferenceId_ReferenceType"); + + b.ToTable("ReportMedias", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ReputationRawEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsProcessed") + .HasColumnType("boolean"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PointsChange") + .HasColumnType("integer"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProcessingError") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("uuid"); + + b.Property("ReferenceType") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EventType") + .HasDatabaseName("IX_ReputationRawEvents_EventType"); + + b.HasIndex("IsProcessed") + .HasDatabaseName("IX_ReputationRawEvents_IsProcessed"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_ReputationRawEvents_UserId"); + + b.HasIndex("ReferenceId", "ReferenceType") + .HasDatabaseName("IX_ReputationRawEvents_ReferenceId_ReferenceType"); + + b.ToTable("ReputationRawEvents", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ReputationRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("IX_ReputationRules_Name"); + + b.ToTable("ReputationRules", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ReputationTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NewPoints") + .HasColumnType("integer"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PointsChanged") + .HasColumnType("integer"); + + b.Property("PreviousPoints") + .HasColumnType("integer"); + + b.Property("RawEventId") + .HasColumnType("uuid"); + + b.Property("ReputationRuleId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RawEventId") + .HasDatabaseName("IX_ReputationTransactions_RawEventId"); + + b.HasIndex("ReputationRuleId"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_ReputationTransactions_UserId"); + + b.ToTable("ReputationTransactions", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.RescueMission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualCost") + .HasColumnType("numeric(18,2)"); + + b.Property("ArrivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CancellationReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EstimatedCost") + .HasColumnType("numeric(18,2)"); + + b.Property("IncidentId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Price") + .HasColumnType("numeric(18,2)"); + + b.Property("RescuerId") + .HasColumnType("uuid"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IncidentId") + .IsUnique() + .HasDatabaseName("IX_RescueMissions_IncidentId"); + + b.HasIndex("RescuerId") + .HasDatabaseName("IX_RescueMissions_RescuerId"); + + b.HasIndex("Status") + .HasDatabaseName("IX_RescueMissions_Status"); + + b.ToTable("RescueMissions", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.RescueRequestSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IncidentId") + .HasColumnType("uuid"); + + b.Property("RadiusKm") + .HasColumnType("integer"); + + b.Property("RescuersPinged") + .HasColumnType("integer"); + + b.Property("SessionNumber") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TriggerType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IncidentId") + .HasDatabaseName("IX_RescueRequestSessions_IncidentId"); + + b.HasIndex("Status") + .HasDatabaseName("IX_RescueRequestSessions_Status"); + + b.HasIndex("IncidentId", "SessionNumber") + .IsUnique() + .HasDatabaseName("IX_RescueRequestSessions_IncidentId_SessionNumber"); + + b.ToTable("RescueRequestSessions", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.RescuerProfile", b => + { + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("CompletedMissions") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsOnline") + .HasColumnType("boolean"); + + b.Property("LastLocationUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Rating") + .HasColumnType("numeric(3,2)"); + + b.Property("RatingCount") + .HasColumnType("integer"); + + b.Property("TotalMissions") + .HasColumnType("integer"); + + b.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("AccountId"); + + b.HasIndex("IsOnline") + .HasDatabaseName("IX_RescuerProfiles_IsOnline"); + + b.HasIndex("Type") + .HasDatabaseName("IX_RescuerProfiles_Type"); + + b.ToTable("RescuerProfiles", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.RescuerRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IncidentId") + .HasColumnType("uuid"); + + b.Property("RequestSentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RescuerId") + .HasColumnType("uuid"); + + b.Property("ResponseAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ExpiredAt") + .HasDatabaseName("IX_RescuerRequests_ExpiredAt"); + + b.HasIndex("IncidentId") + .HasDatabaseName("IX_RescuerRequests_IncidentId"); + + b.HasIndex("RescuerId") + .HasDatabaseName("IX_RescuerRequests_RescuerId"); + + b.HasIndex("SessionId") + .HasDatabaseName("IX_RescuerRequests_SessionId"); + + b.HasIndex("Status") + .HasDatabaseName("IX_RescuerRequests_Status"); + + b.ToTable("RescuerRequests", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakeAIRecognitionResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AIModelId") + .HasColumnType("integer"); + + b.Property("AllDetections") + .HasColumnType("jsonb"); + + b.Property("ClassMappingId") + .HasColumnType("uuid"); + + b.Property("Confidence") + .HasColumnType("numeric(5,4)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DetectedSpeciesId") + .HasColumnType("integer"); + + b.Property("ExpertCorrectedSpeciesId") + .HasColumnType("integer"); + + b.Property("ExpertId") + .HasColumnType("uuid"); + + b.Property("ExpertNotes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ExpertVerifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsMapped") + .HasColumnType("boolean"); + + b.Property("ReportMediaId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("YoloClassName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("AIModelId") + .HasDatabaseName("IX_SnakeAIRecognitionResults_AIModelId"); + + b.HasIndex("ClassMappingId"); + + b.HasIndex("DetectedSpeciesId"); + + b.HasIndex("ExpertCorrectedSpeciesId"); + + b.HasIndex("ExpertId"); + + b.HasIndex("IsMapped") + .HasDatabaseName("IX_SnakeAIRecognitionResults_IsMapped"); + + b.HasIndex("ReportMediaId") + .HasDatabaseName("IX_SnakeAIRecognitionResults_ReportMediaId"); + + b.HasIndex("Status") + .HasDatabaseName("IX_SnakeAIRecognitionResults_Status"); + + b.ToTable("SnakeAIRecognitionResults", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakeCatchingMission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualCost") + .HasColumnType("numeric(18,2)"); + + b.Property("ArrivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CancellationReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EstimatedCost") + .HasColumnType("numeric(18,2)"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Price") + .HasColumnType("numeric(18,2)"); + + b.Property("RescuerId") + .HasColumnType("uuid"); + + b.Property("SnakeCatchingRequestId") + .HasColumnType("uuid"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("RescuerId") + .HasDatabaseName("IX_SnakeCatchingMissions_RescuerId"); + + b.HasIndex("SnakeCatchingRequestId") + .IsUnique() + .HasDatabaseName("IX_SnakeCatchingMissions_RequestId"); + + b.HasIndex("Status") + .HasDatabaseName("IX_SnakeCatchingMissions_Status"); + + b.ToTable("SnakeCatchingMissions", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakeCatchingRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdditionalDetails") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedRescuerId") + .HasColumnType("uuid"); + + b.Property("CancellationReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EstimatedPrice") + .HasColumnType("numeric(18,2)"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("PreferredTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("RequestDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssignedRescuerId") + .HasDatabaseName("IX_SnakeCatchingRequests_AssignedRescuerId"); + + b.HasIndex("RequestDate") + .HasDatabaseName("IX_SnakeCatchingRequests_RequestDate"); + + b.HasIndex("Status") + .HasDatabaseName("IX_SnakeCatchingRequests_Status"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_SnakeCatchingRequests_UserId"); + + b.ToTable("SnakeCatchingRequests", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakeCatchingTariff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BasePrice") + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("SizeCategory") + .HasColumnType("integer"); + + b.Property("SnakeSpeciesId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_SnakeCatchingTariffs_IsActive"); + + b.HasIndex("SnakeSpeciesId", "SizeCategory") + .IsUnique() + .HasDatabaseName("IX_SnakeCatchingTariffs_SnakeSpeciesId_SizeCategory"); + + b.ToTable("SnakeCatchingTariffs", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakeSpecies", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CommonName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("FirstAidGuidelineOverride") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Identification") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("IdentificationSummary") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsVenomous") + .HasColumnType("boolean"); + + b.Property("PrimaryVenomType") + .HasColumnType("integer"); + + b.Property("RiskLevel") + .HasColumnType("real"); + + b.Property("ScientificName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SymptomsByTime") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_SnakeSpecies_IsActive"); + + b.HasIndex("IsVenomous") + .HasDatabaseName("IX_SnakeSpecies_IsVenomous"); + + b.HasIndex("ScientificName") + .IsUnique() + .HasDatabaseName("IX_SnakeSpecies_ScientificName"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("IX_SnakeSpecies_Slug"); + + b.ToTable("SnakeSpecies", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakeSpeciesName", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("SnakeSpeciesId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_SnakeSpeciesNames_Name"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("IX_SnakeSpeciesNames_Slug"); + + b.HasIndex("SnakeSpeciesId") + .HasDatabaseName("IX_SnakeSpeciesNames_SnakeSpeciesId"); + + b.ToTable("SnakeSpeciesNames", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakebiteIncident", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedRescuerId") + .HasColumnType("uuid"); + + b.Property("CancellationReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentRadiusKm") + .HasColumnType("integer"); + + b.Property("CurrentSessionNumber") + .HasColumnType("integer"); + + b.Property("IncidentOccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSessionAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SeverityLevel") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SymptomsReport") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssignedRescuerId") + .HasDatabaseName("IX_SnakebiteIncidents_AssignedRescuerId"); + + b.HasIndex("Status") + .HasDatabaseName("IX_SnakebiteIncidents_Status"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_SnakebiteIncidents_UserId"); + + b.ToTable("SnakebiteIncidents", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Specialization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("IX_Specializations_Name"); + + b.ToTable("Specializations", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SpeciesAntivenom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AntivenomId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("SnakeSpeciesId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AntivenomId"); + + b.HasIndex("SnakeSpeciesId", "AntivenomId") + .IsUnique() + .HasDatabaseName("IX_SpeciesAntivenoms_SnakeSpeciesId_AntivenomId"); + + b.ToTable("SpeciesAntivenoms", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SpeciesVenom", b => + { + b.Property("SnakeSpeciesId") + .HasColumnType("integer"); + + b.Property("VenomTypeId") + .HasColumnType("integer"); + + b.HasKey("SnakeSpeciesId", "VenomTypeId"); + + b.HasIndex("VenomTypeId"); + + b.ToTable("SpeciesVenoms", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SymptomConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttributeKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("AttributeLabel") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DisplayOrder") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("TimeScoresJson") + .HasColumnType("jsonb"); + + b.Property("UIHint") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VenomTypeId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AttributeKey") + .HasDatabaseName("IX_SymptomConfigs_AttributeKey"); + + b.HasIndex("DisplayOrder") + .HasDatabaseName("IX_SymptomConfigs_DisplayOrder"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_SymptomConfigs_IsActive"); + + b.HasIndex("VenomTypeId") + .HasDatabaseName("IX_SymptomConfigs_VenomTypeId"); + + b.ToTable("SymptomConfigs", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SystemSetting", b => + { + b.Property("SettingKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ValueType") + .HasColumnType("integer"); + + b.HasKey("SettingKey"); + + b.ToTable("SystemSettings", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.TrackingSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DistanceMeters") + .HasColumnType("double precision"); + + b.Property("EtaMinutes") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MemberLastUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("RescuerLastUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("uuid"); + + b.Property("SessionType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_TrackingSessions_IsActive"); + + b.HasIndex("SessionId") + .HasDatabaseName("IX_TrackingSessions_SessionId"); + + b.HasIndex("SessionType") + .HasDatabaseName("IX_TrackingSessions_SessionType"); + + b.HasIndex("SessionId", "SessionType") + .IsUnique() + .HasDatabaseName("IX_TrackingSessions_SessionId_SessionType"); + + b.ToTable("TrackingSessions", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ExternalTransactionId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaymentMethod") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ReferenceId") + .HasColumnType("uuid"); + + b.Property("TransactionType") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_Transactions_CreatedAt"); + + b.HasIndex("ReferenceId") + .HasDatabaseName("IX_Transactions_ReferenceId"); + + b.HasIndex("TransactionType") + .HasDatabaseName("IX_Transactions_TransactionType"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_Transactions_UserId"); + + b.ToTable("Transactions", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.TreatmentFacility", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ContactNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_TreatmentFacilities_IsActive"); + + b.HasIndex("Name") + .HasDatabaseName("IX_TreatmentFacilities_Name"); + + b.ToTable("TreatmentFacilities", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.UserFeedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Comments") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RaterId") + .HasColumnType("uuid"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("ReferenceId") + .HasColumnType("uuid"); + + b.Property("TargetUserId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("RaterId") + .HasDatabaseName("IX_UserFeedbacks_RaterId"); + + b.HasIndex("TargetUserId") + .HasDatabaseName("IX_UserFeedbacks_TargetUserId"); + + b.HasIndex("Type") + .HasDatabaseName("IX_UserFeedbacks_Type"); + + b.HasIndex("ReferenceId", "Type") + .HasDatabaseName("IX_UserFeedbacks_ReferenceId_Type"); + + b.ToTable("UserFeedbacks", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.VenomType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("FirstAidGuidelineId") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ScientificName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SeverityIndex") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FirstAidGuidelineId") + .IsUnique(); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_VenomTypes_IsActive"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("IX_VenomTypes_Name"); + + b.ToTable("VenomTypes", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Wallet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Balance") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasDatabaseName("IX_Wallets_UserId"); + + b.ToTable("Wallets", "SnakeAid"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.WalletWithdraw", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric(18,2)"); + + b.Property("BankAccount") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("BankName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RejectionReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("WalletId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Status") + .HasDatabaseName("IX_WalletWithdraws_Status"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_WalletWithdraws_UserId"); + + b.HasIndex("WalletId") + .HasDatabaseName("IX_WalletWithdraws_WalletId"); + + b.ToTable("WalletWithdraws", "SnakeAid"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.AISnakeClassMapping", b => + { + b.HasOne("SnakeAid.Core.Domains.AIModel", "AIModel") + .WithMany("ClassMappings") + .HasForeignKey("AIModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.SnakeSpecies", "SnakeSpecies") + .WithMany() + .HasForeignKey("SnakeSpeciesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AIModel"); + + b.Navigation("SnakeSpecies"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Antivenom", b => + { + b.HasOne("SnakeAid.Core.Domains.TreatmentFacility", null) + .WithMany("AntivenomStocks") + .HasForeignKey("TreatmentFacilityId"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.AppNotification", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Blog", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.CatchingMissionDetail", b => + { + b.HasOne("SnakeAid.Core.Domains.SnakeCatchingMission", "SnakeCatchingMission") + .WithMany() + .HasForeignKey("SnakeCatchingMissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.SnakeSpecies", "SnakeSpecies") + .WithMany() + .HasForeignKey("SnakeSpeciesId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("SnakeCatchingMission"); + + b.Navigation("SnakeSpecies"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.CatchingRequestDetail", b => + { + b.HasOne("SnakeAid.Core.Domains.SnakeCatchingRequest", "SnakeCatchingRequest") + .WithMany() + .HasForeignKey("SnakeCatchingRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.SnakeSpecies", "SnakeSpecies") + .WithMany() + .HasForeignKey("SnakeSpeciesId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("SnakeCatchingRequest"); + + b.Navigation("SnakeSpecies"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ChatMessage", b => + { + b.HasOne("SnakeAid.Core.Domains.Consultation", "Consultation") + .WithMany() + .HasForeignKey("ConsultationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.Account", "Sender") + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Consultation"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.CommunityReport", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Consultation", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "Callee") + .WithMany() + .HasForeignKey("CalleeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.Account", "Caller") + .WithMany() + .HasForeignKey("CallerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.ExpertTimeSlot", null) + .WithMany("Consultations") + .HasForeignKey("ExpertTimeSlotId"); + + b.Navigation("Callee"); + + b.Navigation("Caller"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ConsultationBooking", b => + { + b.HasOne("SnakeAid.Core.Domains.Consultation", "Consultation") + .WithMany() + .HasForeignKey("ConsultationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SnakeAid.Core.Domains.Account", "Expert") + .WithMany() + .HasForeignKey("ExpertId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.ExpertTimeSlot", "TimeSlot") + .WithMany("RescueMissions") + .HasForeignKey("TimeSlotId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.Account", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Consultation"); + + b.Navigation("Expert"); + + b.Navigation("TimeSlot"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ConsultationPingRequest", b => + { + b.HasOne("SnakeAid.Core.Domains.Consultation", "Consultation") + .WithMany() + .HasForeignKey("ConsultationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SnakeAid.Core.Domains.Account", "Expert") + .WithMany() + .HasForeignKey("ExpertId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.RescueMission", "RescueMission") + .WithMany() + .HasForeignKey("RescueMissionId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SnakeAid.Core.Domains.Account", "Rescuer") + .WithMany() + .HasForeignKey("RescuerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Consultation"); + + b.Navigation("Expert"); + + b.Navigation("RescueMission"); + + b.Navigation("Rescuer"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ExpertProfile", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "Account") + .WithOne("ExpertProfile") + .HasForeignKey("SnakeAid.Core.Domains.ExpertProfile", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ExpertSpecialization", b => + { + b.HasOne("SnakeAid.Core.Domains.ExpertProfile", "Expert") + .WithMany("Specializations") + .HasForeignKey("ExpertId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.Specialization", "Specialization") + .WithMany("ExpertSpecializations") + .HasForeignKey("SpecializationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Expert"); + + b.Navigation("Specialization"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ExpertTimeSlot", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "Expert") + .WithMany() + .HasForeignKey("ExpertId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Expert"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.FilterOption", b => + { + b.HasOne("SnakeAid.Core.Domains.FilterQuestion", "Question") + .WithMany("FilterOptions") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.FilterSnakeMapping", b => + { + b.HasOne("SnakeAid.Core.Domains.FilterOption", "FilterOption") + .WithMany("FilterSnakeMappings") + .HasForeignKey("FilterOptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.SnakeSpecies", "SnakeSpecies") + .WithMany("FilterSnakeMappings") + .HasForeignKey("SnakeSpeciesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FilterOption"); + + b.Navigation("SnakeSpecies"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.LibraryMedia", b => + { + b.HasOne("SnakeAid.Core.Domains.SnakeSpecies", "SnakeSpecies") + .WithMany("LibraryMedias") + .HasForeignKey("SnakeSpeciesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.Account", "UploadedBy") + .WithMany() + .HasForeignKey("UploadedById") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("SnakeSpecies"); + + b.Navigation("UploadedBy"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.MemberProfile", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "Account") + .WithOne("MemberProfile") + .HasForeignKey("SnakeAid.Core.Domains.MemberProfile", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Otp", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "User") + .WithMany("Otps") + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ReportMedia", b => + { + b.HasOne("SnakeAid.Core.Domains.SnakeCatchingRequest", null) + .WithMany("Media") + .HasForeignKey("SnakeCatchingRequestId"); + + b.HasOne("SnakeAid.Core.Domains.SnakebiteIncident", null) + .WithMany("Media") + .HasForeignKey("SnakebiteIncidentId"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ReputationRawEvent", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ReputationTransaction", b => + { + b.HasOne("SnakeAid.Core.Domains.ReputationRawEvent", "RawEvent") + .WithMany() + .HasForeignKey("RawEventId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.ReputationRule", "ReputationRule") + .WithMany() + .HasForeignKey("ReputationRuleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.Account", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("RawEvent"); + + b.Navigation("ReputationRule"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.RescueMission", b => + { + b.HasOne("SnakeAid.Core.Domains.SnakebiteIncident", "Incident") + .WithOne("RescueMission") + .HasForeignKey("SnakeAid.Core.Domains.RescueMission", "IncidentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.RescuerProfile", "Rescuer") + .WithMany("Missions") + .HasForeignKey("RescuerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Incident"); + + b.Navigation("Rescuer"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.RescueRequestSession", b => + { + b.HasOne("SnakeAid.Core.Domains.SnakebiteIncident", "Incident") + .WithMany("Sessions") + .HasForeignKey("IncidentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Incident"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.RescuerProfile", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "Account") + .WithOne("RescuerProfile") + .HasForeignKey("SnakeAid.Core.Domains.RescuerProfile", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.RescuerRequest", b => + { + b.HasOne("SnakeAid.Core.Domains.SnakebiteIncident", "Incident") + .WithMany("AllRequests") + .HasForeignKey("IncidentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.RescuerProfile", "Rescuer") + .WithMany("RescuerRequests") + .HasForeignKey("RescuerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.RescueRequestSession", "Session") + .WithMany("Requests") + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Incident"); + + b.Navigation("Rescuer"); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakeAIRecognitionResult", b => + { + b.HasOne("SnakeAid.Core.Domains.AIModel", "AIModel") + .WithMany("RecognitionResults") + .HasForeignKey("AIModelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.AISnakeClassMapping", "ClassMapping") + .WithMany() + .HasForeignKey("ClassMappingId"); + + b.HasOne("SnakeAid.Core.Domains.SnakeSpecies", "DetectedSpecies") + .WithMany() + .HasForeignKey("DetectedSpeciesId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SnakeAid.Core.Domains.SnakeSpecies", "ExpertCorrectedSpecies") + .WithMany() + .HasForeignKey("ExpertCorrectedSpeciesId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SnakeAid.Core.Domains.Account", "Expert") + .WithMany() + .HasForeignKey("ExpertId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SnakeAid.Core.Domains.ReportMedia", "ReportMedia") + .WithMany("AIRecognitionResults") + .HasForeignKey("ReportMediaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AIModel"); + + b.Navigation("ClassMapping"); + + b.Navigation("DetectedSpecies"); + + b.Navigation("Expert"); + + b.Navigation("ExpertCorrectedSpecies"); + + b.Navigation("ReportMedia"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakeCatchingMission", b => + { + b.HasOne("SnakeAid.Core.Domains.RescuerProfile", "Rescuer") + .WithMany("CatchingMissions") + .HasForeignKey("RescuerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.SnakeCatchingRequest", "SnakeCatchingRequest") + .WithOne("Mission") + .HasForeignKey("SnakeAid.Core.Domains.SnakeCatchingMission", "SnakeCatchingRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Rescuer"); + + b.Navigation("SnakeCatchingRequest"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakeCatchingRequest", b => + { + b.HasOne("SnakeAid.Core.Domains.RescuerProfile", "AssignedRescuer") + .WithMany() + .HasForeignKey("AssignedRescuerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SnakeAid.Core.Domains.MemberProfile", "User") + .WithMany("SnakeCatchingRequests") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AssignedRescuer"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakeCatchingTariff", b => + { + b.HasOne("SnakeAid.Core.Domains.SnakeSpecies", "SnakeSpecies") + .WithMany("SnakeCatchingTariffs") + .HasForeignKey("SnakeSpeciesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SnakeSpecies"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakeSpeciesName", b => + { + b.HasOne("SnakeAid.Core.Domains.SnakeSpecies", "SnakeSpecies") + .WithMany("AlternativeNames") + .HasForeignKey("SnakeSpeciesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SnakeSpecies"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakebiteIncident", b => + { + b.HasOne("SnakeAid.Core.Domains.RescuerProfile", "AssignedRescuer") + .WithMany() + .HasForeignKey("AssignedRescuerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("SnakeAid.Core.Domains.MemberProfile", "User") + .WithMany("SnakebiteIncidents") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AssignedRescuer"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SpeciesAntivenom", b => + { + b.HasOne("SnakeAid.Core.Domains.Antivenom", "Antivenom") + .WithMany("SpeciesAntivenoms") + .HasForeignKey("AntivenomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.SnakeSpecies", "SnakeSpecies") + .WithMany("SpeciesAntivenoms") + .HasForeignKey("SnakeSpeciesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Antivenom"); + + b.Navigation("SnakeSpecies"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SpeciesVenom", b => + { + b.HasOne("SnakeAid.Core.Domains.SnakeSpecies", "SnakeSpecies") + .WithMany("SpeciesVenoms") + .HasForeignKey("SnakeSpeciesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.VenomType", "VenomType") + .WithMany("SpeciesVenoms") + .HasForeignKey("VenomTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SnakeSpecies"); + + b.Navigation("VenomType"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SymptomConfig", b => + { + b.HasOne("SnakeAid.Core.Domains.VenomType", "VenomType") + .WithMany("SymptomConfigs") + .HasForeignKey("VenomTypeId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("VenomType"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Transaction", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.UserFeedback", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "Rater") + .WithMany() + .HasForeignKey("RaterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.Account", "TargetUser") + .WithMany() + .HasForeignKey("TargetUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Rater"); + + b.Navigation("TargetUser"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.VenomType", b => + { + b.HasOne("SnakeAid.Core.Domains.FirstAidGuideline", "FirstAidGuideline") + .WithOne() + .HasForeignKey("SnakeAid.Core.Domains.VenomType", "FirstAidGuidelineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("FirstAidGuideline"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Wallet", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "Account") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.WalletWithdraw", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SnakeAid.Core.Domains.Wallet", "Wallet") + .WithMany() + .HasForeignKey("WalletId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.AIModel", b => + { + b.Navigation("ClassMappings"); + + b.Navigation("RecognitionResults"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Account", b => + { + b.Navigation("ExpertProfile"); + + b.Navigation("MemberProfile"); + + b.Navigation("Otps"); + + b.Navigation("RescuerProfile"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Antivenom", b => + { + b.Navigation("SpeciesAntivenoms"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ExpertProfile", b => + { + b.Navigation("Specializations"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ExpertTimeSlot", b => + { + b.Navigation("Consultations"); + + b.Navigation("RescueMissions"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.FilterOption", b => + { + b.Navigation("FilterSnakeMappings"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.FilterQuestion", b => + { + b.Navigation("FilterOptions"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.MemberProfile", b => + { + b.Navigation("SnakeCatchingRequests"); + + b.Navigation("SnakebiteIncidents"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.ReportMedia", b => + { + b.Navigation("AIRecognitionResults"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.RescueRequestSession", b => + { + b.Navigation("Requests"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.RescuerProfile", b => + { + b.Navigation("CatchingMissions"); + + b.Navigation("Missions"); + + b.Navigation("RescuerRequests"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakeCatchingRequest", b => + { + b.Navigation("Media"); + + b.Navigation("Mission"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakeSpecies", b => + { + b.Navigation("AlternativeNames"); + + b.Navigation("FilterSnakeMappings"); + + b.Navigation("LibraryMedias"); + + b.Navigation("SnakeCatchingTariffs"); + + b.Navigation("SpeciesAntivenoms"); + + b.Navigation("SpeciesVenoms"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.SnakebiteIncident", b => + { + b.Navigation("AllRequests"); + + b.Navigation("Media"); + + b.Navigation("RescueMission"); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.Specialization", b => + { + b.Navigation("ExpertSpecializations"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.TreatmentFacility", b => + { + b.Navigation("AntivenomStocks"); + }); + + modelBuilder.Entity("SnakeAid.Core.Domains.VenomType", b => + { + b.Navigation("SpeciesVenoms"); + + b.Navigation("SymptomConfigs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SnakeAid.Repository/Migrations/20260131094719_ChangeProfileSchema.cs b/SnakeAid.Repository/Migrations/20260131094719_ChangeProfileSchema.cs new file mode 100644 index 00000000..a131913c --- /dev/null +++ b/SnakeAid.Repository/Migrations/20260131094719_ChangeProfileSchema.cs @@ -0,0 +1,118 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SnakeAid.Repository.Migrations +{ + /// + public partial class ChangeProfileSchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Location", + schema: "SnakeAid", + table: "SnakebiteIncidents"); + + migrationBuilder.DropColumn( + name: "RejectionReason", + schema: "SnakeAid", + table: "RescuerRequests"); + + migrationBuilder.DropColumn( + name: "IsAvailable", + schema: "SnakeAid", + table: "RescuerProfiles"); + + migrationBuilder.DropColumn( + name: "IsAvailable", + schema: "SnakeAid", + table: "ExpertProfiles"); + + migrationBuilder.DropColumn( + name: "UnavailableReason", + schema: "SnakeAid", + table: "ExpertProfiles"); + + migrationBuilder.CreateTable( + name: "Otp", + schema: "SnakeAid", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Email = table.Column(type: "character varying(150)", maxLength: 150, nullable: false), + UserId = table.Column(type: "uuid", nullable: true), + OtpCode = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), + ExpirationTime = table.Column(type: "timestamp with time zone", nullable: false), + AttemptLeft = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Otp", x => x.Id); + table.ForeignKey( + name: "FK_Otp_Accounts_UserId", + column: x => x.UserId, + principalSchema: "AspNetIdentity", + principalTable: "Accounts", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Otp_UserId", + schema: "SnakeAid", + table: "Otp", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Otp", + schema: "SnakeAid"); + + migrationBuilder.AddColumn( + name: "Location", + schema: "SnakeAid", + table: "SnakebiteIncidents", + type: "character varying(1000)", + maxLength: 1000, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "RejectionReason", + schema: "SnakeAid", + table: "RescuerRequests", + type: "character varying(500)", + maxLength: 500, + nullable: true); + + migrationBuilder.AddColumn( + name: "IsAvailable", + schema: "SnakeAid", + table: "RescuerProfiles", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsAvailable", + schema: "SnakeAid", + table: "ExpertProfiles", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "UnavailableReason", + schema: "SnakeAid", + table: "ExpertProfiles", + type: "character varying(500)", + maxLength: 500, + nullable: true); + } + } +} diff --git a/SnakeAid.Repository/Migrations/SnakeAidDbContextModelSnapshot.cs b/SnakeAid.Repository/Migrations/SnakeAidDbContextModelSnapshot.cs index ee616f3e..98f0da5b 100644 --- a/SnakeAid.Repository/Migrations/SnakeAidDbContextModelSnapshot.cs +++ b/SnakeAid.Repository/Migrations/SnakeAidDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("SnakeAid") - .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -812,9 +812,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); - b.Property("IsAvailable") - .HasColumnType("boolean"); - b.Property("IsOnline") .HasColumnType("boolean"); @@ -824,10 +821,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RatingCount") .HasColumnType("integer"); - b.Property("UnavailableReason") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -1219,6 +1212,38 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("MemberProfiles", "SnakeAid"); }); + modelBuilder.Entity("SnakeAid.Core.Domains.Otp", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttemptLeft") + .HasColumnType("integer"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("OtpCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Otp", "SnakeAid"); + }); + modelBuilder.Entity("SnakeAid.Core.Domains.PaymentCard", b => { b.Property("Id") @@ -1592,9 +1617,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); - b.Property("IsAvailable") - .HasColumnType("boolean"); - b.Property("IsOnline") .HasColumnType("boolean"); @@ -1644,10 +1666,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IncidentId") .HasColumnType("uuid"); - b.Property("RejectionReason") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - b.Property("RequestSentAt") .HasColumnType("timestamp with time zone"); @@ -2097,11 +2115,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LastSessionAt") .HasColumnType("timestamp with time zone"); - b.Property("Location") - .IsRequired() - .HasMaxLength(1000) - .HasColumnType("character varying(1000)"); - b.Property("SeverityLevel") .HasColumnType("integer"); @@ -2969,6 +2982,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Account"); }); + modelBuilder.Entity("SnakeAid.Core.Domains.Otp", b => + { + b.HasOne("SnakeAid.Core.Domains.Account", "User") + .WithMany("Otps") + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + modelBuilder.Entity("SnakeAid.Core.Domains.ReportMedia", b => { b.HasOne("SnakeAid.Core.Domains.SnakeCatchingRequest", null) @@ -3337,14 +3359,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("SnakeAid.Core.Domains.Account", b => { - b.Navigation("ExpertProfile") - .IsRequired(); + b.Navigation("ExpertProfile"); - b.Navigation("MemberProfile") - .IsRequired(); + b.Navigation("MemberProfile"); - b.Navigation("RescuerProfile") - .IsRequired(); + b.Navigation("Otps"); + + b.Navigation("RescuerProfile"); }); modelBuilder.Entity("SnakeAid.Core.Domains.Antivenom", b => From 7dd8b6ab19ed4c2cd9f0c6b49548c101206d1d6f Mon Sep 17 00:00:00 2001 From: NhanNPSE184696 Date: Sat, 31 Jan 2026 17:11:02 +0700 Subject: [PATCH 08/12] feat: add functionality to retrieve first aid guidelines by snake species ID and update content type to object --- .../FirstAidGuidelineController.cs | 16 ++- .../CreateFirstAidGuidelineRequest.cs | 2 +- .../UpdateFirstAidGuidelineRequest.cs | 2 +- SnakeAid.Core/Requests/PaginationRequest.cs | 15 +++ .../Implements/FirstAidGuidelineService.cs | 115 ++++++++++++++---- .../Implements/SymptomConfigService.cs | 106 +++++++--------- .../Interfaces/IFirstAidGuidelineService.cs | 5 + 7 files changed, 166 insertions(+), 95 deletions(-) create mode 100644 SnakeAid.Core/Requests/PaginationRequest.cs diff --git a/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs b/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs index 7d59df21..118155ac 100644 --- a/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs +++ b/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs @@ -30,7 +30,6 @@ public FirstAidGuidelineController( /// Create a new first aid guideline /// [HttpPost] - [Authorize(Roles = "Admin")] [ValidateModel] [SwaggerOperation(Summary = "Create First Aid Guideline", Description = "Create a new first aid guideline (Admin only)")] [SwaggerResponse(200, "Created successfully", typeof(ApiResponse))] @@ -80,11 +79,23 @@ public async Task GetAllFirstAidGuideline() return StatusCode(result.StatusCode, result); } + /// + /// Get first aid guidelines by snake species ID + /// + [HttpGet("by-snake-species/{snakeSpeciesId}")] + [SwaggerOperation(Summary = "Get First Aid Guidelines by Snake Species", Description = "Get first aid guidelines based on the venom types of a specific snake species")] + [SwaggerResponse(200, "Success", typeof(ApiResponse>))] + [SwaggerResponse(404, "Snake species not found")] + public async Task GetFirstAidGuidelinesBySnakeSpecies(int snakeSpeciesId) + { + var result = await _guidelineService.GetFirstAidGuidelinesBySnakeSpeciesIdAsync(snakeSpeciesId); + return StatusCode(result.StatusCode, result); + } + /// /// Update an existing first aid guideline /// [HttpPut("{id}")] - [Authorize(Roles = "Admin")] [ValidateModel] [SwaggerOperation(Summary = "Update First Aid Guideline", Description = "Update an existing first aid guideline (Admin only)")] [SwaggerResponse(200, "Updated successfully", typeof(ApiResponse))] @@ -102,7 +113,6 @@ public async Task UpdateFirstAidGuideline(int id, [FromBody] Upda /// Delete a first aid guideline /// [HttpDelete("{id}")] - [Authorize(Roles = "Admin")] [SwaggerOperation(Summary = "Delete First Aid Guideline", Description = "Delete a first aid guideline (Admin only)")] [SwaggerResponse(200, "Deleted successfully", typeof(ApiResponse))] [SwaggerResponse(401, "Unauthorized")] diff --git a/SnakeAid.Core/Requests/FirstAidGuideline/CreateFirstAidGuidelineRequest.cs b/SnakeAid.Core/Requests/FirstAidGuideline/CreateFirstAidGuidelineRequest.cs index 620b3b25..1a3ccec3 100644 --- a/SnakeAid.Core/Requests/FirstAidGuideline/CreateFirstAidGuidelineRequest.cs +++ b/SnakeAid.Core/Requests/FirstAidGuideline/CreateFirstAidGuidelineRequest.cs @@ -16,7 +16,7 @@ public class CreateFirstAidGuidelineRequest /// Nội dung chi tiết (JSON format) /// [Required(ErrorMessage = "Content is required")] - public string Content { get; set; } + public object Content { get; set; } /// /// Loại hướng dẫn: General (0) hoặc SpeciesSpecific (1) diff --git a/SnakeAid.Core/Requests/FirstAidGuideline/UpdateFirstAidGuidelineRequest.cs b/SnakeAid.Core/Requests/FirstAidGuideline/UpdateFirstAidGuidelineRequest.cs index 50e65d81..d17253a9 100644 --- a/SnakeAid.Core/Requests/FirstAidGuideline/UpdateFirstAidGuidelineRequest.cs +++ b/SnakeAid.Core/Requests/FirstAidGuideline/UpdateFirstAidGuidelineRequest.cs @@ -14,7 +14,7 @@ public class UpdateFirstAidGuidelineRequest /// /// Nội dung chi tiết (JSON format) /// - public string? Content { get; set; } + public object? Content { get; set; } /// /// Loại hướng dẫn: General (0) hoặc SpeciesSpecific (1) diff --git a/SnakeAid.Core/Requests/PaginationRequest.cs b/SnakeAid.Core/Requests/PaginationRequest.cs new file mode 100644 index 00000000..636a652b --- /dev/null +++ b/SnakeAid.Core/Requests/PaginationRequest.cs @@ -0,0 +1,15 @@ +namespace SnakeAid.Core.Requests +{ + public class PaginationRequest + { + /// + /// Page number (starting from 1) + /// + public int PageNumber { get; set; } = 1; + + /// + /// Number of items per page + /// + public int PageSize { get; set; } = 10; + } +} diff --git a/SnakeAid.Service/Implements/FirstAidGuidelineService.cs b/SnakeAid.Service/Implements/FirstAidGuidelineService.cs index 69b3ebec..39f54de0 100644 --- a/SnakeAid.Service/Implements/FirstAidGuidelineService.cs +++ b/SnakeAid.Service/Implements/FirstAidGuidelineService.cs @@ -9,6 +9,8 @@ using SnakeAid.Repository.Data; using SnakeAid.Repository.Interfaces; using SnakeAid.Service.Interfaces; +using System.Linq.Expressions; +using System.Net; namespace SnakeAid.Service.Implements { @@ -36,10 +38,16 @@ public async Task> CreateFirstAidGuidelin return await _unitOfWork.ExecuteInTransactionAsync(async () => { + var jsonOptions = new System.Text.Json.JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = false + }; + var guideline = new FirstAidGuideline { Name = request.Name, - Content = request.Content, + Content = System.Text.Json.JsonSerializer.Serialize(request.Content, jsonOptions), Type = request.Type, Summary = request.Summary, CreatedAt = DateTime.UtcNow, @@ -56,7 +64,8 @@ public async Task> CreateFirstAidGuidelin catch (Exception ex) { _logger.LogError(ex, "Error creating first aid guideline"); - return ApiResponseBuilder.BuildFailureResponse(ex.Message); + return ApiResponseBuilder.CreateResponse( + default, false, ex.Message, HttpStatusCode.BadRequest); } } @@ -78,7 +87,8 @@ public async Task> GetFirstAidGuidelineBy catch (Exception ex) { _logger.LogError(ex, "Error getting first aid guideline by ID {Id}", id); - return ApiResponseBuilder.BuildFailureResponse(ex.Message); + return ApiResponseBuilder.CreateResponse( + default, false, ex.Message, HttpStatusCode.BadRequest); } } @@ -86,44 +96,44 @@ public async Task>> FilterFirst { try { - var query = _unitOfWork.GetRepository() - .GetQueryable(); + Expression>? predicate = null; - // Apply filters - if (!string.IsNullOrWhiteSpace(request.Name)) + // Build predicate + if (!string.IsNullOrWhiteSpace(request.Name) && request.Type.HasValue) { - query = query.Where(g => g.Name.Contains(request.Name)); + predicate = g => g.Name.Contains(request.Name) && g.Type == request.Type.Value; } - - if (request.Type.HasValue) + else if (!string.IsNullOrWhiteSpace(request.Name)) { - query = query.Where(g => g.Type == request.Type.Value); + predicate = g => g.Name.Contains(request.Name); + } + else if (request.Type.HasValue) + { + predicate = g => g.Type == request.Type.Value; } // Get paginated data var pagedData = await _unitOfWork.GetRepository() .GetPagingListAsync( - predicate: query.Expression, - orderBy: o => o.OrderByDescending(g => g.CreatedAt), - page: request.Page, - size: request.Size + predicate: predicate, + orderBy: o => o.OrderBy(g => g.Name), + page: request.PageNumber, + size: request.PageSize ); var response = new PagedData { Items = pagedData.Items.Adapt>(), - TotalCount = pagedData.TotalCount, - Page = pagedData.Page, - Size = pagedData.Size, - TotalPages = pagedData.TotalPages + Meta = pagedData.Meta }; return ApiResponseBuilder.BuildSuccessResponse(response); } catch (Exception ex) { - _logger.LogError(ex, "Error getting first aid guidelines"); - return ApiResponseBuilder.BuildFailureResponse>(ex.Message); + _logger.LogError(ex, "Error filtering first aid guidelines"); + return ApiResponseBuilder.CreateResponse>( + default, false, ex.Message, HttpStatusCode.BadRequest); } } @@ -152,9 +162,14 @@ public async Task> UpdateFirstAidGuidelin guideline.Name = request.Name; } - if (!string.IsNullOrWhiteSpace(request.Content)) + if (request.Content != null) { - guideline.Content = request.Content; + var jsonOptions = new System.Text.Json.JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = false + }; + guideline.Content = System.Text.Json.JsonSerializer.Serialize(request.Content, jsonOptions); } if (request.Type.HasValue) @@ -179,7 +194,8 @@ public async Task> UpdateFirstAidGuidelin catch (Exception ex) { _logger.LogError(ex, "Error updating first aid guideline with ID {Id}", id); - return ApiResponseBuilder.BuildFailureResponse(ex.Message); + return ApiResponseBuilder.CreateResponse( + default, false, ex.Message, HttpStatusCode.BadRequest); } } @@ -206,7 +222,8 @@ public async Task> DeleteFirstAidGuidelineAsync(int id) catch (Exception ex) { _logger.LogError(ex, "Error deleting first aid guideline with ID {Id}", id); - return ApiResponseBuilder.BuildFailureResponse(ex.Message); + return ApiResponseBuilder.CreateResponse( + false, false, ex.Message, HttpStatusCode.BadRequest); } } @@ -223,8 +240,54 @@ public async Task>> GetAllFirstAidGu catch (Exception ex) { _logger.LogError(ex, "Error getting all first aid guidelines"); - return ApiResponseBuilder.BuildFailureResponse>(ex.Message); + return ApiResponseBuilder.CreateResponse>( + default, false, ex.Message, HttpStatusCode.BadRequest); + } + } + + public async Task>> GetFirstAidGuidelinesBySnakeSpeciesIdAsync(int snakeSpeciesId) + { + try + { + // Check if snake species exists + var snakeSpecies = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + predicate: s => s.Id == snakeSpeciesId, + include: q => q.Include(s => s.SpeciesVenoms) + .ThenInclude(sv => sv.VenomType) + .ThenInclude(vt => vt.FirstAidGuideline) + ); + + if (snakeSpecies == null) + { + throw new NotFoundException($"Snake species with ID {snakeSpeciesId} not found."); + } + + // Get all first aid guidelines from venom types + var guidelines = snakeSpecies.SpeciesVenoms + .Where(sv => sv.VenomType != null && sv.VenomType.FirstAidGuideline != null) + .Select(sv => sv.VenomType.FirstAidGuideline) + .Distinct() + .ToList(); + + if (!guidelines.Any()) + { + return ApiResponseBuilder.BuildSuccessResponse( + new List(), + "No first aid guidelines found for this snake species." + ); + } + + var response = guidelines.Adapt>(); + return ApiResponseBuilder.BuildSuccessResponse(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting first aid guidelines for snake species ID {SnakeSpeciesId}", snakeSpeciesId); + return ApiResponseBuilder.CreateResponse>( + default, false, ex.Message, HttpStatusCode.BadRequest); } } } } + diff --git a/SnakeAid.Service/Implements/SymptomConfigService.cs b/SnakeAid.Service/Implements/SymptomConfigService.cs index 61773d8f..5a6d21d5 100644 --- a/SnakeAid.Service/Implements/SymptomConfigService.cs +++ b/SnakeAid.Service/Implements/SymptomConfigService.cs @@ -10,6 +10,8 @@ using SnakeAid.Repository.Interfaces; using SnakeAid.Service.Interfaces; using System.Text.Json; +using System.Linq.Expressions; +using System.Net; namespace SnakeAid.Service.Implements { @@ -88,7 +90,8 @@ public async Task> CreateSymptomConfigAsync(C catch (Exception ex) { _logger.LogError(ex, "Error creating symptom configuration"); - return ApiResponseBuilder.BuildFailureResponse(ex.Message); + return ApiResponseBuilder.CreateResponse( + default, false, ex.Message, HttpStatusCode.BadRequest); } } @@ -113,7 +116,8 @@ public async Task> GetSymptomConfigByIdAsync( catch (Exception ex) { _logger.LogError(ex, "Error getting symptom configuration by ID {Id}", id); - return ApiResponseBuilder.BuildFailureResponse(ex.Message); + return ApiResponseBuilder.CreateResponse( + default, false, ex.Message, HttpStatusCode.BadRequest); } } @@ -121,72 +125,41 @@ public async Task>> FilterSymptomCo { try { - var query = _unitOfWork.GetRepository() - .GetQueryable() - .Include(sc => sc.VenomType); - - // Apply filters - if (!string.IsNullOrWhiteSpace(request.AttributeKey)) - { - query = query.Where(sc => sc.AttributeKey.Contains(request.AttributeKey)); - } - - if (!string.IsNullOrWhiteSpace(request.Name)) - { - query = query.Where(sc => sc.Name.Contains(request.Name)); - } - - if (request.UIHint.HasValue) - { - query = query.Where(sc => sc.UIHint == request.UIHint.Value); - } - - if (request.Category.HasValue) - { - query = query.Where(sc => sc.Category == request.Category.Value); - } - - if (request.IsActive.HasValue) - { - query = query.Where(sc => sc.IsActive == request.IsActive.Value); - } - - if (request.VenomTypeId.HasValue) - { - query = query.Where(sc => sc.VenomTypeId == request.VenomTypeId.Value); - } - - // Get total count - var totalCount = await query.CountAsync(); - - // Apply ordering - query = query.OrderBy(sc => sc.DisplayOrder) - .ThenBy(sc => sc.AttributeKey) - .ThenBy(sc => sc.Name); - - // Apply pagination - var items = await query - .Skip((request.Page - 1) * request.Size) - .Take(request.Size) - .ToListAsync(); - - var responseItems = items.Select(MapToResponse).ToList(); + Expression>? predicate = null; + + // Build combined predicate + predicate = sc => + (string.IsNullOrWhiteSpace(request.AttributeKey) || sc.AttributeKey.Contains(request.AttributeKey)) && + (string.IsNullOrWhiteSpace(request.Name) || sc.Name.Contains(request.Name)) && + (!request.UIHint.HasValue || sc.UIHint == request.UIHint.Value) && + (!request.Category.HasValue || sc.Category == request.Category.Value) && + (!request.IsActive.HasValue || sc.IsActive == request.IsActive.Value) && + (!request.VenomTypeId.HasValue || sc.VenomTypeId == request.VenomTypeId.Value); + + // Get paginated data + var pagedData = await _unitOfWork.GetRepository() + .GetPagingListAsync( + predicate: predicate, + orderBy: o => o.OrderBy(sc => sc.DisplayOrder).ThenBy(sc => sc.AttributeKey).ThenBy(sc => sc.Name), + include: q => q.Include(sc => sc.VenomType), + page: request.PageNumber, + size: request.PageSize + ); - var pagedData = new PagedData + var responseItems = pagedData.Items.Select(MapToResponse).ToList(); + var response = new PagedData { Items = responseItems, - TotalCount = totalCount, - Page = request.Page, - Size = request.Size, - TotalPages = (int)Math.Ceiling(totalCount / (double)request.Size) + Meta = pagedData.Meta }; - return ApiResponseBuilder.BuildSuccessResponse(pagedData); + return ApiResponseBuilder.BuildSuccessResponse(response); } catch (Exception ex) { - _logger.LogError(ex, "Error getting symptom configurations"); - return ApiResponseBuilder.BuildFailureResponse>(ex.Message); + _logger.LogError(ex, "Error filtering symptom configurations"); + return ApiResponseBuilder.CreateResponse>( + default, false, ex.Message, HttpStatusCode.BadRequest); } } @@ -301,7 +274,8 @@ public async Task> UpdateSymptomConfigAsync(i catch (Exception ex) { _logger.LogError(ex, "Error updating symptom configuration with ID {Id}", id); - return ApiResponseBuilder.BuildFailureResponse(ex.Message); + return ApiResponseBuilder.CreateResponse( + default, false, ex.Message, HttpStatusCode.BadRequest); } } @@ -328,7 +302,8 @@ public async Task> DeleteSymptomConfigAsync(int id) catch (Exception ex) { _logger.LogError(ex, "Error deleting symptom configuration with ID {Id}", id); - return ApiResponseBuilder.BuildFailureResponse(ex.Message); + return ApiResponseBuilder.CreateResponse( + false, false, ex.Message, HttpStatusCode.BadRequest); } } @@ -355,7 +330,8 @@ public async Task>>> catch (Exception ex) { _logger.LogError(ex, "Error getting symptom configurations grouped by key"); - return ApiResponseBuilder.BuildFailureResponse>>(ex.Message); + return ApiResponseBuilder.CreateResponse>>( + default, false, ex.Message, HttpStatusCode.BadRequest); } } @@ -375,7 +351,8 @@ public async Task>> GetAllSymptomConfigA catch (Exception ex) { _logger.LogError(ex, "Error getting all symptom configurations"); - return ApiResponseBuilder.BuildFailureResponse>(ex.Message); + return ApiResponseBuilder.CreateResponse>( + default, false, ex.Message, HttpStatusCode.BadRequest); } } @@ -409,3 +386,4 @@ private SymptomConfigResponse MapToResponse(Core.Domains.SymptomConfig symptomCo } } } + diff --git a/SnakeAid.Service/Interfaces/IFirstAidGuidelineService.cs b/SnakeAid.Service/Interfaces/IFirstAidGuidelineService.cs index e19f4d84..ccaab362 100644 --- a/SnakeAid.Service/Interfaces/IFirstAidGuidelineService.cs +++ b/SnakeAid.Service/Interfaces/IFirstAidGuidelineService.cs @@ -35,5 +35,10 @@ public interface IFirstAidGuidelineService /// Get all first aid guidelines without pagination /// Task>> GetAllFirstAidGuidelineAsync(); + + /// + /// Get first aid guidelines by snake species ID + /// + Task>> GetFirstAidGuidelinesBySnakeSpeciesIdAsync(int snakeSpeciesId); } } From 8e1c6bdcd938afbda8e885c53cf4596557db3a1f Mon Sep 17 00:00:00 2001 From: NhanNPSE184696 Date: Sat, 31 Jan 2026 17:17:31 +0700 Subject: [PATCH 09/12] feat: update First Aid and Symptom Configuration endpoints for filtering and remove admin restrictions on certain actions --- SnakeAid.Api/Controllers/FirstAidGuidelineController.cs | 4 ++-- SnakeAid.Api/Controllers/SymptomConfigController.cs | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs b/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs index 118155ac..e7cdb96d 100644 --- a/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs +++ b/SnakeAid.Api/Controllers/FirstAidGuidelineController.cs @@ -58,7 +58,7 @@ public async Task GetFirstAidGuidelineById(int id) /// /// Get list of first aid guidelines with pagination and filters /// - [HttpGet] + [HttpGet("filter")] [SwaggerOperation(Summary = "Filter First Aid Guidelines", Description = "Get paginated list of first aid guidelines with optional filters")] [SwaggerResponse(200, "Success", typeof(ApiResponse>))] public async Task FilterFirstAidGuidelines([FromQuery] GetFirstAidGuidelineRequest request) @@ -70,7 +70,7 @@ public async Task FilterFirstAidGuidelines([FromQuery] GetFirstAi /// /// Get all first aid guidelines without pagination /// - [HttpGet("all")] + [HttpGet] [SwaggerOperation(Summary = "Get All First Aid Guidelines", Description = "Get all first aid guidelines without pagination")] [SwaggerResponse(200, "Success", typeof(ApiResponse>))] public async Task GetAllFirstAidGuideline() diff --git a/SnakeAid.Api/Controllers/SymptomConfigController.cs b/SnakeAid.Api/Controllers/SymptomConfigController.cs index 868eb913..7725f56c 100644 --- a/SnakeAid.Api/Controllers/SymptomConfigController.cs +++ b/SnakeAid.Api/Controllers/SymptomConfigController.cs @@ -30,7 +30,6 @@ public SymptomConfigController( /// Create a new symptom configuration /// [HttpPost] - [Authorize(Roles = "Admin")] [ValidateModel] [SwaggerOperation(Summary = "Create Symptom Configuration", Description = "Create a new symptom configuration (Admin only)")] [SwaggerResponse(200, "Created successfully", typeof(ApiResponse))] @@ -60,7 +59,7 @@ public async Task GetSymptomConfigById(int id) /// /// Get list of symptom configurations with pagination and filters /// - [HttpGet] + [HttpGet("filter")] [SwaggerOperation(Summary = "Filter Symptom Configurations", Description = "Get paginated list of symptom configurations with optional filters")] [SwaggerResponse(200, "Success", typeof(ApiResponse>))] public async Task FilterSymptomConfigs([FromQuery] GetSymptomConfigRequest request) @@ -72,7 +71,7 @@ public async Task FilterSymptomConfigs([FromQuery] GetSymptomConf /// /// Get all symptom configurations without pagination /// - [HttpGet("all")] + [HttpGet] [SwaggerOperation(Summary = "Get All Symptom Configurations", Description = "Get all symptom configurations without pagination")] [SwaggerResponse(200, "Success", typeof(ApiResponse>))] public async Task GetAllSymptomConfig() @@ -97,7 +96,6 @@ public async Task GetSymptomConfigsGrouped() /// Update an existing symptom configuration /// [HttpPut("{id}")] - [Authorize(Roles = "Admin")] [ValidateModel] [SwaggerOperation(Summary = "Update Symptom Configuration", Description = "Update an existing symptom configuration (Admin only)")] [SwaggerResponse(200, "Updated successfully", typeof(ApiResponse))] @@ -115,7 +113,6 @@ public async Task UpdateSymptomConfig(int id, [FromBody] UpdateSy /// Delete a symptom configuration /// [HttpDelete("{id}")] - [Authorize(Roles = "Admin")] [SwaggerOperation(Summary = "Delete Symptom Configuration", Description = "Delete a symptom configuration (Admin only)")] [SwaggerResponse(200, "Deleted successfully", typeof(ApiResponse))] [SwaggerResponse(401, "Unauthorized")] From da6cc5e9c3aa30109c2ad7886a399caabae135fa Mon Sep 17 00:00:00 2001 From: NhanNPSE184696 Date: Sat, 31 Jan 2026 18:46:43 +0700 Subject: [PATCH 10/12] feat: add mappings for FirstAidGuideline and SymptomConfig, and implement pagination request model --- SnakeAid.Core/Mappings/DomainMapper.cs | 20 ++++++ .../{Requests => Meta}/PaginationRequest.cs | 2 +- .../Implements/SymptomConfigService.cs | 66 ++++++------------- 3 files changed, 40 insertions(+), 48 deletions(-) rename SnakeAid.Core/{Requests => Meta}/PaginationRequest.cs (90%) diff --git a/SnakeAid.Core/Mappings/DomainMapper.cs b/SnakeAid.Core/Mappings/DomainMapper.cs index 9deb59dd..1f3dab90 100644 --- a/SnakeAid.Core/Mappings/DomainMapper.cs +++ b/SnakeAid.Core/Mappings/DomainMapper.cs @@ -6,6 +6,8 @@ using SnakeAid.Core.Domains; using SnakeAid.Core.Responses.SnakebiteIncident; using SnakeAid.Core.Responses.RescueRequestSession; +using SnakeAid.Core.Responses.FirstAidGuideline; +using SnakeAid.Core.Responses.SymptomConfig; namespace SnakeAid.Core.Mappings { @@ -22,6 +24,24 @@ public void Register(TypeAdapterConfig config) // SnakebiteIncident mappings config.NewConfig(); + + // FirstAidGuideline mappings + config.NewConfig(); + + // SymptomConfig mappings + config.NewConfig() + .Map(dest => dest.UIHintDisplay, src => src.UIHint.ToString()) + .Map(dest => dest.CategoryDisplay, src => src.Category.ToString()) + .Map(dest => dest.VenomType, src => src.VenomType != null + ? new VenomTypeInfo + { + Id = src.VenomType.Id, + Name = src.VenomType.Name + } + : null); + + // VenomType to VenomTypeInfo mapping + config.NewConfig(); } } } \ No newline at end of file diff --git a/SnakeAid.Core/Requests/PaginationRequest.cs b/SnakeAid.Core/Meta/PaginationRequest.cs similarity index 90% rename from SnakeAid.Core/Requests/PaginationRequest.cs rename to SnakeAid.Core/Meta/PaginationRequest.cs index 636a652b..3060fcae 100644 --- a/SnakeAid.Core/Requests/PaginationRequest.cs +++ b/SnakeAid.Core/Meta/PaginationRequest.cs @@ -1,4 +1,4 @@ -namespace SnakeAid.Core.Requests +namespace SnakeAid.Core.Meta { public class PaginationRequest { diff --git a/SnakeAid.Service/Implements/SymptomConfigService.cs b/SnakeAid.Service/Implements/SymptomConfigService.cs index 5a6d21d5..f9a24911 100644 --- a/SnakeAid.Service/Implements/SymptomConfigService.cs +++ b/SnakeAid.Service/Implements/SymptomConfigService.cs @@ -51,7 +51,7 @@ public async Task> CreateSymptomConfigAsync(C } } - var symptomConfig = new Core.Domains.SymptomConfig + var symptomConfig = new SymptomConfig { AttributeKey = request.AttributeKey, AttributeLabel = request.AttributeLabel, @@ -69,13 +69,13 @@ public async Task> CreateSymptomConfigAsync(C UpdatedAt = DateTime.UtcNow }; - await _unitOfWork.GetRepository().InsertAsync(symptomConfig); + await _unitOfWork.GetRepository().InsertAsync(symptomConfig); await _unitOfWork.CommitAsync(); // Load VenomType if exists if (symptomConfig.VenomTypeId.HasValue) { - symptomConfig = await _unitOfWork.GetRepository() + symptomConfig = await _unitOfWork.GetRepository() .FirstOrDefaultAsync( predicate: sc => sc.Id == symptomConfig.Id, include: q => q.Include(sc => sc.VenomType), @@ -83,7 +83,7 @@ public async Task> CreateSymptomConfigAsync(C ); } - var response = MapToResponse(symptomConfig); + var response = symptomConfig.Adapt(); return ApiResponseBuilder.BuildSuccessResponse(response, "Symptom configuration created successfully!"); }); } @@ -99,7 +99,7 @@ public async Task> GetSymptomConfigByIdAsync( { try { - var symptomConfig = await _unitOfWork.GetRepository() + var symptomConfig = await _unitOfWork.GetRepository() .FirstOrDefaultAsync( predicate: sc => sc.Id == id, include: q => q.Include(sc => sc.VenomType) @@ -110,7 +110,7 @@ public async Task> GetSymptomConfigByIdAsync( throw new NotFoundException($"Symptom configuration with ID {id} not found."); } - var response = MapToResponse(symptomConfig); + var response = symptomConfig.Adapt(); return ApiResponseBuilder.BuildSuccessResponse(response); } catch (Exception ex) @@ -125,7 +125,7 @@ public async Task>> FilterSymptomCo { try { - Expression>? predicate = null; + Expression>? predicate = null; // Build combined predicate predicate = sc => @@ -137,7 +137,7 @@ public async Task>> FilterSymptomCo (!request.VenomTypeId.HasValue || sc.VenomTypeId == request.VenomTypeId.Value); // Get paginated data - var pagedData = await _unitOfWork.GetRepository() + var pagedData = await _unitOfWork.GetRepository() .GetPagingListAsync( predicate: predicate, orderBy: o => o.OrderBy(sc => sc.DisplayOrder).ThenBy(sc => sc.AttributeKey).ThenBy(sc => sc.Name), @@ -146,7 +146,7 @@ public async Task>> FilterSymptomCo size: request.PageSize ); - var responseItems = pagedData.Items.Select(MapToResponse).ToList(); + var responseItems = pagedData.Items.Adapt>(); var response = new PagedData { Items = responseItems, @@ -174,7 +174,7 @@ public async Task> UpdateSymptomConfigAsync(i return await _unitOfWork.ExecuteInTransactionAsync(async () => { - var symptomConfig = await _unitOfWork.GetRepository() + var symptomConfig = await _unitOfWork.GetRepository() .FirstOrDefaultAsync( predicate: sc => sc.Id == id, include: q => q.Include(sc => sc.VenomType), @@ -253,13 +253,13 @@ public async Task> UpdateSymptomConfigAsync(i symptomConfig.UpdatedAt = DateTime.UtcNow; - _unitOfWork.GetRepository().Update(symptomConfig); + _unitOfWork.GetRepository().Update(symptomConfig); await _unitOfWork.CommitAsync(); // Reload to get updated VenomType if (symptomConfig.VenomTypeId.HasValue) { - symptomConfig = await _unitOfWork.GetRepository() + symptomConfig = await _unitOfWork.GetRepository() .FirstOrDefaultAsync( predicate: sc => sc.Id == id, include: q => q.Include(sc => sc.VenomType), @@ -267,7 +267,7 @@ public async Task> UpdateSymptomConfigAsync(i ); } - var response = MapToResponse(symptomConfig); + var response = symptomConfig.Adapt(); return ApiResponseBuilder.BuildSuccessResponse(response, "Symptom configuration updated successfully!"); }); } @@ -285,7 +285,7 @@ public async Task> DeleteSymptomConfigAsync(int id) { return await _unitOfWork.ExecuteInTransactionAsync(async () => { - var symptomConfig = await _unitOfWork.GetRepository() + var symptomConfig = await _unitOfWork.GetRepository() .GetByIdAsync(id); if (symptomConfig == null) @@ -293,7 +293,7 @@ public async Task> DeleteSymptomConfigAsync(int id) throw new NotFoundException($"Symptom configuration with ID {id} not found."); } - _unitOfWork.GetRepository().Delete(symptomConfig); + _unitOfWork.GetRepository().Delete(symptomConfig); await _unitOfWork.CommitAsync(); return ApiResponseBuilder.BuildSuccessResponse(true, "Symptom configuration deleted successfully!"); @@ -311,7 +311,7 @@ public async Task>>> { try { - var symptomConfigs = await _unitOfWork.GetRepository() + var symptomConfigs = await _unitOfWork.GetRepository() .GetListAsync( predicate: sc => sc.IsActive, orderBy: o => o.OrderBy(sc => sc.DisplayOrder).ThenBy(sc => sc.Name), @@ -322,7 +322,7 @@ public async Task>>> .GroupBy(sc => sc.AttributeKey) .ToDictionary( g => g.Key, - g => g.Select(MapToResponse).ToList() + g => g.ToList().Adapt>() ); return ApiResponseBuilder.BuildSuccessResponse(grouped); @@ -339,13 +339,13 @@ public async Task>> GetAllSymptomConfigA { try { - var symptomConfigs = await _unitOfWork.GetRepository() + var symptomConfigs = await _unitOfWork.GetRepository() .GetListAsync( orderBy: o => o.OrderBy(sc => sc.DisplayOrder).ThenBy(sc => sc.Name), include: q => q.Include(sc => sc.VenomType) ); - var response = symptomConfigs.Select(MapToResponse).ToList(); + var response = symptomConfigs.Adapt>(); return ApiResponseBuilder.BuildSuccessResponse(response); } catch (Exception ex) @@ -356,34 +356,6 @@ public async Task>> GetAllSymptomConfigA } } - private SymptomConfigResponse MapToResponse(Core.Domains.SymptomConfig symptomConfig) - { - return new SymptomConfigResponse - { - Id = symptomConfig.Id, - AttributeKey = symptomConfig.AttributeKey, - AttributeLabel = symptomConfig.AttributeLabel, - UIHint = symptomConfig.UIHint, - UIHintDisplay = symptomConfig.UIHint.ToString(), - DisplayOrder = symptomConfig.DisplayOrder, - Name = symptomConfig.Name, - Description = symptomConfig.Description, - IsActive = symptomConfig.IsActive, - Category = symptomConfig.Category, - CategoryDisplay = symptomConfig.Category.ToString(), - TimeScoreList = symptomConfig.TimeScoreList, - VenomTypeId = symptomConfig.VenomTypeId, - VenomType = symptomConfig.VenomType != null - ? new VenomTypeInfo - { - Id = symptomConfig.VenomType.Id, - Name = symptomConfig.VenomType.Name - } - : null, - CreatedAt = symptomConfig.CreatedAt, - UpdatedAt = symptomConfig.UpdatedAt - }; - } } } From e04a0c8ca19692b153a678bcc6fd6e90ed9fbc34 Mon Sep 17 00:00:00 2001 From: NhanNPSE184696 Date: Sat, 31 Jan 2026 20:13:48 +0700 Subject: [PATCH 11/12] feat: implement Update Symptom Report functionality with severity calculation --- .../SnakebiteIncidentController.cs | 15 +++ SnakeAid.Core/Mappings/DomainMapper.cs | 1 + .../Requests/UpdateSymptomReportRequest.cs | 13 ++ .../UpdateSymptomReportResponse.cs | 21 ++++ SnakeAid.Repository/Implements/UnitOfWork.cs | 2 +- .../Implements/SnakebiteIncidentService.cs | 119 +++++++++++++++++- .../Interfaces/ISnakebiteIncidentService.cs | 4 + 7 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 SnakeAid.Core/Requests/UpdateSymptomReportRequest.cs create mode 100644 SnakeAid.Core/Responses/SnakebiteIncident/UpdateSymptomReportResponse.cs diff --git a/SnakeAid.Api/Controllers/SnakebiteIncidentController.cs b/SnakeAid.Api/Controllers/SnakebiteIncidentController.cs index 425680ed..44cc6ee8 100644 --- a/SnakeAid.Api/Controllers/SnakebiteIncidentController.cs +++ b/SnakeAid.Api/Controllers/SnakebiteIncidentController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using SnakeAid.Core.Meta; +using SnakeAid.Core.Requests; using SnakeAid.Core.Requests.RescueRequestSession; using SnakeAid.Core.Requests.SnakebiteIncident; using SnakeAid.Core.Responses.Auth; @@ -61,5 +62,19 @@ public async Task RaiseSessionRange(Guid incidentId) var result = await _incidentService.RaiseSessionRangeAsync(request); return StatusCode(result.StatusCode, result); } + + /// + /// Update symptom report with time-based severity calculation + /// + [HttpPut("{incidentId}/symptoms-tracking")] + [SwaggerOperation(Summary = "Update Symptom Report", Description = "Update incident symptoms and calculate severity based on elapsed time")] + [SwaggerResponse(200, "Symptom report updated successfully", typeof(ApiResponse))] + [SwaggerResponse(404, "Incident not found")] + [SwaggerResponse(422, "Validation error")] + public async Task UpdateSymptomReport(Guid incidentId, [FromBody] UpdateSymptomReportRequest request) + { + var result = await _incidentService.UpdateSymptomReportAsync(incidentId, request); + return StatusCode(result.StatusCode, result); + } } } diff --git a/SnakeAid.Core/Mappings/DomainMapper.cs b/SnakeAid.Core/Mappings/DomainMapper.cs index 1f3dab90..578aab6e 100644 --- a/SnakeAid.Core/Mappings/DomainMapper.cs +++ b/SnakeAid.Core/Mappings/DomainMapper.cs @@ -24,6 +24,7 @@ public void Register(TypeAdapterConfig config) // SnakebiteIncident mappings config.NewConfig(); + config.NewConfig(); // FirstAidGuideline mappings config.NewConfig(); diff --git a/SnakeAid.Core/Requests/UpdateSymptomReportRequest.cs b/SnakeAid.Core/Requests/UpdateSymptomReportRequest.cs new file mode 100644 index 00000000..60dc62b1 --- /dev/null +++ b/SnakeAid.Core/Requests/UpdateSymptomReportRequest.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SnakeAid.Core.Requests +{ + public class UpdateSymptomReportRequest + { + public List SymptomIdList { get; set; } + } +} diff --git a/SnakeAid.Core/Responses/SnakebiteIncident/UpdateSymptomReportResponse.cs b/SnakeAid.Core/Responses/SnakebiteIncident/UpdateSymptomReportResponse.cs new file mode 100644 index 00000000..4239b289 --- /dev/null +++ b/SnakeAid.Core/Responses/SnakebiteIncident/UpdateSymptomReportResponse.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SnakeAid.Core.Responses.SnakebiteIncident +{ + public class UpdateSymptomReportResponse + { + public Guid Id { get; set; } + + public Guid UserId { get; set; } + + [Column(TypeName = "jsonb")] + public string? SymptomsReport { get; set; } + + public int? SeverityLevel { get; set; } + } +} diff --git a/SnakeAid.Repository/Implements/UnitOfWork.cs b/SnakeAid.Repository/Implements/UnitOfWork.cs index 8e83b742..53464f49 100644 --- a/SnakeAid.Repository/Implements/UnitOfWork.cs +++ b/SnakeAid.Repository/Implements/UnitOfWork.cs @@ -97,7 +97,7 @@ private void TrackChanges() var validationResults = new List(); var entries = Context.ChangeTracker.Entries() - .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified); + .Where(e => e.State == EntityState.Added ); foreach (var entry in entries) { diff --git a/SnakeAid.Service/Implements/SnakebiteIncidentService.cs b/SnakeAid.Service/Implements/SnakebiteIncidentService.cs index 163d3eca..e06d30eb 100644 --- a/SnakeAid.Service/Implements/SnakebiteIncidentService.cs +++ b/SnakeAid.Service/Implements/SnakebiteIncidentService.cs @@ -1,10 +1,14 @@ -using Microsoft.EntityFrameworkCore; +using Mapster; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using SnakeAid.Core.Domains; using SnakeAid.Core.Exceptions; using SnakeAid.Core.Meta; +using SnakeAid.Core.Requests; +using SnakeAid.Core.Requests.RescueRequestSession; using SnakeAid.Core.Requests.SnakebiteIncident; +using SnakeAid.Core.Responses.RescueRequestSession; using SnakeAid.Core.Responses.SnakebiteIncident; using SnakeAid.Repository.Data; using SnakeAid.Repository.Interfaces; @@ -14,9 +18,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using Mapster; -using SnakeAid.Core.Responses.RescueRequestSession; -using SnakeAid.Core.Requests.RescueRequestSession; namespace SnakeAid.Service.Implements { @@ -214,5 +215,115 @@ await _unitOfWork.Context.Entry(existingIncident) throw; } } + + public async Task> UpdateSymptomReportAsync(Guid incidentId, UpdateSymptomReportRequest request) + { + try + { + if (request == null) + { + throw new ArgumentNullException(nameof(request), "Request data cannot be null."); + } + return await _unitOfWork.ExecuteInTransactionAsync(async () => + { + var existingIncident = await _unitOfWork.GetRepository().FirstOrDefaultAsync( + predicate: s => s.Id == incidentId + ); + if (existingIncident == null) + { + throw new NotFoundException("Snakebite incident not found."); + } + + // Calculate elapsed time from incident occurrence + var currentTime = DateTime.UtcNow; + var elapsedMinutes = existingIncident.IncidentOccurredAt.HasValue + ? (int)(currentTime - existingIncident.IncidentOccurredAt.Value).TotalMinutes + : 0; + + // Collect symptom descriptions and calculate severity + var symptomNames = new List(); + var coreSymptomScores = new List(); + var modifierSymptomScores = new List(); + + foreach (var symptomId in request.SymptomIdList) + { + var symptom = await _unitOfWork.GetRepository().FirstOrDefaultAsync( + predicate: s => s.Id == symptomId + ); + + if (symptom != null) + { + // Add symptom description + if (!string.IsNullOrEmpty(symptom.Name)) + { + symptomNames.Add(symptom.Name); + } + + // Calculate score based on TimeScoreList + var score = CalculateScoreByElapsedTime(symptom.TimeScoreList, elapsedMinutes); + + // Categorize by symptom category + if (symptom.Category == SymptomCategory.Core) + { + coreSymptomScores.Add(score); + } + else if (symptom.Category == SymptomCategory.Modifier) + { + modifierSymptomScores.Add(score); + } + } + } + + // Calculate severity level + // Core: take maximum score + var severityLevel = 0; + if (coreSymptomScores.Any()) + { + severityLevel = coreSymptomScores.Max(); + } + + // Modifier: sum all scores + if (modifierSymptomScores.Any()) + { + severityLevel += modifierSymptomScores.Sum(); + } + + // Update symptom report and severity level + existingIncident.SymptomsReport = string.Join(", ", symptomNames); + existingIncident.SeverityLevel = severityLevel; + _unitOfWork.GetRepository().Update(existingIncident); + await _unitOfWork.CommitAsync(); + + var responseData = existingIncident.Adapt(); + return ApiResponseBuilder.BuildSuccessResponse(responseData, "Symptom report updated successfully!"); + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating symptom report for incident: {Message}", ex.Message); + throw; + } + } + + /// + /// Calculate score based on elapsed time and TimeScoreList ranges + /// + /// List of time score ranges + /// Minutes elapsed since incident occurred + /// Score matching the elapsed time range, or 0 if no match found + private int CalculateScoreByElapsedTime(List timeScoreList, int elapsedMinutes) + { + if (timeScoreList == null || !timeScoreList.Any()) + { + return 0; + } + + // Find the TimeScorePoint where elapsedMinutes falls within MinMinutes and MaxMinutes + var matchingScore = timeScoreList.FirstOrDefault(ts => + elapsedMinutes >= ts.MinMinutes && elapsedMinutes <= ts.MaxMinutes + ); + + return matchingScore?.Score ?? 0; + } } } diff --git a/SnakeAid.Service/Interfaces/ISnakebiteIncidentService.cs b/SnakeAid.Service/Interfaces/ISnakebiteIncidentService.cs index 2ba9a0ed..8c4edf31 100644 --- a/SnakeAid.Service/Interfaces/ISnakebiteIncidentService.cs +++ b/SnakeAid.Service/Interfaces/ISnakebiteIncidentService.cs @@ -1,4 +1,5 @@ using SnakeAid.Core.Meta; +using SnakeAid.Core.Requests; using SnakeAid.Core.Requests.RescueRequestSession; using SnakeAid.Core.Requests.SnakebiteIncident; using SnakeAid.Core.Responses.Auth; @@ -17,5 +18,8 @@ public interface ISnakebiteIncidentService Task> RaiseSessionRangeAsync(RaiseSessionRangeRequest request); + Task> UpdateSymptomReportAsync(Guid incidentId, UpdateSymptomReportRequest request); + + } } From 43bad1b71e94325af266905db20bd8009157a3dd Mon Sep 17 00:00:00 2001 From: NhanNPSE184696 Date: Sat, 31 Jan 2026 20:24:53 +0700 Subject: [PATCH 12/12] feat: enhance symptom report by using descriptions and include modified entries in transaction tracking --- SnakeAid.Repository/Implements/UnitOfWork.cs | 2 +- .../Implements/SnakebiteIncidentService.cs | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/SnakeAid.Repository/Implements/UnitOfWork.cs b/SnakeAid.Repository/Implements/UnitOfWork.cs index 53464f49..8e83b742 100644 --- a/SnakeAid.Repository/Implements/UnitOfWork.cs +++ b/SnakeAid.Repository/Implements/UnitOfWork.cs @@ -97,7 +97,7 @@ private void TrackChanges() var validationResults = new List(); var entries = Context.ChangeTracker.Entries() - .Where(e => e.State == EntityState.Added ); + .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified); foreach (var entry in entries) { diff --git a/SnakeAid.Service/Implements/SnakebiteIncidentService.cs b/SnakeAid.Service/Implements/SnakebiteIncidentService.cs index e06d30eb..55af1b29 100644 --- a/SnakeAid.Service/Implements/SnakebiteIncidentService.cs +++ b/SnakeAid.Service/Implements/SnakebiteIncidentService.cs @@ -241,7 +241,7 @@ public async Task> UpdateSymptomReportA : 0; // Collect symptom descriptions and calculate severity - var symptomNames = new List(); + var symptomDescriptions = new List(); var coreSymptomScores = new List(); var modifierSymptomScores = new List(); @@ -254,9 +254,9 @@ public async Task> UpdateSymptomReportA if (symptom != null) { // Add symptom description - if (!string.IsNullOrEmpty(symptom.Name)) + if (!string.IsNullOrEmpty(symptom.Description)) { - symptomNames.Add(symptom.Name); + symptomDescriptions.Add(symptom.Description); } // Calculate score based on TimeScoreList @@ -289,7 +289,12 @@ public async Task> UpdateSymptomReportA } // Update symptom report and severity level - existingIncident.SymptomsReport = string.Join(", ", symptomNames); + var jsonOptions = new System.Text.Json.JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = false + }; + existingIncident.SymptomsReport = System.Text.Json.JsonSerializer.Serialize(symptomDescriptions, jsonOptions); existingIncident.SeverityLevel = severityLevel; _unitOfWork.GetRepository().Update(existingIncident); await _unitOfWork.CommitAsync();