Ver código fonte

thêm cột turn cho báo cáo, thêm fiter cho báo cáo check sub

student 5 horas atrás
pai
commit
5ee7a53a36

+ 74 - 16
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Controllers/SubscriptionChargeReportController.cs

@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
 using Kitty_Fall.Cms.Helpers;
 using Kitty_Fall.Cms.Models;
+using System.Globalization;
 
 namespace Kitty_Fall.Cms.Controllers;
 
@@ -20,16 +21,21 @@ public class SubscriptionChargeReportController : CmsAuthorizedController
     [HttpGet("/SubscriptionChargeReport")]
     [HttpGet("/SubscriptionChargeReport/Index")]
     [HttpGet("/Admin/SubscriptionChargeReport")]
-    public async Task<IActionResult> Index(string? msisdn = null, int regPage = 1, int chargePage = 1)
+    public async Task<IActionResult> Index(string? msisdn = null, string? selectedDate = null, int regPage = 1, int chargePage = 1)
     {
         if (!CheckLogin()) return RedirectToLogin();
         if (!HasMenuPermission(CmsSecurityHelper.SubscriptionChargeReportsMenuKey)) return RedirectNoPermission();
 
-        var submittedSearch = Request.Query.ContainsKey("msisdn");
         var searchMsisdn = NormalizeRawMsisdn(msisdn);
-        var normalizedMsisdn = CommonLogic.ValidateMsisdn(CommonConstants.CountryCode, msisdn);
         var hasSearch = !string.IsNullOrWhiteSpace(searchMsisdn);
-        var isInvalidMsisdn = submittedSearch && (!hasSearch || string.IsNullOrWhiteSpace(normalizedMsisdn));
+        var normalizedMsisdn = hasSearch
+            ? CommonLogic.ValidateMsisdn(CommonConstants.CountryCode, msisdn)
+            : null;
+        var isInvalidMsisdn = hasSearch && string.IsNullOrWhiteSpace(normalizedMsisdn);
+        var hasDateValue = !string.IsNullOrWhiteSpace(selectedDate);
+        var reportDate = DateTime.Today;
+        var isInvalidDate = hasDateValue && !DateTime.TryParseExact(
+            selectedDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out reportDate);
 
         var model = new SubscriptionChargeReportViewModel
         {
@@ -38,19 +44,25 @@ public class SubscriptionChargeReportController : CmsAuthorizedController
             HasSearch = hasSearch,
             IsInvalidMsisdn = isInvalidMsisdn,
             MsisdnErrorMessage = isInvalidMsisdn
-                ? (!hasSearch ? "Please enter a phone number." : "Invalid MSISDN. Please enter digits only, with or without country code.")
+                ? "Invalid MSISDN. Please enter digits only, with or without country code."
                 : null,
+            SelectedDate = isInvalidDate
+                ? (selectedDate ?? string.Empty).Trim()
+                : reportDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
+            IsInvalidDate = isInvalidDate,
+            DateErrorMessage = isInvalidDate ? "Invalid date. Please select one valid date." : null,
             PageSize = PageSize,
             RegPage = Math.Max(1, regPage),
             ChargePage = Math.Max(1, chargePage)
         };
 
-        if (!hasSearch || isInvalidMsisdn)
+        if (isInvalidMsisdn || isInvalidDate)
         {
             return View("~/Views/Reports/SubscriptionCharge.cshtml", model);
         }
 
-        var regQuery = BuildRegInfoQuery(searchMsisdn, normalizedMsisdn);
+        var selectedDay = reportDate.Date;
+        var regQuery = BuildRegInfoQuery(searchMsisdn, normalizedMsisdn, selectedDay);
         model.RegTotalRows = await regQuery.CountAsync();
         model.RegTotalPages = CalculateTotalPages(model.RegTotalRows);
         model.RegPage = Math.Min(model.RegPage, model.RegTotalPages);
@@ -80,7 +92,7 @@ public class SubscriptionChargeReportController : CmsAuthorizedController
             })
             .ToListAsync();
 
-        var chargeQuery = BuildChargeLogQuery(searchMsisdn, normalizedMsisdn);
+        var chargeQuery = BuildChargeLogQuery(searchMsisdn, normalizedMsisdn, selectedDay);
         model.ChargeTotalRows = await chargeQuery.CountAsync();
         model.ChargeTotalPages = CalculateTotalPages(model.ChargeTotalRows);
         model.ChargePage = Math.Min(model.ChargePage, model.ChargeTotalPages);
@@ -102,23 +114,69 @@ public class SubscriptionChargeReportController : CmsAuthorizedController
             })
             .ToListAsync();
 
+        var packageLookup = await _db.Packgs
+            .AsNoTracking()
+            .Where(x => x.Fee.HasValue && x.ProductName != null)
+            .OrderBy(x => x.ProductId)
+            .Select(x => new { x.Fee, x.ProductName, x.NumberSpin })
+            .ToListAsync();
+
+        foreach (var chargeLog in model.ChargeLogs)
+        {
+            var productName = ExtractProductName(chargeLog.Description);
+            if (string.IsNullOrWhiteSpace(productName)) continue;
+
+            chargeLog.Turn = packageLookup.FirstOrDefault(x =>
+                x.Fee == chargeLog.Fee &&
+                string.Equals(x.ProductName?.Trim(), productName, StringComparison.OrdinalIgnoreCase))?.NumberSpin;
+        }
+
         return View("~/Views/Reports/SubscriptionCharge.cshtml", model);
     }
 
-    private IQueryable<RegInfo> BuildRegInfoQuery(string searchMsisdn, string? normalizedMsisdn)
+    private IQueryable<RegInfo> BuildRegInfoQuery(string searchMsisdn, string? normalizedMsisdn, DateTime selectedDay)
     {
         var query = _db.RegInfos.AsNoTracking();
-        return HasDistinctNormalizedMsisdn(searchMsisdn, normalizedMsisdn)
-            ? query.Where(x => x.Msisdn == searchMsisdn || x.Msisdn == normalizedMsisdn)
-            : query.Where(x => x.Msisdn == searchMsisdn);
+        if (!string.IsNullOrWhiteSpace(searchMsisdn))
+        {
+            query = HasDistinctNormalizedMsisdn(searchMsisdn, normalizedMsisdn)
+                ? query.Where(x => x.Msisdn == searchMsisdn || x.Msisdn == normalizedMsisdn)
+                : query.Where(x => x.Msisdn == searchMsisdn);
+        }
+
+        var end = selectedDay.AddDays(1);
+        query = query.Where(x => x.RegisterTime.HasValue && x.RegisterTime.Value >= selectedDay && x.RegisterTime.Value < end);
+
+        return query;
     }
 
-    private IQueryable<ChargeLog> BuildChargeLogQuery(string searchMsisdn, string? normalizedMsisdn)
+    private IQueryable<ChargeLog> BuildChargeLogQuery(string searchMsisdn, string? normalizedMsisdn, DateTime selectedDay)
     {
         var query = _db.ChargeLogs.AsNoTracking();
-        return HasDistinctNormalizedMsisdn(searchMsisdn, normalizedMsisdn)
-            ? query.Where(x => x.Msisdn == searchMsisdn || x.Msisdn == normalizedMsisdn)
-            : query.Where(x => x.Msisdn == searchMsisdn);
+        if (!string.IsNullOrWhiteSpace(searchMsisdn))
+        {
+            query = HasDistinctNormalizedMsisdn(searchMsisdn, normalizedMsisdn)
+                ? query.Where(x => x.Msisdn == searchMsisdn || x.Msisdn == normalizedMsisdn)
+                : query.Where(x => x.Msisdn == searchMsisdn);
+        }
+
+        var end = selectedDay.AddDays(1);
+        query = query.Where(x =>
+            (x.ChargeTime.HasValue && x.ChargeTime.Value >= selectedDay && x.ChargeTime.Value < end) ||
+            (!x.ChargeTime.HasValue && x.InsertTime.HasValue && x.InsertTime.Value >= selectedDay && x.InsertTime.Value < end));
+
+        return query;
+    }
+
+    private static string? ExtractProductName(string? description)
+    {
+        if (string.IsNullOrWhiteSpace(description)) return null;
+
+        var value = description.Trim();
+        var separatorIndex = value.IndexOf(' ');
+        return separatorIndex >= 0 && separatorIndex < value.Length - 1
+            ? value[(separatorIndex + 1)..].Trim()
+            : null;
     }
 
     private static bool HasDistinctNormalizedMsisdn(string searchMsisdn, string? normalizedMsisdn)

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

@@ -12,6 +12,12 @@ public class SubscriptionChargeReportViewModel
 
     public string? MsisdnErrorMessage { get; set; }
 
+    public string SelectedDate { get; set; } = string.Empty;
+
+    public bool IsInvalidDate { get; set; }
+
+    public string? DateErrorMessage { get; set; }
+
     public int PageSize { get; set; } = 10;
 
     public int RegPage { get; set; } = 1;
@@ -76,6 +82,8 @@ public class ChargeLogReportItemViewModel
 
     public decimal? Fee { get; set; }
 
+    public short? Turn { get; set; }
+
     public string? Description { get; set; }
 
     public DateTime? ChargeTime { get; set; }

+ 26 - 12
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Views/Reports/SubscriptionCharge.cshtml

@@ -14,6 +14,10 @@
         {
             query.Add("msisdn=" + System.Uri.EscapeDataString(Model.Msisdn));
         }
+        if (!string.IsNullOrWhiteSpace(Model.SelectedDate))
+        {
+            query.Add("selectedDate=" + System.Uri.EscapeDataString(Model.SelectedDate));
+        }
 
         query.Add("regPage=" + (pageParam == "regPage" ? page : Model.RegPage));
         query.Add("chargePage=" + (pageParam == "chargePage" ? page : Model.ChargePage));
@@ -28,13 +32,17 @@
         <div class="border-b border-gray-100 px-5 py-5 sm:px-6">
             <div class="flex flex-col gap-2">
                 <h1 class="text-2xl font-bold text-gray-900">Subscription & Charge Check</h1>
-                <p class="text-sm text-gray-500">Search by MSISDN to view subscription records and charge logs separately.</p>
+                <p class="text-sm text-gray-500">MSISDN and date are optional. A blank date automatically uses the current day.</p>
             </div>
 
-            <form method="get" action="@Url.Content("~/SubscriptionChargeReport")" class="mt-5 grid gap-3 md:grid-cols-[minmax(0,1fr)_auto]">
+            <form method="get" action="@Url.Content("~/SubscriptionChargeReport")" class="mt-5 grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(180px,240px)_auto]">
                 <label class="block">
                     <span class="mb-2 block text-sm font-semibold text-gray-700">MSISDN</span>
-                    <input type="text" name="msisdn" value="@Model.Msisdn" placeholder="e.g. 959681454634" required inputmode="numeric" pattern="[0-9+ ]+" 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" />
+                    <input type="text" name="msisdn" value="@Model.Msisdn" placeholder="All MSISDNs" inputmode="numeric" pattern="[0-9+ ]*" 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">Date</span>
+                    <input type="date" name="selectedDate" value="@Model.SelectedDate" 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">
@@ -57,15 +65,15 @@
                     @(Model.MsisdnErrorMessage ?? "Invalid MSISDN. Please enter digits only, with or without country code.")
                 </div>
             }
+            @if (Model.IsInvalidDate)
+            {
+                <div class="mt-4 rounded-2xl border border-red-100 bg-red-50 px-4 py-3 text-sm font-semibold text-red-700">
+                    @(Model.DateErrorMessage ?? "Invalid date. Please select one valid date.")
+                </div>
+            }
         </div>
 
-        @if (!Model.HasSearch)
-        {
-            <div class="px-5 py-10 text-center sm:px-6">
-                <div class="text-base font-semibold text-gray-800">Enter an MSISDN to load report data.</div>
-            </div>
-        }
-        else if (!Model.IsInvalidMsisdn)
+        @if (!Model.IsInvalidMsisdn && !Model.IsInvalidDate)
         {
             <section id="reg-info" class="px-5 py-6 sm:px-6">
                 <div class="mb-4 flex flex-col gap-1 sm:flex-row sm:items-end sm:justify-between">
@@ -79,7 +87,7 @@
                 @if (Model.RegInfos.Count == 0)
                 {
                     <div class="rounded-3xl border border-dashed border-gray-200 bg-gray-50 px-6 py-8 text-center text-sm font-semibold text-gray-600">
-                        No REG_INFO data found for this MSISDN.
+                        No REG_INFO data found for the selected filters.
                     </div>
                 }
                 else
@@ -203,7 +211,7 @@
                 @if (Model.ChargeLogs.Count == 0)
                 {
                     <div class="rounded-3xl border border-dashed border-gray-200 bg-gray-50 px-6 py-8 text-center text-sm font-semibold text-gray-600">
-                        No CHARGE_LOG data found for this MSISDN.
+                        No CHARGE_LOG data found for the selected filters.
                     </div>
                 }
                 else
@@ -233,6 +241,10 @@
                                             <div class="text-[11px] font-bold uppercase tracking-[0.14em] text-gray-400">Fee</div>
                                             <div class="mt-1 font-semibold text-gray-900">@FormatDecimal(item.Fee)</div>
                                         </div>
+                                        <div>
+                                            <div class="text-[11px] font-bold uppercase tracking-[0.14em] text-gray-400">Turn</div>
+                                            <div class="mt-1 font-semibold text-gray-900">@FormatDecimal(item.Turn)</div>
+                                        </div>
                                     </div>
                                     <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
                                         <div>
@@ -262,6 +274,7 @@
                                         <th class="px-4 py-3">MSISDN</th>
                                         <th class="px-4 py-3">Account ID</th>
                                         <th class="px-4 py-3">Fee</th>
+                                        <th class="px-4 py-3">Turn</th>
                                         <th class="px-4 py-3">Status</th>
                                         <th class="px-4 py-3">Charge Time</th>
                                         <th class="px-4 py-3">Insert Time</th>
@@ -276,6 +289,7 @@
                                             <td class="whitespace-nowrap px-4 py-4">@FormatText(item.Msisdn)</td>
                                             <td class="whitespace-nowrap px-4 py-4">@FormatText(item.AccountId)</td>
                                             <td class="whitespace-nowrap px-4 py-4 font-semibold text-gray-900">@FormatDecimal(item.Fee)</td>
+                                            <td class="whitespace-nowrap px-4 py-4 font-semibold text-gray-900">@FormatDecimal(item.Turn)</td>
                                             <td class="whitespace-nowrap px-4 py-4">@FormatDecimal(item.Status)</td>
                                             <td class="whitespace-nowrap px-4 py-4">@FormatDate(item.ChargeTime)</td>
                                             <td class="whitespace-nowrap px-4 py-4">@FormatDate(item.InsertTime)</td>