Started implementing admin ticket ui. Cleaned up some stuff
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Moonlight.App.Database.Entities.Tickets;
|
using Moonlight.App.Database.Entities.Tickets;
|
||||||
|
using Moonlight.App.Database.Enums;
|
||||||
using Moonlight.App.Event;
|
using Moonlight.App.Event;
|
||||||
using Moonlight.App.Event.Args;
|
using Moonlight.App.Event.Args;
|
||||||
using Moonlight.App.Extensions;
|
using Moonlight.App.Extensions;
|
||||||
@@ -50,7 +51,31 @@ public class TicketChatService
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Stop()
|
public async Task Update(bool open, TicketPriority priority) // Updated and syncs ticket states to all listeners
|
||||||
|
{
|
||||||
|
if (Ticket.Open != open)
|
||||||
|
{
|
||||||
|
Ticket.Open = open;
|
||||||
|
|
||||||
|
if(open)
|
||||||
|
await SendSystemMessage("Ticket has been opened");
|
||||||
|
else
|
||||||
|
await SendSystemMessage("Ticket has been closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Ticket.Priority != priority)
|
||||||
|
{
|
||||||
|
Ticket.Priority = priority;
|
||||||
|
|
||||||
|
await SendSystemMessage($"Ticket priority to {priority}");
|
||||||
|
}
|
||||||
|
|
||||||
|
TicketRepository.Update(Ticket);
|
||||||
|
|
||||||
|
await Events.OnTicketUpdated.InvokeAsync(Ticket);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Stop() // Clear cache and stop listeners
|
||||||
{
|
{
|
||||||
Events.OnTicketMessage -= OnTicketMessage;
|
Events.OnTicketMessage -= OnTicketMessage;
|
||||||
Events.OnTicketUpdated -= OnTicketUpdated;
|
Events.OnTicketUpdated -= OnTicketUpdated;
|
||||||
@@ -60,7 +85,24 @@ public class TicketChatService
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendMessage(string content, Stream? attachmentStream = null, string? attachmentName = null)
|
#region Sending
|
||||||
|
|
||||||
|
public async Task SendSystemMessage(string content) // use this to send a message shown in a seperator
|
||||||
|
{
|
||||||
|
// Build the message model
|
||||||
|
var message = new TicketMessage()
|
||||||
|
{
|
||||||
|
Content = content,
|
||||||
|
Attachment = null,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Sender = null,
|
||||||
|
IsSupport = IsSupporter
|
||||||
|
};
|
||||||
|
|
||||||
|
await SyncMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendMessage(string content, Stream? attachmentStream = null, string? attachmentName = null) // Regular send method
|
||||||
{
|
{
|
||||||
if(string.IsNullOrEmpty(content))
|
if(string.IsNullOrEmpty(content))
|
||||||
return;
|
return;
|
||||||
@@ -87,6 +129,11 @@ public class TicketChatService
|
|||||||
IsSupport = IsSupporter
|
IsSupport = IsSupporter
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await SyncMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SyncMessage(TicketMessage message) // Use this function to save and sync function to others
|
||||||
|
{
|
||||||
// Save ticket to the db
|
// Save ticket to the db
|
||||||
var t = TicketRepository
|
var t = TicketRepository
|
||||||
.Get()
|
.Get()
|
||||||
@@ -103,6 +150,8 @@ public class TicketChatService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
private async void OnTicketUpdated(object? _, Ticket ticket)
|
private async void OnTicketUpdated(object? _, Ticket ticket)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,6 +47,17 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<_ContentIncludedByDefault Remove="storage\config.json" />
|
<_ContentIncludedByDefault Remove="storage\config.json" />
|
||||||
|
<_ContentIncludedByDefault Remove="Shared\Components\Partials\TicketPopup\LiveChatCreate.razor" />
|
||||||
|
<_ContentIncludedByDefault Remove="Shared\Components\Partials\TicketPopup\LiveChatMain.razor" />
|
||||||
|
<_ContentIncludedByDefault Remove="Shared\Components\Partials\TicketPopup\LiveChatOverview.razor" />
|
||||||
|
<_ContentIncludedByDefault Remove="Shared\Components\Partials\TicketPopup\LiveChatView.razor" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AdditionalFiles Include="Shared\Components\TicketPopup\LiveChatCreate.razor" />
|
||||||
|
<AdditionalFiles Include="Shared\Components\TicketPopup\LiveChatMain.razor" />
|
||||||
|
<AdditionalFiles Include="Shared\Components\TicketPopup\LiveChatOverview.razor" />
|
||||||
|
<AdditionalFiles Include="Shared\Components\TicketPopup\LiveChatView.razor" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
|
|||||||
var logConfig = new LoggerConfiguration();
|
var logConfig = new LoggerConfiguration();
|
||||||
|
|
||||||
logConfig = logConfig.Enrich.FromLogContext()
|
logConfig = logConfig.Enrich.FromLogContext()
|
||||||
|
.MinimumLevel.Debug()
|
||||||
.WriteTo.Console(
|
.WriteTo.Console(
|
||||||
outputTemplate:
|
outputTemplate:
|
||||||
"{Timestamp:HH:mm:ss} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}");
|
"{Timestamp:HH:mm:ss} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}");
|
||||||
|
|||||||
@@ -106,6 +106,17 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link " href="/admin/tickets">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-sm bx-support"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
Tickets
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="menu-item">
|
<div class="menu-item">
|
||||||
<a class="menu-link " href="/admin/sys">
|
<a class="menu-link " href="/admin/sys">
|
||||||
<span class="menu-icon">
|
<span class="menu-icon">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title fs-5">Create a new ticket</span>
|
<span class="card-title fs-5">Create a new ticket</span>
|
||||||
<div class="card-toolbar">
|
<div class="card-toolbar">
|
||||||
<button @onclick="() => LiveChatMain.SetViewIndex(1)" class="btn btn-rounded-circle btn-icon">
|
<button @onclick="() => TicketPopupMain.SetViewIndex(1)" class="btn btn-rounded-circle btn-icon">
|
||||||
<i class="bx bx-sm bx-chevron-left"></i>
|
<i class="bx bx-sm bx-chevron-left"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public LiveChatMain LiveChatMain { get; set; }
|
public TicketPopupMain TicketPopupMain { get; set; }
|
||||||
|
|
||||||
private Service[] Services;
|
private Service[] Services;
|
||||||
private CreateTicketForm Form = new();
|
private CreateTicketForm Form = new();
|
||||||
@@ -77,6 +77,6 @@
|
|||||||
Form.Service
|
Form.Service
|
||||||
);
|
);
|
||||||
|
|
||||||
await LiveChatMain.OpenTicket(ticket);
|
await TicketPopupMain.OpenTicket(ticket);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,15 +12,15 @@
|
|||||||
<div class="card border border-2 border-warning" style="pointer-events: all; height: 70vh">
|
<div class="card border border-2 border-warning" style="pointer-events: all; height: 70vh">
|
||||||
@if (ViewIndex == 1)
|
@if (ViewIndex == 1)
|
||||||
{
|
{
|
||||||
<LiveChatOverview />
|
<TicketPopupOverview />
|
||||||
}
|
}
|
||||||
else if (ViewIndex == 2)
|
else if (ViewIndex == 2)
|
||||||
{
|
{
|
||||||
<LiveChatView />
|
<TicketPopupView />
|
||||||
}
|
}
|
||||||
else if (ViewIndex == 3)
|
else if (ViewIndex == 3)
|
||||||
{
|
{
|
||||||
<LiveChatCreate />
|
<TicketPopupCreate />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title fs-5">Your tickets</span>
|
<span class="card-title fs-5">Your tickets</span>
|
||||||
<div class="card-toolbar">
|
<div class="card-toolbar">
|
||||||
<button @onclick="() => LiveChatMain.SetViewIndex(0)" class="btn btn-rounded-circle btn-icon">
|
<button @onclick="() => TicketPopupMain.SetViewIndex(0)" class="btn btn-rounded-circle btn-icon">
|
||||||
<i class="bx bx-sm bx-x"></i>
|
<i class="bx bx-sm bx-x"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="card-body pt-5">
|
<div class="card-body pt-5">
|
||||||
<div class="scroll-y me-n5 pe-5" style="height: 50vh; width: 40vh; display: flex; flex-direction: column;">
|
<div class="scroll-y me-n5 pe-5" style="height: 50vh; width: 40vh; display: flex; flex-direction: column;">
|
||||||
<div class="d-flex flex-stack py-2 justify-content-center">
|
<div class="d-flex flex-stack py-2 justify-content-center">
|
||||||
<h3 class="align-middle text-center">Need help? Create a <a @onclick="() => LiveChatMain.SetViewIndex(3)" @onclick:preventDefault href="#" class="text-primary">ticket</a></h3>
|
<h3 class="align-middle text-center">Need help? Create a <a @onclick="() => TicketPopupMain.SetViewIndex(3)" @onclick:preventDefault href="#" class="text-primary">ticket</a></h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex flex-stack py-4">
|
<div class="d-flex flex-stack py-4">
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
{
|
{
|
||||||
foreach (var ticket in Tickets)
|
foreach (var ticket in Tickets)
|
||||||
{
|
{
|
||||||
<a href="#" @onclick="() => LiveChatMain.OpenTicket(ticket)" @onclick:preventDefault class="d-flex flex-stack py-4">
|
<a href="#" @onclick="() => TicketPopupMain.OpenTicket(ticket)" @onclick:preventDefault class="d-flex flex-stack py-4">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary mb-2">@(ticket.Name)</a>
|
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary mb-2">@(ticket.Name)</a>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public LiveChatMain LiveChatMain { get; set; }
|
public TicketPopupMain TicketPopupMain { get; set; }
|
||||||
|
|
||||||
private Ticket[] Tickets;
|
private Ticket[] Tickets;
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title fs-5">@(HasStarted ? TicketService.Chat.Ticket.Name : "Loading")</span>
|
<span class="card-title fs-5">@(HasStarted ? TicketService.Chat.Ticket.Name : "Loading")</span>
|
||||||
<div class="card-toolbar">
|
<div class="card-toolbar">
|
||||||
<button @onclick="() => LiveChatMain.SetViewIndex(1)" class="btn btn-rounded-circle btn-icon">
|
<button @onclick="() => TicketPopupMain.SetViewIndex(1)" class="btn btn-rounded-circle btn-icon">
|
||||||
<i class="bx bx-sm bx-chevron-left"></i>
|
<i class="bx bx-sm bx-chevron-left"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<textarea @bind="MyMessageContent" class="form-control form-control-solid-bg rounded-end me-3" placeholder="Type a message" style="height: 1vh"></textarea>
|
<textarea @bind="MessageContent" class="form-control form-control-solid-bg rounded-end me-3" placeholder="Type a message" style="height: 1vh"></textarea>
|
||||||
<ChatFileSelect @ref="FileSelect"/>
|
<ChatFileSelect @ref="FileSelect"/>
|
||||||
<WButton OnClick="SendMessage" CssClasses="ms-2 btn btn-icon btn-bg-light btn-color-white">
|
<WButton OnClick="SendMessage" CssClasses="ms-2 btn btn-icon btn-bg-light btn-color-white">
|
||||||
<i class="bx bx-sm bx-send"></i>
|
<i class="bx bx-sm bx-send"></i>
|
||||||
@@ -78,12 +78,12 @@
|
|||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public LiveChatMain LiveChatMain { get; set; }
|
public TicketPopupMain TicketPopupMain { get; set; }
|
||||||
|
|
||||||
private ChatFileSelect FileSelect;
|
|
||||||
|
|
||||||
private bool HasStarted = false;
|
private bool HasStarted = false;
|
||||||
private string MyMessageContent = "";
|
|
||||||
|
private ChatFileSelect FileSelect;
|
||||||
|
private string MessageContent = "";
|
||||||
|
|
||||||
private async Task Load(LazyLoader lazyLoader)
|
private async Task Load(LazyLoader lazyLoader)
|
||||||
{
|
{
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
|
|
||||||
// Initialize chat service and start it
|
// Initialize chat service and start it
|
||||||
TicketService.Chat.OnUpdate = OnUpdate;
|
TicketService.Chat.OnUpdate = OnUpdate;
|
||||||
await TicketService.Chat.Start(LiveChatMain.CurrentTicket);
|
await TicketService.Chat.Start(TicketPopupMain.CurrentTicket);
|
||||||
|
|
||||||
// Let the ui know that we are ready
|
// Let the ui know that we are ready
|
||||||
HasStarted = true;
|
HasStarted = true;
|
||||||
@@ -105,18 +105,18 @@
|
|||||||
|
|
||||||
private async Task SendMessage()
|
private async Task SendMessage()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(MyMessageContent) && FileSelect.SelectedFile == null)
|
if (string.IsNullOrEmpty(MessageContent) && FileSelect.SelectedFile == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!HasStarted)
|
if (!HasStarted)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (FileSelect.SelectedFile == null)
|
if (FileSelect.SelectedFile == null)
|
||||||
await TicketService.Chat.SendMessage(MyMessageContent);
|
await TicketService.Chat.SendMessage(MessageContent);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await TicketService.Chat.SendMessage(
|
await TicketService.Chat.SendMessage(
|
||||||
string.IsNullOrEmpty(MyMessageContent) ? $"Upload of {FileSelect.SelectedFile.Name}" : MyMessageContent,
|
string.IsNullOrEmpty(MessageContent) ? $"Upload of {FileSelect.SelectedFile.Name}" : MessageContent,
|
||||||
FileSelect.SelectedFile.OpenReadStream(1024 * 1024 * 5),
|
FileSelect.SelectedFile.OpenReadStream(1024 * 1024 * 5),
|
||||||
FileSelect.SelectedFile.Name
|
FileSelect.SelectedFile.Name
|
||||||
);
|
);
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
await FileSelect.RemoveSelection();
|
await FileSelect.RemoveSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
MyMessageContent = "";
|
MessageContent = "";
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@using Moonlight.Shared.Components.Partials.LiveChat
|
@using Moonlight.Shared.Components.TicketPopup
|
||||||
|
|
||||||
<div class="d-flex flex-column flex-root app-root">
|
<div class="d-flex flex-column flex-root app-root">
|
||||||
<div class="app-page flex-column flex-column-fluid">
|
<div class="app-page flex-column flex-column-fluid">
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="app-container container-fluid">
|
<div class="app-container container-fluid">
|
||||||
@ChildContent
|
@ChildContent
|
||||||
|
|
||||||
<LiveChatMain />
|
<TicketPopupMain />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
265
Moonlight/Shared/Views/Admin/Tickets/Index.razor
Normal file
265
Moonlight/Shared/Views/Admin/Tickets/Index.razor
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
@page "/admin/tickets"
|
||||||
|
|
||||||
|
@using Moonlight.App.Extensions.Attributes
|
||||||
|
@using Moonlight.App.Models.Enums
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.App.Database.Entities.Tickets
|
||||||
|
@using BlazorTable
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using Moonlight.App.Database.Enums
|
||||||
|
@using Moonlight.App.Event
|
||||||
|
@using Moonlight.App.Event.Args
|
||||||
|
|
||||||
|
@attribute [RequirePermission(Permission.AdminTickets)]
|
||||||
|
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
@inject Repository<Ticket> TicketRepository
|
||||||
|
|
||||||
|
<div class="row mb-5">
|
||||||
|
<LazyLoader @ref="StatisticsLazyLoader" Load="LoadStatistics" ShowAsCard="true">
|
||||||
|
<div class="col-md-4 col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center gx-0">
|
||||||
|
<div class="col">
|
||||||
|
<h6 class="text-uppercase text-muted mb-2">
|
||||||
|
<TL>Total Tickets</TL>
|
||||||
|
</h6>
|
||||||
|
<span class="h2 mb-0">
|
||||||
|
@(TotalTicketsCount)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="h2 text-muted mb-0">
|
||||||
|
<i class="text-primary bx bx-purchase-tag bx-lg"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center gx-0">
|
||||||
|
<div class="col">
|
||||||
|
<h6 class="text-uppercase text-muted mb-2">
|
||||||
|
<TL>Pending tickets</TL>
|
||||||
|
</h6>
|
||||||
|
<span class="h2 mb-0">
|
||||||
|
@(PendingTicketsCount)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="h2 text-muted mb-">
|
||||||
|
<i class="text-primary bx bx-hourglass bx-lg"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center gx-0">
|
||||||
|
<div class="col">
|
||||||
|
<h6 class="text-uppercase text-muted mb-2">
|
||||||
|
<TL>Closed tickets</TL>
|
||||||
|
</h6>
|
||||||
|
<span class="h2 mb-0">
|
||||||
|
@(ClosedTicketsCount)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="h2 text-muted mb-">
|
||||||
|
<i class="text-primary bx bx-lock bx-lg"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
<TL>Ticket overview</TL>
|
||||||
|
</span>
|
||||||
|
<div class="card-toolbar">
|
||||||
|
<div class="btn-group">
|
||||||
|
<WButton Text="Overview" CssClasses="btn-secondary" OnClick="() => UpdateFilter(0)" />
|
||||||
|
<WButton Text="Closed tickets" CssClasses="btn-secondary" OnClick="() => UpdateFilter(1)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<LazyLoader @ref="TicketLazyLoader" Load="LoadTickets" ShowAsCard="true">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<Table TableItem="Ticket" Items="AllTickets" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
|
||||||
|
<Column TableItem="Ticket" Title="Id" Field="@(x => x.Id)" Filterable="true" Sortable="true"/>
|
||||||
|
<Column TableItem="Ticket" Title="Name" Field="@(x => x.Name)" Filterable="true" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<a href="/admin/tickets/view/@(context.Id)">@(context.Name)</a>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Ticket" Title="User" Field="@(x => x.Id)" Filterable="false" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<a href="/admin/users/view/@(context.Creator.Id)">@(context.Creator.Username)</a>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Ticket" Title="Created at" Field="@(x => x.CreatedAt)" Filterable="true" Sortable="true">
|
||||||
|
<Template>
|
||||||
|
<span>@(Formatter.FormatDate(context.CreatedAt))</span>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Ticket" Title="Priority" Field="@(x => x.Priority)" Filterable="true" Sortable="true">
|
||||||
|
<Template>
|
||||||
|
@switch (context.Priority)
|
||||||
|
{
|
||||||
|
case TicketPriority.Low:
|
||||||
|
<span class="badge bg-success">@(context.Priority)</span>
|
||||||
|
break;
|
||||||
|
case TicketPriority.Medium:
|
||||||
|
<span class="badge bg-primary">@(context.Priority)</span>
|
||||||
|
break;
|
||||||
|
case TicketPriority.High:
|
||||||
|
<span class="badge bg-warning">@(context.Priority)</span>
|
||||||
|
break;
|
||||||
|
case TicketPriority.Critical:
|
||||||
|
<span class="badge bg-danger">@(context.Priority)</span>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Ticket" Title="Status" Field="@(x => x.Open)" Filterable="true" Sortable="true">
|
||||||
|
<Template>
|
||||||
|
@if (context.Open)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">Open</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-danger">Closed</span>
|
||||||
|
}
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Pager AlwaysShow="true" ShowPageNumber="true" ShowTotalCount="true"/>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
// Lazy loaders
|
||||||
|
private LazyLoader TicketLazyLoader;
|
||||||
|
private LazyLoader StatisticsLazyLoader;
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
private int TotalTicketsCount;
|
||||||
|
private int ClosedTicketsCount;
|
||||||
|
private int PendingTicketsCount;
|
||||||
|
|
||||||
|
// Data
|
||||||
|
private int Filter = 0;
|
||||||
|
private Ticket[] AllTickets;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Events.OnTicketCreated += OnTicketCreated;
|
||||||
|
Events.OnTicketUpdated += OnTicketUpdated;
|
||||||
|
Events.OnTicketMessage += OnTicketMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Lazyloaders
|
||||||
|
|
||||||
|
private Task LoadStatistics(LazyLoader lazyLoader)
|
||||||
|
{
|
||||||
|
TotalTicketsCount = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Count();
|
||||||
|
|
||||||
|
ClosedTicketsCount = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Count(x => !x.Open);
|
||||||
|
|
||||||
|
PendingTicketsCount = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Where(x => x.Open)
|
||||||
|
.Count(x => x.Messages.All(x => !x.IsSupport));
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task LoadTickets(LazyLoader lazyLoader)
|
||||||
|
{
|
||||||
|
if (Filter == 0)
|
||||||
|
{
|
||||||
|
AllTickets = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Creator)
|
||||||
|
.Include(x => x.Service)
|
||||||
|
.ThenInclude(x => x.Product)
|
||||||
|
.Where(x => x.Open)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
else if (Filter == 1)
|
||||||
|
{
|
||||||
|
AllTickets = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Creator)
|
||||||
|
.Include(x => x.Service)
|
||||||
|
.ThenInclude(x => x.Product)
|
||||||
|
.Where(x => !x.Open)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private async Task UpdateFilter(int filter)
|
||||||
|
{
|
||||||
|
Filter = filter;
|
||||||
|
|
||||||
|
await TicketLazyLoader.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Events
|
||||||
|
|
||||||
|
private async void OnTicketMessage(object? sender, TicketMessageEventArgs message)
|
||||||
|
{
|
||||||
|
if(!message.TicketMessage.IsSupport) // Only update if support has sent messages as the pending tickets depend on that
|
||||||
|
return;
|
||||||
|
|
||||||
|
await StatisticsLazyLoader.Reload();
|
||||||
|
await TicketLazyLoader.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnTicketUpdated(object? o, Ticket e)
|
||||||
|
{
|
||||||
|
await StatisticsLazyLoader.Reload();
|
||||||
|
await TicketLazyLoader.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnTicketCreated(object? o, Ticket e)
|
||||||
|
{
|
||||||
|
await StatisticsLazyLoader.Reload();
|
||||||
|
await TicketLazyLoader.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Dispose() // Unsubscribe to events
|
||||||
|
{
|
||||||
|
Events.OnTicketCreated -= OnTicketCreated;
|
||||||
|
Events.OnTicketUpdated -= OnTicketUpdated;
|
||||||
|
Events.OnTicketMessage -= OnTicketMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
272
Moonlight/Shared/Views/Admin/Tickets/View.razor
Normal file
272
Moonlight/Shared/Views/Admin/Tickets/View.razor
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
@page "/admin/tickets/view/{Id:int}"
|
||||||
|
|
||||||
|
@using Moonlight.App.Extensions.Attributes
|
||||||
|
@using Moonlight.App.Models.Enums
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.App.Services.Ticketing
|
||||||
|
@using Moonlight.App.Database.Entities.Tickets
|
||||||
|
@using System.Text.RegularExpressions
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using Moonlight.App.Database.Enums
|
||||||
|
|
||||||
|
@attribute [RequirePermission(Permission.AdminTickets)]
|
||||||
|
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
@inject TicketService TicketService
|
||||||
|
@inject ToastService ToastService
|
||||||
|
@inject Repository<Ticket> TicketRepository
|
||||||
|
|
||||||
|
<LazyLoader Load="LoadTicket" ShowAsCard="true">
|
||||||
|
@if (Ticket == null)
|
||||||
|
{
|
||||||
|
<NotFoundAlert/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 col-12 mb-5">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
|
||||||
|
<li class="nav-item mt-2">
|
||||||
|
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(0 == 0 ? "active" : "")" href="/account">
|
||||||
|
Request
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item mt-2">
|
||||||
|
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(0 == 1 ? "active" : "")" href="/account/security">
|
||||||
|
Details
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-borderless align-middle mb-0 fs-5">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span>Ticket ID</span>
|
||||||
|
</th>
|
||||||
|
<td>@(Ticket.Id)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span>User</span>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/users/view/@(Ticket.Creator.Id)">@(Ticket.Creator.Username)</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span>Service</span>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
@if (Ticket.Service == null)
|
||||||
|
{
|
||||||
|
<span>None</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="/service/@(Ticket.Service.Id)">@(Ticket.Service.Nickname ?? $"Service {Ticket.Service.Id}")</a>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span>Status</span>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" @bind="EditOpen"/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span>Priority</span>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<SmartEnumSelect @bind-Value="EditPriority"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span>Created at</span>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<span>@(Formatter.FormatDate(Ticket.CreatedAt))</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<td>
|
||||||
|
<WButton OnClick="Save" Text="Save" CssClasses="btn-primary" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9 col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body bg-black p-8">
|
||||||
|
<LazyLoader Load="LoadChatClient">
|
||||||
|
<div class="scroll-y" style="display: flex; flex-direction: column-reverse; height: 70vh">
|
||||||
|
@foreach (var message in TicketService.Chat.Messages.OrderByDescending(x => x.CreatedAt))
|
||||||
|
{
|
||||||
|
var orientation = message.IsSupport ? "end" : "start";
|
||||||
|
|
||||||
|
if (message.Sender != null)
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-@(orientation) mb-10 ">
|
||||||
|
<div class="d-flex flex-column align-items-@(orientation)">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<div class="symbol symbol-35px symbol-circle ">
|
||||||
|
<img alt="Avatar" src="/api/bucket/avatars/@(message.Sender.Avatar)">
|
||||||
|
</div>
|
||||||
|
<div class="ms-3">
|
||||||
|
<div class="fs-5 fw-bold text-gray-900 me-1">@(message.Sender.Username)</div>
|
||||||
|
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt))</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 rounded bg-light-@(message.IsSupport ? "info" : "primary") text-dark fw-semibold mw-lg-400px text-@(orientation)">
|
||||||
|
@(Formatter.FormatLineBreaks(message.Content))
|
||||||
|
|
||||||
|
@if (message.Attachment != null)
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="/api/bucket/ticketAttachments/@(message.Attachment)" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="/api/bucket/ticketAttachments/@(message.Attachment)" target="_blank" class="btn btn-secondary">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="separator separator-content my-15">@(message.Content)</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="row">
|
||||||
|
<div class="input-group">
|
||||||
|
<textarea @bind="MessageContent" class="form-control form-control-solid-bg rounded-end me-3" placeholder="Type a message" style="height: 1vh"></textarea>
|
||||||
|
<ChatFileSelect @ref="FileSelect"/>
|
||||||
|
<WButton OnClick="SendMessage" CssClasses="ms-2 btn btn-icon btn-bg-light btn-color-white">
|
||||||
|
<i class="bx bx-sm bx-send"></i>
|
||||||
|
</WButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</LazyLoader>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
private Ticket? Ticket;
|
||||||
|
private bool HasStarted = false;
|
||||||
|
|
||||||
|
// Message compose cache
|
||||||
|
private ChatFileSelect FileSelect;
|
||||||
|
private string MessageContent = "";
|
||||||
|
|
||||||
|
// Edit cache
|
||||||
|
private bool EditOpen;
|
||||||
|
private TicketPriority EditPriority;
|
||||||
|
|
||||||
|
private Task LoadTicket(LazyLoader _)
|
||||||
|
{
|
||||||
|
Ticket = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Creator)
|
||||||
|
.Include(x => x.Service)
|
||||||
|
.FirstOrDefault(x => x.Id == Id);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadChatClient(LazyLoader lazyLoader)
|
||||||
|
{
|
||||||
|
if (Ticket != null)
|
||||||
|
{
|
||||||
|
await lazyLoader.SetText("Starting chat client");
|
||||||
|
|
||||||
|
TicketService.Chat.OnUpdate += OnUpdate;
|
||||||
|
await TicketService.Chat.Start(Ticket, true);
|
||||||
|
|
||||||
|
EditOpen = TicketService.Chat.Ticket.Open;
|
||||||
|
EditPriority = TicketService.Chat.Ticket.Priority;
|
||||||
|
|
||||||
|
HasStarted = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendMessage()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(MessageContent) && FileSelect.SelectedFile == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!HasStarted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (FileSelect.SelectedFile == null)
|
||||||
|
await TicketService.Chat.SendMessage(MessageContent);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await TicketService.Chat.SendMessage(
|
||||||
|
string.IsNullOrEmpty(MessageContent) ? $"Upload of {FileSelect.SelectedFile.Name}" : MessageContent,
|
||||||
|
FileSelect.SelectedFile.OpenReadStream(1024 * 1024 * 5),
|
||||||
|
FileSelect.SelectedFile.Name
|
||||||
|
);
|
||||||
|
|
||||||
|
await FileSelect.RemoveSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageContent = "";
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Save()
|
||||||
|
{
|
||||||
|
await TicketService.Chat.Update(EditOpen, EditPriority);
|
||||||
|
await ToastService.Success("Successfully updated ticket");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnUpdate()
|
||||||
|
{
|
||||||
|
// Overwrite current cached data
|
||||||
|
EditOpen = TicketService.Chat.Ticket.Open;
|
||||||
|
EditPriority = TicketService.Chat.Ticket.Priority;
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Dispose()
|
||||||
|
{
|
||||||
|
await TicketService.Chat.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user