Browse Source

nâng cấp cms: thêm báo cáo theo giờ cho sub và doanh thu
thêm báo cáo thống kê giải thưởng

student 1 month ago
parent
commit
e5c0eb2a60

+ 12 - 0
FIle/KITTY_FALL.sql

@@ -1739,6 +1739,12 @@ DISABLE ROW MOVEMENT
 INSERT INTO "PERMISSION" VALUES ('2029', 'SubscriptionChargeReport', 'ACTIVE', '/SubscriptionChargeReport', 'VIEW');
 INSERT INTO "PERMISSION" VALUES ('2029', 'SubscriptionChargeReport', 'ACTIVE', '/SubscriptionChargeReport', 'VIEW');
 INSERT INTO "PERMISSION" VALUES ('2030', 'subscriptionchargereport index', 'ACTIVE', '/subscriptionchargereport/index', 'VIEW');
 INSERT INTO "PERMISSION" VALUES ('2030', 'subscriptionchargereport index', 'ACTIVE', '/subscriptionchargereport/index', 'VIEW');
 INSERT INTO "PERMISSION" VALUES ('2031', 'subscriptionchargereport', 'ACTIVE', '/admin/subscriptionchargereport', 'VIEW');
 INSERT INTO "PERMISSION" VALUES ('2031', 'subscriptionchargereport', 'ACTIVE', '/admin/subscriptionchargereport', 'VIEW');
+INSERT INTO "PERMISSION" VALUES ('2038', 'SubscriptionChargeReportNoDate', 'ACTIVE', '/SubscriptionChargeReportNoDate', 'VIEW');
+INSERT INTO "PERMISSION" VALUES ('2039', 'subscriptionchargereportnodate index', 'ACTIVE', '/subscriptionchargereportnodate/index', 'VIEW');
+INSERT INTO "PERMISSION" VALUES ('2040', 'subscriptionchargereportnodate admin', 'ACTIVE', '/admin/subscriptionchargereportnodate', 'VIEW');
+INSERT INTO "PERMISSION" VALUES ('2041', 'Monthly Reward Payout', 'ACTIVE', '/MonthlyRewardPayoutReport', 'VIEW');
+INSERT INTO "PERMISSION" VALUES ('2042', 'monthly reward payout index', 'ACTIVE', '/monthlyrewardpayoutreport/index', 'VIEW');
+INSERT INTO "PERMISSION" VALUES ('2043', 'monthly reward payout admin', 'ACTIVE', '/admin/monthlyrewardpayoutreport', 'VIEW');
 INSERT INTO "PERMISSION" VALUES ('1', 'Get User Register', 'ACTIVE', '/portal/user-register', '/');
 INSERT INTO "PERMISSION" VALUES ('1', 'Get User Register', 'ACTIVE', '/portal/user-register', '/');
 INSERT INTO "PERMISSION" VALUES ('1005', 'Revenue Reports', 'ACTIVE', '/RevenueReport', 'VIEW');
 INSERT INTO "PERMISSION" VALUES ('1005', 'Revenue Reports', 'ACTIVE', '/RevenueReport', 'VIEW');
 INSERT INTO "PERMISSION" VALUES ('1006', 'Revenue Reports', 'ACTIVE', '/RevenueReport/Index', 'VIEW');
 INSERT INTO "PERMISSION" VALUES ('1006', 'Revenue Reports', 'ACTIVE', '/RevenueReport/Index', 'VIEW');
@@ -1886,6 +1892,12 @@ INSERT INTO "ROLE_PERMISSION" VALUES ('2288', '1', '2029', 'ACTIVE');
 INSERT INTO "ROLE_PERMISSION" VALUES ('2289', '1', '1008', 'ACTIVE');
 INSERT INTO "ROLE_PERMISSION" VALUES ('2289', '1', '1008', 'ACTIVE');
 INSERT INTO "ROLE_PERMISSION" VALUES ('2308', '1', '2031', 'ACTIVE');
 INSERT INTO "ROLE_PERMISSION" VALUES ('2308', '1', '2031', 'ACTIVE');
 INSERT INTO "ROLE_PERMISSION" VALUES ('2309', '1', '2030', 'ACTIVE');
 INSERT INTO "ROLE_PERMISSION" VALUES ('2309', '1', '2030', 'ACTIVE');
+INSERT INTO "ROLE_PERMISSION" VALUES ('2313', '1', '2038', 'ACTIVE');
+INSERT INTO "ROLE_PERMISSION" VALUES ('2314', '1', '2039', 'ACTIVE');
+INSERT INTO "ROLE_PERMISSION" VALUES ('2315', '1', '2040', 'ACTIVE');
+INSERT INTO "ROLE_PERMISSION" VALUES ('2316', '1', '2041', 'ACTIVE');
+INSERT INTO "ROLE_PERMISSION" VALUES ('2317', '1', '2042', 'ACTIVE');
+INSERT INTO "ROLE_PERMISSION" VALUES ('2318', '1', '2043', 'ACTIVE');
 INSERT INTO "ROLE_PERMISSION" VALUES ('2310', '2004', '2029', 'ACTIVE');
 INSERT INTO "ROLE_PERMISSION" VALUES ('2310', '2004', '2029', 'ACTIVE');
 INSERT INTO "ROLE_PERMISSION" VALUES ('2311', '2004', '2031', 'ACTIVE');
 INSERT INTO "ROLE_PERMISSION" VALUES ('2311', '2004', '2031', 'ACTIVE');
 INSERT INTO "ROLE_PERMISSION" VALUES ('2312', '2004', '2030', 'ACTIVE');
 INSERT INTO "ROLE_PERMISSION" VALUES ('2312', '2004', '2030', 'ACTIVE');

+ 12 - 0
FIle/database_usesAndSub.sql

@@ -534,6 +534,12 @@ SET DEFINE OFF;
 Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2029,'SubscriptionChargeReport','ACTIVE','/SubscriptionChargeReport','VIEW');
 Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2029,'SubscriptionChargeReport','ACTIVE','/SubscriptionChargeReport','VIEW');
 Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2030,'subscriptionchargereport index','ACTIVE','/subscriptionchargereport/index','VIEW');
 Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2030,'subscriptionchargereport index','ACTIVE','/subscriptionchargereport/index','VIEW');
 Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2031,'subscriptionchargereport','ACTIVE','/admin/subscriptionchargereport','VIEW');
 Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2031,'subscriptionchargereport','ACTIVE','/admin/subscriptionchargereport','VIEW');
+Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2038,'SubscriptionChargeReportNoDate','ACTIVE','/SubscriptionChargeReportNoDate','VIEW');
+Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2039,'subscriptionchargereportnodate index','ACTIVE','/subscriptionchargereportnodate/index','VIEW');
+Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2040,'subscriptionchargereportnodate admin','ACTIVE','/admin/subscriptionchargereportnodate','VIEW');
+Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2041,'Monthly Reward Payout','ACTIVE','/MonthlyRewardPayoutReport','VIEW');
+Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2042,'monthly reward payout index','ACTIVE','/monthlyrewardpayoutreport/index','VIEW');
+Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2043,'monthly reward payout admin','ACTIVE','/admin/monthlyrewardpayoutreport','VIEW');
 Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2032,'cmsjobposting','ACTIVE','/cmsjobposting','VIEW');
 Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2032,'cmsjobposting','ACTIVE','/cmsjobposting','VIEW');
 Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2033,'cmsjobposting index','ACTIVE','/cmsjobposting/index','VIEW');
 Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2033,'cmsjobposting index','ACTIVE','/cmsjobposting/index','VIEW');
 Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2034,'cmsjobposting create','ACTIVE','/cmsjobposting/create','EDIT');
 Insert into PERMISSION (ID,NAME,STATUS,PATH,ACTION) values (2034,'cmsjobposting create','ACTIVE','/cmsjobposting/create','EDIT');
@@ -680,6 +686,12 @@ Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2306,1,201
 Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2307,1,2022,'ACTIVE');
 Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2307,1,2022,'ACTIVE');
 Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2308,1,2031,'ACTIVE');
 Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2308,1,2031,'ACTIVE');
 Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2309,1,2030,'ACTIVE');
 Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2309,1,2030,'ACTIVE');
+Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2313,1,2038,'ACTIVE');
+Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2314,1,2039,'ACTIVE');
+Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2315,1,2040,'ACTIVE');
+Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2316,1,2041,'ACTIVE');
+Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2317,1,2042,'ACTIVE');
+Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2318,1,2043,'ACTIVE');
 Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2310,2004,2029,'ACTIVE');
 Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2310,2004,2029,'ACTIVE');
 Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2311,2004,2031,'ACTIVE');
 Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2311,2004,2031,'ACTIVE');
 Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2312,2004,2030,'ACTIVE');
 Insert into ROLE_PERMISSION (ID,ROLE_ID,PERMISSION_ID,STATUS) values (2312,2004,2030,'ACTIVE');

+ 15 - 9
KittyFallWs/KittyFallWs/src/com/vas/webservices/KittyFallWs.java

@@ -975,7 +975,7 @@ public class KittyFallWs extends WebserviceAbstract {
                         String REQ = reqDf.format(new Date()) + "1" + msisdn.substring(Common.COUNTRY_CODE.length());
                         String REQ = reqDf.format(new Date()) + "1" + msisdn.substring(Common.COUNTRY_CODE.length());
                         logger.info("after get product info " + fee);
                         logger.info("after get product info " + fee);
                         // right -----------------------------------------------
                         // right -----------------------------------------------
-//                        resultPaid = ws.requestMpsOtp(msisdn, fee, REQ, productInfo.getProductName());
+                        resultPaid = ws.requestMpsOtp(msisdn, fee, REQ, productInfo.getProductName());
                         
                         
                         String errorCode = resultPaid.split("\\|")[0];
                         String errorCode = resultPaid.split("\\|")[0];
                         if (WSProcessor.LIST_SUCCESS_RES.contains(resultPaid)) {
                         if (WSProcessor.LIST_SUCCESS_RES.contains(resultPaid)) {
@@ -1988,7 +1988,17 @@ public class KittyFallWs extends WebserviceAbstract {
                     response.setContent("Authenticate fail!");
                     response.setContent("Authenticate fail!");
                 } else {
                 } else {
                     msisdn = formatMsisdn(msisdn);
                     msisdn = formatMsisdn(msisdn);
-                    int moneyCharge = Common.ADD_SPIN_FEE;
+                    ProductInfo productInfo = getProductByName(packageName);
+                    if (productInfo == null) {
+                        response.setErrorCode(Common.ErrorCode.QUERY_ERROR);
+                        response.setContent("Your package name is not valid");
+                        return response;
+                    }
+                    int moneyCharge = 0;
+
+                    if (productInfo != null) {
+                        moneyCharge = (int) Math.round(productInfo.getFee()); 
+                    }
 //                    String fee = "0";
 //                    String fee = "0";
 //                    if (fee != null && fee.length() > 0) {
 //                    if (fee != null && fee.length() > 0) {
 //                        moneyCharge = Integer.parseInt(fee);
 //                        moneyCharge = Integer.parseInt(fee);
@@ -1998,16 +2008,12 @@ public class KittyFallWs extends WebserviceAbstract {
 //                        response.setContent("Charge fail");
 //                        response.setContent("Charge fail");
 //                        return response;
 //                        return response;
 //                    }
 //                    }
-                    ProductInfo productInfo = getProductByName(packageName);
-                    if (productInfo == null) {
-                        response.setErrorCode(Common.ErrorCode.QUERY_ERROR);
-                        response.setContent("Your package name is not valid");
-                        return response;
-                    }
+                    
+                    
                     String REQ = reqDf.format(new Date()) + "1" + msisdn.substring(Common.COUNTRY_CODE.length());
                     String REQ = reqDf.format(new Date()) + "1" + msisdn.substring(Common.COUNTRY_CODE.length());
 
 
                     // charge
                     // charge
-                    String resultPaid = ws.chargeFeeOtp(msisdn, (int) productInfo.getFee(), REQ);
+                    String resultPaid = ws.chargeFeeOtp(msisdn, moneyCharge, REQ);
 //                    String resultPaid = "0";
 //                    String resultPaid = "0";
                     boolean registered = false;
                     boolean registered = false;
                     List<RegisterInfo> listRegInday = null;
                     List<RegisterInfo> listRegInday = null;

+ 24 - 6
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Controllers/CmsAccountController.cs

@@ -271,13 +271,31 @@ public class CmsAccountController : CmsAuthorizedController
             var requestedPermissionIds = (permissionIds ?? new List<decimal>())
             var requestedPermissionIds = (permissionIds ?? new List<decimal>())
                 .Distinct()
                 .Distinct()
                 .ToList();
                 .ToList();
-            var validPermissionIds = await _db.Permissions.AsNoTracking()
-                .Where(x => requestedPermissionIds.Contains(x.Id))
-                .Select(x => x.Id)
-                .ToListAsync();
-            if (validPermissionIds.Count != requestedPermissionIds.Count)
+            var validPermissionIds = new List<decimal>();
+            var invalidPermissionIds = new List<decimal>();
+            foreach (var permissionId in requestedPermissionIds)
             {
             {
-                return Json(new { errorCode = 400, message = "One or more permissions are invalid." });
+                var permissionExists = await _db.Permissions
+                    .AsNoTracking()
+                    .AnyAsync(x => x.Id == permissionId);
+
+                if (permissionExists)
+                {
+                    validPermissionIds.Add(permissionId);
+                }
+                else
+                {
+                    invalidPermissionIds.Add(permissionId);
+                }
+            }
+
+            if (invalidPermissionIds.Count > 0)
+            {
+                return Json(new
+                {
+                    errorCode = 400,
+                    message = $"Invalid permission IDs: {string.Join(", ", invalidPermissionIds)}. Please reload the page and try again."
+                });
             }
             }
 
 
             var replacements = new List<RolePermission>();
             var replacements = new List<RolePermission>();

+ 115 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Controllers/MonthlyRewardPayoutReportController.cs

@@ -0,0 +1,115 @@
+using System.Globalization;
+using Database.Database;
+using Kitty_Fall.Cms.Helpers;
+using Kitty_Fall.Cms.Models;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace Kitty_Fall.Cms.Controllers;
+
+public class MonthlyRewardPayoutReportController : CmsAuthorizedController
+{
+    private const int PageSize = 20;
+    private const decimal DataToMmkRate = 0.665m;
+    private const decimal LoyaltyToMmkRate = 1.33m;
+    private readonly ModelContext _db;
+
+    public MonthlyRewardPayoutReportController(ModelContext db)
+    {
+        _db = db;
+    }
+
+    [HttpGet("/MonthlyRewardPayoutReport")]
+    [HttpGet("/MonthlyRewardPayoutReport/Index")]
+    [HttpGet("/Admin/MonthlyRewardPayoutReport")]
+    public async Task<IActionResult> Index(string? selectedMonth = null, int page = 1)
+    {
+        if (!CheckLogin()) return RedirectToLogin();
+        if (!HasMenuPermission(CmsSecurityHelper.MonthlyRewardPayoutReportsMenuKey)) return RedirectNoPermission();
+
+        var monthText = string.IsNullOrWhiteSpace(selectedMonth)
+            ? DateTime.Today.ToString("yyyy-MM", CultureInfo.InvariantCulture)
+            : selectedMonth.Trim();
+        var isValidMonth = DateTime.TryParseExact(
+            monthText,
+            "yyyy-MM",
+            CultureInfo.InvariantCulture,
+            DateTimeStyles.None,
+            out var monthStart);
+
+        var model = new MonthlyRewardPayoutReportViewModel
+        {
+            SelectedMonth = monthText,
+            IsInvalidMonth = !isValidMonth,
+            Page = Math.Max(1, page),
+            PageSize = PageSize
+        };
+
+        if (!isValidMonth) return View(model);
+
+        monthStart = new DateTime(monthStart.Year, monthStart.Month, 1);
+        var monthEnd = monthStart.AddMonths(1);
+
+        var rewardOptions = await _db.GameLevelRewardOptions
+            .AsNoTracking()
+            .Where(x => x.RewardType == "DATA" || x.RewardType == "LOYALTY")
+            .Select(x => new { x.RewardType, x.RewardValue, x.RewardUnit })
+            .Distinct()
+            .ToListAsync();
+
+        var payouts = await _db.GameRewardLogs
+            .AsNoTracking()
+            .Where(x => x.Status == 2
+                && x.CreatedTime >= monthStart
+                && x.CreatedTime < monthEnd
+                && (x.RewardType == "DATA" || x.RewardType == "LOYALTY"))
+            .GroupBy(x => new { PayoutDate = x.CreatedTime.Date, x.RewardType, x.RewardValue, x.RewardUnit })
+            .Select(x => new
+            {
+                x.Key.PayoutDate,
+                x.Key.RewardType,
+                x.Key.RewardValue,
+                x.Key.RewardUnit,
+                Count = x.Count()
+            })
+            .ToListAsync();
+
+        var items = payouts
+            .Where(payout => rewardOptions.Any(option =>
+                option.RewardType == payout.RewardType
+                && option.RewardValue == payout.RewardValue
+                && option.RewardUnit == payout.RewardUnit))
+            .Select(payout => new MonthlyRewardPayoutItemViewModel
+            {
+                PayoutDate = payout.PayoutDate,
+                RewardType = payout.RewardType,
+                RewardValue = payout.RewardValue,
+                RewardUnit = payout.RewardUnit,
+                PayoutCount = payout.Count,
+                MmkRate = GetMmkRate(payout.RewardType)
+            })
+            .OrderByDescending(x => x.PayoutDate)
+            .ThenBy(x => x.RewardType)
+            .ThenBy(x => x.RewardValue)
+            .ToList();
+
+        model.TotalRows = items.Count;
+        model.TotalPages = Math.Max(1, (int)Math.Ceiling(model.TotalRows / (double)PageSize));
+        model.Page = Math.Min(model.Page, model.TotalPages);
+        model.Items = items
+            .Skip((model.Page - 1) * PageSize)
+            .Take(PageSize)
+            .ToList();
+
+        return View(model);
+    }
+
+    private static decimal GetMmkRate(string rewardType)
+    {
+        return string.Equals(rewardType, "DATA", StringComparison.OrdinalIgnoreCase)
+            ? DataToMmkRate
+            : string.Equals(rewardType, "LOYALTY", StringComparison.OrdinalIgnoreCase)
+                ? LoyaltyToMmkRate
+                : 0;
+    }
+}

+ 48 - 18
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Controllers/RevenueReportController.cs

@@ -9,6 +9,7 @@ namespace Kitty_Fall.Cms.Controllers;
 
 
 public class RevenueReportController : CmsAuthorizedController
 public class RevenueReportController : CmsAuthorizedController
 {
 {
+    private const int PageSize = 10;
     private readonly ModelContext _db;
     private readonly ModelContext _db;
     private readonly IReadOnlyList<RevenueReportServiceOption> _services;
     private readonly IReadOnlyList<RevenueReportServiceOption> _services;
     private readonly string _defaultService;
     private readonly string _defaultService;
@@ -39,44 +40,73 @@ public class RevenueReportController : CmsAuthorizedController
     [HttpGet("/RevenueReport")]
     [HttpGet("/RevenueReport")]
     [HttpGet("/RevenueReport/Index")]
     [HttpGet("/RevenueReport/Index")]
     [HttpGet("/Admin/RevenueReport")]
     [HttpGet("/Admin/RevenueReport")]
-    public async Task<IActionResult> Index(string? reportType = null, DateTime? fromDate = null, DateTime? toDate = null, string? serviceFilter = null)
+    public async Task<IActionResult> Index(string? tab = null, DateTime? fromDate = null, DateTime? toDate = null, string? serviceFilter = null, int page = 1)
     {
     {
         if (!CheckLogin()) return RedirectToLogin();
         if (!CheckLogin()) return RedirectToLogin();
         if (!HasMenuPermission(CmsSecurityHelper.RevenueReportsMenuKey)) return RedirectNoPermission();
         if (!HasMenuPermission(CmsSecurityHelper.RevenueReportsMenuKey)) return RedirectNoPermission();
 
 
-        var normalizedType = NormalizeReportType(reportType);
+        var activeTab = string.Equals(tab?.Trim(), "subscriptions", StringComparison.OrdinalIgnoreCase)
+            ? "subscriptions"
+            : "revenue";
         var (normalizedFromDate, normalizedToDate) = NormalizeDateRange(fromDate, toDate);
         var (normalizedFromDate, normalizedToDate) = NormalizeDateRange(fromDate, toDate);
         var normalizedServiceFilter = NormalizeServiceFilter(serviceFilter);
         var normalizedServiceFilter = NormalizeServiceFilter(serviceFilter);
 
 
-        var items = await BuildBaseQuery(normalizedType, normalizedFromDate, normalizedToDate, normalizedServiceFilter)
+        var hourlyRecords = await BuildBaseQuery(CommonConstants.HOUR_REPORT, normalizedFromDate, normalizedToDate, normalizedServiceFilter)
             .OrderByDescending(x => x.TimeReport)
             .OrderByDescending(x => x.TimeReport)
-            .Select(x => new RevenueReportItemViewModel
+            .Select(x => new
             {
             {
                 TimeReport = x.TimeReport,
                 TimeReport = x.TimeReport,
-                Type = x.Type ?? string.Empty,
+                ServiceName = x.ServiceName ?? string.Empty,
                 Revenue = x.Revenue ?? 0,
                 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
+                TotalSubscriptions = x.UserActive ?? 0
             })
             })
             .ToListAsync();
             .ToListAsync();
 
 
+        var hourlyItems = hourlyRecords
+            .GroupBy(x => new { x.ServiceName, ReportDate = x.TimeReport.Date })
+            .Select(group =>
+            {
+                var hours = new decimal[24];
+                foreach (var record in group)
+                {
+                    hours[record.TimeReport.Hour] += activeTab == "subscriptions"
+                        ? record.TotalSubscriptions
+                        : record.Revenue;
+                }
+
+                return new HourlyRevenueReportRowViewModel
+                {
+                    ServiceName = group.Key.ServiceName,
+                    ReportDate = group.Key.ReportDate,
+                    Hours = hours
+                };
+            })
+            .OrderByDescending(x => x.ReportDate)
+            .ThenBy(x => x.ServiceName)
+            .ToList();
+
+        var totalRows = hourlyItems.Count;
+        var totalPages = Math.Max(1, (int)Math.Ceiling(totalRows / (double)PageSize));
+        var currentPage = Math.Min(Math.Max(1, page), totalPages);
+        var pagedItems = hourlyItems
+            .Skip((currentPage - 1) * PageSize)
+            .Take(PageSize)
+            .ToList();
+
         var model = new RevenueReportViewModel
         var model = new RevenueReportViewModel
         {
         {
-            ReportType = normalizedType,
+            ReportType = CommonConstants.HOUR_REPORT,
+            ActiveTab = activeTab,
             FromDate = normalizedFromDate,
             FromDate = normalizedFromDate,
             ToDate = normalizedToDate,
             ToDate = normalizedToDate,
             ServiceFilter = normalizedServiceFilter,
             ServiceFilter = normalizedServiceFilter,
             Services = _services.ToList(),
             Services = _services.ToList(),
-            TotalRows = items.Count,
-            TotalRevenue = items.Sum(x => x.Revenue),
-            AverageRevenue = items.Count == 0 ? 0 : items.Average(x => x.Revenue),
-            LatestReportTime = items.FirstOrDefault()?.TimeReport,
-            Items = items
+            TotalRows = totalRows,
+            Page = currentPage,
+            PageSize = PageSize,
+            TotalPages = totalPages,
+            LatestReportTime = hourlyRecords.FirstOrDefault()?.TimeReport,
+            HourlyItems = pagedItems
         };
         };
 
 
         return View(model);
         return View(model);

+ 44 - 11
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Controllers/SubscriptionChargeReportController.cs

@@ -26,19 +26,44 @@ public class SubscriptionChargeReportController : CmsAuthorizedController
         if (!CheckLogin()) return RedirectToLogin();
         if (!CheckLogin()) return RedirectToLogin();
         if (!HasMenuPermission(CmsSecurityHelper.SubscriptionChargeReportsMenuKey)) return RedirectNoPermission();
         if (!HasMenuPermission(CmsSecurityHelper.SubscriptionChargeReportsMenuKey)) return RedirectNoPermission();
 
 
+        return await BuildReport(msisdn, selectedDate, regPage, chargePage, true);
+    }
+
+    [HttpGet("/SubscriptionChargeReportNoDate")]
+    [HttpGet("/SubscriptionChargeReportNoDate/Index")]
+    [HttpGet("/Admin/SubscriptionChargeReportNoDate")]
+    public async Task<IActionResult> IndexNoDate(string? msisdn = null, int regPage = 1, int chargePage = 1)
+    {
+        if (!CheckLogin()) return RedirectToLogin();
+        if (!HasMenuPermission(CmsSecurityHelper.SubscriptionChargeReportsNoDateMenuKey)) return RedirectNoPermission();
+
+        return await BuildReport(msisdn, null, regPage, chargePage, false);
+    }
+
+    private async Task<IActionResult> BuildReport(
+        string? msisdn,
+        string? selectedDate,
+        int regPage,
+        int chargePage,
+        bool filterByDate)
+    {
+
         var searchMsisdn = NormalizeRawMsisdn(msisdn);
         var searchMsisdn = NormalizeRawMsisdn(msisdn);
         var hasSearch = !string.IsNullOrWhiteSpace(searchMsisdn);
         var hasSearch = !string.IsNullOrWhiteSpace(searchMsisdn);
         var normalizedMsisdn = hasSearch
         var normalizedMsisdn = hasSearch
             ? CommonLogic.ValidateMsisdn(CommonConstants.CountryCode, msisdn)
             ? CommonLogic.ValidateMsisdn(CommonConstants.CountryCode, msisdn)
             : null;
             : null;
         var isInvalidMsisdn = hasSearch && string.IsNullOrWhiteSpace(normalizedMsisdn);
         var isInvalidMsisdn = hasSearch && string.IsNullOrWhiteSpace(normalizedMsisdn);
-        var hasDateValue = !string.IsNullOrWhiteSpace(selectedDate);
+        var hasDateValue = filterByDate && !string.IsNullOrWhiteSpace(selectedDate);
         var reportDate = DateTime.Today;
         var reportDate = DateTime.Today;
         var isInvalidDate = hasDateValue && !DateTime.TryParseExact(
         var isInvalidDate = hasDateValue && !DateTime.TryParseExact(
             selectedDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out reportDate);
             selectedDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out reportDate);
 
 
         var model = new SubscriptionChargeReportViewModel
         var model = new SubscriptionChargeReportViewModel
         {
         {
+            ReportUrl = filterByDate ? "/SubscriptionChargeReport" : "/SubscriptionChargeReportNoDate",
+            ReportTitle = filterByDate ? "Subscription & Charge Check" : "Subscription & Charge Check (No Date)",
+            ShowDateFilter = filterByDate,
             Msisdn = (msisdn ?? string.Empty).Trim(),
             Msisdn = (msisdn ?? string.Empty).Trim(),
             NormalizedMsisdn = normalizedMsisdn,
             NormalizedMsisdn = normalizedMsisdn,
             HasSearch = hasSearch,
             HasSearch = hasSearch,
@@ -46,7 +71,7 @@ public class SubscriptionChargeReportController : CmsAuthorizedController
             MsisdnErrorMessage = isInvalidMsisdn
             MsisdnErrorMessage = isInvalidMsisdn
                 ? "Invalid MSISDN. Please enter digits only, with or without country code."
                 ? "Invalid MSISDN. Please enter digits only, with or without country code."
                 : null,
                 : null,
-            SelectedDate = isInvalidDate
+            SelectedDate = !filterByDate ? string.Empty : isInvalidDate
                 ? (selectedDate ?? string.Empty).Trim()
                 ? (selectedDate ?? string.Empty).Trim()
                 : reportDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
                 : reportDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
             IsInvalidDate = isInvalidDate,
             IsInvalidDate = isInvalidDate,
@@ -61,7 +86,7 @@ public class SubscriptionChargeReportController : CmsAuthorizedController
             return View("~/Views/Reports/SubscriptionCharge.cshtml", model);
             return View("~/Views/Reports/SubscriptionCharge.cshtml", model);
         }
         }
 
 
-        var selectedDay = reportDate.Date;
+        DateTime? selectedDay = filterByDate ? reportDate.Date : null;
         var regQuery = BuildRegInfoQuery(searchMsisdn, normalizedMsisdn, selectedDay);
         var regQuery = BuildRegInfoQuery(searchMsisdn, normalizedMsisdn, selectedDay);
         model.RegTotalRows = await regQuery.CountAsync();
         model.RegTotalRows = await regQuery.CountAsync();
         model.RegTotalPages = CalculateTotalPages(model.RegTotalRows);
         model.RegTotalPages = CalculateTotalPages(model.RegTotalRows);
@@ -134,7 +159,7 @@ public class SubscriptionChargeReportController : CmsAuthorizedController
         return View("~/Views/Reports/SubscriptionCharge.cshtml", model);
         return View("~/Views/Reports/SubscriptionCharge.cshtml", model);
     }
     }
 
 
-    private IQueryable<RegInfo> BuildRegInfoQuery(string searchMsisdn, string? normalizedMsisdn, DateTime selectedDay)
+    private IQueryable<RegInfo> BuildRegInfoQuery(string searchMsisdn, string? normalizedMsisdn, DateTime? selectedDay)
     {
     {
         var query = _db.RegInfos.AsNoTracking();
         var query = _db.RegInfos.AsNoTracking();
         if (!string.IsNullOrWhiteSpace(searchMsisdn))
         if (!string.IsNullOrWhiteSpace(searchMsisdn))
@@ -144,13 +169,17 @@ public class SubscriptionChargeReportController : CmsAuthorizedController
                 : query.Where(x => x.Msisdn == searchMsisdn);
                 : 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);
+        if (selectedDay.HasValue)
+        {
+            var start = selectedDay.Value;
+            var end = start.AddDays(1);
+            query = query.Where(x => x.RegisterTime.HasValue && x.RegisterTime.Value >= start && x.RegisterTime.Value < end);
+        }
 
 
         return query;
         return query;
     }
     }
 
 
-    private IQueryable<ChargeLog> BuildChargeLogQuery(string searchMsisdn, string? normalizedMsisdn, DateTime selectedDay)
+    private IQueryable<ChargeLog> BuildChargeLogQuery(string searchMsisdn, string? normalizedMsisdn, DateTime? selectedDay)
     {
     {
         var query = _db.ChargeLogs.AsNoTracking();
         var query = _db.ChargeLogs.AsNoTracking();
         if (!string.IsNullOrWhiteSpace(searchMsisdn))
         if (!string.IsNullOrWhiteSpace(searchMsisdn))
@@ -160,10 +189,14 @@ public class SubscriptionChargeReportController : CmsAuthorizedController
                 : query.Where(x => x.Msisdn == searchMsisdn);
                 : 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));
+        if (selectedDay.HasValue)
+        {
+            var start = selectedDay.Value;
+            var end = start.AddDays(1);
+            query = query.Where(x =>
+                (x.ChargeTime.HasValue && x.ChargeTime.Value >= start && x.ChargeTime.Value < end) ||
+                (!x.ChargeTime.HasValue && x.InsertTime.HasValue && x.InsertTime.Value >= start && x.InsertTime.Value < end));
+        }
 
 
         return query;
         return query;
     }
     }

+ 28 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Helpers/CmsSecurityHelper.cs

@@ -17,6 +17,8 @@ public static class CmsSecurityHelper
     public const string RevenueReportsMenuKey = "revenue-reports";
     public const string RevenueReportsMenuKey = "revenue-reports";
     public const string DailyReportsMenuKey = "daily-reports";
     public const string DailyReportsMenuKey = "daily-reports";
     public const string SubscriptionChargeReportsMenuKey = "subscription-charge-reports";
     public const string SubscriptionChargeReportsMenuKey = "subscription-charge-reports";
+    public const string SubscriptionChargeReportsNoDateMenuKey = "subscription-charge-reports-no-date";
+    public const string MonthlyRewardPayoutReportsMenuKey = "monthly-reward-payout-reports";
     public const string MonthlyWinnersMenuKey = "monthly-winners";
     public const string MonthlyWinnersMenuKey = "monthly-winners";
     public const string EmployersMenuKey = "employers";
     public const string EmployersMenuKey = "employers";
     public const string AccountsMenuKey = "accounts";
     public const string AccountsMenuKey = "accounts";
@@ -201,6 +203,32 @@ public static class CmsSecurityHelper
             }
             }
         },
         },
         new()
         new()
+        {
+            Key = SubscriptionChargeReportsNoDateMenuKey,
+            Title = "SubscriptionChargeReportNoDate",
+            Url = "/SubscriptionChargeReportNoDate",
+            IconClass = "fas fa-receipt",
+            PermissionPaths = new List<string>
+            {
+                "/subscriptionchargereportnodate",
+                "/subscriptionchargereportnodate/index",
+                "/admin/subscriptionchargereportnodate"
+            }
+        },
+        new()
+        {
+            Key = MonthlyRewardPayoutReportsMenuKey,
+            Title = "Monthly Reward Payout",
+            Url = "/MonthlyRewardPayoutReport",
+            IconClass = "fas fa-gift",
+            PermissionPaths = new List<string>
+            {
+                "/monthlyrewardpayoutreport",
+                "/monthlyrewardpayoutreport/index",
+                "/admin/monthlyrewardpayoutreport"
+            }
+        },
+        new()
         {
         {
             Key = MonthlyWinnersMenuKey,
             Key = MonthlyWinnersMenuKey,
             Title = "Monthly Winners",
             Title = "Monthly Winners",

+ 43 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Models/MonthlyRewardPayoutReportViewModel.cs

@@ -0,0 +1,43 @@
+namespace Kitty_Fall.Cms.Models;
+
+public class MonthlyRewardPayoutReportViewModel
+{
+    public string SelectedMonth { get; set; } = string.Empty;
+
+    public bool IsInvalidMonth { get; set; }
+
+    public int Page { get; set; } = 1;
+
+    public int PageSize { get; set; } = 20;
+
+    public int TotalRows { get; set; }
+
+    public int TotalPages { get; set; } = 1;
+
+    public List<MonthlyRewardPayoutItemViewModel> Items { get; set; } = new();
+
+    public decimal TotalPayoutCount => Items.Sum(x => x.PayoutCount);
+
+    public decimal TotalMmk => Items.Sum(x => x.TotalMmk);
+}
+
+public class MonthlyRewardPayoutItemViewModel
+{
+    public DateTime PayoutDate { get; set; }
+
+    public string RewardType { get; set; } = string.Empty;
+
+    public decimal RewardValue { get; set; }
+
+    public string RewardUnit { get; set; } = string.Empty;
+
+    public decimal PayoutCount { get; set; }
+
+    public decimal TotalRewardValue => RewardValue * PayoutCount;
+
+    public decimal MmkRate { get; set; }
+
+    public decimal MmkPerPrize => RewardValue * MmkRate;
+
+    public decimal TotalMmk => TotalRewardValue * MmkRate;
+}

+ 21 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Models/RevenueReportViewModel.cs

@@ -2,6 +2,8 @@ namespace Kitty_Fall.Cms.Models;
 
 
 public class RevenueReportViewModel
 public class RevenueReportViewModel
 {
 {
+    public string ActiveTab { get; set; } = "revenue";
+
     public string ReportType { get; set; } = string.Empty;
     public string ReportType { get; set; } = string.Empty;
 
 
     public DateTime? FromDate { get; set; }
     public DateTime? FromDate { get; set; }
@@ -14,6 +16,12 @@ public class RevenueReportViewModel
 
 
     public int TotalRows { get; set; }
     public int TotalRows { get; set; }
 
 
+    public int Page { get; set; } = 1;
+
+    public int PageSize { get; set; } = 10;
+
+    public int TotalPages { get; set; } = 1;
+
     public decimal TotalRevenue { get; set; }
     public decimal TotalRevenue { get; set; }
 
 
     public decimal AverageRevenue { get; set; }
     public decimal AverageRevenue { get; set; }
@@ -21,6 +29,19 @@ public class RevenueReportViewModel
     public DateTime? LatestReportTime { get; set; }
     public DateTime? LatestReportTime { get; set; }
 
 
     public List<RevenueReportItemViewModel> Items { get; set; } = new();
     public List<RevenueReportItemViewModel> Items { get; set; } = new();
+
+    public List<HourlyRevenueReportRowViewModel> HourlyItems { get; set; } = new();
+}
+
+public class HourlyRevenueReportRowViewModel
+{
+    public string ServiceName { get; set; } = string.Empty;
+
+    public DateTime ReportDate { get; set; }
+
+    public decimal[] Hours { get; set; } = new decimal[24];
+
+    public decimal Total => Hours.Sum();
 }
 }
 
 
 public class RevenueTrendReportViewModel
 public class RevenueTrendReportViewModel

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

@@ -2,6 +2,12 @@ namespace Kitty_Fall.Cms.Models;
 
 
 public class SubscriptionChargeReportViewModel
 public class SubscriptionChargeReportViewModel
 {
 {
+    public string ReportUrl { get; set; } = "/SubscriptionChargeReport";
+
+    public string ReportTitle { get; set; } = "Subscription & Charge Check";
+
+    public bool ShowDateFilter { get; set; } = true;
+
     public string Msisdn { get; set; } = string.Empty;
     public string Msisdn { get; set; } = string.Empty;
 
 
     public string? NormalizedMsisdn { get; set; }
     public string? NormalizedMsisdn { get; set; }

+ 10 - 1
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Views/CmsAccount/RoleDetails.cshtml

@@ -77,7 +77,12 @@
 
 
         $('#permissionForm').on('submit', function (e) {
         $('#permissionForm').on('submit', function (e) {
             e.preventDefault();
             e.preventDefault();
-            const formData = $(this).serialize();
+            const form = $(this);
+            const submitButton = form.find('button[type="submit"]');
+            if (submitButton.prop('disabled')) return;
+
+            submitButton.prop('disabled', true);
+            const formData = form.serialize();
             
             
             $.post('@Url.Content("~/CmsAccount/SaveRolePermissions")', formData, function (res) {
             $.post('@Url.Content("~/CmsAccount/SaveRolePermissions")', formData, function (res) {
                 if (res.errorCode === 200) {
                 if (res.errorCode === 200) {
@@ -87,7 +92,11 @@
                     }, 1000);
                     }, 1000);
                 } else {
                 } else {
                     showToast(res.message, 'error');
                     showToast(res.message, 'error');
+                    submitButton.prop('disabled', false);
                 }
                 }
+            }).fail(function () {
+                showToast('Unable to update permissions. Please try again.', 'error');
+                submitButton.prop('disabled', false);
             });
             });
         });
         });
     </script>
     </script>

+ 99 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Views/MonthlyRewardPayoutReport/Index.cshtml

@@ -0,0 +1,99 @@
+@model Kitty_Fall.Cms.Models.MonthlyRewardPayoutReportViewModel
+@{
+    ViewData["Title"] = "Monthly Reward Payout";
+    string Number(decimal value) => value.ToString("#,##0.##");
+    string Money(decimal value) => value.ToString("#,##0.00");
+    string PageUrl(int page) => Url.Content("~/MonthlyRewardPayoutReport")
+        + "?selectedMonth=" + System.Uri.EscapeDataString(Model.SelectedMonth)
+        + "&page=" + page;
+}
+
+<div class="py-6 sm:py-8">
+    <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">
+            <h1 class="text-2xl font-bold text-gray-900">Monthly Reward Payout Report</h1>
+
+            <form method="get" action="@Url.Content("~/MonthlyRewardPayoutReport")" class="mt-5 flex flex-col gap-3 sm:flex-row sm:items-end">
+                <label class="block sm:w-64">
+                    <span class="mb-2 block text-sm font-semibold text-gray-700">Month</span>
+                    <input type="month" name="selectedMonth" value="@Model.SelectedMonth" class="w-full rounded-2xl border border-gray-200 px-4 py-3 text-sm outline-none transition focus:border-admin-active focus:ring-2 focus:ring-purple-100" />
+                </label>
+                <button type="submit" class="rounded-2xl bg-admin-active px-5 py-3 text-sm font-semibold text-white transition hover:bg-purple-700">Apply filter</button>
+            </form>
+
+            @if (Model.IsInvalidMonth)
+            {
+                <div class="mt-4 rounded-2xl border border-red-100 bg-red-50 px-4 py-3 text-sm font-semibold text-red-700">Invalid month. Please select a valid month.</div>
+            }
+        </div>
+
+        @if (!Model.IsInvalidMonth)
+        {
+            <div class="px-5 py-6 sm:px-6">
+                <div class="mb-4 flex items-center justify-between gap-3">
+                    <div class="text-sm text-gray-500">@Model.TotalRows.ToString("#,##0") rows found</div>
+                    <div class="text-xs font-semibold text-gray-400">Page @Model.Page / @Model.TotalPages</div>
+                </div>
+                <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-wider text-gray-500">
+                                    <th class="px-4 py-3">Date</th>
+                                    <th class="px-4 py-3">Reward type</th>
+                                    <th class="px-4 py-3">Prize value</th>
+                                    <th class="px-4 py-3">Paid count</th>
+                                    <th class="px-4 py-3">Total reward</th>
+                                    <th class="px-4 py-3">MMK / unit</th>
+                                    <th class="px-4 py-3">MMK / prize</th>
+                                    <th class="px-4 py-3">Total MMK</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.PayoutDate.ToString("dd/MM/yyyy")</td>
+                                        <td class="px-4 py-4"><span class="rounded-full px-3 py-1 text-xs font-bold @(item.RewardType == "DATA" ? "bg-blue-50 text-blue-700" : "bg-amber-50 text-amber-700")">@item.RewardType</span></td>
+                                        <td class="whitespace-nowrap px-4 py-4 font-semibold text-gray-900">@Number(item.RewardValue) @item.RewardUnit</td>
+                                        <td class="whitespace-nowrap px-4 py-4">@Number(item.PayoutCount)</td>
+                                        <td class="whitespace-nowrap px-4 py-4">@Number(item.TotalRewardValue) @item.RewardUnit</td>
+                                        <td class="whitespace-nowrap px-4 py-4">@Number(item.MmkRate)</td>
+                                        <td class="whitespace-nowrap px-4 py-4">@Money(item.MmkPerPrize) MMK</td>
+                                        <td class="whitespace-nowrap px-4 py-4 font-bold text-gray-900">@Money(item.TotalMmk) MMK</td>
+                                    </tr>
+                                }
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+
+                @if (Model.TotalPages > 1)
+                {
+                    var start = Math.Max(1, Model.Page - 2);
+                    var end = Math.Min(Model.TotalPages, start + 4);
+                    start = Math.Max(1, end - 4);
+
+                    <nav class="mt-5 flex flex-wrap items-center justify-between gap-3" aria-label="Reward payout pagination">
+                        <div class="text-xs text-gray-400">Page <span class="font-bold text-gray-700">@Model.Page</span> / <span class="font-bold text-gray-700">@Model.TotalPages</span></div>
+                        <div class="flex items-center gap-1">
+                            <a href="@PageUrl(Math.Max(1, Model.Page - 1))" class="rounded-lg px-3 py-2 text-xs font-bold text-gray-500 transition hover:bg-gray-100"><i class="fas fa-angle-left"></i></a>
+                            @for (var p = start; p <= end; p++)
+                            {
+                                if (p == Model.Page)
+                                {
+                                    <span class="min-w-[36px] rounded-lg bg-admin-active px-3 py-2 text-center text-xs font-bold text-white">@p</span>
+                                }
+                                else
+                                {
+                                    <a href="@PageUrl(p)" class="min-w-[36px] rounded-lg px-3 py-2 text-center text-xs font-bold text-gray-600 transition hover:bg-gray-100">@p</a>
+                                }
+                            }
+                            <a href="@PageUrl(Math.Min(Model.TotalPages, Model.Page + 1))" class="rounded-lg px-3 py-2 text-xs font-bold text-gray-500 transition hover:bg-gray-100"><i class="fas fa-angle-right"></i></a>
+                        </div>
+                    </nav>
+                }
+            </div>
+        }
+    </div>
+</div>

+ 13 - 10
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Views/Reports/SubscriptionCharge.cshtml

@@ -1,6 +1,6 @@
 @model Kitty_Fall.Cms.Models.SubscriptionChargeReportViewModel
 @model Kitty_Fall.Cms.Models.SubscriptionChargeReportViewModel
 @{
 @{
-    ViewData["Title"] = "Subscription & Charge";
+    ViewData["Title"] = Model.ReportTitle;
 
 
     string FormatDate(DateTime? value) => value.HasValue ? value.Value.ToString("dd/MM/yyyy HH:mm:ss") : "-";
     string FormatDate(DateTime? value) => value.HasValue ? value.Value.ToString("dd/MM/yyyy HH:mm:ss") : "-";
     string FormatDecimal(decimal? value) => value.HasValue ? value.Value.ToString("#,##0.##") : "-";
     string FormatDecimal(decimal? value) => value.HasValue ? value.Value.ToString("#,##0.##") : "-";
@@ -14,7 +14,7 @@
         {
         {
             query.Add("msisdn=" + System.Uri.EscapeDataString(Model.Msisdn));
             query.Add("msisdn=" + System.Uri.EscapeDataString(Model.Msisdn));
         }
         }
-        if (!string.IsNullOrWhiteSpace(Model.SelectedDate))
+        if (Model.ShowDateFilter && !string.IsNullOrWhiteSpace(Model.SelectedDate))
         {
         {
             query.Add("selectedDate=" + System.Uri.EscapeDataString(Model.SelectedDate));
             query.Add("selectedDate=" + System.Uri.EscapeDataString(Model.SelectedDate));
         }
         }
@@ -23,7 +23,7 @@
         query.Add("chargePage=" + (pageParam == "chargePage" ? page : Model.ChargePage));
         query.Add("chargePage=" + (pageParam == "chargePage" ? page : Model.ChargePage));
 
 
         var anchor = pageParam == "regPage" ? "#reg-info" : "#charge-log";
         var anchor = pageParam == "regPage" ? "#reg-info" : "#charge-log";
-        return Url.Content("~/SubscriptionChargeReport") + "?" + string.Join("&", query) + anchor;
+        return Url.Content("~" + Model.ReportUrl) + "?" + string.Join("&", query) + anchor;
     }
     }
 }
 }
 
 
@@ -31,19 +31,22 @@
     <div class="rounded-3xl bg-white shadow-sm ring-1 ring-gray-100">
     <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 class="border-b border-gray-100 px-5 py-5 sm:px-6">
             <div class="flex flex-col gap-2">
             <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">MSISDN and date are optional. A blank date automatically uses the current day.</p>
+                <h1 class="text-2xl font-bold text-gray-900">@Model.ReportTitle</h1>
+                <p class="text-sm text-gray-500">@(Model.ShowDateFilter ? "MSISDN and date are optional. A blank date automatically uses the current day." : "MSISDN is optional. Results are not restricted by date.")</p>
             </div>
             </div>
 
 
-            <form method="get" action="@Url.Content("~/SubscriptionChargeReport")" class="mt-5 grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(180px,240px)_auto]">
+            <form method="get" action="@Url.Content("~" + Model.ReportUrl)" class="mt-5 grid gap-3 @(Model.ShowDateFilter ? "md:grid-cols-[minmax(0,1fr)_minmax(180px,240px)_auto]" : "md:grid-cols-[minmax(0,1fr)_auto]")">
                 <label class="block">
                 <label class="block">
                     <span class="mb-2 block text-sm font-semibold text-gray-700">MSISDN</span>
                     <span class="mb-2 block text-sm font-semibold text-gray-700">MSISDN</span>
                     <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" />
                     <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>
-                <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>
+                @if (Model.ShowDateFilter)
+                {
+                    <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">
                 <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">
                     <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">
                         <i class="fas fa-search mr-2"></i>
                         <i class="fas fa-search mr-2"></i>

+ 118 - 154
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Views/RevenueReport/Index.cshtml

@@ -1,203 +1,167 @@
-@using Common
 @model Kitty_Fall.Cms.Models.RevenueReportViewModel
 @model Kitty_Fall.Cms.Models.RevenueReportViewModel
 @{
 @{
     ViewData["Title"] = "Revenue Reports";
     ViewData["Title"] = "Revenue Reports";
-    var isDaily = Model.ReportType == CommonConstants.DAY_REPORT;
     var fromDateValue = Model.FromDate?.ToString("yyyy-MM-dd");
     var fromDateValue = Model.FromDate?.ToString("yyyy-MM-dd");
     var toDateValue = Model.ToDate?.ToString("yyyy-MM-dd");
     var toDateValue = Model.ToDate?.ToString("yyyy-MM-dd");
+    var isRevenue = Model.ActiveTab == "revenue";
 
 
-    string BuildTypeUrl(string reportType)
+    string TabUrl(string tab)
     {
     {
-        var query = new System.Collections.Generic.List<string> { $"reportType={System.Uri.EscapeDataString(reportType)}" };
-        if (!string.IsNullOrWhiteSpace(fromDateValue))
-        {
-            query.Add($"fromDate={System.Uri.EscapeDataString(fromDateValue)}");
-        }
-
-        if (!string.IsNullOrWhiteSpace(toDateValue))
-        {
-            query.Add($"toDate={System.Uri.EscapeDataString(toDateValue)}");
-        }
-
-        if (!string.IsNullOrWhiteSpace(Model.ServiceFilter))
-        {
-            query.Add($"serviceFilter={System.Uri.EscapeDataString(Model.ServiceFilter)}");
-        }
-
+        var query = new List<string> { "tab=" + Uri.EscapeDataString(tab) };
+        if (!string.IsNullOrWhiteSpace(fromDateValue)) query.Add("fromDate=" + Uri.EscapeDataString(fromDateValue));
+        if (!string.IsNullOrWhiteSpace(toDateValue)) query.Add("toDate=" + Uri.EscapeDataString(toDateValue));
+        if (!string.IsNullOrWhiteSpace(Model.ServiceFilter)) query.Add("serviceFilter=" + Uri.EscapeDataString(Model.ServiceFilter));
         return Url.Content("~/RevenueReport") + "?" + string.Join("&", query);
         return Url.Content("~/RevenueReport") + "?" + string.Join("&", query);
     }
     }
 
 
-    string BuildTrendUrl()
-    {
-        var query = new System.Collections.Generic.List<string>();
-        if (!string.IsNullOrWhiteSpace(fromDateValue))
-        {
-            query.Add($"fromDate={System.Uri.EscapeDataString(fromDateValue)}");
-        }
-
-        if (!string.IsNullOrWhiteSpace(toDateValue))
-        {
-            query.Add($"toDate={System.Uri.EscapeDataString(toDateValue)}");
-        }
+    string FormatValue(decimal value) => isRevenue ? value.ToString("#,##0.##") : value.ToString("#,##0");
 
 
-        if (!string.IsNullOrWhiteSpace(Model.ServiceFilter))
+    string PageUrl(int page)
+    {
+        var query = new List<string>
         {
         {
-            query.Add($"serviceFilter={System.Uri.EscapeDataString(Model.ServiceFilter)}");
-        }
-
-        return query.Count == 0
-            ? Url.Content("~/RevenueReport/DailyTrend")
-            : Url.Content("~/RevenueReport/DailyTrend") + "?" + string.Join("&", query);
+            "tab=" + Uri.EscapeDataString(Model.ActiveTab),
+            "page=" + page
+        };
+        if (!string.IsNullOrWhiteSpace(fromDateValue)) query.Add("fromDate=" + Uri.EscapeDataString(fromDateValue));
+        if (!string.IsNullOrWhiteSpace(toDateValue)) query.Add("toDate=" + Uri.EscapeDataString(toDateValue));
+        if (!string.IsNullOrWhiteSpace(Model.ServiceFilter)) query.Add("serviceFilter=" + Uri.EscapeDataString(Model.ServiceFilter));
+        return Url.Content("~/RevenueReport") + "?" + string.Join("&", query);
     }
     }
-
-    string FormatRevenue(decimal value) => value.ToString("#,##0.##");
-    string FormatCount(decimal value) => value.ToString("#,##0");
-    string FormatTime(DateTime value) => isDaily ? value.ToString("dd/MM/yyyy") : value.ToString("dd/MM/yyyy HH:mm");
 }
 }
 
 
-<div class="py-6 sm:py-8">
-    <div class="rounded-3xl bg-white shadow-sm ring-1 ring-gray-100">
+<div class="min-w-0 max-w-full py-6 sm:py-8">
+    <div class="min-w-0 max-w-full overflow-hidden 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 class="border-b border-gray-100 px-5 py-5 sm:px-6">
-            <div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
-                <div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
-                    <a href="@BuildTypeUrl(CommonConstants.DAY_REPORT)"
-                       class="inline-flex items-center justify-center rounded-2xl px-4 py-3 text-sm font-semibold transition @(isDaily ? "bg-admin-active text-white shadow-lg shadow-purple-100" : "bg-gray-100 text-gray-700 hover:bg-gray-200")">
-                        REVENUE by day
-                    </a>
-                    <a href="@BuildTypeUrl(CommonConstants.HOUR_REPORT)"
-                       class="inline-flex items-center justify-center rounded-2xl px-4 py-3 text-sm font-semibold transition @(!isDaily ? "bg-admin-active text-white shadow-lg shadow-purple-100" : "bg-gray-100 text-gray-700 hover:bg-gray-200")">
-                        REVENUE by hour
-                    </a>
-                    @* <a href="@BuildTrendUrl()"
-                       class="inline-flex items-center justify-center rounded-2xl bg-gray-100 px-4 py-3 text-sm font-semibold text-gray-700 transition hover:bg-gray-200">
-                        REVENUE by date
-                    </a> *@
-                </div>
+            <h1 class="text-2xl font-bold text-gray-900">Hourly Reports</h1>
+            <p class="mt-1 text-sm text-gray-500">Revenue and total subscriptions by hour.</p>
+
+            <div class="mt-5 flex flex-wrap gap-2">
+                <a href="@TabUrl("revenue")" class="rounded-2xl px-5 py-3 text-sm font-semibold transition @(isRevenue ? "bg-admin-active text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200")">Revenue</a>
+                <a href="@TabUrl("subscriptions")" class="rounded-2xl px-5 py-3 text-sm font-semibold transition @(!isRevenue ? "bg-admin-active text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200")">Total subscriptions</a>
             </div>
             </div>
 
 
             <form method="get" action="@Url.Content("~/RevenueReport")" class="mt-5 grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_auto]">
             <form method="get" action="@Url.Content("~/RevenueReport")" class="mt-5 grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_auto]">
-                <input type="hidden" name="reportType" value="@Model.ReportType" />
+                <input type="hidden" name="tab" value="@Model.ActiveTab" />
                 <label class="block">
                 <label class="block">
                     <span class="mb-2 block text-sm font-semibold text-gray-700">Service</span>
                     <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">
+                    <select name="serviceFilter" class="w-full rounded-2xl border border-gray-200 px-4 py-3 text-sm outline-none focus:border-admin-active focus:ring-2 focus:ring-purple-100">
                         @foreach (var service in Model.Services)
                         @foreach (var service in Model.Services)
                         {
                         {
                             <option value="@service.Value" selected="@(string.Equals(Model.ServiceFilter, service.Value, StringComparison.OrdinalIgnoreCase) ? "selected" : null)">@service.Name</option>
                             <option value="@service.Value" selected="@(string.Equals(Model.ServiceFilter, service.Value, StringComparison.OrdinalIgnoreCase) ? "selected" : null)">@service.Name</option>
                         }
                         }
                     </select>
                     </select>
                 </label>
                 </label>
-                <label class="block">
-                    <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>
-                </div>
+                <label class="block"><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 outline-none 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 outline-none focus:border-admin-active focus:ring-2 focus:ring-purple-100" /></label>
+                <div class="flex items-end"><button type="submit" class="w-full rounded-2xl bg-admin-active px-5 py-3 text-sm font-semibold text-white hover:bg-purple-700 md:w-auto">Apply filter</button></div>
             </form>
             </form>
         </div>
         </div>
 
 
-
-        @if (Model.Items.Count == 0)
+        @if (Model.HourlyItems.Count == 0)
         {
         {
-            <div class="px-5 pb-6 sm:px-6">
-                <div class="rounded-3xl border border-dashed border-gray-200 bg-gray-50 px-6 py-10 text-center">
-                    <div class="text-base font-semibold text-gray-800">No report data found.</div>
-                    <div class="mt-2 text-sm text-gray-500">Adjust the report type or date range and try again.</div>
-                </div>
-            </div>
+            <div class="px-5 py-10 text-center text-sm font-semibold text-gray-600 sm:px-6">No hourly report data found.</div>
         }
         }
         else
         else
         {
         {
-            <div class="px-5 pb-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="flex items-start justify-between gap-3">
-                                <div>
-                                    <div class="text-xs font-bold uppercase tracking-[0.18em] text-admin-active">@item.Type</div>
-                                    <div class="mt-1 text-lg font-bold text-gray-900">@FormatTime(item.TimeReport)</div>
-                                </div>
-                                <div class="rounded-2xl bg-white px-3 py-2 text-right shadow-sm">
-                                    <div class="text-xs font-medium text-gray-500">Revenue</div>
-                                    <div class="text-base font-bold text-gray-900">@FormatRevenue(item.Revenue)</div>
-                                </div>
-                            </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">Register web</div>
-                                    <div class="mt-1 font-semibold text-gray-900">@FormatCount(item.RegisterOnWeb)</div>
-                                </div>
-                                <div class="rounded-2xl bg-white px-3 py-2">
-                                    <div class="text-gray-500">Register MPS</div>
-                                    <div class="mt-1 font-semibold text-gray-900">@FormatCount(item.RegisterOnMps)</div>
-                                </div>
-                                <div class="rounded-2xl bg-white px-3 py-2">
-                                    <div class="text-gray-500">Renew</div>
-                                    <div class="mt-1 font-semibold text-gray-900">@FormatCount(item.Renew)</div>
-                                </div>
-                                <div class="rounded-2xl bg-white px-3 py-2">
-                                    <div class="text-gray-500">Buy web</div>
-                                    <div class="mt-1 font-semibold text-gray-900">@FormatCount(item.BuyOnWeb)</div>
-                                </div>
-                                <div class="rounded-2xl bg-white px-3 py-2">
-                                    <div class="text-gray-500">Buy MPS</div>
-                                    <div class="mt-1 font-semibold text-gray-900">@FormatCount(item.BuyOnMps)</div>
-                                </div>
-                                <div class="rounded-2xl bg-white px-3 py-2">
-                                    <div class="text-gray-500">Users active</div>
-                                    <div class="mt-1 font-semibold text-gray-900">@FormatCount(item.UserActive)</div>
-                                </div>
-                            </div>
-                        </div>
-                    }
+            <div class="min-w-0 max-w-full px-5 py-6 sm:px-6">
+                <div class="mb-4 flex items-center justify-between gap-3">
+                    <div class="text-sm text-gray-500">@Model.TotalRows.ToString("#,##0") rows found</div>
+                    <div class="text-xs font-semibold text-gray-400">Page @Model.Page / @Model.TotalPages</div>
                 </div>
                 </div>
-            </div>
-
-            <div class="hidden px-5 pb-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">
+                <div class="min-w-0 max-w-full overflow-hidden rounded-3xl border border-gray-100">
+                    <div id="hourlyScrollTop" class="overflow-x-auto overflow-y-hidden border-b border-gray-100">
+                        <div id="hourlyScrollTopContent" class="h-4"></div>
+                    </div>
+                    <div id="hourlyScrollBottom" class="overflow-x-auto">
+                        <table class="min-w-max divide-y divide-gray-100 text-sm">
                             <thead class="bg-gray-50">
                             <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">Time</th>
-                                    <th class="px-4 py-3">Revenue</th>
-                                    <th class="px-4 py-3">Register web</th>
-                                    <th class="px-4 py-3">Register MPS</th>
-                                    <th class="px-4 py-3">Renew</th>
-                                    <th class="px-4 py-3">Buy web</th>
-                                    <th class="px-4 py-3">Buy MPS</th>
-                                    <th class="px-4 py-3">Users active</th>
-                                    <th class="px-4 py-3">Users inactive</th>
+                                <tr class="text-left text-xs font-bold uppercase tracking-wider text-gray-500">
+                                    <th class="sticky left-0 z-20 bg-gray-50 px-4 py-3">No.</th>
+                                    <th class="sticky left-[58px] z-20 bg-gray-50 px-4 py-3">Service</th>
+                                    <th class="px-4 py-3">Report date</th>
+                                    <th class="px-4 py-3">Total</th>
+                                    @for (var hour = 0; hour < 24; hour++)
+                                    {
+                                        <th class="px-4 py-3">@($"H{hour:00}")</th>
+                                    }
                                 </tr>
                                 </tr>
                             </thead>
                             </thead>
-                            <tbody class="divide-y divide-gray-100 bg-white text-sm text-gray-700">
-                                @foreach (var item in Model.Items)
+                            <tbody class="divide-y divide-gray-100 bg-white text-gray-700">
+                                @for (var index = 0; index < Model.HourlyItems.Count; index++)
                                 {
                                 {
-                                    <tr class="align-top">
-                                        <td class="whitespace-nowrap px-4 py-4 font-semibold text-gray-900">@FormatTime(item.TimeReport)</td>
-                                        <td class="whitespace-nowrap px-4 py-4 font-semibold text-gray-900">@FormatRevenue(item.Revenue)</td>
-                                        <td class="whitespace-nowrap px-4 py-4">@FormatCount(item.RegisterOnWeb)</td>
-                                        <td class="whitespace-nowrap px-4 py-4">@FormatCount(item.RegisterOnMps)</td>
-                                        <td class="whitespace-nowrap px-4 py-4">@FormatCount(item.Renew)</td>
-                                        <td class="whitespace-nowrap px-4 py-4">@FormatCount(item.BuyOnWeb)</td>
-                                        <td class="whitespace-nowrap px-4 py-4">@FormatCount(item.BuyOnMps)</td>
-                                        <td class="whitespace-nowrap px-4 py-4">@FormatCount(item.UserActive)</td>
-                                        <td class="whitespace-nowrap px-4 py-4">@FormatCount(item.UserInactive)</td>
+                                    var item = Model.HourlyItems[index];
+                                    <tr>
+                                        <td class="sticky left-0 bg-white px-4 py-4">@((Model.Page - 1) * Model.PageSize + index + 1)</td>
+                                        <td class="sticky left-[58px] whitespace-nowrap bg-white px-4 py-4 font-semibold text-gray-900">@item.ServiceName</td>
+                                        <td class="whitespace-nowrap px-4 py-4">@item.ReportDate.ToString("dd/MM/yyyy")</td>
+                                        <td class="whitespace-nowrap px-4 py-4 font-bold text-gray-900">@FormatValue(item.Total)</td>
+                                        @for (var hour = 0; hour < 24; hour++)
+                                        {
+                                            <td class="whitespace-nowrap px-4 py-4">@FormatValue(item.Hours[hour])</td>
+                                        }
                                     </tr>
                                     </tr>
                                 }
                                 }
                             </tbody>
                             </tbody>
                         </table>
                         </table>
                     </div>
                     </div>
                 </div>
                 </div>
+
+                @if (Model.TotalPages > 1)
+                {
+                    var start = Math.Max(1, Model.Page - 2);
+                    var end = Math.Min(Model.TotalPages, start + 4);
+                    start = Math.Max(1, end - 4);
+                    <nav class="mt-5 flex flex-wrap items-center justify-between gap-3" aria-label="Hourly report pagination">
+                        <div class="text-xs text-gray-400">Page <span class="font-bold text-gray-700">@Model.Page</span> / <span class="font-bold text-gray-700">@Model.TotalPages</span></div>
+                        <div class="flex items-center gap-1">
+                            <a href="@PageUrl(Math.Max(1, Model.Page - 1))" class="rounded-lg px-3 py-2 text-xs font-bold text-gray-500 hover:bg-gray-100"><i class="fas fa-angle-left"></i></a>
+                            @for (var p = start; p <= end; p++)
+                            {
+                                if (p == Model.Page)
+                                {
+                                    <span class="min-w-[36px] rounded-lg bg-admin-active px-3 py-2 text-center text-xs font-bold text-white">@p</span>
+                                }
+                                else
+                                {
+                                    <a href="@PageUrl(p)" class="min-w-[36px] rounded-lg px-3 py-2 text-center text-xs font-bold text-gray-600 hover:bg-gray-100">@p</a>
+                                }
+                            }
+                            <a href="@PageUrl(Math.Min(Model.TotalPages, Model.Page + 1))" class="rounded-lg px-3 py-2 text-xs font-bold text-gray-500 hover:bg-gray-100"><i class="fas fa-angle-right"></i></a>
+                        </div>
+                    </nav>
+                }
             </div>
             </div>
         }
         }
     </div>
     </div>
 </div>
 </div>
+
+@section Scripts {
+    <script>
+        (() => {
+            const top = document.getElementById('hourlyScrollTop');
+            const topContent = document.getElementById('hourlyScrollTopContent');
+            const bottom = document.getElementById('hourlyScrollBottom');
+            if (!top || !topContent || !bottom) return;
+
+            const resizeTopScrollbar = () => {
+                topContent.style.width = `${bottom.scrollWidth}px`;
+            };
+            let syncing = false;
+            top.addEventListener('scroll', () => {
+                if (syncing) return;
+                syncing = true;
+                bottom.scrollLeft = top.scrollLeft;
+                syncing = false;
+            });
+            bottom.addEventListener('scroll', () => {
+                if (syncing) return;
+                syncing = true;
+                top.scrollLeft = bottom.scrollLeft;
+                syncing = false;
+            });
+            resizeTopScrollbar();
+            window.addEventListener('resize', resizeTopScrollbar);
+        })();
+    </script>
+}

+ 47 - 5
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Views/Shared/_Layout.cshtml

@@ -15,6 +15,8 @@
     var canViewRevenueReports = CmsSecurityHelper.HasMenuAccess(cmsSession, CmsSecurityHelper.RevenueReportsMenuKey);
     var canViewRevenueReports = CmsSecurityHelper.HasMenuAccess(cmsSession, CmsSecurityHelper.RevenueReportsMenuKey);
     var canViewDailyReports = CmsSecurityHelper.HasMenuAccess(cmsSession, CmsSecurityHelper.DailyReportsMenuKey);
     var canViewDailyReports = CmsSecurityHelper.HasMenuAccess(cmsSession, CmsSecurityHelper.DailyReportsMenuKey);
     var canViewSubscriptionChargeReports = CmsSecurityHelper.HasMenuAccess(cmsSession, CmsSecurityHelper.SubscriptionChargeReportsMenuKey);
     var canViewSubscriptionChargeReports = CmsSecurityHelper.HasMenuAccess(cmsSession, CmsSecurityHelper.SubscriptionChargeReportsMenuKey);
+    var canViewSubscriptionChargeReportsNoDate = CmsSecurityHelper.HasMenuAccess(cmsSession, CmsSecurityHelper.SubscriptionChargeReportsNoDateMenuKey);
+    var canViewMonthlyRewardPayoutReports = CmsSecurityHelper.HasMenuAccess(cmsSession, CmsSecurityHelper.MonthlyRewardPayoutReportsMenuKey);
     var canViewMonthlyWinners = CmsSecurityHelper.HasMenuAccess(cmsSession, CmsSecurityHelper.MonthlyWinnersMenuKey);
     var canViewMonthlyWinners = CmsSecurityHelper.HasMenuAccess(cmsSession, CmsSecurityHelper.MonthlyWinnersMenuKey);
     var canViewEmployers = CmsSecurityHelper.HasMenuAccess(cmsSession, CmsSecurityHelper.EmployersMenuKey);
     var canViewEmployers = CmsSecurityHelper.HasMenuAccess(cmsSession, CmsSecurityHelper.EmployersMenuKey);
     var canViewAccounts = CmsSecurityHelper.HasMenuAccess(cmsSession, CmsSecurityHelper.AccountsMenuKey);
     var canViewAccounts = CmsSecurityHelper.HasMenuAccess(cmsSession, CmsSecurityHelper.AccountsMenuKey);
@@ -189,7 +191,7 @@
                         <span class="@NavMarkerClass("/revenuereport", "/admin/revenuereport") absolute inset-y-0 left-0 w-1 bg-admin-active rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
                         <span class="@NavMarkerClass("/revenuereport", "/admin/revenuereport") absolute inset-y-0 left-0 w-1 bg-admin-active rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
                         <a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 @NavLinkClass("/revenuereport", "/admin/revenuereport")" href="~/RevenueReport" onclick="closeMobileSidebar()">
                         <a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 @NavLinkClass("/revenuereport", "/admin/revenuereport")" href="~/RevenueReport" onclick="closeMobileSidebar()">
                             <i class="fas fa-chart-line w-5 h-5"></i>
                             <i class="fas fa-chart-line w-5 h-5"></i>
-                            <span class="ml-4">Revenue Reports</span>
+                            <span class="ml-4">Hourly Reports</span>
                         </a>
                         </a>
                     </li>
                     </li>
                 }
                 }
@@ -213,6 +215,26 @@
                         </a>
                         </a>
                     </li>
                     </li>
                 }
                 }
+                @if (canViewSubscriptionChargeReportsNoDate)
+                {
+                    <li class="relative px-6 py-3">
+                        <span class="@NavMarkerClass("/subscriptionchargereportnodate", "/admin/subscriptionchargereportnodate") absolute inset-y-0 left-0 w-1 bg-admin-active rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
+                        <a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 @NavLinkClass("/subscriptionchargereportnodate", "/admin/subscriptionchargereportnodate")" href="~/SubscriptionChargeReportNoDate" onclick="closeMobileSidebar()">
+                            <i class="fas fa-receipt w-5 h-5"></i>
+                            <span class="ml-4">SubscriptionChargeReportNoDate</span>
+                        </a>
+                    </li>
+                }
+                @if (canViewMonthlyRewardPayoutReports)
+                {
+                    <li class="relative px-6 py-3">
+                        <span class="@NavMarkerClass("/monthlyrewardpayoutreport", "/admin/monthlyrewardpayoutreport") absolute inset-y-0 left-0 w-1 bg-admin-active rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
+                        <a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 @NavLinkClass("/monthlyrewardpayoutreport", "/admin/monthlyrewardpayoutreport")" href="~/MonthlyRewardPayoutReport" onclick="closeMobileSidebar()">
+                            <i class="fas fa-gift w-5 h-5"></i>
+                            <span class="ml-4">Monthly Reward Payout</span>
+                        </a>
+                    </li>
+                }
                 @if (canViewMonthlyWinners)
                 @if (canViewMonthlyWinners)
                 {
                 {
                     <li class="relative px-6 py-3">
                     <li class="relative px-6 py-3">
@@ -387,7 +409,7 @@
                             <span class="@NavMarkerClass("/revenuereport", "/admin/revenuereport") absolute inset-y-0 left-0 w-1 bg-admin-active rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
                             <span class="@NavMarkerClass("/revenuereport", "/admin/revenuereport") absolute inset-y-0 left-0 w-1 bg-admin-active rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
                             <a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 @NavLinkClass("/revenuereport", "/admin/revenuereport")" href="~/RevenueReport">
                             <a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 @NavLinkClass("/revenuereport", "/admin/revenuereport")" href="~/RevenueReport">
                                 <i class="fas fa-chart-line w-5 h-5"></i>
                                 <i class="fas fa-chart-line w-5 h-5"></i>
-                                <span class="ml-4">Revenue Reports</span>
+                                <span class="ml-4">Hourly Reports</span>
                             </a>
                             </a>
                         </li>
                         </li>
                     }
                     }
@@ -411,6 +433,26 @@
                             </a>
                             </a>
                         </li>
                         </li>
                     }
                     }
+                    @if (canViewSubscriptionChargeReportsNoDate)
+                    {
+                        <li class="relative px-6 py-3">
+                            <span class="@NavMarkerClass("/subscriptionchargereportnodate", "/admin/subscriptionchargereportnodate") absolute inset-y-0 left-0 w-1 bg-admin-active rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
+                            <a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 @NavLinkClass("/subscriptionchargereportnodate", "/admin/subscriptionchargereportnodate")" href="~/SubscriptionChargeReportNoDate">
+                                <i class="fas fa-receipt w-5 h-5"></i>
+                                <span class="ml-4">SubscriptionChargeReportNoDate</span>
+                            </a>
+                        </li>
+                    }
+                    @if (canViewMonthlyRewardPayoutReports)
+                    {
+                        <li class="relative px-6 py-3">
+                            <span class="@NavMarkerClass("/monthlyrewardpayoutreport", "/admin/monthlyrewardpayoutreport") absolute inset-y-0 left-0 w-1 bg-admin-active rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
+                            <a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 @NavLinkClass("/monthlyrewardpayoutreport", "/admin/monthlyrewardpayoutreport")" href="~/MonthlyRewardPayoutReport">
+                                <i class="fas fa-gift w-5 h-5"></i>
+                                <span class="ml-4">Monthly Reward Payout</span>
+                            </a>
+                        </li>
+                    }
                     @if (canViewMonthlyWinners)
                     @if (canViewMonthlyWinners)
                     {
                     {
                         <li class="relative px-6 py-3">
                         <li class="relative px-6 py-3">
@@ -477,7 +519,7 @@
             </div>
             </div>
         </aside>
         </aside>
 
 
-        <div class="flex w-full flex-1 flex-col">
+        <div class="flex min-w-0 w-full flex-1 flex-col">
             <header class="z-10 bg-white shadow-sm">
             <header class="z-10 bg-white shadow-sm">
                 <div class="flex h-full items-center justify-between gap-4 px-4 py-4 text-purple-600 sm:px-6 lg:px-8 xl:px-10">
                 <div class="flex h-full items-center justify-between gap-4 px-4 py-4 text-purple-600 sm:px-6 lg:px-8 xl:px-10">
                     <div class="flex min-w-0 flex-1 items-center gap-3">
                     <div class="flex min-w-0 flex-1 items-center gap-3">
@@ -517,8 +559,8 @@
                 </div>
                 </div>
             </header>
             </header>
 
 
-            <main class="flex-1 overflow-y-auto">
-                <div class="w-full px-4 sm:px-6 lg:px-8 xl:px-10">
+            <main class="min-w-0 flex-1 overflow-y-auto overflow-x-hidden">
+                <div class="min-w-0 w-full max-w-full px-4 sm:px-6 lg:px-8 xl:px-10">
                     @if (TempData["CmsErrorMessage"] != null)
                     @if (TempData["CmsErrorMessage"] != null)
                     {
                     {
                         <div class="mt-6 rounded-2xl border border-red-100 bg-red-50 px-4 py-3 text-sm font-medium text-red-700">
                         <div class="mt-6 rounded-2xl border border-red-100 bg-red-50 px-4 py-3 text-sm font-medium text-red-700">

+ 5 - 5
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/appsettings.json

@@ -1,13 +1,13 @@
 {
 {
   "PathBase": "/cms",
   "PathBase": "/cms",
-  // "Connection": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1539))(CONNECT_DATA=(SERVICE_NAME=ORA12C)));User Id=myjob;Password=123456;Connection Timeout=120;",
-  "Connection": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1539))(CONNECT_DATA=(SERVICE_NAME=ORA12C)));User Id=Kitty_Fall;Password=123456;Connection Timeout=120;",
+  "Connection": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORA12C)));User Id=kitty_fall;Password=yBwS7bF5pL0qJtJ;Connection Timeout=120;",
+  //"Connection": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1539))(CONNECT_DATA=(SERVICE_NAME=ORA12C)));User Id=Kitty_Fall;Password=123456;Connection Timeout=120;",
   "RevenueReport": {
   "RevenueReport": {
-    "DefaultService": "Kitty_Fall_DAYLY",
+    "DefaultService": "KITTY_DAILY",
     "Services": [
     "Services": [
       {
       {
-        "Value": "Kitty_Fall_DAYLY",
-        "Name": "Kitty_Fall_DAYLY"
+        "Value": "KITTY_DAILY",
+        "Name": "KITTY_DAILY"
       }
       }
     ]
     ]
   },
   },