Improved ticket system ui and some backend code

This commit is contained in:
Marcel Baumgartner
2023-08-09 00:35:49 +02:00
parent 388deacf60
commit c2c533675b
14 changed files with 1351 additions and 885 deletions

View File

@@ -7,6 +7,7 @@
@inject ResourceService ResourceService
@inject SmartTranslateService SmartTranslateService
<div class="scroll-y me-n5 pe-5" style="max-height: 50vh; display: flex; flex-direction: column-reverse;">
@foreach (var message in Messages.OrderByDescending(x => x.Id)) // Reverse messages to use auto scrolling
{
if (message.IsSupportMessage)
@@ -18,10 +19,16 @@
<div class="d-flex align-items-center mb-2">
<div class="me-3">
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">@(message.Sender!.FirstName) @(message.Sender!.LastName)</a>
@if (message.Sender != null)
{
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">@(message.Sender!.FirstName) @(message.Sender!.LastName)</a>
}
</div>
<div class="symbol symbol-35px symbol-circle ">
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender!))">
@if (message.Sender != null)
{
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender))">
}
</div>
</div>
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end">
@@ -62,8 +69,11 @@
<div class="d-flex justify-content-start mb-10 ">
<div class="d-flex flex-column align-items-start">
<div class="d-flex align-items-center mb-2">
<div class="symbol symbol-35px symbol-circle ">
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender!))">
<div class="symbol symbol-35px symbol-circle ">
@if (message.Sender != null)
{
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender))">
}
</div>
<div class="ms-3">
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">@(message.Sender!.FirstName) @(message.Sender!.LastName)</a>
@@ -120,7 +130,10 @@
<div class="d-flex flex-column align-items-start">
<div class="d-flex align-items-center mb-2">
<div class="symbol symbol-35px symbol-circle ">
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender!))">
@if (message.Sender != null)
{
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender))">
}
</div>
<div class="ms-3">
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">@(message.Sender!.FirstName) @(message.Sender!.LastName)</a>
@@ -169,8 +182,11 @@
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">@(message.Sender!.FirstName) @(message.Sender!.LastName)</a>
</div>
<div class="symbol symbol-35px symbol-circle ">
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender!))">
<div class="symbol symbol-35px symbol-circle ">
@if (message.Sender != null)
{
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender))">
}
</div>
</div>
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end">
@@ -208,6 +224,7 @@
}
}
}
</div>
@code
{

View File

@@ -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<Ticket> TicketRepository
@inject SmartTranslateService SmartTranslateService
@inject EventSystem EventSystem
@inject IdentityService IdentityService
@attribute [PermissionRequired(nameof(Permissions.AdminSupport))]
@implements IDisposable
<div class="d-flex flex-column flex-lg-row">
<div class="flex-column flex-lg-row-auto w-100 w-lg-300px w-xl-400px mb-10 mb-lg-0">
<div class="card card-flush">
<div class="card-body pt-5">
<div class="scroll-y me-n5 pe-5 h-200px h-lg-auto" data-kt-scroll="true" data-kt-scroll-activate="{default: false, lg: true}" data-kt-scroll-max-height="auto" data-kt-scroll-dependencies="#kt_header, #kt_app_header, #kt_toolbar, #kt_app_toolbar, #kt_footer, #kt_app_footer, #kt_chat_contacts_header" data-kt-scroll-wrappers="#kt_content, #kt_app_content, #kt_chat_contacts_body" data-kt-scroll-offset="5px" style="max-height: 601px;">
<div class="separator separator-content border-primary mb-10 mt-5">
<span class="w-250px fw-bold fs-5">
<TL>Unassigned tickets</TL>
</span>
<div class="row mb-5">
<LazyLoader Load="LoadStatistics">
<div class="col-12 col-lg-6 col-xl">
<a class="mt-4 card" href="/admin/servers">
<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">
@(TotalTicketCount)
</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>
@foreach (var ticket in UnAssignedTickets)
{
<div class="d-flex flex-stack py-4">
<div class="d-flex align-items-center">
<div class="ms-5">
<a href="/admin/support/@(ticket.Key.Id)" class="fs-5 fw-bold text-gray-900 text-hover-primary mb-2">@(ticket.Key.IssueTopic)</a>
@if (ticket.Value != null)
{
<div class="fw-semibold text-muted">
@(ticket.Value.Content)
</div>
}
</div>
</a>
</div>
<div class="col-12 col-lg-6 col-xl">
<a class="mt-4 card" href="/admin/webspaces">
<div class="card-body">
<div class="row align-items-center gx-0">
<div class="col">
<h6 class="text-uppercase text-muted mb-2">
<TL>Unassigned tickets</TL>
</h6>
<span class="h2 mb-0">
@(UnAssignedTicketCount)
</span>
</div>
<div class="d-flex flex-column align-items-end ms-2">
@if (ticket.Value != null)
{
<span class="text-muted fs-7 mb-1">
@(Formatter.FormatAgoFromDateTime(ticket.Value.CreatedAt, SmartTranslateService))
</span>
}
<div class="col-auto">
<span class="h2 text-muted mb-0">
<i class="text-primary bx bxs-bell-ring bx-lg"></i>
</span>
</div>
</div>
if (ticket.Key != UnAssignedTickets.Last().Key)
{
<div class="separator"></div>
}
}
@if (AssignedTickets.Any())
{
<div class="separator separator-content border-primary mb-5 mt-8">
<span class="w-250px fw-bold fs-5">
<TL>Assigned tickets</TL>
</span>
</div>
}
@foreach (var ticket in AssignedTickets)
{
<div class="d-flex flex-stack py-4">
<div class="d-flex align-items-center">
<div class="ms-5">
<a href="/admin/support/@(ticket.Key.Id)" class="fs-5 fw-bold text-gray-900 text-hover-primary mb-2">@(ticket.Key.IssueTopic)</a>
@if (ticket.Value != null)
{
<div class="fw-semibold text-muted">
@(ticket.Value.Content)
</div>
}
</div>
</div>
</a>
</div>
<div class="col-12 col-lg-6 col-xl">
<a class="mt-4 card" href="/admin/domains">
<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">
@(PendingTicketCount)
</span>
</div>
<div class="d-flex flex-column align-items-end ms-2">
@if (ticket.Value != null)
{
<span class="text-muted fs-7 mb-1">
@(Formatter.FormatAgoFromDateTime(ticket.Value.CreatedAt, SmartTranslateService))
</span>
}
<div class="col-auto">
<span class="h2 text-muted mb-">
<i class="text-primary bx bx-hourglass bx-lg"></i>
</span>
</div>
</div>
</div>
</a>
</div>
<div class="col-12 col-lg-6 col-xl">
<a class="mt-4 card" href="/admin/users">
<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">
@(ClosedTicketCount)
</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>
</a>
</div>
</LazyLoader>
</div>
if (ticket.Key != AssignedTickets.Last().Key)
{
<div class="separator"></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="@(SmartTranslateService.Translate("Overview"))" CssClasses="btn-secondary" OnClick="() => UpdateFilter(0)" />
<WButton Text="@(SmartTranslateService.Translate("Unassigned tickets"))" CssClasses="btn-secondary" OnClick="() => UpdateFilter(1)" />
<WButton Text="@(SmartTranslateService.Translate("My tickets"))" CssClasses="btn-secondary" OnClick="() => UpdateFilter(2)" />
<WButton Text="@(SmartTranslateService.Translate("All tickets"))" CssClasses="btn-secondary" OnClick="() => UpdateFilter(3)" />
</div>
</div>
</div>
</div>
<div class="flex-lg-row-fluid ms-lg-7 ms-xl-10">
<div class="card">
<div class="card-header">
@if (AdminService.Ticket != null)
{
<div class="card-title">
<div class="d-flex justify-content-center flex-column me-3">
<span class="fs-3 fw-bold text-gray-900 me-1 mb-2 lh-1">@(AdminService.Ticket.IssueTopic)</span>
<div class="mb-0 lh-1">
<span class="fs-6 fw-bold text-muted me-2">
<TL>Status</TL>
</span>
@switch (AdminService.Ticket.Status)
{
case TicketStatus.Closed:
<span class="badge badge-danger badge-circle w-10px h-10px me-1"></span>
break;
case TicketStatus.Open:
<span class="badge badge-success badge-circle w-10px h-10px me-1"></span>
break;
case TicketStatus.Pending:
<span class="badge badge-warning badge-circle w-10px h-10px me-1"></span>
break;
case TicketStatus.WaitingForUser:
<span class="badge badge-primary badge-circle w-10px h-10px me-1"></span>
break;
}
<span class="fs-6 fw-semibold text-muted me-5">@(AdminService.Ticket.Status)</span>
<span class="fs-6 fw-bold text-muted me-2">
<TL>Priority</TL>
</span>
@switch (AdminService.Ticket.Priority)
<div class="card-body">
<LazyLoader @ref="TicketLazyLoader" Load="LoadTickets">
<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="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Filterable="true" Sortable="true"/>
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("Assigned to"))" Field="@(x => x.AssignedTo)" Filterable="true" Sortable="true">
<Template>
<span>@(context.AssignedTo == null ? "None" : context.AssignedTo.FirstName + " " + context.AssignedTo.LastName)</span>
</Template>
</Column>
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("Ticket title"))" Field="@(x => x.IssueTopic)" Filterable="true" Sortable="false"/>
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("User"))" Field="@(x => x.CreatedBy)" Filterable="true" Sortable="true">
<Template>
<span>@(context.CreatedBy.FirstName) @(context.CreatedBy.LastName)</span>
</Template>
</Column>
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("Created at"))" Field="@(x => x.CreatedAt)" Filterable="true" Sortable="true">
<Template>
<span>@(Formatter.FormatDate(context.CreatedAt))</span>
</Template>
</Column>
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("Priority"))" Field="@(x => x.Priority)" Filterable="true" Sortable="true">
<Template>
@switch (context.Priority)
{
case TicketPriority.Low:
<span class="badge badge-success badge-circle w-10px h-10px me-1"></span>
<span class="badge bg-success">@(context.Priority)</span>
break;
case TicketPriority.Medium:
<span class="badge badge-primary badge-circle w-10px h-10px me-1"></span>
<span class="badge bg-primary">@(context.Priority)</span>
break;
case TicketPriority.High:
<span class="badge badge-warning badge-circle w-10px h-10px me-1"></span>
<span class="badge bg-warning">@(context.Priority)</span>
break;
case TicketPriority.Critical:
<span class="badge badge-danger badge-circle w-10px h-10px me-1"></span>
<span class="badge bg-danger">@(context.Priority)</span>
break;
}
<span class="fs-6 fw-semibold text-muted">@(AdminService.Ticket.Priority)</span>
</div>
</div>
</div>
<div class="card-toolbar">
<div class="input-group">
<div class="me-3">
@if (AdminService.Ticket!.AssignedTo == null)
</Template>
</Column>
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("Status"))" Field="@(x => x.Status)" Filterable="true" Sortable="true">
<Template>
@switch (context.Status)
{
<WButton Text="@(SmartTranslateService.Translate("Claim"))" OnClick="AdminService.Claim"/>
case TicketStatus.Closed:
<span class="badge bg-danger">@(context.Status)</span>
break;
case TicketStatus.Open:
<span class="badge bg-success">@(context.Status)</span>
break;
case TicketStatus.Pending:
<span class="badge bg-warning">@(context.Status)</span>
break;
case TicketStatus.WaitingForUser:
<span class="badge bg-primary">@(context.Status)</span>
break;
}
else
{
<WButton Text="@(SmartTranslateService.Translate("Unclaim"))" OnClick="AdminService.UnClaim"/>
}
</div>
<select @bind="Priority" class="form-select rounded-start">
@foreach (var priority in (TicketPriority[])Enum.GetValues(typeof(TicketPriority)))
{
if (Priority == priority)
{
<option value="@(priority)" selected="">@(priority)</option>
}
else
{
<option value="@(priority)">@(priority)</option>
}
}
</select>
<WButton Text="@(SmartTranslateService.Translate("Update priority"))"
CssClasses="btn-primary"
OnClick="UpdatePriority">
</WButton>
<select @bind="Status" class="form-select">
@foreach (var status in (TicketStatus[])Enum.GetValues(typeof(TicketStatus)))
{
if (Status == status)
{
<option value="@(status)" selected="">@(status)</option>
}
else
{
<option value="@(status)">@(status)</option>
}
}
</select>
<WButton Text="@(SmartTranslateService.Translate("Update status"))"
CssClasses="btn-primary"
OnClick="UpdateStatus">
</WButton>
</div>
</div>
}
else
{
<div class="card-title">
<div class="d-flex justify-content-center flex-column me-3">
<span class="fs-4 fw-bold text-gray-900 me-1 mb-2 lh-1">
</span>
</div>
</div>
}
</div>
<div class="card-body">
<div class="scroll-y me-n5 pe-5" style="max-height: 55vh; display: flex; flex-direction: column-reverse;">
@if (AdminService.Ticket == null)
{
}
else
{
<TicketMessageView Messages="Messages" ViewAsSupport="true"/>
}
</Template>
</Column>
<Column TableItem="Ticket" Title="" Field="@(x => x.Id)" Filterable="false" Sortable="false">
<Template>
<a class="btn btn-sm btn-primary" href="/admin/support/view/@(context.Id)">
<TL>Open</TL>
</a>
</Template>
</Column>
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
</Table>
</div>
</div>
@if (AdminService.Ticket != null)
{
<div class="card-footer pt-4" id="kt_chat_messenger_footer">
<div class="d-flex flex-stack">
<table class="w-100">
<tr>
<td class="align-top">
<SmartFileSelect @ref="FileSelect"></SmartFileSelect>
</td>
<td class="w-100">
<textarea @bind="MessageText" class="form-control mb-3 form-control-flush" rows="1" placeholder="@(SmartTranslateService.Translate("Type a message"))"></textarea>
</td>
<td class="align-top">
<WButton Text="@(SmartTranslateService.Translate("Send"))"
WorkingText="@(SmartTranslateService.Translate("Sending"))"
CssClasses="btn-primary ms-2"
OnClick="SendMessage">
</WButton>
</td>
</tr>
</table>
</div>
</div>
}
</LazyLoader>
</div>
</div>
</div>
@code
{
[Parameter]
public int Id { get; set; }
private int TotalTicketCount;
private int UnAssignedTicketCount;
private int PendingTicketCount;
private int ClosedTicketCount;
private Dictionary<Ticket, TicketMessage?> AssignedTickets;
private Dictionary<Ticket, TicketMessage?> UnAssignedTickets;
private List<TicketMessage> 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<Ticket>("tickets.new", this, async _ =>
switch (Filter)
{
await ReloadTickets(false);
await InvokeAsync(StateHasChanged);
});
if (AdminService.Ticket != null)
{
await EventSystem.On<TicketMessage>($"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<Ticket>($"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();
}
}

View File

@@ -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
<div class="d-flex flex-column flex-lg-row">
<div class="flex-column flex-lg-row-auto w-100 w-lg-300px w-xl-400px mb-10 mb-lg-0">
<div class="card card-flush">
<div class="card-body pt-5">
<div class="scroll-y me-n5 pe-5 h-200px h-lg-auto" data-kt-scroll="true" data-kt-scroll-activate="{default: false, lg: true}" data-kt-scroll-max-height="auto" data-kt-scroll-dependencies="#kt_header, #kt_app_header, #kt_toolbar, #kt_app_toolbar, #kt_footer, #kt_app_footer, #kt_chat_contacts_header" data-kt-scroll-wrappers="#kt_content, #kt_app_content, #kt_chat_contacts_body" data-kt-scroll-offset="5px" style="max-height: 601px;">
<div class="separator separator-content border-primary mb-10 mt-5">
<span class="w-250px fw-bold fs-5">
<TL>Unassigned tickets</TL>
</span>
</div>
@foreach (var ticket in UnAssignedTickets)
{
<div class="d-flex flex-stack py-4">
<div class="d-flex align-items-center">
<div class="ms-5">
<a href="/admin/support/@(ticket.Key.Id)" class="fs-5 fw-bold text-gray-900 text-hover-primary mb-2">@(ticket.Key.IssueTopic)</a>
@if (ticket.Value != null)
{
<div class="fw-semibold text-muted">
@(ticket.Value.Content)
</div>
}
</div>
</div>
<div class="d-flex flex-column align-items-end ms-2">
@if (ticket.Value != null)
{
<span class="text-muted fs-7 mb-1">
@(Formatter.FormatAgoFromDateTime(ticket.Value.CreatedAt, SmartTranslateService))
</span>
}
</div>
</div>
if (ticket.Key != UnAssignedTickets.Last().Key)
{
<div class="separator"></div>
}
}
@if (AssignedTickets.Any())
{
<div class="separator separator-content border-primary mb-5 mt-8">
<span class="w-250px fw-bold fs-5">
<TL>Assigned tickets</TL>
</span>
</div>
}
@foreach (var ticket in AssignedTickets)
{
<div class="d-flex flex-stack py-4">
<div class="d-flex align-items-center">
<div class="ms-5">
<a href="/admin/support/@(ticket.Key.Id)" class="fs-5 fw-bold text-gray-900 text-hover-primary mb-2">@(ticket.Key.IssueTopic)</a>
@if (ticket.Value != null)
{
<div class="fw-semibold text-muted">
@(ticket.Value.Content)
</div>
}
</div>
</div>
<div class="d-flex flex-column align-items-end ms-2">
@if (ticket.Value != null)
{
<span class="text-muted fs-7 mb-1">
@(Formatter.FormatAgoFromDateTime(ticket.Value.CreatedAt, SmartTranslateService))
</span>
}
</div>
</div>
if (ticket.Key != AssignedTickets.Last().Key)
{
<div class="separator"></div>
}
}
</div>
</div>
</div>
</div>
<div class="flex-lg-row-fluid ms-lg-7 ms-xl-10">
<div class="card">
<div class="card-header">
@if (AdminService.Ticket != null)
{
<div class="card-title">
<div class="d-flex justify-content-center flex-column me-3">
<span class="fs-3 fw-bold text-gray-900 me-1 mb-2 lh-1">@(AdminService.Ticket.IssueTopic)</span>
<div class="mb-0 lh-1">
<span class="fs-6 fw-bold text-muted me-2">
<TL>Status</TL>
</span>
@switch (AdminService.Ticket.Status)
{
case TicketStatus.Closed:
<span class="badge badge-danger badge-circle w-10px h-10px me-1"></span>
break;
case TicketStatus.Open:
<span class="badge badge-success badge-circle w-10px h-10px me-1"></span>
break;
case TicketStatus.Pending:
<span class="badge badge-warning badge-circle w-10px h-10px me-1"></span>
break;
case TicketStatus.WaitingForUser:
<span class="badge badge-primary badge-circle w-10px h-10px me-1"></span>
break;
}
<span class="fs-6 fw-semibold text-muted me-5">@(AdminService.Ticket.Status)</span>
<span class="fs-6 fw-bold text-muted me-2">
<TL>Priority</TL>
</span>
@switch (AdminService.Ticket.Priority)
{
case TicketPriority.Low:
<span class="badge badge-success badge-circle w-10px h-10px me-1"></span>
break;
case TicketPriority.Medium:
<span class="badge badge-primary badge-circle w-10px h-10px me-1"></span>
break;
case TicketPriority.High:
<span class="badge badge-warning badge-circle w-10px h-10px me-1"></span>
break;
case TicketPriority.Critical:
<span class="badge badge-danger badge-circle w-10px h-10px me-1"></span>
break;
}
<span class="fs-6 fw-semibold text-muted">@(AdminService.Ticket.Priority)</span>
</div>
</div>
</div>
<div class="card-toolbar">
<div class="input-group">
<div class="me-3">
@if (AdminService.Ticket!.AssignedTo == null)
{
<WButton Text="@(SmartTranslateService.Translate("Claim"))"/>
}
else
{
<WButton Text="@(SmartTranslateService.Translate("Unclaim"))"/>
}
</div>
<select @bind="Priority" class="form-select rounded-start">
@foreach (var priority in (TicketPriority[])Enum.GetValues(typeof(TicketPriority)))
{
if (Priority == priority)
{
<option value="@(priority)" selected="">@(priority)</option>
}
else
{
<option value="@(priority)">@(priority)</option>
}
}
</select>
<WButton Text="@(SmartTranslateService.Translate("Update priority"))"
CssClasses="btn-primary"
OnClick="UpdatePriority">
</WButton>
<select @bind="Status" class="form-select">
@foreach (var status in (TicketStatus[])Enum.GetValues(typeof(TicketStatus)))
{
if (Status == status)
{
<option value="@(status)" selected="">@(status)</option>
}
else
{
<option value="@(status)">@(status)</option>
}
}
</select>
<WButton Text="@(SmartTranslateService.Translate("Update status"))"
CssClasses="btn-primary"
OnClick="UpdateStatus">
</WButton>
</div>
</div>
}
else
{
<div class="card-title">
<div class="d-flex justify-content-center flex-column me-3">
<span class="fs-4 fw-bold text-gray-900 me-1 mb-2 lh-1">
</span>
</div>
</div>
}
</div>
<div class="card-body">
<div class="scroll-y me-n5 pe-5" style="max-height: 55vh; display: flex; flex-direction: column-reverse;">
@if (AdminService.Ticket == null)
{
}
else
{
<TicketMessageView Messages="Messages" ViewAsSupport="true"/>
}
</div>
</div>
@if (AdminService.Ticket != null)
{
<div class="card-footer pt-4" id="kt_chat_messenger_footer">
<div class="d-flex flex-stack">
<table class="w-100">
<tr>
<td class="align-top">
<SmartFileSelect @ref="FileSelect"></SmartFileSelect>
</td>
<td class="w-100">
<textarea @bind="MessageText" class="form-control mb-3 form-control-flush" rows="1" placeholder="@(SmartTranslateService.Translate("Type a message"))"></textarea>
</td>
<td class="align-top">
<WButton Text="@(SmartTranslateService.Translate("Send"))"
WorkingText="@(SmartTranslateService.Translate("Sending"))"
CssClasses="btn-primary ms-2"
OnClick="SendMessage">
</WButton>
</td>
</tr>
</table>
</div>
</div>
}
</div>
</div>
</div>
@code
{
[Parameter]
public int Id { get; set; }
private Dictionary<Ticket, TicketMessage?> AssignedTickets;
private Dictionary<Ticket, TicketMessage?> UnAssignedTickets;
private List<TicketMessage> 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<Ticket>("tickets.new", this, async _ =>
{
await ReloadTickets(false);
await InvokeAsync(StateHasChanged);
});
if (AdminService.Ticket != null)
{
await EventSystem.On<TicketMessage>($"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<Ticket>($"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();
}
}

View File

@@ -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<Ticket> TicketRepository
@inject EventSystem Event
@inject IdentityService IdentityService
@implements IDisposable
@attribute [PermissionRequired(nameof(Permissions.AdminSupportView))]
<LazyLoader @ref="LazyLoader" Load="Load">
@if (Ticket == null)
{
<NotFoundAlert/>
}
else
{
<div class="card">
<div class="row g-0">
<div class="col-xl-9 col-lg-8">
<div class="card-body border-end">
<div class="row mb-4 pb-2 g-3">
<span class="fs-2 fw-bold">@(Ticket.IssueTopic)</span>
</div>
<span class="fs-4">
<TL>Issue description</TL>:
</span>
<p class="fs-5 text-muted">
@(Formatter.FormatLineBreaks(Ticket.IssueDescription))
</p>
<span class="fs-4">
<TL>Issue resolve tries</TL>:
</span>
<p class="fs-5 text-muted">
@(Formatter.FormatLineBreaks(Ticket.IssueTries))
</p>
</div>
<div class="card-body border-end border-top bg-black">
<TicketMessageView Messages="Messages" ViewAsSupport="true"/>
</div>
<div class="card-footer pt-4">
<div class="d-flex flex-stack">
<table class="w-100">
<tr>
<td class="align-top">
<SmartFileSelect @ref="FileSelect"></SmartFileSelect>
</td>
<td class="w-100">
<textarea @bind="MessageContent" class="form-control mb-3 form-control-flush" rows="1" placeholder="@(SmartTranslateService.Translate("Type a message"))"></textarea>
</td>
<td class="align-top">
<WButton Text="@(SmartTranslateService.Translate("Send"))"
WorkingText="@(SmartTranslateService.Translate("Sending"))"
CssClasses="btn-primary ms-2"
OnClick="SendMessage"/>
</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-xl-3 col-lg-4">
<div class="card-header">
<h6 class="card-title mb-0">
<TL>Ticket details</TL>
</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-borderless align-middle mb-0">
<tbody>
<tr>
<th>
<TL>Ticket ID</TL>
</th>
<td>@(Ticket.Id)</td>
</tr>
<tr>
<th>
<TL>User</TL>
</th>
<td>
<a href="/admin/users/view/@(Ticket.CreatedBy.Id)">
@(Ticket.CreatedBy.FirstName) @(Ticket.CreatedBy.LastName)
</a>
</td>
</tr>
<tr>
<th>
<TL>Subject</TL>
</th>
<td>
<TL>@(Ticket.Subject)</TL>
</td>
</tr>
<tr>
<th>
<TL>Subject ID</TL>
</th>
<td>@(Ticket.SubjectId)</td>
</tr>
<tr>
<th>
<TL>Assigned to</TL>
</th>
@if (Ticket.AssignedTo == null)
{
<td>
<TL>None</TL>
</td>
}
else
{
<td>@(Ticket.AssignedTo.FirstName) @(Ticket.AssignedTo.LastName)</td>
}
</tr>
<tr>
<th>
<TL>Status</TL>
</th>
<td>
<select @bind="StatusModified" class="form-select">
@foreach (var status in (TicketStatus[])Enum.GetValues(typeof(TicketStatus)))
{
if (StatusModified == status)
{
<option value="@(status)" selected="">@(status)</option>
}
else
{
<option value="@(status)">@(status)</option>
}
}
</select>
</td>
</tr>
<tr>
<th>
<TL>Priority</TL>
</th>
<td>
<select @bind="PriorityModified" class="form-select">
@foreach (var priority in (TicketPriority[])Enum.GetValues(typeof(TicketPriority)))
{
if (PriorityModified == priority)
{
<option value="@(priority)" selected="">@(priority)</option>
}
else
{
<option value="@(priority)">@(priority)</option>
}
}
</select>
</td>
</tr>
<tr>
<th>
<TL>Created at</TL>
</th>
<td>@(Formatter.FormatDate(Ticket.CreatedAt))</td>
</tr>
<tr>
<th></th>
<td>
<WButton Text="@(SmartTranslateService.Translate("Save"))" OnClick="Save"/>
</td>
</tr>
<tr>
<th>
<WButton Text="@(SmartTranslateService.Translate("Claim"))" OnClick="() => SetClaim(IdentityService.User)"/>
</th>
<td>
<WButton Text="@(SmartTranslateService.Translate("Unclaim"))" OnClick="() => SetClaim(null)"/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
}
</LazyLoader>
@code
{
[Parameter]
public int Id { get; set; }
private Ticket? Ticket { get; set; }
private TicketPriority PriorityModified;
private TicketStatus StatusModified;
private List<TicketMessage> 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<TicketMessage>($"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<Ticket>($"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);
}
}
}

View File

@@ -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<Server> ServerRepository
@inject Repository<WebSpace> WebSpaceRepository
@inject Repository<Domain> DomainRepository
@inject TicketClientService TicketClientService
@inject Repository<Ticket> TicketRepository
@inject SmartTranslateService SmartTranslateService
@inject IdentityService IdentityService
@inject Repository<WebSpace> WebSpaceRepository
@inject Repository<Domain> DomainRepository
@inject Repository<Server> ServerRepository
@inject NavigationManager NavigationManager
@inject ResourceService ResourceService
@inject EventSystem EventSystem
<div class="d-flex flex-column flex-lg-row">
<div class="flex-column flex-lg-row-auto w-100 w-lg-300px w-xl-400px mb-10 mb-lg-0">
<div class="card card-flush">
<div class="card-body pt-5">
<div class="scroll-y me-n5 pe-5 h-200px h-lg-auto">
<div class="d-flex flex-stack d-flex justify-content-center mb-5">
<a href="/support" class="btn btn-primary">
<TL>Create new ticket</TL>
</a>
</div>
<div class="separator"></div>
@foreach (var ticket in Tickets)
{
<div class="d-flex flex-stack py-4">
<div class="d-flex align-items-center">
<div class="ms-5">
<a href="/support/@(ticket.Key.Id)" class="fs-5 fw-bold text-gray-900 text-hover-primary mb-2">@(ticket.Key.IssueTopic)</a>
@if (ticket.Value != null)
{
<div class="fw-semibold text-muted">
@(ticket.Value.Content)
</div>
}
</div>
</div>
<div class="d-flex flex-column align-items-end ms-2">
@if (ticket.Value != null)
{
<span class="text-muted fs-7 mb-1">
@(Formatter.FormatAgoFromDateTime(ticket.Value.CreatedAt, SmartTranslateService))
</span>
}
</div>
</div>
if (ticket.Key != Tickets.Last().Key)
{
<div class="separator"></div>
}
}
</div>
</div>
</div>
</div>
<div class="flex-lg-row-fluid ms-lg-7 ms-xl-10">
<div class="card">
<div class="card-header">
@if (ClientService.Ticket != null)
{
<div class="card-title">
<div class="d-flex justify-content-center flex-column me-3">
<span class="fs-3 fw-bold text-gray-900 me-1 mb-2 lh-1">@(ClientService.Ticket.IssueTopic)</span>
<div class="mb-0 lh-1">
<span class="fs-6 fw-bold text-muted me-2">
<TL>Status</TL>
</span>
@switch (ClientService.Ticket.Status)
{
case TicketStatus.Closed:
<span class="badge badge-danger badge-circle w-10px h-10px me-1"></span>
break;
case TicketStatus.Open:
<span class="badge badge-success badge-circle w-10px h-10px me-1"></span>
break;
case TicketStatus.Pending:
<span class="badge badge-warning badge-circle w-10px h-10px me-1"></span>
break;
case TicketStatus.WaitingForUser:
<span class="badge badge-primary badge-circle w-10px h-10px me-1"></span>
break;
}
<span class="fs-6 fw-semibold text-muted me-5">@(ClientService.Ticket.Status)</span>
<span class="fs-6 fw-bold text-muted me-2">
<TL>Priority</TL>
</span>
@switch (ClientService.Ticket.Priority)
{
case TicketPriority.Low:
<span class="badge badge-success badge-circle w-10px h-10px me-1"></span>
break;
case TicketPriority.Medium:
<span class="badge badge-primary badge-circle w-10px h-10px me-1"></span>
break;
case TicketPriority.High:
<span class="badge badge-warning badge-circle w-10px h-10px me-1"></span>
break;
case TicketPriority.Critical:
<span class="badge badge-danger badge-circle w-10px h-10px me-1"></span>
break;
}
<span class="fs-6 fw-semibold text-muted">@(ClientService.Ticket.Priority)</span>
</div>
</div>
</div>
<div class="card-toolbar">
<div class="me-n3">
<button class="btn btn-sm btn-icon btn-active-light-primary" data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
<i class="ki-duotone ki-dots-square fs-2">
<span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span>
</i>
</button>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg-light-primary fw-semibold w-200px py-3" data-kt-menu="true">
<div class="menu-item px-3">
<div class="menu-content text-muted pb-2 px-3 fs-7 text-uppercase">
Contacts
</div>
</div>
<div class="menu-item px-3">
<a href="#" class="menu-link px-3" data-bs-toggle="modal" data-bs-target="#kt_modal_users_search">
Add Contact
</a>
</div>
<div class="menu-item px-3">
<a href="#" class="menu-link flex-stack px-3" data-bs-toggle="modal" data-bs-target="#kt_modal_invite_friends">
Invite Contacts
<span class="ms-2" data-bs-toggle="tooltip" aria-label="Specify a contact email to send an invitation" data-bs-original-title="Specify a contact email to send an invitation" data-kt-initialized="1">
<i class="ki-duotone ki-information fs-7">
<span class="path1"></span><span class="path2"></span><span class="path3"></span>
</i>
</span>
</a>
</div>
<div class="menu-item px-3" data-kt-menu-trigger="hover" data-kt-menu-placement="right-start">
<a href="#" class="menu-link px-3">
<span class="menu-title">Groups</span>
<span class="menu-arrow"></span>
</a>
<div class="menu-sub menu-sub-dropdown w-175px py-4">
<div class="menu-item px-3">
<a href="#" class="menu-link px-3" data-bs-toggle="tooltip" data-bs-original-title="Coming soon" data-kt-initialized="1">
Create Group
</a>
</div>
<div class="menu-item px-3">
<a href="#" class="menu-link px-3" data-bs-toggle="tooltip" data-bs-original-title="Coming soon" data-kt-initialized="1">
Invite Members
</a>
</div>
<div class="menu-item px-3">
<a href="#" class="menu-link px-3" data-bs-toggle="tooltip" data-bs-original-title="Coming soon" data-kt-initialized="1">
Settings
</a>
</div>
</div>
</div>
<div class="menu-item px-3 my-1">
<a href="#" class="menu-link px-3" data-bs-toggle="tooltip" data-bs-original-title="Coming soon" data-kt-initialized="1">
Settings
</a>
</div>
</div>
</div>
</div>
}
else
{
<div class="card-title">
<div class="d-flex justify-content-center flex-column me-3">
<span class="fs-4 fw-bold text-gray-900 me-1 mb-2 lh-1">
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">
<span class="card-title">
<TL>Create a new ticket</TL>
</span>
</div>
</div>
}
</div>
<div class="card-body">
<div class="scroll-y me-n5 pe-5" style="max-height: 55vh; display: flex; flex-direction: column-reverse;">
@if (ClientService.Ticket == null)
{
<LazyLoader Load="LoadTicketCreate">
<SmartForm Model="Model" OnValidSubmit="OnValidSubmit">
<div class="mb-3">
<InputText @bind-Value="Model.IssueTopic"
placeholder="@(SmartTranslateService.Translate("Enter a title for your ticket"))"
class="form-control">
</InputText>
</div>
<div class="mb-3">
<InputTextArea @bind-Value="Model.IssueDescription"
placeholder="@(SmartTranslateService.Translate("Describe the issue you are experiencing"))"
<div class="card-body">
<LazyLoader Load="LoadTicketCreate">
<SmartForm Model="Model" OnValidSubmit="OnValidSubmit">
<div class="mb-3">
<InputText @bind-Value="Model.IssueTopic"
placeholder="@(SmartTranslateService.Translate("Enter a title for your ticket"))"
class="form-control">
</InputTextArea>
</div>
<div class="mb-3">
<InputTextArea @bind-Value="Model.IssueTries"
placeholder="@(SmartTranslateService.Translate("Describe what you have tried to solve this issue"))"
class="form-control">
</InputTextArea>
</div>
<div class="mb-3">
<select @bind="Model.Subject" class="form-select">
@foreach (var subject in (TicketSubject[])Enum.GetValues(typeof(TicketSubject)))
</InputText>
</div>
<div class="mb-3">
<InputTextArea @bind-Value="Model.IssueDescription"
placeholder="@(SmartTranslateService.Translate("Describe the issue you are experiencing"))"
class="form-control">
</InputTextArea>
</div>
<div class="mb-3">
<InputTextArea @bind-Value="Model.IssueTries"
placeholder="@(SmartTranslateService.Translate("Describe what you have tried to solve this issue"))"
class="form-control">
</InputTextArea>
</div>
<div class="mb-3">
<select @bind="Model.Subject" class="form-select">
@foreach (var subject in (TicketSubject[])Enum.GetValues(typeof(TicketSubject)))
{
if (Model.Subject == subject)
{
<option value="@(subject)" selected="">@(subject)</option>
}
else
{
<option value="@(subject)">@(subject)</option>
}
}
</select>
</div>
<div class="mb-3">
@if (Model.Subject == TicketSubject.Domain)
{
if (Model.Subject == subject)
{
<option value="@(subject)" selected="">@(subject)</option>
}
else
{
<option value="@(subject)">@(subject)</option>
}
<select @bind="Model.SubjectId" class="form-select">
@foreach (var domain in Domains)
{
if (Model.SubjectId == domain.Id)
{
<option value="@(domain.Id)" selected="">@(domain.Name).@(domain.SharedDomain.Name)</option>
}
else
{
<option value="@(domain.Id)">@(domain.Name).@(domain.SharedDomain.Name)</option>
}
}
</select>
}
</select>
</div>
<div class="mb-3">
@if (Model.Subject == TicketSubject.Domain)
{
<select @bind="Model.SubjectId" class="form-select">
@foreach (var domain in Domains)
{
if (Model.SubjectId == domain.Id)
else if (Model.Subject == TicketSubject.Server)
{
<select @bind="Model.SubjectId" class="form-select">
@foreach (var server in Servers)
{
<option value="@(domain.Id)" selected="">@(domain.Name).@(domain.SharedDomain.Name)</option>
if (Model.SubjectId == server.Id)
{
<option value="@(server.Id)" selected="">@(server.Name)</option>
}
else
{
<option value="@(server.Id)">@(server.Name)</option>
}
}
else
</select>
}
else if (Model.Subject == TicketSubject.Webspace)
{
<select @bind="Model.SubjectId" class="form-select">
@foreach (var webSpace in WebSpaces)
{
<option value="@(domain.Id)">@(domain.Name).@(domain.SharedDomain.Name)</option>
if (Model.SubjectId == webSpace.Id)
{
<option value="@(webSpace.Id)" selected="">@(webSpace.Domain)</option>
}
else
{
<option value="@(webSpace.Id)">@(webSpace.Domain)</option>
}
}
}
</select>
}
else if (Model.Subject == TicketSubject.Server)
{
<select @bind="Model.SubjectId" class="form-select">
@foreach (var server in Servers)
{
if (Model.SubjectId == server.Id)
{
<option value="@(server.Id)" selected="">@(server.Name)</option>
}
else
{
<option value="@(server.Id)">@(server.Name)</option>
}
}
</select>
}
else if (Model.Subject == TicketSubject.Webspace)
{
<select @bind="Model.SubjectId" class="form-select">
@foreach (var webSpace in WebSpaces)
{
if (Model.SubjectId == webSpace.Id)
{
<option value="@(webSpace.Id)" selected="">@(webSpace.Domain)</option>
}
else
{
<option value="@(webSpace.Id)">@(webSpace.Domain)</option>
}
}
</select>
}
</div>
<div class="text-end">
<button class="btn btn-primary" type="submit">
<TL>Create ticket</TL>
</button>
</div>
</SmartForm>
</LazyLoader>
}
else
{
<TicketMessageView Messages="Messages"/>
}
</div>
</div>
@if (ClientService.Ticket != null)
{
<div class="card-footer pt-4">
<div class="d-flex flex-stack">
<table class="w-100">
<tr>
<td class="align-top">
<SmartFileSelect @ref="FileSelect"></SmartFileSelect>
</td>
<td class="w-100">
<textarea @bind="MessageText" class="form-control mb-3 form-control-flush" rows="1" placeholder="@(SmartTranslateService.Translate("Type a message"))"></textarea>
</td>
<td class="align-top">
<WButton Text="@(SmartTranslateService.Translate("Send"))"
WorkingText="@(SmartTranslateService.Translate("Sending"))"
CssClasses="btn-primary ms-2"
OnClick="SendMessage">
</WButton>
</td>
</tr>
</table>
</select>
}
</div>
<div class="text-end">
<button class="btn btn-primary" type="submit">
<TL>Create ticket</TL>
</button>
</div>
</SmartForm>
</LazyLoader>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-header">
<span class="card-title">
<TL>Your tickets</TL>
</span>
</div>
<div class="card-body">
<LazyLoader Load="LoadTickets">
<div class="table-responsive">
<Table TableItem="Ticket" Items="Tickets" 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="@(SmartTranslateService.Translate("Ticket title"))" Field="@(x => x.IssueTopic)" Filterable="true" Sortable="false">
<Template>
<a href="/support/view/@(context.Id)">@(context.IssueTopic)</a>
</Template>
</Column>
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("Assigned to"))" Field="@(x => x.AssignedTo)" Filterable="true" Sortable="true">
<Template>
<span>@(context.AssignedTo == null ? "None" : context.AssignedTo.FirstName + " " + context.AssignedTo.LastName)</span>
</Template>
</Column>
<Column TableItem="Ticket" Title="@(SmartTranslateService.Translate("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="@(SmartTranslateService.Translate("Status"))" Field="@(x => x.Status)" Filterable="true" Sortable="true">
<Template>
@switch (context.Status)
{
case TicketStatus.Closed:
<span class="badge bg-danger">@(context.Status)</span>
break;
case TicketStatus.Open:
<span class="badge bg-success">@(context.Status)</span>
break;
case TicketStatus.Pending:
<span class="badge bg-warning">@(context.Status)</span>
break;
case TicketStatus.WaitingForUser:
<span class="badge bg-primary">@(context.Status)</span>
break;
}
</Template>
</Column>
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
</Table>
</div>
</LazyLoader>
</div>
</div>
</div>
}
</div>
</div>
</div>
@code
{
[Parameter]
public int Id { get; set; }
private Dictionary<Ticket, TicketMessage?> Tickets;
private List<TicketMessage> 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<Ticket>("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<TicketMessage>($"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<Ticket>($"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);
}
}

View File

@@ -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<Ticket> TicketRepository
@inject IdentityService IdentityService
@inject EventSystem Event
@implements IDisposable
<LazyLoader Load="Load">
@if (Ticket == null)
{
<NotFoundAlert />
}
else
{
<div class="card">
<div class="card-header">
<span class="card-title">@(Ticket.IssueTopic)</span>
</div>
<div class="card-body border-end border-top bg-black">
<TicketMessageView Messages="Messages"/>
</div>
<div class="card-footer pt-4">
<div class="d-flex flex-stack">
<table class="w-100">
<tr>
<td class="align-top">
<SmartFileSelect @ref="FileSelect"></SmartFileSelect>
</td>
<td class="w-100">
<textarea @bind="MessageContent" class="form-control mb-3 form-control-flush" rows="1" placeholder="@(SmartTranslateService.Translate("Type a message"))"></textarea>
</td>
<td class="align-top">
<WButton Text="@(SmartTranslateService.Translate("Send"))"
WorkingText="@(SmartTranslateService.Translate("Sending"))"
CssClasses="btn-primary ms-2"
OnClick="SendMessage"/>
</td>
</tr>
</table>
</div>
</div>
</div>
}
</LazyLoader>
@code
{
[Parameter]
public int Id { get; set; }
private Ticket? Ticket;
private List<TicketMessage> 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<TicketMessage>($"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<Ticket>($"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);
}
}
}