student 2 nedēļas atpakaļ
vecāks
revīzija
d4a18f37e4

+ 13 - 13
Kitty_Fall/Kitty_Fall/Database/Database/ReportByTime.cs

@@ -9,33 +9,33 @@ public partial class ReportByTime
 
     public DateTime TimeReport { get; set; }
 
-    public decimal RegisterOnMps { get; set; }
+    public decimal? RegisterOnMps { get; set; }
 
-    public decimal RegisterOnWeb { get; set; }
+    public decimal? RegisterOnWeb { get; set; }
 
-    public decimal Renew { get; set; }
+    public decimal? Renew { get; set; }
 
-    public decimal BuyOnWeb { get; set; }
+    public decimal? BuyOnWeb { get; set; }
 
-    public decimal BuyOnMps { get; set; }
+    public decimal? BuyOnMps { get; set; }
 
-    public decimal UserActive { get; set; }
+    public decimal? UserActive { get; set; }
 
     public decimal? UserActiveTotal { get; set; }
 
-    public decimal UserInactive { get; set; }
+    public decimal? UserInactive { get; set; }
 
-    public decimal Revenue { get; set; }
+    public decimal? Revenue { get; set; }
 
     public decimal? Bonus { get; set; }
 
-    public string Type { get; set; } = null!;
+    public string? Type { get; set; }
 
-    public decimal DailyPrize { get; set; }
+    public decimal? DailyPrize { get; set; }
 
-    public decimal WeeklyPrize { get; set; }
+    public decimal? WeeklyPrize { get; set; }
 
-    public decimal MonthlyPrize { get; set; }
+    public decimal? MonthlyPrize { get; set; }
 
-    public string ServiceName { get; set; } = null!;
+    public string? ServiceName { get; set; }
 }

+ 53 - 46
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Controllers/DailyReportController.cs

@@ -9,92 +9,99 @@ namespace Kitty_Fall.Cms.Controllers;
 public class DailyReportController : CmsAuthorizedController
 {
     private const string DailyReportType = "DAILY";
-
     private readonly ModelContext _db;
-    private readonly IReadOnlyList<ServiceFilterOptionViewModel> _serviceFilters;
-    private readonly HashSet<string> _supportedServiceFilters;
-    private readonly string _defaultServiceFilter;
+    private readonly IReadOnlyList<RevenueReportServiceOption> _services;
+    private readonly string _defaultService;
 
     public DailyReportController(ModelContext db, IConfiguration configuration)
     {
         _db = db;
-
-        _serviceFilters = configuration
-            .GetSection("RevenueReport:Services")
-            .Get<List<ServiceFilterOptionViewModel>>()?
-            .Where(x => !string.IsNullOrWhiteSpace(x.Value) && !string.IsNullOrWhiteSpace(x.Name))
-            .Select(x => new ServiceFilterOptionViewModel
+        _services = (configuration
+                .GetSection("RevenueReport:Services")
+                .Get<List<RevenueReportServiceOption>>()
+            ?? new List<RevenueReportServiceOption>())
+            .Where(x => !string.IsNullOrWhiteSpace(x.Value))
+            .Select(x => new RevenueReportServiceOption
             {
                 Value = x.Value.Trim(),
-                Name = x.Name.Trim()
+                Name = string.IsNullOrWhiteSpace(x.Name) ? x.Value.Trim() : x.Name.Trim()
             })
             .DistinctBy(x => x.Value, StringComparer.OrdinalIgnoreCase)
-            .ToList()
-            ?? new List<ServiceFilterOptionViewModel>();
-
-        if (_serviceFilters.Count == 0)
-        {
-            throw new InvalidOperationException("RevenueReport:Services must contain at least one service.");
-        }
-
-        _supportedServiceFilters = _serviceFilters
-            .Select(x => x.Value)
-            .ToHashSet(StringComparer.OrdinalIgnoreCase);
+            .ToList();
 
         var configuredDefault = configuration["RevenueReport:DefaultService"]?.Trim();
-        _defaultServiceFilter = !string.IsNullOrWhiteSpace(configuredDefault)
-            && _supportedServiceFilters.Contains(configuredDefault)
-                ? _serviceFilters.First(x => string.Equals(x.Value, configuredDefault, StringComparison.OrdinalIgnoreCase)).Value
-                : _serviceFilters[0].Value;
+        _defaultService = _services.FirstOrDefault(x =>
+                string.Equals(x.Value, configuredDefault, StringComparison.OrdinalIgnoreCase))?.Value
+            ?? _services.FirstOrDefault()?.Value
+            ?? string.Empty;
     }
 
     [HttpGet("/DailyReport")]
     [HttpGet("/DailyReport/Index")]
     [HttpGet("/Admin/DailyReport")]
-    public async Task<IActionResult> Index(DateTime? dateData = null, string? serviceFilter = null)
+    public async Task<IActionResult> Index(
+        DateTime? fromDate = null,
+        DateTime? toDate = null,
+        string? serviceFilter = null)
     {
         if (!CheckLogin()) return RedirectToLogin();
         if (!HasMenuPermission(CmsSecurityHelper.DailyReportsMenuKey)) return RedirectNoPermission();
 
-        var normalizedDate = dateData?.Date;
+        var normalizedFromDate = fromDate?.Date;
+        var normalizedToDate = toDate?.Date;
         var normalizedService = NormalizeServiceFilter(serviceFilter);
+        if (normalizedFromDate.HasValue && normalizedToDate.HasValue && normalizedFromDate > normalizedToDate)
+        {
+            (normalizedFromDate, normalizedToDate) = (normalizedToDate, normalizedFromDate);
+        }
+
         var query = _db.ReportByTimes
             .AsNoTracking()
-            .Where(x => x.Type == DailyReportType && x.ServiceName == normalizedService);
+            .Where(x => x.Type == DailyReportType);
+
+        if (!string.IsNullOrWhiteSpace(normalizedService))
+        {
+            query = query.Where(x => x.ServiceName == normalizedService);
+        }
+
+        if (normalizedFromDate.HasValue)
+        {
+            query = query.Where(x => x.TimeReport >= normalizedFromDate.Value);
+        }
 
-        if (normalizedDate.HasValue)
+        if (normalizedToDate.HasValue)
         {
-            var nextDate = normalizedDate.Value.AddDays(1);
-            query = query.Where(x => x.TimeReport >= normalizedDate.Value && x.TimeReport < nextDate);
+            var endDateExclusive = normalizedToDate.Value.AddDays(1);
+            query = query.Where(x => x.TimeReport < endDateExclusive);
         }
 
         var items = await query
             .OrderByDescending(x => x.TimeReport)
-            .Select(x => new DailyReportItemViewModel
+            .Select(x => new ReportByTimesItemViewModel
             {
-                Date = x.TimeReport,
-                TotalSubscribers = x.UserActiveTotal ?? 0,
-                Registered = x.UserActive,
-                Cancelled = x.UserInactive,
-                Revenue = x.Revenue,
-                Bonus = x.Bonus ?? 0
+                Ngay = x.TimeReport,
+                TongSub = x.UserActiveTotal ?? 0,
+                DangKy = x.UserActive ?? 0,
+                Huy = x.UserInactive ?? 0,
+                DoanhThu = x.Revenue ?? 0,
+                CongTien = x.Bonus ?? 0
             })
             .ToListAsync();
 
-        return View(new DailyReportViewModel
+        return View(new ReportByTimesViewModel
         {
-            DateData = normalizedDate,
+            FromDate = normalizedFromDate,
+            ToDate = normalizedToDate,
             ServiceFilter = normalizedService,
-            ServiceFilters = _serviceFilters,
+            Services = _services.ToList(),
             Items = items
         });
     }
 
     private string NormalizeServiceFilter(string? serviceFilter)
     {
-        var normalized = (serviceFilter ?? string.Empty).Trim();
-        return _supportedServiceFilters.Contains(normalized)
-            ? _serviceFilters.First(x => string.Equals(x.Value, normalized, StringComparison.OrdinalIgnoreCase)).Value
-            : _defaultServiceFilter;
+        return _services.FirstOrDefault(x =>
+                string.Equals(x.Value, serviceFilter?.Trim(), StringComparison.OrdinalIgnoreCase))?.Value
+            ?? _defaultService;
     }
 }

+ 28 - 1
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Controllers/HomeController.cs

@@ -1,4 +1,6 @@
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Diagnostics;
+using Kitty_Fall.Cms.Helpers;
 using Kitty_Fall.Cms.Models;
 using System.Diagnostics;
 
@@ -6,10 +8,35 @@ namespace Kitty_Fall.Cms.Controllers;
 
 public class HomeController : Controller
 {
+    private readonly ILogger<HomeController> _logger;
+
+    public HomeController(ILogger<HomeController> logger)
+    {
+        _logger = logger;
+    }
+
     [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
     [HttpGet("/Home/Error")]
     public IActionResult Error()
     {
-        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        var requestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
+        var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
+        if (exceptionFeature?.Error != null)
+        {
+            _logger.LogError(
+                exceptionFeature.Error,
+                "Unhandled CMS exception. RequestId: {RequestId}; Path: {Path}",
+                requestId,
+                exceptionFeature.Path);
+        }
+
+        var cmsSession = CmsSecurityHelper.GetSession(HttpContext);
+        var isAdmin = string.Equals(cmsSession?.RoleCode, "ADMIN", StringComparison.OrdinalIgnoreCase);
+
+        return View(new ErrorViewModel
+        {
+            RequestId = requestId,
+            ErrorMessage = isAdmin ? exceptionFeature?.Error.GetBaseException().Message : null
+        });
     }
 }

+ 31 - 42
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Controllers/RevenueReportController.cs

@@ -10,41 +10,30 @@ namespace Kitty_Fall.Cms.Controllers;
 public class RevenueReportController : CmsAuthorizedController
 {
     private readonly ModelContext _db;
-    private readonly IReadOnlyList<ServiceFilterOptionViewModel> _serviceFilters;
-    private readonly HashSet<string> _supportedServiceFilters;
-    private readonly string _defaultServiceFilter;
+    private readonly IReadOnlyList<RevenueReportServiceOption> _services;
+    private readonly string _defaultService;
 
     public RevenueReportController(ModelContext db, IConfiguration configuration)
     {
         _db = db;
-
-        _serviceFilters = configuration
-            .GetSection("RevenueReport:Services")
-            .Get<List<ServiceFilterOptionViewModel>>()?
-            .Where(x => !string.IsNullOrWhiteSpace(x.Value) && !string.IsNullOrWhiteSpace(x.Name))
-            .Select(x => new ServiceFilterOptionViewModel
+        _services = (configuration
+                .GetSection("RevenueReport:Services")
+                .Get<List<RevenueReportServiceOption>>()
+            ?? new List<RevenueReportServiceOption>())
+            .Where(x => !string.IsNullOrWhiteSpace(x.Value))
+            .Select(x => new RevenueReportServiceOption
             {
                 Value = x.Value.Trim(),
-                Name = x.Name.Trim()
+                Name = string.IsNullOrWhiteSpace(x.Name) ? x.Value.Trim() : x.Name.Trim()
             })
             .DistinctBy(x => x.Value, StringComparer.OrdinalIgnoreCase)
-            .ToList()
-            ?? new List<ServiceFilterOptionViewModel>();
-
-        if (_serviceFilters.Count == 0)
-        {
-            throw new InvalidOperationException("RevenueReport:Services must contain at least one service.");
-        }
-
-        _supportedServiceFilters = _serviceFilters
-            .Select(x => x.Value)
-            .ToHashSet(StringComparer.OrdinalIgnoreCase);
+            .ToList();
 
         var configuredDefault = configuration["RevenueReport:DefaultService"]?.Trim();
-        _defaultServiceFilter = !string.IsNullOrWhiteSpace(configuredDefault)
-            && _supportedServiceFilters.Contains(configuredDefault)
-                ? _serviceFilters.First(x => string.Equals(x.Value, configuredDefault, StringComparison.OrdinalIgnoreCase)).Value
-                : _serviceFilters[0].Value;
+        _defaultService = _services.FirstOrDefault(x =>
+                string.Equals(x.Value, configuredDefault, StringComparison.OrdinalIgnoreCase))?.Value
+            ?? _services.FirstOrDefault()?.Value
+            ?? string.Empty;
     }
 
     [HttpGet("/RevenueReport")]
@@ -65,14 +54,14 @@ public class RevenueReportController : CmsAuthorizedController
             {
                 TimeReport = x.TimeReport,
                 Type = x.Type ?? string.Empty,
-                Revenue = x.Revenue,
-                RegisterOnMps = x.RegisterOnMps,
-                RegisterOnWeb = x.RegisterOnWeb,
-                Renew = x.Renew,
-                BuyOnWeb = x.BuyOnWeb,
-                BuyOnMps = x.BuyOnMps,
-                UserActive = x.UserActive,
-                UserInactive = x.UserInactive
+                Revenue = x.Revenue ?? 0,
+                RegisterOnMps = x.RegisterOnMps ?? 0,
+                RegisterOnWeb = x.RegisterOnWeb ?? 0,
+                Renew = x.Renew ?? 0,
+                BuyOnWeb = x.BuyOnWeb ?? 0,
+                BuyOnMps = x.BuyOnMps ?? 0,
+                UserActive = x.UserActive ?? 0,
+                UserInactive = x.UserInactive ?? 0
             })
             .ToListAsync();
 
@@ -82,7 +71,7 @@ public class RevenueReportController : CmsAuthorizedController
             FromDate = normalizedFromDate,
             ToDate = normalizedToDate,
             ServiceFilter = normalizedServiceFilter,
-            ServiceFilters = _serviceFilters,
+            Services = _services.ToList(),
             TotalRows = items.Count,
             TotalRevenue = items.Sum(x => x.Revenue),
             AverageRevenue = items.Count == 0 ? 0 : items.Average(x => x.Revenue),
@@ -90,7 +79,7 @@ public class RevenueReportController : CmsAuthorizedController
             Items = items
         };
 
-        return View("~/Views/Reports/Revenue.cshtml", model);
+        return View(model);
     }
 
     [HttpGet("/RevenueReport/DailyTrend")]
@@ -109,7 +98,7 @@ public class RevenueReportController : CmsAuthorizedController
             .Select(x => new RevenueTrendItemViewModel
             {
                 TimeReport = x.Key,
-                Revenue = x.Sum(y => y.Revenue)
+                Revenue = x.Sum(y => y.Revenue) ?? 0
             })
             .ToListAsync();
 
@@ -118,14 +107,14 @@ public class RevenueReportController : CmsAuthorizedController
             FromDate = normalizedFromDate,
             ToDate = normalizedToDate,
             ServiceFilter = normalizedServiceFilter,
-            ServiceFilters = _serviceFilters,
+            Services = _services.ToList(),
             TotalColumns = items.Count,
             TotalRevenue = items.Sum(x => x.Revenue),
             AverageRevenue = items.Count == 0 ? 0 : items.Average(x => x.Revenue),
             Items = items
         };
 
-        return View("~/Views/Reports/RevenueTrend.cshtml", model);
+        return View("DailyTrend", model);
     }
 
     private static string NormalizeReportType(string? reportType)
@@ -152,10 +141,10 @@ public class RevenueReportController : CmsAuthorizedController
 
     private string NormalizeServiceFilter(string? serviceFilter)
     {
-        var normalizedServiceFilter = (serviceFilter ?? string.Empty).Trim();
-        return _supportedServiceFilters.Contains(normalizedServiceFilter)
-            ? _serviceFilters.First(x => string.Equals(x.Value, normalizedServiceFilter, StringComparison.OrdinalIgnoreCase)).Value
-            : _defaultServiceFilter;
+        var configuredService = _services.FirstOrDefault(x =>
+            string.Equals(x.Value, serviceFilter?.Trim(), StringComparison.OrdinalIgnoreCase));
+
+        return configuredService?.Value ?? _defaultService;
     }
 
     private IQueryable<ReportByTime> BuildBaseQuery(string reportType, DateTime? fromDate, DateTime? toDate, string serviceFilter)

+ 0 - 27
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Models/DailyReportViewModel.cs

@@ -1,27 +0,0 @@
-namespace Kitty_Fall.Cms.Models;
-
-public class DailyReportViewModel
-{
-    public DateTime? DateData { get; set; }
-
-    public string ServiceFilter { get; set; } = string.Empty;
-
-    public IReadOnlyList<ServiceFilterOptionViewModel> ServiceFilters { get; set; } = Array.Empty<ServiceFilterOptionViewModel>();
-
-    public List<DailyReportItemViewModel> Items { get; set; } = new();
-}
-
-public class DailyReportItemViewModel
-{
-    public DateTime Date { get; set; }
-
-    public decimal TotalSubscribers { get; set; }
-
-    public decimal Registered { get; set; }
-
-    public decimal Cancelled { get; set; }
-
-    public decimal Revenue { get; set; }
-
-    public decimal Bonus { get; set; }
-}

+ 4 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Models/ErrorViewModel.cs

@@ -5,5 +5,9 @@ namespace Kitty_Fall.Cms.Models
         public string? RequestId { get; set; }
 
         public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
+
+        public string? ErrorMessage { get; set; }
+
+        public bool ShowErrorMessage => !string.IsNullOrWhiteSpace(ErrorMessage);
     }
 }

+ 29 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Models/ReportByTimesViewModel.cs

@@ -0,0 +1,29 @@
+namespace Kitty_Fall.Cms.Models;
+
+public class ReportByTimesViewModel
+{
+    public DateTime? FromDate { get; set; }
+
+    public DateTime? ToDate { get; set; }
+
+    public string ServiceFilter { get; set; } = string.Empty;
+
+    public List<RevenueReportServiceOption> Services { get; set; } = new();
+
+    public List<ReportByTimesItemViewModel> Items { get; set; } = new();
+}
+
+public class ReportByTimesItemViewModel
+{
+    public DateTime Ngay { get; set; }
+
+    public decimal TongSub { get; set; }
+
+    public decimal DangKy { get; set; }
+
+    public decimal Huy { get; set; }
+
+    public decimal DoanhThu { get; set; }
+
+    public decimal CongTien { get; set; }
+}

+ 8 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Models/RevenueReportServiceOption.cs

@@ -0,0 +1,8 @@
+namespace Kitty_Fall.Cms.Models;
+
+public class RevenueReportServiceOption
+{
+    public string Value { get; set; } = string.Empty;
+
+    public string Name { get; set; } = string.Empty;
+}

+ 2 - 9
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Models/RevenueReportViewModel.cs

@@ -10,7 +10,7 @@ public class RevenueReportViewModel
 
     public string? ServiceFilter { get; set; }
 
-    public IReadOnlyList<ServiceFilterOptionViewModel> ServiceFilters { get; set; } = Array.Empty<ServiceFilterOptionViewModel>();
+    public List<RevenueReportServiceOption> Services { get; set; } = new();
 
     public int TotalRows { get; set; }
 
@@ -31,7 +31,7 @@ public class RevenueTrendReportViewModel
 
     public string? ServiceFilter { get; set; }
 
-    public IReadOnlyList<ServiceFilterOptionViewModel> ServiceFilters { get; set; } = Array.Empty<ServiceFilterOptionViewModel>();
+    public List<RevenueReportServiceOption> Services { get; set; } = new();
 
     public int TotalColumns { get; set; }
 
@@ -49,13 +49,6 @@ public class RevenueTrendItemViewModel
     public decimal Revenue { get; set; }
 }
 
-public class ServiceFilterOptionViewModel
-{
-    public string Value { get; set; } = string.Empty;
-
-    public string Name { get; set; } = string.Empty;
-}
-
 public class RevenueReportItemViewModel
 {
     public DateTime TimeReport { get; set; }

+ 23 - 16
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Program.cs

@@ -56,33 +56,40 @@ if (!app.Environment.IsDevelopment())
     app.UseHsts();
 }
 
-var pathBase = builder.Configuration["PathBase"]?.Trim().TrimEnd('/');
-if (!string.IsNullOrWhiteSpace(pathBase))
+var pathBaseValue = builder.Configuration["PathBase"]?.Trim();
+var configuredPathBase = PathString.Empty;
+if (!string.IsNullOrWhiteSpace(pathBaseValue))
 {
-    if (!pathBase.StartsWith('/'))
-    {
-        pathBase = $"/{pathBase}";
-    }
+    configuredPathBase = new PathString("/" + pathBaseValue.Trim('/'));
+    app.UsePathBase(configuredPathBase);
 
-    // launchSettings chi mo applicationUrl (duong dan "/"). Redirect tai runtime
-    // de URL khoi dong luon bam theo PathBase trong appsettings cua moi moi truong.
+    // UsePathBase chi gan Request.PathBase khi URL vao Kestrel van con prefix.
+    // Reverse proxy production co the strip prefix truoc, nen can khoi phuc
+    // public PathBase tu appsettings de Url.Content/TagHelper sinh URL dung.
     app.Use(async (context, next) =>
     {
-        if (context.Request.Path == "/"
-            || context.Request.Path == pathBase
-            || context.Request.Path == $"{pathBase}/")
+        if (!context.Request.PathBase.HasValue)
         {
-            // Dung URL tuong doi de domain, protocol va port tu dong theo request.
-            context.Response.Redirect($"{pathBase}/Home/Login");
-            return;
+            context.Request.PathBase = configuredPathBase;
         }
 
         await next();
     });
-
-    app.UsePathBase(pathBase);
 }
 
+// IDE chi mo URL goc; dich den login duoc tao dong tu PathBase. Neu khong
+// cau hinh PathBase thi URL van la /Home/Login.
+app.Use(async (context, next) =>
+{
+    if (context.Request.Path == "/")
+    {
+        context.Response.Redirect($"{context.Request.PathBase}/Home/Login");
+        return;
+    }
+
+    await next();
+});
+
 app.UseHttpsRedirection();
 app.UseStaticFiles();
 

+ 60 - 51
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Views/DailyReport/Index.cshtml

@@ -1,7 +1,8 @@
-@model Kitty_Fall.Cms.Models.DailyReportViewModel
+@model Kitty_Fall.Cms.Models.ReportByTimesViewModel
 @{
     ViewData["Title"] = "Daily Report";
-    var dateValue = Model.DateData?.ToString("yyyy-MM-dd");
+    var fromDateValue = Model.FromDate?.ToString("yyyy-MM-dd");
+    var toDateValue = Model.ToDate?.ToString("yyyy-MM-dd");
     string FormatNumber(decimal value) => value.ToString("#,##0.##");
 }
 
@@ -9,28 +10,30 @@
     <div class="rounded-3xl bg-white shadow-sm ring-1 ring-gray-100">
         <div class="border-b border-gray-100 px-5 py-5 sm:px-6">
             <div>
-                <h1 class="text-2xl font-bold text-gray-900">Daily Report</h1>
-                <p class="mt-1 text-sm text-gray-500">Daily subscriber, registration, cancellation, revenue and bonus report.</p>
+                <h1 class="text-xl font-bold text-gray-900">ReportByTimes - Daily</h1>
+                <p class="mt-1 text-sm text-gray-500">Daily subscription, cancellation, revenue and bonus report.</p>
             </div>
 
-            <form method="get" action="@Url.Content("~/DailyReport")" class="mt-5 grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]">
+            <form method="get" action="@Url.Content("~/DailyReport")" class="mt-5 grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_auto]">
                 <label class="block">
                     <span class="mb-2 block text-sm font-semibold text-gray-700">Service</span>
                     <select name="serviceFilter" class="w-full rounded-2xl border border-gray-200 px-4 py-3 text-sm text-gray-700 outline-none transition focus:border-admin-active focus:ring-2 focus:ring-purple-100">
-                        @foreach (var service in Model.ServiceFilters)
+                        @foreach (var service in Model.Services)
                         {
                             <option value="@service.Value" selected="@(string.Equals(Model.ServiceFilter, service.Value, StringComparison.OrdinalIgnoreCase) ? "selected" : null)">@service.Name</option>
                         }
                     </select>
                 </label>
                 <label class="block">
-                    <span class="mb-2 block text-sm font-semibold text-gray-700">Date</span>
-                    <input type="date" name="dateData" value="@dateValue" class="w-full rounded-2xl border border-gray-200 px-4 py-3 text-sm text-gray-700 outline-none transition focus:border-admin-active focus:ring-2 focus:ring-purple-100" />
+                    <span class="mb-2 block text-sm font-semibold text-gray-700">From date</span>
+                    <input type="date" name="fromDate" value="@fromDateValue" class="w-full rounded-2xl border border-gray-200 px-4 py-3 text-sm text-gray-700 outline-none transition focus:border-admin-active focus:ring-2 focus:ring-purple-100" />
+                </label>
+                <label class="block">
+                    <span class="mb-2 block text-sm font-semibold text-gray-700">To date</span>
+                    <input type="date" name="toDate" value="@toDateValue" class="w-full rounded-2xl border border-gray-200 px-4 py-3 text-sm text-gray-700 outline-none transition focus:border-admin-active focus:ring-2 focus:ring-purple-100" />
                 </label>
                 <div class="flex items-end">
-                    <button type="submit" class="inline-flex w-full items-center justify-center rounded-2xl bg-admin-active px-4 py-3 text-sm font-semibold text-white transition hover:bg-purple-700 md:w-auto">
-                        Apply filter
-                    </button>
+                    <button type="submit" class="inline-flex w-full items-center justify-center rounded-2xl bg-admin-active px-4 py-3 text-sm font-semibold text-white transition hover:bg-purple-700 md:w-auto">Apply filter</button>
                 </div>
             </form>
         </div>
@@ -39,53 +42,59 @@
         {
             <div class="px-5 py-10 text-center sm:px-6">
                 <div class="text-base font-semibold text-gray-800">No daily report data found.</div>
-                <div class="mt-2 text-sm text-gray-500">Adjust the service or date and try again.</div>
+                <div class="mt-2 text-sm text-gray-500">Adjust the date range and try again.</div>
             </div>
         }
         else
         {
-            <div class="space-y-3 px-5 py-6 md:hidden">
-                @foreach (var item in Model.Items)
-                {
-                    <div class="rounded-2xl border border-gray-100 bg-white p-4 shadow-sm">
-                        <div class="text-base font-bold text-gray-900">@item.Date.ToString("dd/MM/yyyy")</div>
-                        <div class="mt-4 grid grid-cols-2 gap-4 text-sm">
-                            <div><div class="text-xs font-bold uppercase text-gray-400">Total subscribers</div><div class="mt-1 font-semibold text-gray-800">@FormatNumber(item.TotalSubscribers)</div></div>
-                            <div><div class="text-xs font-bold uppercase text-gray-400">Registered</div><div class="mt-1 font-semibold text-gray-800">@FormatNumber(item.Registered)</div></div>
-                            <div><div class="text-xs font-bold uppercase text-gray-400">Cancelled</div><div class="mt-1 font-semibold text-gray-800">@FormatNumber(item.Cancelled)</div></div>
-                            <div><div class="text-xs font-bold uppercase text-gray-400">Revenue</div><div class="mt-1 font-semibold text-gray-800">@FormatNumber(item.Revenue)</div></div>
-                            <div><div class="text-xs font-bold uppercase text-gray-400">Bonus</div><div class="mt-1 font-semibold text-gray-800">@FormatNumber(item.Bonus)</div></div>
+            <div class="px-5 py-5 sm:hidden">
+                <div class="space-y-4">
+                    @foreach (var item in Model.Items)
+                    {
+                        <div class="rounded-3xl border border-gray-100 bg-gray-50 p-4 shadow-sm">
+                            <div class="text-lg font-bold text-gray-900">@item.Ngay.ToString("dd/MM/yyyy")</div>
+                            <div class="mt-4 grid grid-cols-2 gap-3 text-sm">
+                                <div class="rounded-2xl bg-white px-3 py-2"><div class="text-gray-500">Total subscriptions</div><div class="mt-1 font-semibold text-gray-900">@FormatNumber(item.TongSub)</div></div>
+                                <div class="rounded-2xl bg-white px-3 py-2"><div class="text-gray-500">Registrations</div><div class="mt-1 font-semibold text-gray-900">@FormatNumber(item.DangKy)</div></div>
+                                <div class="rounded-2xl bg-white px-3 py-2"><div class="text-gray-500">Cancellations</div><div class="mt-1 font-semibold text-gray-900">@FormatNumber(item.Huy)</div></div>
+                                <div class="rounded-2xl bg-white px-3 py-2"><div class="text-gray-500">Revenue</div><div class="mt-1 font-semibold text-gray-900">@FormatNumber(item.DoanhThu)</div></div>
+                                <div class="col-span-2 rounded-2xl bg-white px-3 py-2"><div class="text-gray-500">Bonus</div><div class="mt-1 font-semibold text-gray-900">@FormatNumber(item.CongTien)</div></div>
+                            </div>
                         </div>
-                    </div>
-                }
+                    }
+                </div>
             </div>
 
-            <div class="hidden overflow-x-auto md:block">
-                <table class="min-w-full divide-y divide-gray-100">
-                    <thead class="bg-gray-50">
-                        <tr class="text-left text-xs font-bold uppercase tracking-[0.14em] text-gray-500">
-                            <th class="px-5 py-4">Date</th>
-                            <th class="px-5 py-4 text-right">Total subscribers</th>
-                            <th class="px-5 py-4 text-right">Registered</th>
-                            <th class="px-5 py-4 text-right">Cancelled</th>
-                            <th class="px-5 py-4 text-right">Revenue</th>
-                            <th class="px-5 py-4 text-right">Bonus</th>
-                        </tr>
-                    </thead>
-                    <tbody class="divide-y divide-gray-100 bg-white text-sm text-gray-700">
-                        @foreach (var item in Model.Items)
-                        {
-                            <tr>
-                                <td class="whitespace-nowrap px-5 py-4 font-semibold text-gray-900">@item.Date.ToString("dd/MM/yyyy")</td>
-                                <td class="whitespace-nowrap px-5 py-4 text-right">@FormatNumber(item.TotalSubscribers)</td>
-                                <td class="whitespace-nowrap px-5 py-4 text-right">@FormatNumber(item.Registered)</td>
-                                <td class="whitespace-nowrap px-5 py-4 text-right">@FormatNumber(item.Cancelled)</td>
-                                <td class="whitespace-nowrap px-5 py-4 text-right font-semibold text-gray-900">@FormatNumber(item.Revenue)</td>
-                                <td class="whitespace-nowrap px-5 py-4 text-right">@FormatNumber(item.Bonus)</td>
-                            </tr>
-                        }
-                    </tbody>
-                </table>
+            <div class="hidden px-5 py-6 sm:block sm:px-6">
+                <div class="overflow-hidden rounded-3xl border border-gray-100">
+                    <div class="overflow-x-auto">
+                        <table class="min-w-full divide-y divide-gray-100">
+                            <thead class="bg-gray-50">
+                                <tr class="text-left text-xs font-bold uppercase tracking-[0.14em] text-gray-500">
+                                    <th class="px-4 py-3">Date</th>
+                                    <th class="px-4 py-3">Total subscriptions</th>
+                                    <th class="px-4 py-3">Registrations</th>
+                                    <th class="px-4 py-3">Cancellations</th>
+                                    <th class="px-4 py-3">Revenue</th>
+                                    <th class="px-4 py-3">Bonus</th>
+                                </tr>
+                            </thead>
+                            <tbody class="divide-y divide-gray-100 bg-white text-sm text-gray-700">
+                                @foreach (var item in Model.Items)
+                                {
+                                    <tr>
+                                        <td class="whitespace-nowrap px-4 py-4 font-semibold text-gray-900">@item.Ngay.ToString("dd/MM/yyyy")</td>
+                                        <td class="whitespace-nowrap px-4 py-4">@FormatNumber(item.TongSub)</td>
+                                        <td class="whitespace-nowrap px-4 py-4">@FormatNumber(item.DangKy)</td>
+                                        <td class="whitespace-nowrap px-4 py-4">@FormatNumber(item.Huy)</td>
+                                        <td class="whitespace-nowrap px-4 py-4 font-semibold text-gray-900">@FormatNumber(item.DoanhThu)</td>
+                                        <td class="whitespace-nowrap px-4 py-4">@FormatNumber(item.CongTien)</td>
+                                    </tr>
+                                }
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
             </div>
         }
     </div>

+ 1 - 1
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Views/Reports/RevenueTrend.cshtml → Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Views/RevenueReport/DailyTrend.cshtml

@@ -76,7 +76,7 @@
                 <label class="block">
                     <span class="mb-2 block text-sm font-semibold text-gray-700">Service</span>
                     <select name="serviceFilter" class="w-full rounded-2xl border border-gray-200 px-4 py-3 text-sm text-gray-700 outline-none transition focus:border-admin-active focus:ring-2 focus:ring-purple-100">
-                        @foreach (var service in Model.ServiceFilters)
+                        @foreach (var service in Model.Services)
                         {
                             <option value="@service.Value" selected="@(string.Equals(Model.ServiceFilter, service.Value, StringComparison.OrdinalIgnoreCase) ? "selected" : null)">@service.Name</option>
                         }

+ 1 - 1
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Views/Reports/Revenue.cshtml → Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Views/RevenueReport/Index.cshtml

@@ -80,7 +80,7 @@
                 <label class="block">
                     <span class="mb-2 block text-sm font-semibold text-gray-700">Service</span>
                     <select name="serviceFilter" class="w-full rounded-2xl border border-gray-200 px-4 py-3 text-sm text-gray-700 outline-none transition focus:border-admin-active focus:ring-2 focus:ring-purple-100">
-                        @foreach (var service in Model.ServiceFilters)
+                        @foreach (var service in Model.Services)
                         {
                             <option value="@service.Value" selected="@(string.Equals(Model.ServiceFilter, service.Value, StringComparison.OrdinalIgnoreCase) ? "selected" : null)">@service.Name</option>
                         }

+ 8 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Views/Shared/Error.cshtml

@@ -13,6 +13,14 @@
     </p>
 }
 
+@if (Model.ShowErrorMessage)
+{
+    <div class="mt-4 rounded-xl border border-red-200 bg-red-50 p-4 text-red-800">
+        <strong>Server error detail:</strong>
+        <pre class="mt-2 whitespace-pre-wrap break-words text-sm">@Model.ErrorMessage</pre>
+    </div>
+}
+
 <h3>Development Mode</h3>
 <p>
     Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.