|
|
@@ -0,0 +1,314 @@
|
|
|
+@model Kitty_Fall.Cms.Models.MonthlyWinnerViewModel
|
|
|
+@{
|
|
|
+ ViewData["Title"] = "Monthly Winners";
|
|
|
+
|
|
|
+ string FormatMoney(decimal value) => value.ToString("#,##0.##");
|
|
|
+ string FormatTime(DateTime? value) => value?.ToString("dd/MM/yyyy HH:mm") ?? "-";
|
|
|
+ string StatusClass(byte status) => status switch
|
|
|
+ {
|
|
|
+ 0 => "bg-gray-100 text-gray-700",
|
|
|
+ 1 => "bg-blue-50 text-blue-700",
|
|
|
+ 2 => "bg-emerald-50 text-emerald-700",
|
|
|
+ 3 => "bg-red-50 text-red-700",
|
|
|
+ _ => "bg-gray-100 text-gray-700"
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+<div class="py-6 sm:py-8">
|
|
|
+ <div class="mb-6 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
|
|
+ <div>
|
|
|
+ <h1 class="text-2xl font-bold text-gray-900">Monthly Winners</h1>
|
|
|
+ <p class="mt-1 text-sm text-gray-500">Select cash prize winners manually and publish them to the website.</p>
|
|
|
+ </div>
|
|
|
+ <button onclick="openWinnerModal()" class="inline-flex items-center justify-center rounded-2xl bg-admin-active px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-purple-700">
|
|
|
+ <i class="fas fa-plus mr-2"></i>
|
|
|
+ Add Winner
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <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">
|
|
|
+ <form method="get" action="@Url.Content("~/MonthlyWinner")" class="grid gap-3 md:grid-cols-[minmax(0,260px)_auto]">
|
|
|
+ <label class="block">
|
|
|
+ <span class="mb-2 block text-sm font-semibold text-gray-700">Month</span>
|
|
|
+ <input type="month"
|
|
|
+ name="month"
|
|
|
+ id="monthPicker"
|
|
|
+ value="@($"{Model.MonthKey / 100:D4}-{Model.MonthKey % 100:D2}")"
|
|
|
+ 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="hidden" name="monthKey" id="monthKeyInput" value="@Model.MonthKey" />
|
|
|
+ </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>
|
|
|
+ </form>
|
|
|
+
|
|
|
+ <div class="mt-5 grid gap-3 sm:grid-cols-3">
|
|
|
+ <div class="rounded-2xl bg-gray-50 px-4 py-3">
|
|
|
+ <div class="text-xs font-bold uppercase tracking-[0.16em] text-gray-400">Month</div>
|
|
|
+ <div class="mt-1 text-lg font-bold text-gray-900">@Model.MonthText</div>
|
|
|
+ </div>
|
|
|
+ <div class="rounded-2xl bg-gray-50 px-4 py-3">
|
|
|
+ <div class="text-xs font-bold uppercase tracking-[0.16em] text-gray-400">Eligible / Ticket</div>
|
|
|
+ <div class="mt-1 text-lg font-bold text-gray-900">@Model.TotalEligible.ToString("#,##0") / @Model.TotalTicket.ToString("#,##0")</div>
|
|
|
+ </div>
|
|
|
+ <div class="rounded-2xl bg-gray-50 px-4 py-3">
|
|
|
+ <div class="text-xs font-bold uppercase tracking-[0.16em] text-gray-400">Winners</div>
|
|
|
+ <div class="mt-1 text-lg font-bold text-gray-900">@Model.TotalWinners.ToString("#,##0")</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ @if (Model.Winners.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 monthly winner found.</div>
|
|
|
+ <div class="mt-2 text-sm text-gray-500">Add a winner manually or let the worker generate draft winners.</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ <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">
|
|
|
+ <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">MSISDN</th>
|
|
|
+ <th class="px-4 py-3">Prize</th>
|
|
|
+ <th class="px-4 py-3">Ticket</th>
|
|
|
+ <th class="px-4 py-3">Status</th>
|
|
|
+ <th class="px-4 py-3">Announced</th>
|
|
|
+ <th class="px-4 py-3">Paid</th>
|
|
|
+ <th class="px-4 py-3 text-right">Actions</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody class="divide-y divide-gray-100 bg-white text-sm text-gray-700">
|
|
|
+ @foreach (var winner in Model.Winners)
|
|
|
+ {
|
|
|
+ <tr class="align-top">
|
|
|
+ <td class="whitespace-nowrap px-4 py-4 font-semibold text-gray-900">@winner.Msisdn</td>
|
|
|
+ <td class="whitespace-nowrap px-4 py-4">
|
|
|
+ <div class="font-semibold text-gray-900">@winner.PrizeName</div>
|
|
|
+ <div class="text-xs text-gray-500">@FormatMoney(winner.PrizeValue) @winner.PrizeUnit</div>
|
|
|
+ </td>
|
|
|
+ <td class="whitespace-nowrap px-4 py-4">
|
|
|
+ <div class="font-semibold text-gray-900">@(winner.TicketNo?.ToString() ?? "-")</div>
|
|
|
+ <div class="text-xs text-gray-500">Score: @(winner.TotalScore?.ToString("#,##0") ?? "-")</div>
|
|
|
+ </td>
|
|
|
+ <td class="whitespace-nowrap px-4 py-4">
|
|
|
+ <span class="inline-flex items-center rounded-full px-2.5 py-1 text-xs font-bold @StatusClass(winner.Status)">
|
|
|
+ @winner.StatusText
|
|
|
+ </span>
|
|
|
+ </td>
|
|
|
+ <td class="whitespace-nowrap px-4 py-4">@FormatTime(winner.AnnounceTime)</td>
|
|
|
+ <td class="whitespace-nowrap px-4 py-4">@FormatTime(winner.PaidTime)</td>
|
|
|
+ <td class="whitespace-nowrap px-4 py-4 text-right">
|
|
|
+ <button class="btn-edit-winner mr-3 font-semibold text-admin-active hover:text-purple-800"
|
|
|
+ data-winner='@Json.Serialize(winner)'>Edit</button>
|
|
|
+ @if (winner.Status == 0)
|
|
|
+ {
|
|
|
+ <button onclick="updateWinnerStatus(@winner.WinnerId, 1)" class="mr-3 font-semibold text-blue-600 hover:text-blue-800">Announce</button>
|
|
|
+ }
|
|
|
+ @if (winner.Status != 2)
|
|
|
+ {
|
|
|
+ <button onclick="updateWinnerStatus(@winner.WinnerId, 2)" class="mr-3 font-semibold text-emerald-600 hover:text-emerald-800">Paid</button>
|
|
|
+ }
|
|
|
+ @if (winner.Status != 3)
|
|
|
+ {
|
|
|
+ <button onclick="updateWinnerStatus(@winner.WinnerId, 3)" class="font-semibold text-red-600 hover:text-red-800">Cancel</button>
|
|
|
+ }
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ }
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="px-5 pb-6 sm:hidden">
|
|
|
+ <div class="space-y-4">
|
|
|
+ @foreach (var winner in Model.Winners)
|
|
|
+ {
|
|
|
+ <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">@winner.PrizeCode</div>
|
|
|
+ <div class="mt-1 text-lg font-bold text-gray-900">@winner.Msisdn</div>
|
|
|
+ <div class="mt-1 text-sm text-gray-500">@winner.PrizeName - @FormatMoney(winner.PrizeValue) @winner.PrizeUnit</div>
|
|
|
+ </div>
|
|
|
+ <span class="inline-flex items-center rounded-full px-2.5 py-1 text-xs font-bold @StatusClass(winner.Status)">
|
|
|
+ @winner.StatusText
|
|
|
+ </span>
|
|
|
+ </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">Ticket</div>
|
|
|
+ <div class="mt-1 font-semibold text-gray-900">@(winner.TicketNo?.ToString() ?? "-")</div>
|
|
|
+ </div>
|
|
|
+ <div class="rounded-2xl bg-white px-3 py-2">
|
|
|
+ <div class="text-gray-500">Score</div>
|
|
|
+ <div class="mt-1 font-semibold text-gray-900">@(winner.TotalScore?.ToString("#,##0") ?? "-")</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="mt-4 flex flex-wrap gap-3 text-sm">
|
|
|
+ <button class="btn-edit-winner font-semibold text-admin-active"
|
|
|
+ data-winner='@Json.Serialize(winner)'>Edit</button>
|
|
|
+ <button onclick="updateWinnerStatus(@winner.WinnerId, 1)" class="font-semibold text-blue-600">Announce</button>
|
|
|
+ <button onclick="updateWinnerStatus(@winner.WinnerId, 2)" class="font-semibold text-emerald-600">Paid</button>
|
|
|
+ <button onclick="updateWinnerStatus(@winner.WinnerId, 3)" class="font-semibold text-red-600">Cancel</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<div id="winnerModal" class="fixed inset-0 z-50 hidden overflow-y-auto bg-black/50 p-4 sm:p-6">
|
|
|
+ <div class="mx-auto mt-10 w-full max-w-2xl overflow-hidden rounded-3xl bg-white shadow-xl">
|
|
|
+ <div class="flex items-center justify-between border-b border-gray-100 px-6 py-4">
|
|
|
+ <h3 id="winnerModalTitle" class="text-lg font-bold text-gray-900">Add Monthly Winner</h3>
|
|
|
+ <button onclick="closeWinnerModal()" class="text-gray-400 hover:text-gray-600">
|
|
|
+ <i class="fas fa-times text-xl"></i>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <form id="winnerForm" class="p-6 space-y-4">
|
|
|
+ <input type="hidden" id="winnerId" name="WinnerId" value="0" />
|
|
|
+ <input type="hidden" name="MonthKey" value="@Model.MonthKey" />
|
|
|
+
|
|
|
+ <div class="grid gap-4 md:grid-cols-2">
|
|
|
+ <label class="block">
|
|
|
+ <span class="mb-2 block text-sm font-semibold text-gray-700">Prize</span>
|
|
|
+ <select id="prizeCode" name="PrizeCode" required 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">
|
|
|
+ <option value="">Select prize</option>
|
|
|
+ @foreach (var prize in Model.PrizeConfigs)
|
|
|
+ {
|
|
|
+ <option value="@prize.PrizeCode">@prize.PrizeName - @FormatMoney(prize.PrizeValue) @prize.PrizeUnit</option>
|
|
|
+ }
|
|
|
+ </select>
|
|
|
+ </label>
|
|
|
+ <label class="block">
|
|
|
+ <span class="mb-2 block text-sm font-semibold text-gray-700">Status</span>
|
|
|
+ <select id="winnerStatus" name="Status" 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">
|
|
|
+ <option value="0">Draft</option>
|
|
|
+ <option value="1">Announced</option>
|
|
|
+ <option value="2">Paid manually</option>
|
|
|
+ <option value="3">Cancelled</option>
|
|
|
+ </select>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <label class="block">
|
|
|
+ <span class="mb-2 block text-sm font-semibold text-gray-700">Eligible ticket</span>
|
|
|
+ <select id="ticketId" name="TicketId" 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">
|
|
|
+ <option value="">Manual MSISDN</option>
|
|
|
+ @foreach (var ticket in Model.Tickets)
|
|
|
+ {
|
|
|
+ <option value="@ticket.TicketId" data-msisdn="@ticket.Msisdn">@ticket.Msisdn - ticket @ticket.TicketNo - @ticket.TotalScore.ToString("#,##0") points</option>
|
|
|
+ }
|
|
|
+ </select>
|
|
|
+ </label>
|
|
|
+
|
|
|
+ <label class="block">
|
|
|
+ <span class="mb-2 block text-sm font-semibold text-gray-700">MSISDN</span>
|
|
|
+ <input type="text" id="msisdn" name="Msisdn" placeholder="95123456789"
|
|
|
+ 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="rounded-2xl bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
|
|
+ Monthly cash prizes are paid manually. Saving here only updates <span class="font-semibold">GAME_MONTHLY_WINNER</span>; it does not call WS or reward API.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex items-center justify-end gap-3 pt-4">
|
|
|
+ <button type="button" onclick="closeWinnerModal()" class="rounded-2xl bg-gray-100 px-6 py-2.5 text-sm font-semibold text-gray-700 hover:bg-gray-200">Cancel</button>
|
|
|
+ <button type="submit" class="rounded-2xl bg-admin-active px-6 py-2.5 text-sm font-semibold text-white hover:bg-purple-700">Save Winner</button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
+
|
|
|
+@section Scripts {
|
|
|
+ <script>
|
|
|
+ $('#monthPicker').on('change', function () {
|
|
|
+ const value = $(this).val();
|
|
|
+ $('#monthKeyInput').val(value ? value.replace('-', '') : '@Model.MonthKey');
|
|
|
+ });
|
|
|
+
|
|
|
+ function openWinnerModal() {
|
|
|
+ $('#winnerModalTitle').text('Add Monthly Winner');
|
|
|
+ $('#winnerForm')[0].reset();
|
|
|
+ $('#winnerId').val('0');
|
|
|
+ $('#winnerStatus').val('0');
|
|
|
+ $('#winnerModal').removeClass('hidden');
|
|
|
+ }
|
|
|
+
|
|
|
+ function closeWinnerModal() {
|
|
|
+ $('#winnerModal').addClass('hidden');
|
|
|
+ }
|
|
|
+
|
|
|
+ $('#ticketId').on('change', function () {
|
|
|
+ const msisdn = $(this).find(':selected').data('msisdn') || '';
|
|
|
+ if (msisdn) {
|
|
|
+ $('#msisdn').val(msisdn);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ $('.btn-edit-winner').on('click', function () {
|
|
|
+ const winner = $(this).data('winner');
|
|
|
+ $('#winnerModalTitle').text('Edit Monthly Winner');
|
|
|
+ $('#winnerId').val(winner.winnerId || winner.WinnerId || 0);
|
|
|
+ $('#prizeCode').val(winner.prizeCode || winner.PrizeCode || '');
|
|
|
+ $('#winnerStatus').val(winner.status ?? winner.Status ?? 0);
|
|
|
+ $('#ticketId').val(winner.ticketId || winner.TicketId || '');
|
|
|
+ $('#msisdn').val(winner.msisdn || winner.Msisdn || '');
|
|
|
+ $('#winnerModal').removeClass('hidden');
|
|
|
+ });
|
|
|
+
|
|
|
+ $('#winnerForm').on('submit', function (e) {
|
|
|
+ e.preventDefault();
|
|
|
+ $.ajax({
|
|
|
+ url: '@Url.Content("~/MonthlyWinner/SaveWinner")',
|
|
|
+ type: 'POST',
|
|
|
+ data: $(this).serialize(),
|
|
|
+ success: function (res) {
|
|
|
+ if (res.errorCode === 200) {
|
|
|
+ showToast('Monthly winner saved successfully');
|
|
|
+ setTimeout(() => location.reload(), 800);
|
|
|
+ } else {
|
|
|
+ showToast(res.message, 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ function updateWinnerStatus(winnerId, status) {
|
|
|
+ const label = status === 1 ? 'announce this winner' : status === 2 ? 'mark this winner as paid' : 'cancel this winner';
|
|
|
+ showConfirm(
|
|
|
+ 'Update Winner Status',
|
|
|
+ 'Are you sure you want to ' + label + '?',
|
|
|
+ function () {
|
|
|
+ $.ajax({
|
|
|
+ url: '@Url.Content("~/MonthlyWinner/UpdateStatus")',
|
|
|
+ type: 'POST',
|
|
|
+ data: { winnerId: winnerId, status: status },
|
|
|
+ success: function (res) {
|
|
|
+ if (res.errorCode === 200) {
|
|
|
+ showToast('Winner status updated successfully');
|
|
|
+ setTimeout(() => location.reload(), 800);
|
|
|
+ } else {
|
|
|
+ showToast(res.message, 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+}
|