MonthlyWinnerController.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. using Common;
  2. using ClosedXML.Excel;
  3. using Database.Database;
  4. using Kitty_Fall.Cms.Helpers;
  5. using Kitty_Fall.Cms.Models;
  6. using Microsoft.AspNetCore.Mvc;
  7. using Microsoft.EntityFrameworkCore;
  8. namespace Kitty_Fall.Cms.Controllers;
  9. public class MonthlyWinnerController : CmsAuthorizedController
  10. {
  11. private readonly ModelContext _db;
  12. public MonthlyWinnerController(ModelContext db)
  13. {
  14. _db = db;
  15. }
  16. [HttpGet("/MonthlyWinner")]
  17. [HttpGet("/MonthlyWinner/Index")]
  18. public async Task<IActionResult> Index(int? monthKey = null)
  19. {
  20. if (!CheckLogin()) return RedirectToLogin();
  21. if (!HasMenuPermission(CmsSecurityHelper.MonthlyWinnersMenuKey)) return RedirectNoPermission();
  22. var normalizedMonthKey = NormalizeMonthKey(monthKey);
  23. var draw = await _db.GameMonthlyDraws
  24. .AsNoTracking()
  25. .FirstOrDefaultAsync(x => x.MonthKey == normalizedMonthKey);
  26. var winners = await (
  27. from winner in _db.GameMonthlyWinners.AsNoTracking()
  28. join ticket in _db.GameMonthlyDrawTickets.AsNoTracking()
  29. on winner.TicketId equals ticket.TicketId into ticketJoin
  30. from ticket in ticketJoin.DefaultIfEmpty()
  31. where winner.MonthKey == normalizedMonthKey
  32. orderby winner.CreatedTime descending
  33. select new MonthlyWinnerItemViewModel
  34. {
  35. WinnerId = winner.WinnerId,
  36. DrawId = winner.DrawId,
  37. MonthKey = winner.MonthKey,
  38. Msisdn = winner.Msisdn,
  39. TicketId = winner.TicketId,
  40. TicketNo = ticket == null ? null : ticket.TicketNo,
  41. TotalScore = ticket == null ? null : ticket.TotalScore,
  42. PrizeCode = winner.PrizeCode,
  43. PrizeName = winner.PrizeName,
  44. PrizeValue = winner.PrizeValue,
  45. PrizeUnit = winner.PrizeUnit,
  46. Status = winner.Status,
  47. StatusText = ToStatusText(winner.Status),
  48. AnnounceTime = winner.AnnounceTime,
  49. PaidTime = winner.PaidTime,
  50. CreatedTime = winner.CreatedTime
  51. })
  52. .ToListAsync();
  53. var prizeConfigs = await _db.GameMonthlyPrizeConfigs
  54. .AsNoTracking()
  55. .Where(x => x.Status != false)
  56. .OrderBy(x => x.DisplayOrder)
  57. .ThenBy(x => x.PrizeConfigId)
  58. .Select(x => new MonthlyWinnerPrizeViewModel
  59. {
  60. PrizeCode = x.PrizeCode,
  61. PrizeName = x.PrizeName,
  62. PrizeValue = x.PrizeValue,
  63. PrizeUnit = x.PrizeUnit
  64. })
  65. .ToListAsync();
  66. var selectedMsisdns = winners.Select(x => x.Msisdn).ToHashSet(StringComparer.OrdinalIgnoreCase);
  67. var tickets = await _db.GameMonthlyDrawTickets
  68. .AsNoTracking()
  69. .Where(x => x.MonthKey == normalizedMonthKey && !selectedMsisdns.Contains(x.Msisdn))
  70. .OrderByDescending(x => x.TotalScore)
  71. .ThenBy(x => x.TicketNo)
  72. .Take(300)
  73. .Select(x => new MonthlyWinnerTicketViewModel
  74. {
  75. TicketId = x.TicketId,
  76. Msisdn = x.Msisdn,
  77. TicketNo = x.TicketNo,
  78. TotalScore = x.TotalScore
  79. })
  80. .ToListAsync();
  81. var model = new MonthlyWinnerViewModel
  82. {
  83. MonthKey = normalizedMonthKey,
  84. MonthText = ToMonthText(normalizedMonthKey),
  85. TotalWinners = winners.Count,
  86. TotalEligible = draw?.TotalEligible ?? 0,
  87. TotalTicket = draw?.TotalTicket ?? 0,
  88. Winners = winners,
  89. PrizeConfigs = prizeConfigs,
  90. Tickets = tickets
  91. };
  92. return View(model);
  93. }
  94. [HttpGet("/MonthlyWinner/ExportTickets")]
  95. public async Task<IActionResult> ExportTickets(int? monthKey = null)
  96. {
  97. if (!CheckLogin()) return RedirectToLogin();
  98. if (!HasMenuPermission(CmsSecurityHelper.MonthlyWinnersMenuKey)) return RedirectNoPermission();
  99. var normalizedMonthKey = NormalizeMonthKey(monthKey);
  100. var tickets = await _db.GameMonthlyDrawTickets
  101. .AsNoTracking()
  102. .Where(x => x.MonthKey == normalizedMonthKey)
  103. .OrderBy(x => x.TicketNo)
  104. .Select(x => new
  105. {
  106. x.TicketNo,
  107. x.Msisdn,
  108. x.TotalScore,
  109. x.CreatedTime
  110. })
  111. .ToListAsync();
  112. using var workbook = new XLWorkbook();
  113. var worksheet = workbook.Worksheets.Add("Tickets");
  114. var headers = new[] { "No.", "Ticket No.", "MSISDN", "Total Score", "Created Time" };
  115. for (var column = 0; column < headers.Length; column++)
  116. {
  117. worksheet.Cell(1, column + 1).Value = headers[column];
  118. }
  119. for (var index = 0; index < tickets.Count; index++)
  120. {
  121. var ticket = tickets[index];
  122. var row = index + 2;
  123. worksheet.Cell(row, 1).Value = index + 1;
  124. worksheet.Cell(row, 2).Value = ticket.TicketNo;
  125. worksheet.Cell(row, 3).Value = ticket.Msisdn;
  126. worksheet.Cell(row, 4).Value = ticket.TotalScore;
  127. worksheet.Cell(row, 5).Value = ticket.CreatedTime;
  128. }
  129. var lastRow = Math.Max(1, tickets.Count + 1);
  130. var tableRange = worksheet.Range(1, 1, lastRow, headers.Length);
  131. tableRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
  132. tableRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
  133. tableRange.Style.Border.InsideBorderColor = XLColor.LightGray;
  134. tableRange.Style.Border.OutsideBorderColor = XLColor.LightGray;
  135. var headerRange = worksheet.Range(1, 1, 1, headers.Length);
  136. headerRange.Style.Font.Bold = true;
  137. headerRange.Style.Font.FontColor = XLColor.White;
  138. headerRange.Style.Fill.BackgroundColor = XLColor.FromHtml("#6D28D9");
  139. headerRange.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
  140. worksheet.Column(1).Style.NumberFormat.Format = "#,##0";
  141. worksheet.Column(2).Style.NumberFormat.Format = "#,##0";
  142. worksheet.Column(3).Style.NumberFormat.Format = "@";
  143. worksheet.Column(4).Style.NumberFormat.Format = "#,##0";
  144. worksheet.Column(5).Style.DateFormat.Format = "yyyy-mm-dd hh:mm:ss";
  145. worksheet.SheetView.FreezeRows(1);
  146. worksheet.RangeUsed()?.SetAutoFilter();
  147. worksheet.Columns().AdjustToContents();
  148. worksheet.Column(3).Width = Math.Max(worksheet.Column(3).Width, 18);
  149. worksheet.Column(5).Width = Math.Max(worksheet.Column(5).Width, 21);
  150. using var stream = new MemoryStream();
  151. workbook.SaveAs(stream);
  152. var fileName = $"monthly-tickets-{normalizedMonthKey}.xlsx";
  153. return File(
  154. stream.ToArray(),
  155. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  156. fileName);
  157. }
  158. [HttpPost]
  159. public async Task<JsonResult> SaveWinner(MonthlyWinnerSaveRequest request)
  160. {
  161. if (!CheckLogin()) return Json(new { errorCode = 401, message = "Unauthorized" });
  162. if (!HasMenuPermission(CmsSecurityHelper.MonthlyWinnersMenuKey)) return NoPermissionJson();
  163. try
  164. {
  165. var monthKey = NormalizeMonthKey(request.MonthKey);
  166. var prizeCode = (request.PrizeCode ?? string.Empty).Trim();
  167. if (string.IsNullOrWhiteSpace(prizeCode))
  168. {
  169. return Json(new { errorCode = 400, message = "Prize is required." });
  170. }
  171. var prize = await _db.GameMonthlyPrizeConfigs
  172. .AsNoTracking()
  173. .FirstOrDefaultAsync(x => x.PrizeCode == prizeCode && x.Status != false);
  174. if (prize == null)
  175. {
  176. return Json(new { errorCode = 404, message = "Prize config not found." });
  177. }
  178. var draw = await EnsureDrawAsync(monthKey);
  179. var ticket = await LoadTicketAsync(monthKey, request.TicketId);
  180. var msisdn = NormalizeMsisdn(ticket?.Msisdn ?? request.Msisdn);
  181. if (string.IsNullOrWhiteSpace(msisdn))
  182. {
  183. return Json(new { errorCode = 400, message = "MSISDN or ticket is required." });
  184. }
  185. var prizeWinnerCount = await _db.GameMonthlyWinners
  186. .AsNoTracking()
  187. .CountAsync(x => x.DrawId == draw.DrawId
  188. && x.PrizeCode == prize.PrizeCode
  189. && x.WinnerId != request.WinnerId);
  190. if (prizeWinnerCount >= prize.Quantity)
  191. {
  192. return Json(new { errorCode = 409, message = $"This monthly prize already has {prize.Quantity} winner(s)." });
  193. }
  194. var userAlreadyHasPrize = await _db.GameMonthlyWinners
  195. .AsNoTracking()
  196. .AnyAsync(x => x.DrawId == draw.DrawId
  197. && x.Msisdn == msisdn
  198. && x.WinnerId != request.WinnerId);
  199. if (userAlreadyHasPrize)
  200. {
  201. return Json(new { errorCode = 409, message = "This user already has a prize in the selected month." });
  202. }
  203. var now = DateTime.Now;
  204. GameMonthlyWinner? winner = null;
  205. if (request.WinnerId > 0)
  206. {
  207. winner = await _db.GameMonthlyWinners.FirstOrDefaultAsync(x => x.WinnerId == request.WinnerId);
  208. if (winner == null)
  209. {
  210. return Json(new { errorCode = 404, message = "Winner not found." });
  211. }
  212. }
  213. if (winner == null)
  214. {
  215. winner = new GameMonthlyWinner
  216. {
  217. WinnerId = await NextDecimalIdAsync("SEQ_GAME_MONTHLY_WINNER"),
  218. DrawId = draw.DrawId,
  219. MonthKey = monthKey,
  220. CreatedTime = now
  221. };
  222. _db.GameMonthlyWinners.Add(winner);
  223. }
  224. winner.DrawId = draw.DrawId;
  225. winner.MonthKey = monthKey;
  226. winner.Msisdn = msisdn;
  227. winner.TicketId = ticket?.TicketId;
  228. winner.PrizeCode = prize.PrizeCode;
  229. winner.PrizeName = prize.PrizeName;
  230. winner.PrizeValue = prize.PrizeValue;
  231. winner.PrizeUnit = prize.PrizeUnit;
  232. winner.Status = NormalizeStatus(request.Status);
  233. winner.UpdatedTime = now;
  234. ApplyStatusTime(winner, now);
  235. await _db.SaveChangesAsync();
  236. return Json(new { errorCode = 200, message = "Success" });
  237. }
  238. catch (Exception ex)
  239. {
  240. return Json(new { errorCode = 500, message = ex.Message });
  241. }
  242. }
  243. [HttpPost]
  244. public async Task<JsonResult> UpdateStatus(decimal winnerId, byte status)
  245. {
  246. if (!CheckLogin()) return Json(new { errorCode = 401, message = "Unauthorized" });
  247. if (!HasMenuPermission(CmsSecurityHelper.MonthlyWinnersMenuKey)) return NoPermissionJson();
  248. var winner = await _db.GameMonthlyWinners.FirstOrDefaultAsync(x => x.WinnerId == winnerId);
  249. if (winner == null)
  250. {
  251. return Json(new { errorCode = 404, message = "Winner not found." });
  252. }
  253. var now = DateTime.Now;
  254. winner.Status = NormalizeStatus(status);
  255. winner.UpdatedTime = now;
  256. ApplyStatusTime(winner, now);
  257. await _db.SaveChangesAsync();
  258. return Json(new { errorCode = 200, message = "Success" });
  259. }
  260. private async Task<GameMonthlyDraw> EnsureDrawAsync(int monthKey)
  261. {
  262. var draw = await _db.GameMonthlyDraws.FirstOrDefaultAsync(x => x.MonthKey == monthKey);
  263. if (draw != null) return draw;
  264. var now = DateTime.Now;
  265. draw = new GameMonthlyDraw
  266. {
  267. DrawId = await NextDecimalIdAsync("SEQ_GAME_MONTHLY_DRAW"),
  268. MonthKey = monthKey,
  269. Status = 0,
  270. TotalEligible = 0,
  271. TotalTicket = 0,
  272. CreatedTime = now
  273. };
  274. _db.GameMonthlyDraws.Add(draw);
  275. await _db.SaveChangesAsync();
  276. return draw;
  277. }
  278. private async Task<GameMonthlyDrawTicket?> LoadTicketAsync(int monthKey, decimal? ticketId)
  279. {
  280. if (ticketId == null || ticketId <= 0) return null;
  281. return await _db.GameMonthlyDrawTickets
  282. .AsNoTracking()
  283. .FirstOrDefaultAsync(x => x.TicketId == ticketId.Value && x.MonthKey == monthKey);
  284. }
  285. private static void ApplyStatusTime(GameMonthlyWinner winner, DateTime now)
  286. {
  287. if (winner.Status == 1 && winner.AnnounceTime == null)
  288. {
  289. winner.AnnounceTime = now;
  290. }
  291. if (winner.Status == 2)
  292. {
  293. winner.AnnounceTime ??= now;
  294. winner.PaidTime ??= now;
  295. }
  296. winner.ResponseCode = winner.Status switch
  297. {
  298. 0 => "DRAFT",
  299. 1 => "ANNOUNCED",
  300. 2 => "PAID_MANUAL",
  301. 3 => "CANCELLED",
  302. _ => winner.ResponseCode
  303. };
  304. winner.ResponseMessage = winner.Status switch
  305. {
  306. 0 => "waiting cms confirm",
  307. 1 => "announced by cms",
  308. 2 => "cash prize paid manually",
  309. 3 => "cancelled by cms",
  310. _ => winner.ResponseMessage
  311. };
  312. }
  313. private async Task<decimal> NextDecimalIdAsync(string sequence)
  314. {
  315. return await Database.DbLogic.GenIdAsync(_db, sequence)
  316. ?? throw new InvalidOperationException($"Oracle sequence '{sequence}' did not return an ID.");
  317. }
  318. private static int NormalizeMonthKey(int? monthKey)
  319. {
  320. if (monthKey is >= 200001 and <= 299912)
  321. {
  322. var month = monthKey.Value % 100;
  323. if (month is >= 1 and <= 12) return monthKey.Value;
  324. }
  325. var previousMonth = DateTime.Now.Date.AddMonths(-1);
  326. return previousMonth.Year * 100 + previousMonth.Month;
  327. }
  328. private static string NormalizeMsisdn(string? msisdn)
  329. {
  330. var value = (msisdn ?? string.Empty).Trim();
  331. if (value.StartsWith("0", StringComparison.Ordinal) && value.Length > 1)
  332. {
  333. value = "95" + value[1..];
  334. }
  335. return value;
  336. }
  337. private static byte NormalizeStatus(byte status)
  338. {
  339. return status is <= 3 ? status : (byte)0;
  340. }
  341. private static string ToMonthText(int monthKey)
  342. {
  343. var year = monthKey / 100;
  344. var month = monthKey % 100;
  345. return new DateTime(year, month, 1).ToString("MMMM yyyy", System.Globalization.CultureInfo.InvariantCulture);
  346. }
  347. private static string ToStatusText(byte status)
  348. {
  349. return status switch
  350. {
  351. 0 => "Draft",
  352. 1 => "Announced",
  353. 2 => "Paid",
  354. 3 => "Cancelled",
  355. _ => "Unknown"
  356. };
  357. }
  358. }