Started implementing admin ticket ui. Cleaned up some stuff
This commit is contained in:
parent
f5501f77fe
commit
332937f964
11 changed files with 634 additions and 25 deletions
|
@ -1,5 +1,6 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.App.Database.Entities.Tickets;
|
||||
using Moonlight.App.Database.Enums;
|
||||
using Moonlight.App.Event;
|
||||
using Moonlight.App.Event.Args;
|
||||
using Moonlight.App.Extensions;
|
||||
|
@ -50,7 +51,31 @@ public class TicketChatService
|
|||
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.OnTicketUpdated -= OnTicketUpdated;
|
||||
|
@ -60,7 +85,24 @@ public class TicketChatService
|
|||
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))
|
||||
return;
|
||||
|
@ -87,6 +129,11 @@ public class TicketChatService
|
|||
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
|
||||
var t = TicketRepository
|
||||
.Get()
|
||||
|
@ -103,6 +150,8 @@ public class TicketChatService
|
|||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Event handlers
|
||||
private async void OnTicketUpdated(object? _, Ticket ticket)
|
||||
{
|
||||
|
|
|
@ -47,6 +47,17 @@
|
|||
|
||||
<ItemGroup>
|
||||
<_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>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -23,6 +23,7 @@ Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
|
|||
var logConfig = new LoggerConfiguration();
|
||||
|
||||
logConfig = logConfig.Enrich.FromLogContext()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console(
|
||||
outputTemplate:
|
||||
"{Timestamp:HH:mm:ss} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}");
|
||||
|
|
|
@ -106,6 +106,17 @@
|
|||
</a>
|
||||
</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">
|
||||
<a class="menu-link " href="/admin/sys">
|
||||
<span class="menu-icon">
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<div class="card-header">
|
||||
<span class="card-title fs-5">Create a new ticket</span>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -49,7 +49,7 @@
|
|||
@code
|
||||
{
|
||||
[CascadingParameter]
|
||||
public LiveChatMain LiveChatMain { get; set; }
|
||||
public TicketPopupMain TicketPopupMain { get; set; }
|
||||
|
||||
private Service[] Services;
|
||||
private CreateTicketForm Form = new();
|
||||
|
@ -77,6 +77,6 @@
|
|||
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">
|
||||
@if (ViewIndex == 1)
|
||||
{
|
||||
<LiveChatOverview />
|
||||
<TicketPopupOverview />
|
||||
}
|
||||
else if (ViewIndex == 2)
|
||||
{
|
||||
<LiveChatView />
|
||||
<TicketPopupView />
|
||||
}
|
||||
else if (ViewIndex == 3)
|
||||
{
|
||||
<LiveChatCreate />
|
||||
<TicketPopupCreate />
|
||||
}
|
||||
</div>
|
||||
}
|
|
@ -11,7 +11,7 @@
|
|||
<div class="card-header">
|
||||
<span class="card-title fs-5">Your tickets</span>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@
|
|||
<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="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 class="d-flex flex-stack py-4">
|
||||
|
@ -30,7 +30,7 @@
|
|||
{
|
||||
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>
|
||||
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary mb-2">@(ticket.Name)</a>
|
||||
|
@ -54,7 +54,7 @@
|
|||
@code
|
||||
{
|
||||
[CascadingParameter]
|
||||
public LiveChatMain LiveChatMain { get; set; }
|
||||
public TicketPopupMain TicketPopupMain { get; set; }
|
||||
|
||||
private Ticket[] Tickets;
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
<div class="card-header">
|
||||
<span class="card-title fs-5">@(HasStarted ? TicketService.Chat.Ticket.Name : "Loading")</span>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -66,7 +66,7 @@
|
|||
<div class="card-footer">
|
||||
<div class="row">
|
||||
<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"/>
|
||||
<WButton OnClick="SendMessage" CssClasses="ms-2 btn btn-icon btn-bg-light btn-color-white">
|
||||
<i class="bx bx-sm bx-send"></i>
|
||||
|
@ -78,12 +78,12 @@
|
|||
@code
|
||||
{
|
||||
[CascadingParameter]
|
||||
public LiveChatMain LiveChatMain { get; set; }
|
||||
|
||||
private ChatFileSelect FileSelect;
|
||||
public TicketPopupMain TicketPopupMain { get; set; }
|
||||
|
||||
private bool HasStarted = false;
|
||||
private string MyMessageContent = "";
|
||||
|
||||
private ChatFileSelect FileSelect;
|
||||
private string MessageContent = "";
|
||||
|
||||
private async Task Load(LazyLoader lazyLoader)
|
||||
{
|
||||
|
@ -91,7 +91,7 @@
|
|||
|
||||
// Initialize chat service and start it
|
||||
TicketService.Chat.OnUpdate = OnUpdate;
|
||||
await TicketService.Chat.Start(LiveChatMain.CurrentTicket);
|
||||
await TicketService.Chat.Start(TicketPopupMain.CurrentTicket);
|
||||
|
||||
// Let the ui know that we are ready
|
||||
HasStarted = true;
|
||||
|
@ -105,18 +105,18 @@
|
|||
|
||||
private async Task SendMessage()
|
||||
{
|
||||
if (string.IsNullOrEmpty(MyMessageContent) && FileSelect.SelectedFile == null)
|
||||
if (string.IsNullOrEmpty(MessageContent) && FileSelect.SelectedFile == null)
|
||||
return;
|
||||
|
||||
if (!HasStarted)
|
||||
return;
|
||||
|
||||
if (FileSelect.SelectedFile == null)
|
||||
await TicketService.Chat.SendMessage(MyMessageContent);
|
||||
await TicketService.Chat.SendMessage(MessageContent);
|
||||
else
|
||||
{
|
||||
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.Name
|
||||
);
|
||||
|
@ -124,7 +124,7 @@
|
|||
await FileSelect.RemoveSelection();
|
||||
}
|
||||
|
||||
MyMessageContent = "";
|
||||
MessageContent = "";
|
||||
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="app-page flex-column flex-column-fluid">
|
||||
|
@ -11,7 +11,7 @@
|
|||
<div class="app-container container-fluid">
|
||||
@ChildContent
|
||||
|
||||
<LiveChatMain />
|
||||
<TicketPopupMain />
|
||||
</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();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue