diff --git a/SnakeAid.Api/Pages/Admin/SnakeLibs/Index.cshtml b/SnakeAid.Api/Pages/Admin/SnakeLibs/Index.cshtml new file mode 100644 index 00000000..4c3a9d14 --- /dev/null +++ b/SnakeAid.Api/Pages/Admin/SnakeLibs/Index.cshtml @@ -0,0 +1,64 @@ +@page "/admin/snakelibs" +@model SnakeAid.Api.Pages.Admin.SnakeLibs.IndexModel +@{ + ViewData["Title"] = "Snake species library"; +} + +

Snake Library - Loài rắn (UI) ✅

+ + + + + + + + + + + + + + + + @foreach (var s in Model.Species) + { + + + + + + + + + + + + + + } + +
IdScientific nameCommon nameVenomousPrimary venomAlt namesMediaTariffs
@s.Id@s.ScientificName@(string.IsNullOrWhiteSpace(s.CommonName) ? "-" : s.CommonName)@(s.IsVenomous ? "Yes" : "No")@(s.PrimaryVenomType?.ToString() ?? "-")@(s.AlternativeNames?.Count ?? 0)@(s.LibraryMedias?.Count ?? 0)@(s.SnakeCatchingTariffs?.Count ?? 0)
+ Details: +
+ Alternative names: + @if (s.AlternativeNames?.Any() == true) { +
    + @foreach (var n in s.AlternativeNames) + { +
  • @n.Name
  • + } +
+ } else { } +
+
+ Venom types: + @if (s.SpeciesVenoms?.Any() == true) { +
    + @foreach (var v in s.SpeciesVenoms) + { +
  • @(v.VenomType?.Name ?? "-")
  • + } +
+ } else { } +
+
\ No newline at end of file diff --git a/SnakeAid.Api/Pages/Admin/SnakeLibs/Index.cshtml.cs b/SnakeAid.Api/Pages/Admin/SnakeLibs/Index.cshtml.cs new file mode 100644 index 00000000..d700a1f8 --- /dev/null +++ b/SnakeAid.Api/Pages/Admin/SnakeLibs/Index.cshtml.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using SnakeAid.Core.Domains; +using SnakeAid.Repository.Data; +using SnakeAid.Repository.Interfaces; + +namespace SnakeAid.Api.Pages.Admin.SnakeLibs +{ + public class IndexModel : PageModel + { + private readonly IUnitOfWork _unitOfWork; + + public IList Species { get; set; } = new List(); + + public IndexModel(IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task OnGetAsync() + { + // Eager-load alternative names, media, venoms (+ venom type), and catching tariffs + Func, IQueryable> include = q => q + .Include(s => s.AlternativeNames) + .Include(s => s.LibraryMedias) + .Include(s => s.SpeciesVenoms).ThenInclude(sv => sv.VenomType) + .Include(s => s.SnakeCatchingTariffs); + + Species = (await _unitOfWork.GetRepository().GetListAsync(include: include, asNoTracking: true)).ToList(); + } + } +} \ No newline at end of file diff --git a/SnakeAid.Api/Pages/Admin/SnakebiteIncidents/Index.cshtml b/SnakeAid.Api/Pages/Admin/SnakebiteIncidents/Index.cshtml new file mode 100644 index 00000000..ce9d12ca --- /dev/null +++ b/SnakeAid.Api/Pages/Admin/SnakebiteIncidents/Index.cshtml @@ -0,0 +1,50 @@ +@page "/admin/incidents" +@model SnakeAid.Api.Pages.Admin.SnakebiteIncidents.IndexModel +@{ + ViewData["Title"] = "Snakebite incidents"; +} + +

Snakebite Incidents (UI) ✅

+ + + + + + + + + + + + + + @foreach (var incident in Model.Incidents) + { + + + + + + + + + + + + } + +
IdUserAssigned RescuerStatusSessionsMedia
@incident.Id@(incident.User?.Account?.FullName ?? "-")
@(incident.User?.Account?.Email ?? "")
@(incident.AssignedRescuer?.Account?.FullName ?? "-")@incident.Status@(incident.Sessions?.Count ?? 0)@(incident.Media?.Count ?? 0)
+ Sessions details: +
    + @foreach (var s in incident.Sessions ?? Enumerable.Empty()) + { +
  • Session #@s.SessionNumber - Requests: @s.Requests?.Count
  • +
      + @foreach (var r in s.Requests ?? Enumerable.Empty()) + { +
    • @(r.Rescuer?.Account?.FullName ?? "Unknown") - @r.Status
    • + } +
    + } +
+
\ No newline at end of file diff --git a/SnakeAid.Api/Pages/Admin/SnakebiteIncidents/Index.cshtml.cs b/SnakeAid.Api/Pages/Admin/SnakebiteIncidents/Index.cshtml.cs new file mode 100644 index 00000000..97857f79 --- /dev/null +++ b/SnakeAid.Api/Pages/Admin/SnakebiteIncidents/Index.cshtml.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using SnakeAid.Core.Domains; +using SnakeAid.Repository.Data; +using SnakeAid.Repository.Interfaces; + +namespace SnakeAid.Api.Pages.Admin.SnakebiteIncidents +{ + public class IndexModel : PageModel + { + private readonly IUnitOfWork _unitOfWork; + + public IList Incidents { get; set; } = new List(); + + public IndexModel(IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task OnGetAsync() + { + // Eager-load common related entities (user -> account, assigned rescuer, sessions -> requests -> rescuer -> account, media) + Func, IQueryable> include = query => query + .Include(i => i.User).ThenInclude(u => u.Account) + .Include(i => i.AssignedRescuer).ThenInclude(r => r.Account) + .Include(i => i.Sessions).ThenInclude(s => s.Requests).ThenInclude(r => r.Rescuer).ThenInclude(rp => rp.Account) + .Include(i => i.Media); + + Incidents = (await _unitOfWork.GetRepository().GetListAsync(include: include, asNoTracking: true)).ToList(); + } + } +} \ No newline at end of file diff --git a/SnakeAid.Api/Program.cs b/SnakeAid.Api/Program.cs index 4565c0fc..914685d8 100644 --- a/SnakeAid.Api/Program.cs +++ b/SnakeAid.Api/Program.cs @@ -149,6 +149,9 @@ public static async Task Main(string[] args) }); builder.Services.AddControllers(); + + // Add Razor Pages for lightweight UI admin pages + builder.Services.AddRazorPages(); // Add SignalR builder.Services.AddSignalR(options => @@ -304,6 +307,9 @@ public static async Task Main(string[] args) // Map SignalR Hub with specific CORS policy app.MapHub("/chat-hub").RequireCors("SignalRCorsPolicy"); + // Map Razor pages + app.MapRazorPages(); + app.MapControllers(); // Health checks endpoint diff --git a/SnakeAid.Core/Requests/SymptomConfig/CreateSymptomConfigRequest.cs b/SnakeAid.Core/Requests/SymptomConfig/CreateSymptomConfigRequest.cs index ecfe7506..6650c266 100644 --- a/SnakeAid.Core/Requests/SymptomConfig/CreateSymptomConfigRequest.cs +++ b/SnakeAid.Core/Requests/SymptomConfig/CreateSymptomConfigRequest.cs @@ -6,7 +6,14 @@ namespace SnakeAid.Core.Requests.SymptomConfig public class CreateSymptomConfigRequest { /// - /// Khóa thuộc tính (VD: "BITE_LOCATION", "AGE_GROUP", "SYMPTOMS") + /// Nhóm thuộc tính (VD: "GENERAL", "LOCAL", "CRITICAL") + /// + [Required] + [MaxLength(100, ErrorMessage = "GroupName must not exceed 100 characters")] + public string GroupName { get; set; } + + /// + /// Khóa thuộc tính (VD: "BITE_LOCATION", "CORE_SIGNS") /// [Required(ErrorMessage = "AttributeKey is required")] [MaxLength(100, ErrorMessage = "AttributeKey must not exceed 100 characters")] @@ -39,6 +46,17 @@ public class CreateSymptomConfigRequest [MaxLength(1000, ErrorMessage = "Description must not exceed 1000 characters")] public string? Description { get; set; } + /// + /// Đánh dấu triệu chứng nguy kịch (hiển thị popup cảnh báo) + /// + public bool IsCritical { get; set; } = false; + + /// + /// Nội dung cảnh báo khi triệu chứng nguy kịch được chọn + /// + [MaxLength(1000, ErrorMessage = "AlertMessage must not exceed 1000 characters")] + public string? AlertMessage { get; set; } + /// /// Trạng thái kích hoạt /// diff --git a/SnakeAid.Core/Requests/SymptomConfig/GetSymptomConfigRequest.cs b/SnakeAid.Core/Requests/SymptomConfig/GetSymptomConfigRequest.cs index b24ac219..5d52a0f8 100644 --- a/SnakeAid.Core/Requests/SymptomConfig/GetSymptomConfigRequest.cs +++ b/SnakeAid.Core/Requests/SymptomConfig/GetSymptomConfigRequest.cs @@ -5,6 +5,11 @@ namespace SnakeAid.Core.Requests.SymptomConfig { public class GetSymptomConfigRequest : PaginationRequest { + /// + /// Tìm kiếm theo GroupName + /// + public string? GroupName { get; set; } + /// /// Tìm kiếm theo AttributeKey /// @@ -25,6 +30,11 @@ public class GetSymptomConfigRequest : PaginationRequest /// public bool? IsActive { get; set; } + /// + /// Lọc theo triệu chứng nguy kịch + /// + public bool? IsCritical { get; set; } + /// /// Lọc theo VenomTypeId /// diff --git a/SnakeAid.Core/Requests/SymptomConfig/UpdateSymptomConfigRequest.cs b/SnakeAid.Core/Requests/SymptomConfig/UpdateSymptomConfigRequest.cs index b5611bb6..3948919d 100644 --- a/SnakeAid.Core/Requests/SymptomConfig/UpdateSymptomConfigRequest.cs +++ b/SnakeAid.Core/Requests/SymptomConfig/UpdateSymptomConfigRequest.cs @@ -5,6 +5,13 @@ namespace SnakeAid.Core.Requests.SymptomConfig { public class UpdateSymptomConfigRequest { + /// + /// Nhóm thuộc tính (VD: "GENERAL", "LOCAL", "CRITICAL") + /// + [Required] + [MaxLength(100, ErrorMessage = "GroupName must not exceed 100 characters")] + public string GroupName { get; set; } + /// /// Khóa thuộc tính (VD: "BITE_LOCATION", "AGE_GROUP", "SYMPTOMS") /// @@ -35,6 +42,17 @@ public class UpdateSymptomConfigRequest [MaxLength(1000, ErrorMessage = "Description must not exceed 1000 characters")] public string? Description { get; set; } + /// + /// Đánh dấu triệu chứng nguy kịch (hiển thị popup cảnh báo) + /// + public bool? IsCritical { get; set; } + + /// + /// Nội dung cảnh báo khi triệu chứng nguy kịch được chọn + /// + [MaxLength(1000, ErrorMessage = "AlertMessage must not exceed 1000 characters")] + public string? AlertMessage { get; set; } + /// /// Trạng thái kích hoạt /// diff --git a/SnakeAid.Core/Responses/SymptomConfig/SymptomConfigResponse.cs b/SnakeAid.Core/Responses/SymptomConfig/SymptomConfigResponse.cs index 4996559c..311b69ef 100644 --- a/SnakeAid.Core/Responses/SymptomConfig/SymptomConfigResponse.cs +++ b/SnakeAid.Core/Responses/SymptomConfig/SymptomConfigResponse.cs @@ -5,12 +5,15 @@ namespace SnakeAid.Core.Responses.SymptomConfig public class SymptomConfigResponse { public int Id { get; set; } + public string GroupName { get; set; } public string AttributeKey { get; set; } public string AttributeLabel { 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 IsCritical { get; set; } + public string? AlertMessage { get; set; } public bool IsActive { get; set; } public SymptomCategory Category { get; set; } public string CategoryDisplay { get; set; } // Friendly display name diff --git a/SnakeAid.Service/Implements/SnakebiteIncidentService.cs b/SnakeAid.Service/Implements/SnakebiteIncidentService.cs index e513f2ad..1eb47d57 100644 --- a/SnakeAid.Service/Implements/SnakebiteIncidentService.cs +++ b/SnakeAid.Service/Implements/SnakebiteIncidentService.cs @@ -320,6 +320,9 @@ public async Task> UpdateSymptomReportA severityLevel += modifierSymptomScores.Sum(); } + if (severityLevel > 100) + severityLevel = 100; + // Update symptom report and severity level var jsonOptions = new System.Text.Json.JsonSerializerOptions { diff --git a/SnakeAid.Service/Implements/SymptomConfigService.cs b/SnakeAid.Service/Implements/SymptomConfigService.cs index 1eeb5dce..26ebe972 100644 --- a/SnakeAid.Service/Implements/SymptomConfigService.cs +++ b/SnakeAid.Service/Implements/SymptomConfigService.cs @@ -53,11 +53,14 @@ public async Task> CreateSymptomConfigAsync(C var symptomConfig = new SymptomConfig { + GroupName = request.GroupName, AttributeKey = request.AttributeKey, AttributeLabel = request.AttributeLabel, DisplayOrder = request.DisplayOrder, Name = request.Name, Description = request.Description, + IsCritical = request.IsCritical, + AlertMessage = request.AlertMessage, IsActive = request.IsActive, Category = request.Category, TimeScoresJson = request.TimeScoreList != null && request.TimeScoreList.Any() @@ -128,10 +131,12 @@ public async Task>> FilterSymptomCo // Build combined predicate predicate = sc => + (string.IsNullOrWhiteSpace(request.GroupName) || sc.GroupName.Contains(request.GroupName)) && (string.IsNullOrWhiteSpace(request.AttributeKey) || sc.AttributeKey.Contains(request.AttributeKey)) && (string.IsNullOrWhiteSpace(request.Name) || sc.Name.Contains(request.Name)) && (!request.Category.HasValue || sc.Category == request.Category.Value) && (!request.IsActive.HasValue || sc.IsActive == request.IsActive.Value) && + (!request.IsCritical.HasValue || sc.IsCritical == request.IsCritical.Value) && (!request.VenomTypeId.HasValue || sc.VenomTypeId == request.VenomTypeId.Value); // Get paginated data @@ -197,6 +202,11 @@ public async Task> UpdateSymptomConfigAsync(i } // Update only provided fields + if (!string.IsNullOrWhiteSpace(request.GroupName)) + { + symptomConfig.GroupName = request.GroupName; + } + if (!string.IsNullOrWhiteSpace(request.AttributeKey)) { symptomConfig.AttributeKey = request.AttributeKey; @@ -223,6 +233,16 @@ public async Task> UpdateSymptomConfigAsync(i symptomConfig.Description = request.Description; } + if (request.IsCritical.HasValue) + { + symptomConfig.IsCritical = request.IsCritical.Value; + } + + if (request.AlertMessage != null) + { + symptomConfig.AlertMessage = request.AlertMessage; + } + if (request.IsActive.HasValue) { symptomConfig.IsActive = request.IsActive.Value;