diff --git a/Moonlight/App/Helpers/DatabaseCheckupService.cs b/Moonlight/App/Helpers/DatabaseCheckupService.cs index bfb2fc3..829e531 100644 --- a/Moonlight/App/Helpers/DatabaseCheckupService.cs +++ b/Moonlight/App/Helpers/DatabaseCheckupService.cs @@ -45,9 +45,18 @@ public class DatabaseCheckupService { Logger.Info($"{migrations.Length} migrations pending. Updating now"); - var backupHelper = new BackupHelper(); - await backupHelper.CreateBackup( - PathBuilder.File("storage", "backups", $"{new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds()}.zip")); + try + { + var backupHelper = new BackupHelper(); + await backupHelper.CreateBackup( + PathBuilder.File("storage", "backups", $"{new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds()}.zip")); + } + catch (Exception e) + { + Logger.Fatal("Unable to create backup"); + Logger.Fatal(e); + Logger.Fatal("Moonlight will continue to start and apply the migrations without a backup"); + } Logger.Info("Applying migrations"); diff --git a/Moonlight/App/Helpers/Formatter.cs b/Moonlight/App/Helpers/Formatter.cs index 44d08f5..90df31f 100644 --- a/Moonlight/App/Helpers/Formatter.cs +++ b/Moonlight/App/Helpers/Formatter.cs @@ -1,4 +1,5 @@ using System.Text; +using Microsoft.AspNetCore.Components; using Moonlight.App.Services; namespace Moonlight.App.Helpers; @@ -163,4 +164,22 @@ public static class Formatter double gigabytes = (double)bytes / gbMultiplier; return gigabytes; } + + public static RenderFragment FormatLineBreaks(string content) + { + return builder => + { + int i = 0; + var arr = content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + foreach (var line in arr) + { + builder.AddContent(i, line); + if (i++ != arr.Length - 1) + { + builder.AddMarkupContent(i, "
"); + } + } + }; + } } \ No newline at end of file diff --git a/Moonlight/App/Services/Background/DiscordNotificationService.cs b/Moonlight/App/Services/Background/DiscordNotificationService.cs index 8bc0098..15d810e 100644 --- a/Moonlight/App/Services/Background/DiscordNotificationService.cs +++ b/Moonlight/App/Services/Background/DiscordNotificationService.cs @@ -31,9 +31,8 @@ public class DiscordNotificationService Client = new(config.WebHook); AppUrl = configService.Get().Moonlight.AppUrl; - Event.On("supportChat.new", this, OnNewSupportChat); - Event.On("supportChat.message", this, OnSupportChatMessage); - Event.On("supportChat.close", this, OnSupportChatClose); + Event.On("tickets.new", this, OnNewTicket); + Event.On("tickets.status", this, OnTicketStatusUpdated); Event.On("user.rating", this, OnUserRated); Event.On("billing.completed", this, OnBillingCompleted); Event.On("ddos.add", this, OnIpBlockListed); @@ -44,6 +43,24 @@ public class DiscordNotificationService } } + private async Task OnTicketStatusUpdated(Ticket ticket) + { + await SendNotification("", builder => + { + builder.Title = "Ticket status has been updated"; + builder.AddField("Issue topic", ticket.IssueTopic); + builder.AddField("Status", ticket.Status); + + if (ticket.AssignedTo != null) + { + builder.AddField("Assigned to", $"{ticket.AssignedTo.FirstName} {ticket.AssignedTo.LastName}"); + } + + builder.Color = Color.Green; + builder.Url = $"{AppUrl}/admin/support/view/{ticket.Id}"; + }); + } + private async Task OnIpBlockListed(BlocklistIp blocklistIp) { await SendNotification("", builder => @@ -85,46 +102,14 @@ public class DiscordNotificationService }); } - private async Task OnSupportChatClose(User user) + private async Task OnNewTicket(Ticket ticket) { await SendNotification("", builder => { - builder.Title = "A new support chat has been marked as closed"; - builder.Color = Color.Red; - builder.AddField("Email", user.Email); - builder.AddField("Firstname", user.FirstName); - builder.AddField("Lastname", user.LastName); - builder.Url = $"{AppUrl}/admin/support/view/{user.Id}"; - }); - } - - private async Task OnSupportChatMessage(SupportChatMessage message) - { - if(message.Sender == null) - return; - - await SendNotification("", builder => - { - builder.Title = "New message in support chat"; - builder.Color = Color.Blue; - builder.AddField("Message", message.Content); - builder.Author = new EmbedAuthorBuilder() - .WithName($"{message.Sender.FirstName} {message.Sender.LastName}") - .WithIconUrl(ResourceService.Avatar(message.Sender)); - builder.Url = $"{AppUrl}/admin/support/view/{message.Recipient.Id}"; - }); - } - - private async Task OnNewSupportChat(User user) - { - await SendNotification("", builder => - { - builder.Title = "A new support chat has been marked as active"; + builder.Title = "A new ticket has been created"; + builder.AddField("Issue topic", ticket.IssueTopic); builder.Color = Color.Green; - builder.AddField("Email", user.Email); - builder.AddField("Firstname", user.FirstName); - builder.AddField("Lastname", user.LastName); - builder.Url = $"{AppUrl}/admin/support/view/{user.Id}"; + builder.Url = $"{AppUrl}/admin/support/view/{ticket.Id}"; }); } diff --git a/Moonlight/App/Services/Tickets/TicketAdminService.cs b/Moonlight/App/Services/Tickets/TicketAdminService.cs index bb79709..aefbf0b 100644 --- a/Moonlight/App/Services/Tickets/TicketAdminService.cs +++ b/Moonlight/App/Services/Tickets/TicketAdminService.cs @@ -24,29 +24,6 @@ public class TicketAdminService BucketService = bucketService; } - public async Task> GetAssigned() - { - return await TicketServerService.GetUserAssignedTickets(IdentityService.User); - } - - public async Task> GetUnAssigned() - { - return await TicketServerService.GetUnAssignedTickets(); - } - - public async Task Create(string issueTopic, string issueDescription, string issueTries, - TicketSubject subject, int subjectId) - { - return await TicketServerService.Create( - IdentityService.User, - issueTopic, - issueDescription, - issueTries, - subject, - subjectId - ); - } - public async Task Send(string content, IBrowserFile? file = null) { string? attachment = null; @@ -83,13 +60,8 @@ public class TicketAdminService return await TicketServerService.GetMessages(Ticket!); } - public async Task Claim() + public async Task SetClaim(User? user) { - await TicketServerService.Claim(Ticket!, IdentityService.User); - } - - public async Task UnClaim() - { - await TicketServerService.Claim(Ticket!); + await TicketServerService.SetClaim(Ticket!, user); } } \ No newline at end of file diff --git a/Moonlight/App/Services/Tickets/TicketClientService.cs b/Moonlight/App/Services/Tickets/TicketClientService.cs index 2304324..dee6e7f 100644 --- a/Moonlight/App/Services/Tickets/TicketClientService.cs +++ b/Moonlight/App/Services/Tickets/TicketClientService.cs @@ -23,11 +23,6 @@ public class TicketClientService IdentityService = identityService; BucketService = bucketService; } - - public async Task> Get() - { - return await TicketServerService.GetUserTickets(IdentityService.User); - } public async Task Create(string issueTopic, string issueDescription, string issueTries, TicketSubject subject, int subjectId) { diff --git a/Moonlight/App/Services/Tickets/TicketServerService.cs b/Moonlight/App/Services/Tickets/TicketServerService.cs index 621e7d7..9471d2f 100644 --- a/Moonlight/App/Services/Tickets/TicketServerService.cs +++ b/Moonlight/App/Services/Tickets/TicketServerService.cs @@ -49,6 +49,12 @@ public class TicketServerService // Do automatic stuff here await SendSystemMessage(ticket, ConfigService.Get().Moonlight.Tickets.WelcomeMessage); + + if (ticket.Subject != TicketSubject.Other) + { + await SendMessage(ticket, creatorUser, $"Subject :\n\n{ticket.Subject}: {ticket.SubjectId}"); + } + //TODO: Check for opening times return ticket; @@ -137,85 +143,7 @@ public class TicketServerService return message; } - public Task> GetUserTickets(User u) - { - using var scope = ServiceScopeFactory.CreateScope(); - var ticketRepo = scope.ServiceProvider.GetRequiredService>(); - - var tickets = ticketRepo - .Get() - .Include(x => x.CreatedBy) - .Include(x => x.Messages) - .Where(x => x.CreatedBy.Id == u.Id) - .Where(x => x.Status != TicketStatus.Closed) - .ToArray(); - - var result = new Dictionary(); - - foreach (var ticket in tickets) - { - var message = ticket.Messages - .OrderByDescending(x => x.Id) - .FirstOrDefault(); - - result.Add(ticket, message); - } - - return Task.FromResult(result); - } - public Task> GetUserAssignedTickets(User u) - { - using var scope = ServiceScopeFactory.CreateScope(); - var ticketRepo = scope.ServiceProvider.GetRequiredService>(); - - var tickets = ticketRepo - .Get() - .Include(x => x.CreatedBy) - .Include(x => x.Messages) - .Where(x => x.Status != TicketStatus.Closed) - .Where(x => x.AssignedTo.Id == u.Id) - .ToArray(); - - var result = new Dictionary(); - - foreach (var ticket in tickets) - { - var message = ticket.Messages - .OrderByDescending(x => x.Id) - .FirstOrDefault(); - - result.Add(ticket, message); - } - - return Task.FromResult(result); - } - public Task> GetUnAssignedTickets() - { - using var scope = ServiceScopeFactory.CreateScope(); - var ticketRepo = scope.ServiceProvider.GetRequiredService>(); - - var tickets = ticketRepo - .Get() - .Include(x => x.CreatedBy) - .Include(x => x.Messages) - .Include(x => x.AssignedTo) - .Where(x => x.AssignedTo == null) - .Where(x => x.Status != TicketStatus.Closed) - .ToArray(); - - var result = new Dictionary(); - - foreach (var ticket in tickets) - { - var message = ticket.Messages - .OrderByDescending(x => x.Id) - .FirstOrDefault(); - - result.Add(ticket, message); - } - - return Task.FromResult(result); - } + public Task GetMessages(Ticket ticket) { using var scope = ServiceScopeFactory.CreateScope(); @@ -230,7 +158,7 @@ public class TicketServerService return Task.FromResult(tickets.Messages.ToArray()); } - public async Task Claim(Ticket t, User? u = null) + public async Task SetClaim(Ticket t, User? u = null) { using var scope = ServiceScopeFactory.CreateScope(); var ticketRepo = scope.ServiceProvider.GetRequiredService>(); @@ -245,5 +173,8 @@ public class TicketServerService await Event.Emit("tickets.status", ticket); await Event.Emit($"tickets.{ticket.Id}.status", ticket); + + var claimName = user == null ? "None" : user.FirstName + " " + user.LastName; + await SendSystemMessage(ticket, $"Ticked claim has been set to {claimName}"); } } \ No newline at end of file diff --git a/Moonlight/Shared/Components/Tickets/TicketMessageView.razor b/Moonlight/Shared/Components/Tickets/TicketMessageView.razor index a7366ff..8565391 100644 --- a/Moonlight/Shared/Components/Tickets/TicketMessageView.razor +++ b/Moonlight/Shared/Components/Tickets/TicketMessageView.razor @@ -7,6 +7,7 @@ @inject ResourceService ResourceService @inject SmartTranslateService SmartTranslateService +
@foreach (var message in Messages.OrderByDescending(x => x.Id)) // Reverse messages to use auto scrolling { if (message.IsSupportMessage) @@ -18,10 +19,16 @@
@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService)) - @(message.Sender!.FirstName) @(message.Sender!.LastName) + @if (message.Sender != null) + { + @(message.Sender!.FirstName) @(message.Sender!.LastName) + }
- Avatar + @if (message.Sender != null) + { + Avatar + }
@@ -62,8 +69,11 @@
-
- Avatar +
+ @if (message.Sender != null) + { + Avatar + }
@(message.Sender!.FirstName) @(message.Sender!.LastName) @@ -120,7 +130,10 @@
- Avatar + @if (message.Sender != null) + { + Avatar + }
@(message.Sender!.FirstName) @(message.Sender!.LastName) @@ -169,8 +182,11 @@ @(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService)) @(message.Sender!.FirstName) @(message.Sender!.LastName)
-
- Avatar +
+ @if (message.Sender != null) + { + Avatar + }
@@ -208,6 +224,7 @@ } } } +
@code { diff --git a/Moonlight/Shared/Views/Admin/Support/Index.razor b/Moonlight/Shared/Views/Admin/Support/Index.razor index 792c9d9..ed4b8a6 100644 --- a/Moonlight/Shared/Views/Admin/Support/Index.razor +++ b/Moonlight/Shared/Views/Admin/Support/Index.razor @@ -1,379 +1,278 @@ @page "/admin/support" -@page "/admin/support/{Id:int}" - -@using Moonlight.App.Services.Tickets +@using Moonlight.App.Repositories @using Moonlight.App.Database.Entities -@using Moonlight.App.Events -@using Moonlight.App.Helpers @using Moonlight.App.Models.Misc +@using Microsoft.EntityFrameworkCore +@using BlazorTable +@using Moonlight.App.Helpers @using Moonlight.App.Services @using Moonlight.App.Services.Sessions -@using Moonlight.Shared.Components.Tickets -@inject TicketAdminService AdminService +@inject Repository TicketRepository @inject SmartTranslateService SmartTranslateService -@inject EventSystem EventSystem @inject IdentityService IdentityService @attribute [PermissionRequired(nameof(Permissions.AdminSupport))] -@implements IDisposable - -
-
-
-
-
-
- - Unassigned tickets - +
+ +
+ +
+
+
+
+ Total Tickets +
+ + @(TotalTicketCount) + +
+
+ + + +
+
- - @foreach (var ticket in UnAssignedTickets) - { -
-
-
- @(ticket.Key.IssueTopic) - @if (ticket.Value != null) - { -
- @(ticket.Value.Content) -
- } -
+ +
+
+ +
+
+
+
+ Unassigned tickets +
+ + @(UnAssignedTicketCount) +
-
- @if (ticket.Value != null) - { - - @(Formatter.FormatAgoFromDateTime(ticket.Value.CreatedAt, SmartTranslateService)) - - } +
+ + +
- - if (ticket.Key != UnAssignedTickets.Last().Key) - { -
- } - } - - @if (AssignedTickets.Any()) - { -
- - Assigned tickets - -
- } - - @foreach (var ticket in AssignedTickets) - { -
-
-
- @(ticket.Key.IssueTopic) - @if (ticket.Value != null) - { -
- @(ticket.Value.Content) -
- } -
+
+ +
+ - if (ticket.Key != AssignedTickets.Last().Key) - { -
- } - } +
+
+ + Ticket overview + +
+
+ + + +
-
-
-
-
- @if (AdminService.Ticket != null) - { -
-
- @(AdminService.Ticket.IssueTopic) -
- - Status - - @switch (AdminService.Ticket.Status) - { - case TicketStatus.Closed: - - break; - case TicketStatus.Open: - - break; - case TicketStatus.Pending: - - break; - case TicketStatus.WaitingForUser: - - break; - } - @(AdminService.Ticket.Status) - - - Priority - - @switch (AdminService.Ticket.Priority) +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
-
- @if (AdminService.Ticket != null) - { - - } +
-
@code { - [Parameter] - public int Id { get; set; } + private int TotalTicketCount; + private int UnAssignedTicketCount; + private int PendingTicketCount; + private int ClosedTicketCount; - private Dictionary AssignedTickets; - private Dictionary UnAssignedTickets; - private List Messages = new(); - private string MessageText; - private SmartFileSelect FileSelect; + private Ticket[] AllTickets; + private int Filter = 0; - private TicketPriority Priority; - private TicketStatus Status; + private LazyLoader TicketLazyLoader; - protected override async Task OnParametersSetAsync() + private Task LoadStatistics(LazyLoader _) { - await Unsubscribe(); - await ReloadTickets(); - await Subscribe(); + TotalTicketCount = TicketRepository + .Get() + .Count(); - await InvokeAsync(StateHasChanged); + UnAssignedTicketCount = TicketRepository + .Get() + .Include(x => x.AssignedTo) + .Where(x => x.Status != TicketStatus.Closed) + .Count(x => x.AssignedTo == null); + + PendingTicketCount = TicketRepository + .Get() + .Include(x => x.AssignedTo) + .Where(x => x.AssignedTo != null) + .Count(x => x.Status != TicketStatus.Closed); + + ClosedTicketCount = TicketRepository + .Get() + .Count(x => x.Status == TicketStatus.Closed); + + return Task.CompletedTask; } - private async Task UpdatePriority() + private Task LoadTickets(LazyLoader _) { - await AdminService.UpdatePriority(Priority); - } - - private async Task UpdateStatus() - { - await AdminService.UpdateStatus(Status); - } - - private async Task SendMessage() - { - if (string.IsNullOrEmpty(MessageText) && FileSelect.SelectedFile != null) - MessageText = "File upload"; - - if (string.IsNullOrEmpty(MessageText)) - return; - - var msg = await AdminService.Send(MessageText, FileSelect.SelectedFile); - Messages.Add(msg); - MessageText = ""; - FileSelect.SelectedFile = null; - - await InvokeAsync(StateHasChanged); - } - - private async Task Subscribe() - { - await EventSystem.On("tickets.new", this, async _ => + switch (Filter) { - await ReloadTickets(false); - await InvokeAsync(StateHasChanged); - }); - - if (AdminService.Ticket != null) - { - await EventSystem.On($"tickets.{AdminService.Ticket.Id}.message", this, async message => - { - if (message.Sender != null && message.Sender.Id == IdentityService.User.Id && message.IsSupportMessage) - return; - - Messages.Add(message); - await InvokeAsync(StateHasChanged); - }); - - await EventSystem.On($"tickets.{AdminService.Ticket.Id}.status", this, async _ => - { - await ReloadTickets(false); - await InvokeAsync(StateHasChanged); - }); + default: + AllTickets = TicketRepository + .Get() + .Include(x => x.CreatedBy) + .Include(x => x.AssignedTo) + .Where(x => x.Status != TicketStatus.Closed) + .ToArray(); + break; + case 1: + AllTickets = TicketRepository + .Get() + .Include(x => x.CreatedBy) + .Include(x => x.AssignedTo) + .Where(x => x.AssignedTo == null) + .Where(x => x.Status != TicketStatus.Closed) + .ToArray(); + break; + case 2: + AllTickets = TicketRepository + .Get() + .Include(x => x.CreatedBy) + .Include(x => x.AssignedTo) + .Where(x => x.AssignedTo != null) + .Where(x => x.AssignedTo!.Id == IdentityService.User.Id) + .Where(x => x.Status != TicketStatus.Closed) + .ToArray(); + break; + case 3: + AllTickets = TicketRepository + .Get() + .Include(x => x.CreatedBy) + .Include(x => x.AssignedTo) + .ToArray(); + break; } + + return Task.CompletedTask; } - private async Task Unsubscribe() + private async Task UpdateFilter(int filterId) { - await EventSystem.Off("tickets.new", this); - - if (AdminService.Ticket != null) - { - await EventSystem.Off($"tickets.{AdminService.Ticket.Id}.message", this); - await EventSystem.Off($"tickets.{AdminService.Ticket.Id}.status", this); - } - } - - private async Task ReloadTickets(bool reloadMessages = true) - { - AdminService.Ticket = null; - AssignedTickets = await AdminService.GetAssigned(); - UnAssignedTickets = await AdminService.GetUnAssigned(); - - if (Id != 0) - { - AdminService.Ticket = AssignedTickets - .FirstOrDefault(x => x.Key.Id == Id) - .Key ?? null; - - if (AdminService.Ticket == null) - { - AdminService.Ticket = UnAssignedTickets - .FirstOrDefault(x => x.Key.Id == Id) - .Key ?? null; - } - - if (AdminService.Ticket == null) - return; - - Status = AdminService.Ticket.Status; - Priority = AdminService.Ticket.Priority; - - if (reloadMessages) - { - var msgs = await AdminService.GetMessages(); - Messages = msgs.ToList(); - } - } - } - - public async void Dispose() - { - await Unsubscribe(); + Filter = filterId; + await TicketLazyLoader.Reload(); } } \ No newline at end of file diff --git a/Moonlight/Shared/Views/Admin/Support/Old_Index.razor b/Moonlight/Shared/Views/Admin/Support/Old_Index.razor new file mode 100644 index 0000000..0bed34f --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Support/Old_Index.razor @@ -0,0 +1,379 @@ +@page "/old_admin/support" +@page "/old_admin/support/{Id:int}" + +@using Moonlight.App.Services.Tickets +@using Moonlight.App.Database.Entities +@using Moonlight.App.Events +@using Moonlight.App.Helpers +@using Moonlight.App.Models.Misc +@using Moonlight.App.Services +@using Moonlight.App.Services.Sessions +@using Moonlight.Shared.Components.Tickets + +@inject TicketAdminService AdminService +@inject SmartTranslateService SmartTranslateService +@inject EventSystem EventSystem +@inject IdentityService IdentityService + +@attribute [PermissionRequired(nameof(Permissions.AdminSupport))] + +@implements IDisposable + +
+
+
+
+
+
+ + Unassigned tickets + +
+ + @foreach (var ticket in UnAssignedTickets) + { +
+
+
+ @(ticket.Key.IssueTopic) + @if (ticket.Value != null) + { +
+ @(ticket.Value.Content) +
+ } +
+
+
+ @if (ticket.Value != null) + { + + @(Formatter.FormatAgoFromDateTime(ticket.Value.CreatedAt, SmartTranslateService)) + + } +
+
+ + if (ticket.Key != UnAssignedTickets.Last().Key) + { +
+ } + } + + @if (AssignedTickets.Any()) + { +
+ + Assigned tickets + +
+ } + + @foreach (var ticket in AssignedTickets) + { +
+
+
+ @(ticket.Key.IssueTopic) + @if (ticket.Value != null) + { +
+ @(ticket.Value.Content) +
+ } +
+
+
+ @if (ticket.Value != null) + { + + @(Formatter.FormatAgoFromDateTime(ticket.Value.CreatedAt, SmartTranslateService)) + + } +
+
+ + if (ticket.Key != AssignedTickets.Last().Key) + { +
+ } + } +
+
+
+
+
+
+
+ @if (AdminService.Ticket != null) + { +
+
+ @(AdminService.Ticket.IssueTopic) +
+ + Status + + @switch (AdminService.Ticket.Status) + { + case TicketStatus.Closed: + + break; + case TicketStatus.Open: + + break; + case TicketStatus.Pending: + + break; + case TicketStatus.WaitingForUser: + + break; + } + @(AdminService.Ticket.Status) + + + Priority + + @switch (AdminService.Ticket.Priority) + { + case TicketPriority.Low: + + break; + case TicketPriority.Medium: + + break; + case TicketPriority.High: + + break; + case TicketPriority.Critical: + + break; + } + @(AdminService.Ticket.Priority) +
+
+
+
+
+
+ @if (AdminService.Ticket!.AssignedTo == null) + { + + } + else + { + + } +
+ + + + + + +
+
+ } + else + { +
+
+ + + +
+
+ } +
+
+
+ @if (AdminService.Ticket == null) + { + } + else + { + + } +
+
+ @if (AdminService.Ticket != null) + { + + } +
+
+
+ +@code +{ + [Parameter] + public int Id { get; set; } + + private Dictionary AssignedTickets; + private Dictionary UnAssignedTickets; + private List Messages = new(); + private string MessageText; + private SmartFileSelect FileSelect; + + private TicketPriority Priority; + private TicketStatus Status; + + protected override async Task OnParametersSetAsync() + { + await Unsubscribe(); + await ReloadTickets(); + await Subscribe(); + + await InvokeAsync(StateHasChanged); + } + + private async Task UpdatePriority() + { + await AdminService.UpdatePriority(Priority); + } + + private async Task UpdateStatus() + { + await AdminService.UpdateStatus(Status); + } + + private async Task SendMessage() + { + if (string.IsNullOrEmpty(MessageText) && FileSelect.SelectedFile != null) + MessageText = "File upload"; + + if (string.IsNullOrEmpty(MessageText)) + return; + + var msg = await AdminService.Send(MessageText, FileSelect.SelectedFile); + Messages.Add(msg); + MessageText = ""; + FileSelect.SelectedFile = null; + + await InvokeAsync(StateHasChanged); + } + + private async Task Subscribe() + { + await EventSystem.On("tickets.new", this, async _ => + { + await ReloadTickets(false); + await InvokeAsync(StateHasChanged); + }); + + if (AdminService.Ticket != null) + { + await EventSystem.On($"tickets.{AdminService.Ticket.Id}.message", this, async message => + { + if (message.Sender != null && message.Sender.Id == IdentityService.User.Id && message.IsSupportMessage) + return; + + Messages.Add(message); + await InvokeAsync(StateHasChanged); + }); + + await EventSystem.On($"tickets.{AdminService.Ticket.Id}.status", this, async _ => + { + await ReloadTickets(false); + await InvokeAsync(StateHasChanged); + }); + } + } + + private async Task Unsubscribe() + { + await EventSystem.Off("tickets.new", this); + + if (AdminService.Ticket != null) + { + await EventSystem.Off($"tickets.{AdminService.Ticket.Id}.message", this); + await EventSystem.Off($"tickets.{AdminService.Ticket.Id}.status", this); + } + } + + private async Task ReloadTickets(bool reloadMessages = true) + { + AdminService.Ticket = null; + //AssignedTickets = await AdminService.GetAssigned(); + //UnAssignedTickets = await AdminService.GetUnAssigned(); + + if (Id != 0) + { + AdminService.Ticket = AssignedTickets + .FirstOrDefault(x => x.Key.Id == Id) + .Key ?? null; + + if (AdminService.Ticket == null) + { + AdminService.Ticket = UnAssignedTickets + .FirstOrDefault(x => x.Key.Id == Id) + .Key ?? null; + } + + if (AdminService.Ticket == null) + return; + + Status = AdminService.Ticket.Status; + Priority = AdminService.Ticket.Priority; + + if (reloadMessages) + { + var msgs = await AdminService.GetMessages(); + Messages = msgs.ToList(); + } + } + } + + public async void Dispose() + { + await Unsubscribe(); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Admin/Support/View.razor b/Moonlight/Shared/Views/Admin/Support/View.razor new file mode 100644 index 0000000..b21f56a --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Support/View.razor @@ -0,0 +1,293 @@ +@page "/admin/support/view/{Id:int}" + +@using Moonlight.App.Services.Tickets +@using Moonlight.App.Database.Entities +@using Moonlight.App.Helpers +@using Moonlight.App.Models.Misc +@using Moonlight.App.Repositories +@using Moonlight.App.Services +@using Microsoft.EntityFrameworkCore +@using Moonlight.App.Events +@using Moonlight.App.Services.Sessions +@using Moonlight.Shared.Components.Tickets + +@inject TicketAdminService TicketAdminService +@inject SmartTranslateService SmartTranslateService +@inject Repository TicketRepository +@inject EventSystem Event +@inject IdentityService IdentityService + +@implements IDisposable + +@attribute [PermissionRequired(nameof(Permissions.AdminSupportView))] + + + @if (Ticket == null) + { + + } + else + { +
+
+
+
+
+ @(Ticket.IssueTopic) +
+ + Issue description: + +

+ @(Formatter.FormatLineBreaks(Ticket.IssueDescription)) +

+ + Issue resolve tries: + +

+ @(Formatter.FormatLineBreaks(Ticket.IssueTries)) +

+
+
+ +
+ +
+
+
+
+ Ticket details +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + @if (Ticket.AssignedTo == null) + { + + } + else + { + + } + + + + + + + + + + + + + + + + + + + + + + +
+ Ticket ID + @(Ticket.Id)
+ User + + + @(Ticket.CreatedBy.FirstName) @(Ticket.CreatedBy.LastName) + +
+ Subject + + @(Ticket.Subject) +
+ Subject ID + @(Ticket.SubjectId)
+ Assigned to + + None + @(Ticket.AssignedTo.FirstName) @(Ticket.AssignedTo.LastName)
+ Status + + +
+ Priority + + +
+ Created at + @(Formatter.FormatDate(Ticket.CreatedAt))
+ +
+ + + +
+
+
+
+
+
+ } +
+ +@code +{ + [Parameter] + public int Id { get; set; } + + private Ticket? Ticket { get; set; } + private TicketPriority PriorityModified; + private TicketStatus StatusModified; + + private List Messages = new(); + private SmartFileSelect FileSelect; + private string MessageContent = ""; + + private LazyLoader LazyLoader; + + private async Task Load(LazyLoader _) + { + Ticket = TicketRepository + .Get() + .Include(x => x.AssignedTo) + .Include(x => x.CreatedBy) + .FirstOrDefault(x => x.Id == Id); + + if (Ticket != null) + { + TicketAdminService.Ticket = Ticket; + PriorityModified = Ticket.Priority; + StatusModified = Ticket.Status; + + Messages = (await TicketAdminService.GetMessages()).ToList(); + + // Register events + + await Event.On($"tickets.{Ticket.Id}.message", this, async message => + { + if (message.Sender != null && message.Sender.Id == IdentityService.User.Id && message.IsSupportMessage) + return; + + Messages.Add(message); + await InvokeAsync(StateHasChanged); + }); + + await Event.On($"tickets.{Ticket.Id}.status", this, async _ => + { + //TODO: Does not work because of data caching. So we dont reload because it will look the same anyways + //await LazyLoader.Reload(); + }); + } + } + + private async Task Save() + { + if (PriorityModified != Ticket!.Priority) + await TicketAdminService.UpdatePriority(PriorityModified); + + if (StatusModified != Ticket!.Status) + await TicketAdminService.UpdateStatus(StatusModified); + } + + private async Task SetClaim(User? user) + { + await TicketAdminService.SetClaim(user); + } + + private async Task SendMessage() + { + if (string.IsNullOrEmpty(MessageContent) && FileSelect.SelectedFile != null) + MessageContent = "File upload"; + + if (string.IsNullOrEmpty(MessageContent)) + return; + + var msg = await TicketAdminService.Send( + MessageContent, + FileSelect.SelectedFile + ); + + Messages.Add(msg); + + MessageContent = ""; + FileSelect.SelectedFile = null; + + await InvokeAsync(StateHasChanged); + } + + public async void Dispose() + { + if (Ticket != null) + { + await Event.Off($"tickets.{Ticket.Id}.message", this); + await Event.Off($"tickets.{Ticket.Id}.status", this); + } + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Support/Index.razor b/Moonlight/Shared/Views/Support/Index.razor index cc478b4..9dd4bd2 100644 --- a/Moonlight/Shared/Views/Support/Index.razor +++ b/Moonlight/Shared/Views/Support/Index.razor @@ -1,353 +1,217 @@ @page "/support" -@page "/support/{Id:int}" - @using Moonlight.App.Services.Tickets @using Moonlight.App.Database.Entities -@using Moonlight.App.Helpers -@using Moonlight.App.Models.Forms -@using Moonlight.App.Models.Misc @using Moonlight.App.Repositories -@using Moonlight.App.Services @using Microsoft.EntityFrameworkCore -@using Moonlight.App.Events -@using Moonlight.App.Services.Files +@using Moonlight.App.Models.Misc +@using BlazorTable +@using Moonlight.App.Models.Forms +@using Moonlight.App.Services @using Moonlight.App.Services.Sessions -@using Moonlight.Shared.Components.Tickets -@inject TicketClientService ClientService -@inject Repository ServerRepository -@inject Repository WebSpaceRepository -@inject Repository DomainRepository +@inject TicketClientService TicketClientService +@inject Repository TicketRepository @inject SmartTranslateService SmartTranslateService @inject IdentityService IdentityService +@inject Repository WebSpaceRepository +@inject Repository DomainRepository +@inject Repository ServerRepository @inject NavigationManager NavigationManager -@inject ResourceService ResourceService -@inject EventSystem EventSystem -
-
-
-
-
- - -
- - @foreach (var ticket in Tickets) - { -
-
-
- @(ticket.Key.IssueTopic) - @if (ticket.Value != null) - { -
- @(ticket.Value.Content) -
- } -
-
-
- @if (ticket.Value != null) - { - - @(Formatter.FormatAgoFromDateTime(ticket.Value.CreatedAt, SmartTranslateService)) - - } -
-
- - if (ticket.Key != Tickets.Last().Key) - { -
- } - } -
-
-
-
-
-
-
- @if (ClientService.Ticket != null) - { -
-
- @(ClientService.Ticket.IssueTopic) -
- - Status - - @switch (ClientService.Ticket.Status) - { - case TicketStatus.Closed: - - break; - case TicketStatus.Open: - - break; - case TicketStatus.Pending: - - break; - case TicketStatus.WaitingForUser: - - break; - } - @(ClientService.Ticket.Status) - - - Priority - - @switch (ClientService.Ticket.Priority) - { - case TicketPriority.Low: - - break; - case TicketPriority.Medium: - - break; - case TicketPriority.High: - - break; - case TicketPriority.Critical: - - break; - } - @(ClientService.Ticket.Priority) -
-
-
- - } - else - { -
-
- +
+
+
+
+ Create a new ticket
-
- } -
-
-
- @if (ClientService.Ticket == null) - { - - -
- - -
-
- + + +
+ - -
-
- - -
-
- + @foreach (var subject in (TicketSubject[])Enum.GetValues(typeof(TicketSubject))) + { + if (Model.Subject == subject) + { + + } + else + { + + } + } + +
+
+ @if (Model.Subject == TicketSubject.Domain) { - if (Model.Subject == subject) - { - - } - else - { - - } + } - -
-
- @if (Model.Subject == TicketSubject.Domain) - { - + @foreach (var server in Servers) { - + if (Model.SubjectId == server.Id) + { + + } + else + { + + } } - else + + } + else if (Model.Subject == TicketSubject.Webspace) + { + - } - else if (Model.Subject == TicketSubject.Server) - { - - } - else if (Model.Subject == TicketSubject.Webspace) - { - - } -
-
- -
-
-
- } - else - { - - } -
-
-@if (ClientService.Ticket != null) -{ - +
+
+
+
+
+ + Your tickets + +
+
+ +
+ + + + + + + + + + + + + + +
+
+
+
-} -
-
@code { - [Parameter] - public int Id { get; set; } - - private Dictionary Tickets; - private List Messages = new(); + private Ticket[] Tickets; private CreateTicketDataModel Model = new(); - private string MessageText; - private SmartFileSelect FileSelect; - + private Server[] Servers; private WebSpace[] WebSpaces; private Domain[] Domains; - protected override async Task OnParametersSetAsync() + private Task LoadTickets(LazyLoader _) { - await Unsubscribe(); - await ReloadTickets(); - await Subscribe(); + Tickets = TicketRepository + .Get() + .Include(x => x.CreatedBy) + .Include(x => x.AssignedTo) + .Where(x => x.Status != TicketStatus.Closed) + .Where(x => x.CreatedBy.Id == IdentityService.User.Id) + .ToArray(); - await InvokeAsync(StateHasChanged); + return Task.CompletedTask; } - + private Task LoadTicketCreate(LazyLoader _) { Servers = ServerRepository @@ -374,95 +238,16 @@ private async Task OnValidSubmit() { - var ticket = await ClientService.Create( + var ticket = await TicketClientService.Create( Model.IssueTopic, Model.IssueDescription, Model.IssueTries, Model.Subject, Model.SubjectId - ); + ); Model = new(); - NavigationManager.NavigateTo("/support/" + ticket.Id); - } - - private async Task SendMessage() - { - if (string.IsNullOrEmpty(MessageText) && FileSelect.SelectedFile != null) - MessageText = "File upload"; - - if(string.IsNullOrEmpty(MessageText)) - return; - - var msg = await ClientService.Send(MessageText, FileSelect.SelectedFile); - Messages.Add(msg); - MessageText = ""; - FileSelect.SelectedFile = null; - - await InvokeAsync(StateHasChanged); - } - - private async Task Subscribe() - { - await EventSystem.On("tickets.new", this, async ticket => - { - if (ticket.CreatedBy != null && ticket.CreatedBy.Id != IdentityService.User.Id) - return; - - await ReloadTickets(false); - await InvokeAsync(StateHasChanged); - }); - - if (ClientService.Ticket != null) - { - await EventSystem.On($"tickets.{ClientService.Ticket.Id}.message", this, async message => - { - if (message.Sender != null && message.Sender.Id == IdentityService.User.Id && !message.IsSupportMessage) - return; - - Messages.Add(message); - await InvokeAsync(StateHasChanged); - }); - - await EventSystem.On($"tickets.{ClientService.Ticket.Id}.status", this, async _ => - { - await ReloadTickets(false); - await InvokeAsync(StateHasChanged); - }); - } - } - - private async Task Unsubscribe() - { - await EventSystem.Off("tickets.new", this); - - if (ClientService.Ticket != null) - { - await EventSystem.Off($"tickets.{ClientService.Ticket.Id}.message", this); - await EventSystem.Off($"tickets.{ClientService.Ticket.Id}.status", this); - } - } - - private async Task ReloadTickets(bool reloadMessages = true) - { - ClientService.Ticket = null; - Tickets = await ClientService.Get(); - - if (Id != 0) - { - ClientService.Ticket = Tickets - .FirstOrDefault(x => x.Key.Id == Id) - .Key ?? null; - - if (ClientService.Ticket == null) - return; - - if (reloadMessages) - { - var msgs = await ClientService.GetMessages(); - Messages = msgs.ToList(); - } - } + NavigationManager.NavigateTo("/support/view/" + ticket.Id); } } \ No newline at end of file diff --git a/Moonlight/Shared/Views/Support/View.razor b/Moonlight/Shared/Views/Support/View.razor new file mode 100644 index 0000000..c5eb39c --- /dev/null +++ b/Moonlight/Shared/Views/Support/View.razor @@ -0,0 +1,131 @@ +@page "/support/view/{Id:int}" + +@using Moonlight.App.Services.Tickets +@using Moonlight.App.Database.Entities +@using Moonlight.App.Repositories +@using Moonlight.App.Services +@using Moonlight.Shared.Components.Tickets +@using Microsoft.EntityFrameworkCore +@using Moonlight.App.Events +@using Moonlight.App.Services.Sessions + +@inject TicketClientService TicketClientService +@inject SmartTranslateService SmartTranslateService +@inject Repository TicketRepository +@inject IdentityService IdentityService +@inject EventSystem Event + +@implements IDisposable + + + @if (Ticket == null) + { + + } + else + { +
+
+ @(Ticket.IssueTopic) +
+
+ +
+ +
+ } +
+ +@code +{ + [Parameter] + public int Id { get; set; } + + private Ticket? Ticket; + private List Messages = new(); + + private SmartFileSelect FileSelect; + private string MessageContent = ""; + + private async Task Load(LazyLoader _) + { + Ticket = TicketRepository + .Get() + .Include(x => x.CreatedBy) + .Where(x => x.CreatedBy.Id == IdentityService.User.Id) + .FirstOrDefault(x => x.Id == Id); + + if (Ticket != null) + { + TicketClientService.Ticket = Ticket; + + Messages = (await TicketClientService.GetMessages()).ToList(); + + // Register events + + await Event.On($"tickets.{Ticket.Id}.message", this, async message => + { + if (message.Sender != null && message.Sender.Id == IdentityService.User.Id && !message.IsSupportMessage) + return; + + Messages.Add(message); + await InvokeAsync(StateHasChanged); + }); + + await Event.On($"tickets.{Ticket.Id}.status", this, async _ => + { + //TODO: Does not work because of data caching. So we dont reload because it will look the same anyways + //await LazyLoader.Reload(); + }); + } + } + + private async Task SendMessage() + { + if (string.IsNullOrEmpty(MessageContent) && FileSelect.SelectedFile != null) + MessageContent = "File upload"; + + if (string.IsNullOrEmpty(MessageContent)) + return; + + var msg = await TicketClientService.Send( + MessageContent, + FileSelect.SelectedFile + ); + + Messages.Add(msg); + + MessageContent = ""; + FileSelect.SelectedFile = null; + + await InvokeAsync(StateHasChanged); + } + + public async void Dispose() + { + if (Ticket != null) + { + await Event.Off($"tickets.{Ticket.Id}.message", this); + await Event.Off($"tickets.{Ticket.Id}.status", this); + } + } +} \ No newline at end of file diff --git a/Moonlight/wwwroot/assets/media/svg/create.svg b/Moonlight/wwwroot/assets/media/svg/create.svg new file mode 100644 index 0000000..cea3eba --- /dev/null +++ b/Moonlight/wwwroot/assets/media/svg/create.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Moonlight/wwwroot/assets/media/svg/opentabs.svg b/Moonlight/wwwroot/assets/media/svg/opentabs.svg new file mode 100644 index 0000000..39fcc50 --- /dev/null +++ b/Moonlight/wwwroot/assets/media/svg/opentabs.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file