Improved ticket system ui and some backend code
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
379
Moonlight/Shared/Views/Admin/Support/Old_Index.razor
Normal file
379
Moonlight/Shared/Views/Admin/Support/Old_Index.razor
Normal 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();
|
||||
}
|
||||
}
|
||||
293
Moonlight/Shared/Views/Admin/Support/View.razor
Normal file
293
Moonlight/Shared/Views/Admin/Support/View.razor
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
131
Moonlight/Shared/Views/Support/View.razor
Normal file
131
Moonlight/Shared/Views/Support/View.razor
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user