Sfoglia il codice sorgente

nâng cấp cms thêm chức năng export

student 1 settimana fa
parent
commit
ef0b170769

+ 150 - 52
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Controllers/MonthlyWinnerController.cs

@@ -1,10 +1,13 @@
 using Common;
-using ClosedXML.Excel;
 using Database.Database;
 using Kitty_Fall.Cms.Helpers;
 using Kitty_Fall.Cms.Models;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
+using System.Globalization;
+using System.IO.Compression;
+using System.Text;
+using System.Xml;
 
 namespace Kitty_Fall.Cms.Controllers;
 
@@ -113,63 +116,19 @@ public class MonthlyWinnerController : CmsAuthorizedController
             .AsNoTracking()
             .Where(x => x.MonthKey == normalizedMonthKey)
             .OrderBy(x => x.TicketNo)
-            .Select(x => new
+            .Select(x => new TicketExportRow
             {
-                x.TicketNo,
-                x.Msisdn,
-                x.TotalScore,
-                x.CreatedTime
+                TicketNo = x.TicketNo,
+                Msisdn = x.Msisdn,
+                TotalScore = x.TotalScore,
+                CreatedTime = x.CreatedTime
             })
             .ToListAsync();
 
-        using var workbook = new XLWorkbook();
-        var worksheet = workbook.Worksheets.Add("Tickets");
-        var headers = new[] { "No.", "Ticket No.", "MSISDN", "Total Score", "Created Time" };
-        for (var column = 0; column < headers.Length; column++)
-        {
-            worksheet.Cell(1, column + 1).Value = headers[column];
-        }
-
-        for (var index = 0; index < tickets.Count; index++)
-        {
-            var ticket = tickets[index];
-            var row = index + 2;
-            worksheet.Cell(row, 1).Value = index + 1;
-            worksheet.Cell(row, 2).Value = ticket.TicketNo;
-            worksheet.Cell(row, 3).Value = ticket.Msisdn;
-            worksheet.Cell(row, 4).Value = ticket.TotalScore;
-            worksheet.Cell(row, 5).Value = ticket.CreatedTime;
-        }
-
-        var lastRow = Math.Max(1, tickets.Count + 1);
-        var tableRange = worksheet.Range(1, 1, lastRow, headers.Length);
-        tableRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
-        tableRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
-        tableRange.Style.Border.InsideBorderColor = XLColor.LightGray;
-        tableRange.Style.Border.OutsideBorderColor = XLColor.LightGray;
-
-        var headerRange = worksheet.Range(1, 1, 1, headers.Length);
-        headerRange.Style.Font.Bold = true;
-        headerRange.Style.Font.FontColor = XLColor.White;
-        headerRange.Style.Fill.BackgroundColor = XLColor.FromHtml("#6D28D9");
-        headerRange.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
-
-        worksheet.Column(1).Style.NumberFormat.Format = "#,##0";
-        worksheet.Column(2).Style.NumberFormat.Format = "#,##0";
-        worksheet.Column(3).Style.NumberFormat.Format = "@";
-        worksheet.Column(4).Style.NumberFormat.Format = "#,##0";
-        worksheet.Column(5).Style.DateFormat.Format = "yyyy-mm-dd hh:mm:ss";
-        worksheet.SheetView.FreezeRows(1);
-        worksheet.RangeUsed()?.SetAutoFilter();
-        worksheet.Columns().AdjustToContents();
-        worksheet.Column(3).Width = Math.Max(worksheet.Column(3).Width, 18);
-        worksheet.Column(5).Width = Math.Max(worksheet.Column(5).Width, 21);
-
-        using var stream = new MemoryStream();
-        workbook.SaveAs(stream);
+        var content = BuildTicketWorkbook(tickets);
         var fileName = $"monthly-tickets-{normalizedMonthKey}.xlsx";
         return File(
-            stream.ToArray(),
+            content,
             "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
             fileName);
     }
@@ -234,6 +193,10 @@ public class MonthlyWinnerController : CmsAuthorizedController
                 {
                     return Json(new { errorCode = 404, message = "Winner not found." });
                 }
+                if (winner.Status == 2)
+                {
+                    return Json(new { errorCode = 409, message = "A paid winner cannot be edited." });
+                }
             }
 
             if (winner == null)
@@ -281,6 +244,10 @@ public class MonthlyWinnerController : CmsAuthorizedController
         {
             return Json(new { errorCode = 404, message = "Winner not found." });
         }
+        if (winner.Status == 2)
+        {
+            return Json(new { errorCode = 409, message = "A paid winner cannot be changed." });
+        }
 
         var now = DateTime.Now;
         winner.Status = NormalizeStatus(status);
@@ -291,6 +258,137 @@ public class MonthlyWinnerController : CmsAuthorizedController
         return Json(new { errorCode = 200, message = "Success" });
     }
 
+    private static byte[] BuildTicketWorkbook(IReadOnlyList<TicketExportRow> tickets)
+    {
+        using var output = new MemoryStream();
+        using (var archive = new ZipArchive(output, ZipArchiveMode.Create, true))
+        {
+            WriteZipEntry(archive, "[Content_Types].xml", """
+                <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+                <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
+                  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
+                  <Default Extension="xml" ContentType="application/xml"/>
+                  <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
+                  <Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
+                  <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
+                </Types>
+                """);
+            WriteZipEntry(archive, "_rels/.rels", """
+                <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+                <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
+                  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
+                </Relationships>
+                """);
+            WriteZipEntry(archive, "xl/workbook.xml", """
+                <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+                <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
+                  <sheets><sheet name="Tickets" sheetId="1" r:id="rId1"/></sheets>
+                </workbook>
+                """);
+            WriteZipEntry(archive, "xl/_rels/workbook.xml.rels", """
+                <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+                <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
+                  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
+                  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
+                </Relationships>
+                """);
+            WriteZipEntry(archive, "xl/styles.xml", """
+                <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+                <styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
+                  <numFmts count="1"><numFmt numFmtId="164" formatCode="yyyy-mm-dd hh:mm:ss"/></numFmts>
+                  <fonts count="2"><font/><font><b/><color rgb="FFFFFFFF"/></font></fonts>
+                  <fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="solid"><fgColor rgb="FF6D28D9"/><bgColor indexed="64"/></patternFill></fill></fills>
+                  <borders count="1"><border/></borders>
+                  <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
+                  <cellXfs count="3"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="1" borderId="0" xfId="0" applyFont="1" applyFill="1" applyAlignment="1"><alignment horizontal="center"/></xf><xf numFmtId="164" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/></cellXfs>
+                  <cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
+                </styleSheet>
+                """);
+
+            var sheetEntry = archive.CreateEntry("xl/worksheets/sheet1.xml", CompressionLevel.Fastest);
+            using var sheetStream = sheetEntry.Open();
+            using var writer = XmlWriter.Create(sheetStream, new XmlWriterSettings { Encoding = new UTF8Encoding(false), Indent = false });
+            writer.WriteStartDocument(true);
+            writer.WriteStartElement("worksheet", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
+            writer.WriteStartElement("sheetViews");
+            writer.WriteStartElement("sheetView");
+            writer.WriteAttributeString("workbookViewId", "0");
+            writer.WriteStartElement("pane");
+            writer.WriteAttributeString("ySplit", "1");
+            writer.WriteAttributeString("topLeftCell", "A2");
+            writer.WriteAttributeString("activePane", "bottomLeft");
+            writer.WriteAttributeString("state", "frozen");
+            writer.WriteEndElement();
+            writer.WriteEndElement();
+            writer.WriteEndElement();
+            writer.WriteStartElement("cols");
+            WriteColumn(writer, 1, 1, 8); WriteColumn(writer, 2, 2, 13); WriteColumn(writer, 3, 3, 20); WriteColumn(writer, 4, 4, 15); WriteColumn(writer, 5, 5, 22);
+            writer.WriteEndElement();
+            writer.WriteStartElement("sheetData");
+            writer.WriteStartElement("row"); writer.WriteAttributeString("r", "1");
+            var headers = new[] { "No.", "Ticket No.", "MSISDN", "Total Score", "Created Time" };
+            for (var i = 0; i < headers.Length; i++) WriteInlineCell(writer, $"{(char)('A' + i)}1", headers[i], 1);
+            writer.WriteEndElement();
+            for (var i = 0; i < tickets.Count; i++)
+            {
+                var rowNumber = i + 2;
+                var ticket = tickets[i];
+                writer.WriteStartElement("row"); writer.WriteAttributeString("r", rowNumber.ToString(CultureInfo.InvariantCulture));
+                WriteNumberCell(writer, $"A{rowNumber}", i + 1);
+                WriteNumberCell(writer, $"B{rowNumber}", ticket.TicketNo);
+                WriteInlineCell(writer, $"C{rowNumber}", ticket.Msisdn, 0);
+                WriteNumberCell(writer, $"D{rowNumber}", ticket.TotalScore);
+                WriteNumberCell(writer, $"E{rowNumber}", ticket.CreatedTime.ToOADate(), 2);
+                writer.WriteEndElement();
+            }
+            writer.WriteEndElement();
+            writer.WriteStartElement("autoFilter"); writer.WriteAttributeString("ref", $"A1:E{Math.Max(1, tickets.Count + 1)}"); writer.WriteEndElement();
+            writer.WriteEndElement();
+            writer.WriteEndDocument();
+        }
+        return output.ToArray();
+    }
+
+    private static void WriteZipEntry(ZipArchive archive, string path, string content)
+    {
+        var entry = archive.CreateEntry(path, CompressionLevel.Fastest);
+        using var stream = entry.Open();
+        using var writer = new StreamWriter(stream, new UTF8Encoding(false));
+        writer.Write(content);
+    }
+
+    private static void WriteColumn(XmlWriter writer, int min, int max, double width)
+    {
+        writer.WriteStartElement("col");
+        writer.WriteAttributeString("min", min.ToString(CultureInfo.InvariantCulture));
+        writer.WriteAttributeString("max", max.ToString(CultureInfo.InvariantCulture));
+        writer.WriteAttributeString("width", width.ToString(CultureInfo.InvariantCulture));
+        writer.WriteAttributeString("customWidth", "1");
+        writer.WriteEndElement();
+    }
+
+    private static void WriteInlineCell(XmlWriter writer, string reference, string value, int style)
+    {
+        writer.WriteStartElement("c"); writer.WriteAttributeString("r", reference); writer.WriteAttributeString("t", "inlineStr"); writer.WriteAttributeString("s", style.ToString(CultureInfo.InvariantCulture));
+        writer.WriteStartElement("is"); writer.WriteElementString("t", value ?? string.Empty); writer.WriteEndElement();
+        writer.WriteEndElement();
+    }
+
+    private static void WriteNumberCell(XmlWriter writer, string reference, double value, int style = 0)
+    {
+        writer.WriteStartElement("c"); writer.WriteAttributeString("r", reference); writer.WriteAttributeString("s", style.ToString(CultureInfo.InvariantCulture));
+        writer.WriteElementString("v", value.ToString(CultureInfo.InvariantCulture));
+        writer.WriteEndElement();
+    }
+
+    private sealed class TicketExportRow
+    {
+        public long TicketNo { get; init; }
+        public string Msisdn { get; init; } = string.Empty;
+        public long TotalScore { get; init; }
+        public DateTime CreatedTime { get; init; }
+    }
+
     private async Task<GameMonthlyDraw> EnsureDrawAsync(int monthKey)
     {
         var draw = await _db.GameMonthlyDraws.FirstOrDefaultAsync(x => x.MonthKey == monthKey);

+ 0 - 1
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Kitty_Fall.Cms.csproj

@@ -12,7 +12,6 @@
 	</ItemGroup>
 
 	<ItemGroup>
-		<PackageReference Include="ClosedXML" Version="0.102.3" />
 		<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
 	</ItemGroup>
 

+ 20 - 8
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Views/MonthlyWinner/Index.cshtml

@@ -115,8 +115,11 @@
                                         <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 != 2)
+                                            {
+                                                <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>
@@ -125,7 +128,7 @@
                                             {
                                                 <button onclick="updateWinnerStatus(@winner.WinnerId, 2)" class="mr-3 font-semibold text-emerald-600 hover:text-emerald-800">Paid</button>
                                             }
-                                            @if (winner.Status != 3)
+                                            @if (winner.Status != 2 && winner.Status != 3)
                                             {
                                                 <button type="button" onclick="updateWinnerStatus(@winner.WinnerId, 3)" class="font-semibold text-red-600 hover:text-red-800">Cancel</button>
                                             }
@@ -164,11 +167,20 @@
                                 </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 type="button" onclick="updateWinnerStatus(@winner.WinnerId, 3)" class="font-semibold text-red-600">Cancel</button>
+                                @if (winner.Status != 2)
+                                {
+                                    <button class="btn-edit-winner font-semibold text-admin-active"
+                                            data-winner='@Json.Serialize(winner)'>Edit</button>
+                                    @if (winner.Status == 0)
+                                    {
+                                        <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>
+                                    @if (winner.Status != 3)
+                                    {
+                                        <button type="button" onclick="updateWinnerStatus(@winner.WinnerId, 3)" class="font-semibold text-red-600">Cancel</button>
+                                    }
+                                }
                             </div>
                         </div>
                     }

+ 1 - 1
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/appsettings.json

@@ -1,7 +1,7 @@
 {
   "url": "http://127.0.0.1:8309",
   "CountryCode": "95",
-  "productName": "Kitty_Fall_DAYLY",
+  "productName": "KITTY_DAILY",
   "AccountMenu": {
     "CancelKeyword": "OFF",
     "CancelShortCode": "562"