Merge pull request #248 from Moonlight-Panel/AddTicketSystem
Added ticket system
This commit is contained in:
commit
f95312c1e3
22 changed files with 3078 additions and 664 deletions
|
@ -59,6 +59,15 @@ public class ConfigV1
|
||||||
[JsonProperty("Sentry")] public SentryData Sentry { get; set; } = new();
|
[JsonProperty("Sentry")] public SentryData Sentry { get; set; } = new();
|
||||||
|
|
||||||
[JsonProperty("Stripe")] public StripeData Stripe { get; set; } = new();
|
[JsonProperty("Stripe")] public StripeData Stripe { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonProperty("Tickets")] public TicketsData Tickets { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TicketsData
|
||||||
|
{
|
||||||
|
[JsonProperty("WelcomeMessage")]
|
||||||
|
[Description("The message that will be sent when a user created a ticket")]
|
||||||
|
public string WelcomeMessage { get; set; } = "Welcome to the support";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StripeData
|
public class StripeData
|
||||||
|
|
|
@ -44,6 +44,9 @@ public class DataContext : DbContext
|
||||||
public DbSet<SecurityLog> SecurityLogs { get; set; }
|
public DbSet<SecurityLog> SecurityLogs { get; set; }
|
||||||
public DbSet<BlocklistIp> BlocklistIps { get; set; }
|
public DbSet<BlocklistIp> BlocklistIps { get; set; }
|
||||||
public DbSet<WhitelistIp> WhitelistIps { get; set; }
|
public DbSet<WhitelistIp> WhitelistIps { get; set; }
|
||||||
|
|
||||||
|
public DbSet<Ticket> Tickets { get; set; }
|
||||||
|
public DbSet<TicketMessage> TicketMessages { get; set; }
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
|
|
19
Moonlight/App/Database/Entities/Ticket.cs
Normal file
19
Moonlight/App/Database/Entities/Ticket.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Database.Entities;
|
||||||
|
|
||||||
|
public class Ticket
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string IssueTopic { get; set; } = "";
|
||||||
|
public string IssueDescription { get; set; } = "";
|
||||||
|
public string IssueTries { get; set; } = "";
|
||||||
|
public User CreatedBy { get; set; }
|
||||||
|
public User? AssignedTo { get; set; }
|
||||||
|
public TicketPriority Priority { get; set; }
|
||||||
|
public TicketStatus Status { get; set; }
|
||||||
|
public TicketSubject Subject { get; set; }
|
||||||
|
public int SubjectId { get; set; }
|
||||||
|
public List<TicketMessage> Messages { get; set; } = new();
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
14
Moonlight/App/Database/Entities/TicketMessage.cs
Normal file
14
Moonlight/App/Database/Entities/TicketMessage.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
namespace Moonlight.App.Database.Entities;
|
||||||
|
|
||||||
|
public class TicketMessage
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Content { get; set; } = "";
|
||||||
|
public string? AttachmentUrl { get; set; }
|
||||||
|
public User? Sender { get; set; }
|
||||||
|
public bool IsSystemMessage { get; set; }
|
||||||
|
public bool IsEdited { get; set; }
|
||||||
|
public bool IsSupportMessage { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
1233
Moonlight/App/Database/Migrations/20230803012947_AddNewTicketModels.Designer.cs
generated
Normal file
1233
Moonlight/App/Database/Migrations/20230803012947_AddNewTicketModels.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,117 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Moonlight.App.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddNewTicketModels : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Tickets",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
IssueTopic = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
IssueDescription = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
IssueTries = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
CreatedById = table.Column<int>(type: "int", nullable: false),
|
||||||
|
AssignedToId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
Priority = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Status = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Subject = table.Column<int>(type: "int", nullable: false),
|
||||||
|
SubjectId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Tickets", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Tickets_Users_AssignedToId",
|
||||||
|
column: x => x.AssignedToId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Tickets_Users_CreatedById",
|
||||||
|
column: x => x.CreatedById,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TicketMessages",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
Content = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
AttachmentUrl = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
SenderId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
IsSystemMessage = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
IsEdited = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
IsSupportMessage = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
TicketId = table.Column<int>(type: "int", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TicketMessages", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TicketMessages_Tickets_TicketId",
|
||||||
|
column: x => x.TicketId,
|
||||||
|
principalTable: "Tickets",
|
||||||
|
principalColumn: "Id");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TicketMessages_Users_SenderId",
|
||||||
|
column: x => x.SenderId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id");
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TicketMessages_SenderId",
|
||||||
|
table: "TicketMessages",
|
||||||
|
column: "SenderId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TicketMessages_TicketId",
|
||||||
|
table: "TicketMessages",
|
||||||
|
column: "TicketId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Tickets_AssignedToId",
|
||||||
|
table: "Tickets",
|
||||||
|
column: "AssignedToId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Tickets_CreatedById",
|
||||||
|
table: "Tickets",
|
||||||
|
column: "CreatedById");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TicketMessages");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Tickets");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -714,6 +714,97 @@ namespace Moonlight.App.Database.Migrations
|
||||||
b.ToTable("SupportChatMessages");
|
b.ToTable("SupportChatMessages");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int?>("AssignedToId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("CreatedById")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("IssueDescription")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("IssueTopic")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("IssueTries")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Subject")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SubjectId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AssignedToId");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedById");
|
||||||
|
|
||||||
|
b.ToTable("Tickets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.TicketMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentUrl")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEdited")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSupportMessage")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSystemMessage")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<int?>("SenderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int?>("TicketId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SenderId");
|
||||||
|
|
||||||
|
b.HasIndex("TicketId");
|
||||||
|
|
||||||
|
b.ToTable("TicketMessages");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
@ -1039,6 +1130,36 @@ namespace Moonlight.App.Database.Migrations
|
||||||
b.Navigation("Sender");
|
b.Navigation("Sender");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "AssignedTo")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssignedToId");
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "CreatedBy")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CreatedById")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AssignedTo");
|
||||||
|
|
||||||
|
b.Navigation("CreatedBy");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.TicketMessage", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "Sender")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SenderId");
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Ticket", null)
|
||||||
|
.WithMany("Messages")
|
||||||
|
.HasForeignKey("TicketId");
|
||||||
|
|
||||||
|
b.Navigation("Sender");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Moonlight.App.Database.Entities.Subscription", "CurrentSubscription")
|
b.HasOne("Moonlight.App.Database.Entities.Subscription", "CurrentSubscription")
|
||||||
|
@ -1094,6 +1215,11 @@ namespace Moonlight.App.Database.Migrations
|
||||||
b.Navigation("Variables");
|
b.Navigation("Variables");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Messages");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Databases");
|
b.Navigation("Databases");
|
||||||
|
|
56
Moonlight/App/Helpers/ByteSizeValue.cs
Normal file
56
Moonlight/App/Helpers/ByteSizeValue.cs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
namespace Moonlight.App.Helpers;
|
||||||
|
|
||||||
|
public class ByteSizeValue
|
||||||
|
{
|
||||||
|
public long Bytes { get; set; }
|
||||||
|
|
||||||
|
public long KiloBytes
|
||||||
|
{
|
||||||
|
get => Bytes / 1024;
|
||||||
|
set => Bytes = value * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long MegaBytes
|
||||||
|
{
|
||||||
|
get => KiloBytes / 1024;
|
||||||
|
set => KiloBytes = value * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long GigaBytes
|
||||||
|
{
|
||||||
|
get => MegaBytes / 1024;
|
||||||
|
set => MegaBytes = value * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSizeValue FromBytes(long bytes)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Bytes = bytes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSizeValue FromKiloBytes(long kiloBytes)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
KiloBytes = kiloBytes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSizeValue FromMegaBytes(long megaBytes)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
MegaBytes = megaBytes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteSizeValue FromGigaBytes(long gigaBytes)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
GigaBytes = gigaBytes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
21
Moonlight/App/Models/Forms/CreateTicketDataModel.cs
Normal file
21
Moonlight/App/Models/Forms/CreateTicketDataModel.cs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Models.Forms;
|
||||||
|
|
||||||
|
public class CreateTicketDataModel
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "You need to specify a issue topic")]
|
||||||
|
[MinLength(5, ErrorMessage = "The issue topic needs to be longer than 5 characters")]
|
||||||
|
public string IssueTopic { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify a issue description")]
|
||||||
|
[MinLength(10, ErrorMessage = "The issue description needs to be longer than 10 characters")]
|
||||||
|
public string IssueDescription { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify your tries to solve this issue")]
|
||||||
|
public string IssueTries { get; set; }
|
||||||
|
|
||||||
|
public TicketSubject Subject { get; set; }
|
||||||
|
public int SubjectId { get; set; }
|
||||||
|
}
|
9
Moonlight/App/Models/Misc/TicketPriority.cs
Normal file
9
Moonlight/App/Models/Misc/TicketPriority.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
namespace Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
|
public enum TicketPriority
|
||||||
|
{
|
||||||
|
Low = 0,
|
||||||
|
Medium = 1,
|
||||||
|
High = 2,
|
||||||
|
Critical = 3
|
||||||
|
}
|
9
Moonlight/App/Models/Misc/TicketStatus.cs
Normal file
9
Moonlight/App/Models/Misc/TicketStatus.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
namespace Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
|
public enum TicketStatus
|
||||||
|
{
|
||||||
|
Closed = 0,
|
||||||
|
Open = 1,
|
||||||
|
WaitingForUser = 2,
|
||||||
|
Pending = 3
|
||||||
|
}
|
9
Moonlight/App/Models/Misc/TicketSubject.cs
Normal file
9
Moonlight/App/Models/Misc/TicketSubject.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
namespace Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
|
public enum TicketSubject
|
||||||
|
{
|
||||||
|
Webspace = 0,
|
||||||
|
Server = 1,
|
||||||
|
Domain = 2,
|
||||||
|
Other = 3
|
||||||
|
}
|
95
Moonlight/App/Services/Tickets/TicketAdminService.cs
Normal file
95
Moonlight/App/Services/Tickets/TicketAdminService.cs
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
using Microsoft.AspNetCore.Components.Forms;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Services.Files;
|
||||||
|
using Moonlight.App.Services.Sessions;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Tickets;
|
||||||
|
|
||||||
|
public class TicketAdminService
|
||||||
|
{
|
||||||
|
private readonly TicketServerService TicketServerService;
|
||||||
|
private readonly IdentityService IdentityService;
|
||||||
|
private readonly BucketService BucketService;
|
||||||
|
|
||||||
|
public Ticket? Ticket { get; set; }
|
||||||
|
|
||||||
|
public TicketAdminService(
|
||||||
|
TicketServerService ticketServerService,
|
||||||
|
IdentityService identityService,
|
||||||
|
BucketService bucketService)
|
||||||
|
{
|
||||||
|
TicketServerService = ticketServerService;
|
||||||
|
IdentityService = identityService;
|
||||||
|
BucketService = bucketService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<Ticket, TicketMessage?>> GetAssigned()
|
||||||
|
{
|
||||||
|
return await TicketServerService.GetUserAssignedTickets(IdentityService.User);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<Ticket, TicketMessage?>> GetUnAssigned()
|
||||||
|
{
|
||||||
|
return await TicketServerService.GetUnAssignedTickets();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Ticket> Create(string issueTopic, string issueDescription, string issueTries,
|
||||||
|
TicketSubject subject, int subjectId)
|
||||||
|
{
|
||||||
|
return await TicketServerService.Create(
|
||||||
|
IdentityService.User,
|
||||||
|
issueTopic,
|
||||||
|
issueDescription,
|
||||||
|
issueTries,
|
||||||
|
subject,
|
||||||
|
subjectId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TicketMessage> Send(string content, IBrowserFile? file = null)
|
||||||
|
{
|
||||||
|
string? attachment = null;
|
||||||
|
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
attachment = await BucketService.StoreFile(
|
||||||
|
"tickets",
|
||||||
|
file.OpenReadStream(1024 * 1024 * 5),
|
||||||
|
file.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await TicketServerService.SendMessage(
|
||||||
|
Ticket!,
|
||||||
|
IdentityService.User,
|
||||||
|
content,
|
||||||
|
attachment,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateStatus(TicketStatus status)
|
||||||
|
{
|
||||||
|
await TicketServerService.UpdateStatus(Ticket!, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdatePriority(TicketPriority priority)
|
||||||
|
{
|
||||||
|
await TicketServerService.UpdatePriority(Ticket!, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TicketMessage[]> GetMessages()
|
||||||
|
{
|
||||||
|
return await TicketServerService.GetMessages(Ticket!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Claim()
|
||||||
|
{
|
||||||
|
await TicketServerService.Claim(Ticket!, IdentityService.User);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UnClaim()
|
||||||
|
{
|
||||||
|
await TicketServerService.Claim(Ticket!);
|
||||||
|
}
|
||||||
|
}
|
68
Moonlight/App/Services/Tickets/TicketClientService.cs
Normal file
68
Moonlight/App/Services/Tickets/TicketClientService.cs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
using Microsoft.AspNetCore.Components.Forms;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Services.Files;
|
||||||
|
using Moonlight.App.Services.Sessions;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Tickets;
|
||||||
|
|
||||||
|
public class TicketClientService
|
||||||
|
{
|
||||||
|
private readonly TicketServerService TicketServerService;
|
||||||
|
private readonly IdentityService IdentityService;
|
||||||
|
private readonly BucketService BucketService;
|
||||||
|
|
||||||
|
public Ticket? Ticket { get; set; }
|
||||||
|
|
||||||
|
public TicketClientService(
|
||||||
|
TicketServerService ticketServerService,
|
||||||
|
IdentityService identityService,
|
||||||
|
BucketService bucketService)
|
||||||
|
{
|
||||||
|
TicketServerService = ticketServerService;
|
||||||
|
IdentityService = identityService;
|
||||||
|
BucketService = bucketService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<Ticket, TicketMessage?>> Get()
|
||||||
|
{
|
||||||
|
return await TicketServerService.GetUserTickets(IdentityService.User);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Ticket> Create(string issueTopic, string issueDescription, string issueTries, TicketSubject subject, int subjectId)
|
||||||
|
{
|
||||||
|
return await TicketServerService.Create(
|
||||||
|
IdentityService.User,
|
||||||
|
issueTopic,
|
||||||
|
issueDescription,
|
||||||
|
issueTries,
|
||||||
|
subject,
|
||||||
|
subjectId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TicketMessage> Send(string content, IBrowserFile? file = null)
|
||||||
|
{
|
||||||
|
string? attachment = null;
|
||||||
|
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
attachment = await BucketService.StoreFile(
|
||||||
|
"tickets",
|
||||||
|
file.OpenReadStream(1024 * 1024 * 5),
|
||||||
|
file.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await TicketServerService.SendMessage(
|
||||||
|
Ticket!,
|
||||||
|
IdentityService.User,
|
||||||
|
content,
|
||||||
|
attachment
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TicketMessage[]> GetMessages()
|
||||||
|
{
|
||||||
|
return await TicketServerService.GetMessages(Ticket!);
|
||||||
|
}
|
||||||
|
}
|
249
Moonlight/App/Services/Tickets/TicketServerService.cs
Normal file
249
Moonlight/App/Services/Tickets/TicketServerService.cs
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Events;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Repositories;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Tickets;
|
||||||
|
|
||||||
|
public class TicketServerService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||||
|
private readonly EventSystem Event;
|
||||||
|
private readonly ConfigService ConfigService;
|
||||||
|
|
||||||
|
public TicketServerService(
|
||||||
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
|
EventSystem eventSystem,
|
||||||
|
ConfigService configService)
|
||||||
|
{
|
||||||
|
ServiceScopeFactory = serviceScopeFactory;
|
||||||
|
Event = eventSystem;
|
||||||
|
ConfigService = configService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Ticket> Create(User creator, string issueTopic, string issueDescription, string issueTries, TicketSubject subject, int subjectId)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||||
|
|
||||||
|
var creatorUser = userRepo
|
||||||
|
.Get()
|
||||||
|
.First(x => x.Id == creator.Id);
|
||||||
|
|
||||||
|
var ticket = ticketRepo.Add(new()
|
||||||
|
{
|
||||||
|
Priority = TicketPriority.Low,
|
||||||
|
Status = TicketStatus.Open,
|
||||||
|
AssignedTo = null,
|
||||||
|
IssueTopic = issueTopic,
|
||||||
|
IssueDescription = issueDescription,
|
||||||
|
IssueTries = issueTries,
|
||||||
|
Subject = subject,
|
||||||
|
SubjectId = subjectId,
|
||||||
|
CreatedBy = creatorUser
|
||||||
|
});
|
||||||
|
|
||||||
|
await Event.Emit("tickets.new", ticket);
|
||||||
|
|
||||||
|
// Do automatic stuff here
|
||||||
|
await SendSystemMessage(ticket, ConfigService.Get().Moonlight.Tickets.WelcomeMessage);
|
||||||
|
//TODO: Check for opening times
|
||||||
|
|
||||||
|
return ticket;
|
||||||
|
}
|
||||||
|
public async Task SendSystemMessage(Ticket t, string content, string? attachmentUrl = null)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
|
||||||
|
var ticket = ticketRepo.Get().First(x => x.Id == t.Id);
|
||||||
|
|
||||||
|
var message = new TicketMessage()
|
||||||
|
{
|
||||||
|
Content = content,
|
||||||
|
Sender = null,
|
||||||
|
AttachmentUrl = attachmentUrl,
|
||||||
|
IsSystemMessage = true
|
||||||
|
};
|
||||||
|
|
||||||
|
ticket.Messages.Add(message);
|
||||||
|
ticketRepo.Update(ticket);
|
||||||
|
|
||||||
|
await Event.Emit("tickets.message", message);
|
||||||
|
await Event.Emit($"tickets.{ticket.Id}.message", message);
|
||||||
|
}
|
||||||
|
public async Task UpdatePriority(Ticket t, TicketPriority priority)
|
||||||
|
{
|
||||||
|
if(t.Priority == priority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
|
||||||
|
var ticket = ticketRepo.Get().First(x => x.Id == t.Id);
|
||||||
|
|
||||||
|
ticket.Priority = priority;
|
||||||
|
|
||||||
|
ticketRepo.Update(ticket);
|
||||||
|
|
||||||
|
await Event.Emit("tickets.status", ticket);
|
||||||
|
await Event.Emit($"tickets.{ticket.Id}.status", ticket);
|
||||||
|
|
||||||
|
await SendSystemMessage(ticket, $"The ticket priority has been changed to: {priority}");
|
||||||
|
}
|
||||||
|
public async Task UpdateStatus(Ticket t, TicketStatus status)
|
||||||
|
{
|
||||||
|
if(t.Status == status)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
|
||||||
|
var ticket = ticketRepo.Get().First(x => x.Id == t.Id);
|
||||||
|
|
||||||
|
ticket.Status = status;
|
||||||
|
|
||||||
|
ticketRepo.Update(ticket);
|
||||||
|
|
||||||
|
await Event.Emit("tickets.status", ticket);
|
||||||
|
await Event.Emit($"tickets.{ticket.Id}.status", ticket);
|
||||||
|
|
||||||
|
await SendSystemMessage(ticket, $"The ticket status has been changed to: {status}");
|
||||||
|
}
|
||||||
|
public async Task<TicketMessage> SendMessage(Ticket t, User sender, string content, string? attachmentUrl = null, bool isSupportMessage = false)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||||
|
|
||||||
|
var ticket = ticketRepo.Get().First(x => x.Id == t.Id);
|
||||||
|
var user = userRepo.Get().First(x => x.Id == sender.Id);
|
||||||
|
|
||||||
|
var message = new TicketMessage()
|
||||||
|
{
|
||||||
|
Content = content,
|
||||||
|
Sender = user,
|
||||||
|
AttachmentUrl = attachmentUrl,
|
||||||
|
IsSupportMessage = isSupportMessage
|
||||||
|
};
|
||||||
|
|
||||||
|
ticket.Messages.Add(message);
|
||||||
|
ticketRepo.Update(ticket);
|
||||||
|
|
||||||
|
await Event.Emit("tickets.message", message);
|
||||||
|
await Event.Emit($"tickets.{ticket.Id}.message", message);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
public Task<Dictionary<Ticket, TicketMessage?>> GetUserTickets(User u)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
|
||||||
|
var tickets = ticketRepo
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.CreatedBy)
|
||||||
|
.Include(x => x.Messages)
|
||||||
|
.Where(x => x.CreatedBy.Id == u.Id)
|
||||||
|
.Where(x => x.Status != TicketStatus.Closed)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var result = new Dictionary<Ticket, TicketMessage?>();
|
||||||
|
|
||||||
|
foreach (var ticket in tickets)
|
||||||
|
{
|
||||||
|
var message = ticket.Messages
|
||||||
|
.OrderByDescending(x => x.Id)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
result.Add(ticket, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
public Task<Dictionary<Ticket, TicketMessage?>> GetUserAssignedTickets(User u)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
|
||||||
|
var tickets = ticketRepo
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.CreatedBy)
|
||||||
|
.Include(x => x.Messages)
|
||||||
|
.Where(x => x.Status != TicketStatus.Closed)
|
||||||
|
.Where(x => x.AssignedTo.Id == u.Id)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var result = new Dictionary<Ticket, TicketMessage?>();
|
||||||
|
|
||||||
|
foreach (var ticket in tickets)
|
||||||
|
{
|
||||||
|
var message = ticket.Messages
|
||||||
|
.OrderByDescending(x => x.Id)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
result.Add(ticket, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
public Task<Dictionary<Ticket, TicketMessage?>> GetUnAssignedTickets()
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
|
||||||
|
var tickets = ticketRepo
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.CreatedBy)
|
||||||
|
.Include(x => x.Messages)
|
||||||
|
.Include(x => x.AssignedTo)
|
||||||
|
.Where(x => x.AssignedTo == null)
|
||||||
|
.Where(x => x.Status != TicketStatus.Closed)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var result = new Dictionary<Ticket, TicketMessage?>();
|
||||||
|
|
||||||
|
foreach (var ticket in tickets)
|
||||||
|
{
|
||||||
|
var message = ticket.Messages
|
||||||
|
.OrderByDescending(x => x.Id)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
result.Add(ticket, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
public Task<TicketMessage[]> GetMessages(Ticket ticket)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
|
||||||
|
var tickets = ticketRepo
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.CreatedBy)
|
||||||
|
.Include(x => x.Messages)
|
||||||
|
.First(x => x.Id == ticket.Id);
|
||||||
|
|
||||||
|
return Task.FromResult(tickets.Messages.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Claim(Ticket t, User? u = null)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var ticketRepo = scope.ServiceProvider.GetRequiredService<Repository<Ticket>>();
|
||||||
|
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||||
|
|
||||||
|
var ticket = ticketRepo.Get().Include(x => x.AssignedTo).First(x => x.Id == t.Id);
|
||||||
|
var user = u == null ? u : userRepo.Get().First(x => x.Id == u.Id);
|
||||||
|
|
||||||
|
ticket.AssignedTo = user;
|
||||||
|
|
||||||
|
ticketRepo.Update(ticket);
|
||||||
|
|
||||||
|
await Event.Emit("tickets.status", ticket);
|
||||||
|
await Event.Emit($"tickets.{ticket.Id}.status", ticket);
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ using Moonlight.App.Services.Plugins;
|
||||||
using Moonlight.App.Services.Sessions;
|
using Moonlight.App.Services.Sessions;
|
||||||
using Moonlight.App.Services.Statistics;
|
using Moonlight.App.Services.Statistics;
|
||||||
using Moonlight.App.Services.SupportChat;
|
using Moonlight.App.Services.SupportChat;
|
||||||
|
using Moonlight.App.Services.Tickets;
|
||||||
using Sentry;
|
using Sentry;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Serilog.Events;
|
using Serilog.Events;
|
||||||
|
@ -109,10 +110,6 @@ namespace Moonlight
|
||||||
|
|
||||||
await databaseCheckupService.Perform();
|
await databaseCheckupService.Perform();
|
||||||
|
|
||||||
var backupHelper = new BackupHelper();
|
|
||||||
await backupHelper.CreateBackup(PathBuilder.File("storage", "backups",
|
|
||||||
$"{new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds()}.zip"));
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
var pluginService = new PluginService();
|
var pluginService = new PluginService();
|
||||||
|
@ -217,6 +214,9 @@ namespace Moonlight
|
||||||
builder.Services.AddScoped<SubscriptionService>();
|
builder.Services.AddScoped<SubscriptionService>();
|
||||||
builder.Services.AddScoped<BillingService>();
|
builder.Services.AddScoped<BillingService>();
|
||||||
builder.Services.AddSingleton<PluginStoreService>();
|
builder.Services.AddSingleton<PluginStoreService>();
|
||||||
|
builder.Services.AddSingleton<TicketServerService>();
|
||||||
|
builder.Services.AddScoped<TicketClientService>();
|
||||||
|
builder.Services.AddScoped<TicketAdminService>();
|
||||||
|
|
||||||
builder.Services.AddScoped<SessionClientService>();
|
builder.Services.AddScoped<SessionClientService>();
|
||||||
builder.Services.AddSingleton<SessionServerService>();
|
builder.Services.AddSingleton<SessionServerService>();
|
||||||
|
|
219
Moonlight/Shared/Components/Tickets/TicketMessageView.razor
Normal file
219
Moonlight/Shared/Components/Tickets/TicketMessageView.razor
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
@using Moonlight.App.Database.Entities
|
||||||
|
@using Moonlight.App.Helpers
|
||||||
|
@using Moonlight.App.Services
|
||||||
|
@using Moonlight.App.Services.Files
|
||||||
|
@using System.Text.RegularExpressions
|
||||||
|
|
||||||
|
@inject ResourceService ResourceService
|
||||||
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
|
||||||
|
@foreach (var message in Messages.OrderByDescending(x => x.Id)) // Reverse messages to use auto scrolling
|
||||||
|
{
|
||||||
|
if (message.IsSupportMessage)
|
||||||
|
{
|
||||||
|
if (ViewAsSupport)
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-end mb-10 ">
|
||||||
|
<div class="d-flex flex-column align-items-end">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="symbol symbol-35px symbol-circle ">
|
||||||
|
<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">
|
||||||
|
@{
|
||||||
|
int i = 0;
|
||||||
|
var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
}
|
||||||
|
@foreach (var line in arr)
|
||||||
|
{
|
||||||
|
@line
|
||||||
|
if (i++ != arr.Length - 1)
|
||||||
|
{
|
||||||
|
<br/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(message.AttachmentUrl))
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.AttachmentUrl, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.AttachmentUrl)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
|
||||||
|
@{
|
||||||
|
int i = 0;
|
||||||
|
var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
}
|
||||||
|
@foreach (var line in arr)
|
||||||
|
{
|
||||||
|
@line
|
||||||
|
if (i++ != arr.Length - 1)
|
||||||
|
{
|
||||||
|
<br/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(message.AttachmentUrl))
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.AttachmentUrl, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.AttachmentUrl)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (message.IsSystemMessage)
|
||||||
|
{
|
||||||
|
<div class="separator separator-content border-primary my-15">
|
||||||
|
<span class="w-250px fw-bold">
|
||||||
|
@(message.Content)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ViewAsSupport)
|
||||||
|
{
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
|
||||||
|
@{
|
||||||
|
int i = 0;
|
||||||
|
var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
}
|
||||||
|
@foreach (var line in arr)
|
||||||
|
{
|
||||||
|
@line
|
||||||
|
if (i++ != arr.Length - 1)
|
||||||
|
{
|
||||||
|
<br/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(message.AttachmentUrl))
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.AttachmentUrl, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.AttachmentUrl)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-end mb-10 ">
|
||||||
|
<div class="d-flex flex-column align-items-end">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="symbol symbol-35px symbol-circle ">
|
||||||
|
<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">
|
||||||
|
@{
|
||||||
|
int i = 0;
|
||||||
|
var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
}
|
||||||
|
@foreach (var line in arr)
|
||||||
|
{
|
||||||
|
@line
|
||||||
|
if (i++ != arr.Length - 1)
|
||||||
|
{
|
||||||
|
<br/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(message.AttachmentUrl))
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.AttachmentUrl, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("tickets", message.AttachmentUrl))">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.AttachmentUrl)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public IEnumerable<TicketMessage> Messages { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ViewAsSupport { get; set; }
|
||||||
|
}
|
|
@ -88,7 +88,7 @@ else
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span>
|
<span>
|
||||||
@(Formatter.FormatSize(MemoryMetrics.Used)) <TL>of</TL> @(Formatter.FormatSize(MemoryMetrics.Total)) <TL>memory used</TL>
|
@(Formatter.FormatSize(ByteSizeValue.FromKiloBytes(MemoryMetrics.Used).Bytes)) <TL>of</TL> @(Formatter.FormatSize(ByteSizeValue.FromKiloBytes(MemoryMetrics.Total).Bytes)) <TL>memory used</TL>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,101 +1,379 @@
|
||||||
@page "/admin/support"
|
@page "/admin/support"
|
||||||
|
@page "/admin/support/{Id:int}"
|
||||||
|
|
||||||
|
@using Moonlight.App.Services.Tickets
|
||||||
@using Moonlight.App.Database.Entities
|
@using Moonlight.App.Database.Entities
|
||||||
@using Moonlight.App.Events
|
@using Moonlight.App.Events
|
||||||
@using Moonlight.App.Services.SupportChat
|
@using Moonlight.App.Helpers
|
||||||
|
@using Moonlight.App.Models.Misc
|
||||||
|
@using Moonlight.App.Services
|
||||||
|
@using Moonlight.App.Services.Sessions
|
||||||
|
@using Moonlight.Shared.Components.Tickets
|
||||||
|
|
||||||
@inject SupportChatServerService ServerService
|
@inject TicketAdminService AdminService
|
||||||
@inject EventSystem Event
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
@inject EventSystem EventSystem
|
||||||
@implements IDisposable
|
@inject IdentityService IdentityService
|
||||||
|
|
||||||
@attribute [PermissionRequired(nameof(Permissions.AdminSupport))]
|
@attribute [PermissionRequired(nameof(Permissions.AdminSupport))]
|
||||||
|
|
||||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
@implements IDisposable
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
<div class="d-flex flex-column flex-lg-row">
|
||||||
<div class="d-flex flex-column flex-xl-row p-5 pb-0">
|
<div class="flex-column flex-lg-row-auto w-100 w-lg-300px w-xl-400px mb-10 mb-lg-0">
|
||||||
<div class="flex-lg-row-fluid me-xl-15 mb-20 mb-xl-0">
|
<div class="card card-flush">
|
||||||
<div class="mb-0">
|
<div class="card-body pt-5">
|
||||||
<h1 class="text-dark mb-6">
|
<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;">
|
||||||
<TL>Open chats</TL>
|
<div class="separator separator-content border-primary mb-10 mt-5">
|
||||||
</h1>
|
<span class="w-250px fw-bold fs-5">
|
||||||
<div class="separator"></div>
|
<TL>Unassigned tickets</TL>
|
||||||
<div class="mb-5">
|
</span>
|
||||||
@if (OpenChats.Any())
|
</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)
|
||||||
{
|
{
|
||||||
foreach (var chat in OpenChats)
|
<div class="fw-semibold text-muted">
|
||||||
{
|
@(ticket.Value.Content)
|
||||||
<div class="d-flex mt-3 mb-3 ms-2 me-2">
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td rowspan="2">
|
|
||||||
<span class="svg-icon svg-icon-2x me-5 ms-n1 svg-icon-success">
|
|
||||||
<i class="text-primary bx bx-md bx-message-dots"></i>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="/admin/support/view/@(chat.Key.Id)" class="text-dark text-hover-primary fs-4 me-3 fw-semibold">
|
|
||||||
@(chat.Key.FirstName) @(chat.Key.LastName)
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<span class="text-muted fw-semibold fs-6">
|
|
||||||
@if (chat.Value == null)
|
|
||||||
{
|
|
||||||
<TL>No message sent yet</TL>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
@(chat.Value.Content)
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="separator"></div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<TL>No support chat is currently open</TL>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</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>
|
</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>
|
||||||
</LazyLoader>
|
</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"))" OnClick="AdminService.Claim"/>
|
||||||
|
}
|
||||||
|
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"/>
|
||||||
|
}
|
||||||
|
</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
|
@code
|
||||||
{
|
{
|
||||||
private LazyLoader? LazyLoader;
|
[Parameter]
|
||||||
private Dictionary<User, SupportChatMessage?> OpenChats = new();
|
public int Id { get; set; }
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
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 Event.On<User>("supportChat.new", this, async user =>
|
await Unsubscribe();
|
||||||
{
|
await ReloadTickets();
|
||||||
//TODO: Play sound or smth. Add a config option
|
await Subscribe();
|
||||||
|
|
||||||
OpenChats = await ServerService.GetOpenChats();
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Load(LazyLoader arg) // Only for initial load
|
private async Task UpdatePriority()
|
||||||
{
|
{
|
||||||
OpenChats = await ServerService.GetOpenChats();
|
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()
|
public async void Dispose()
|
||||||
{
|
{
|
||||||
await Event.Off("supportChat.new", this);
|
await Unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,313 +0,0 @@
|
||||||
@page "/admin/support/view/{Id:int}"
|
|
||||||
@using Moonlight.App.Database.Entities
|
|
||||||
@using Moonlight.App.Helpers
|
|
||||||
@using Moonlight.App.Repositories
|
|
||||||
@using Moonlight.App.Services
|
|
||||||
@using Moonlight.App.Services.SupportChat
|
|
||||||
@using System.Text.RegularExpressions
|
|
||||||
@using Moonlight.App.Services.Files
|
|
||||||
|
|
||||||
@inject SupportChatAdminService AdminService
|
|
||||||
@inject UserRepository UserRepository
|
|
||||||
@inject SmartTranslateService SmartTranslateService
|
|
||||||
@inject ResourceService ResourceService
|
|
||||||
|
|
||||||
@implements IDisposable
|
|
||||||
|
|
||||||
@attribute [PermissionRequired(nameof(Permissions.AdminSupportView))]
|
|
||||||
|
|
||||||
<LazyLoader Load="Load">
|
|
||||||
@if (User == null)
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<TL>User not found</TL>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="row">
|
|
||||||
<div class="d-flex flex-column flex-xl-row p-7">
|
|
||||||
<div class="flex-lg-row-fluid me-6 mb-20 mb-xl-0">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<LazyLoader Load="LoadMessages">
|
|
||||||
<div class="scroll-y me-n5 pe-5" style="max-height: 55vh; display: flex; flex-direction: column-reverse;">
|
|
||||||
@foreach (var message in Messages)
|
|
||||||
{
|
|
||||||
if (message.Sender == null || message.Sender.Id != User.Id)
|
|
||||||
{
|
|
||||||
<div class="d-flex justify-content-end mb-10 ">
|
|
||||||
<div class="d-flex flex-column align-items-end">
|
|
||||||
<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 class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">
|
|
||||||
@if (message.Sender != null)
|
|
||||||
{
|
|
||||||
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span>
|
|
||||||
<TL>System</TL>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="symbol symbol-35px symbol-circle ">
|
|
||||||
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end">
|
|
||||||
@if (message.Sender == null)
|
|
||||||
{
|
|
||||||
<TL>@(message.Content)</TL>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var line in message.Content.Split("\n"))
|
|
||||||
{
|
|
||||||
@(line)<br/>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.Attachment != "")
|
|
||||||
{
|
|
||||||
<div class="mt-3">
|
|
||||||
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
|
||||||
{
|
|
||||||
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
|
|
||||||
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<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>
|
|
||||||
<div class="ms-3">
|
|
||||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
|
|
||||||
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</span>
|
|
||||||
</a>
|
|
||||||
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
|
|
||||||
@{
|
|
||||||
int i = 0;
|
|
||||||
var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);}
|
|
||||||
@foreach (var line in arr)
|
|
||||||
{
|
|
||||||
@line
|
|
||||||
if (i++ != arr.Length - 1)
|
|
||||||
{
|
|
||||||
<br />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (message.Attachment != "")
|
|
||||||
{
|
|
||||||
<div class="mt-3">
|
|
||||||
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
|
||||||
{
|
|
||||||
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
|
|
||||||
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</LazyLoader>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
@if (Typing.Any())
|
|
||||||
{
|
|
||||||
<span class="mb-5 fs-5 d-flex flex-row">
|
|
||||||
<div class="wave me-1">
|
|
||||||
<div class="dot h-5px w-5px" style="margin-right: 1px;"></div>
|
|
||||||
<div class="dot h-5px w-5px" style="margin-right: 1px;"></div>
|
|
||||||
<div class="dot h-5px w-5px"></div>
|
|
||||||
</div>
|
|
||||||
@if (Typing.Length > 1)
|
|
||||||
{
|
|
||||||
<span>
|
|
||||||
@(Typing.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span>
|
|
||||||
@(Typing.First()) <TL>is typing</TL>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="d-flex flex-stack">
|
|
||||||
<table class="w-100">
|
|
||||||
<tr>
|
|
||||||
<td class="align-top">
|
|
||||||
<SmartFileSelect @ref="SmartFileSelect"></SmartFileSelect>
|
|
||||||
</td>
|
|
||||||
<td class="w-100">
|
|
||||||
<textarea @bind="Content" @oninput="OnTyping" class="form-control mb-3 form-control-flush" rows="1" placeholder="Type a message">
|
|
||||||
</textarea>
|
|
||||||
</td>
|
|
||||||
<td class="align-top">
|
|
||||||
<WButton Text="@(SmartTranslateService.Translate("Send"))"
|
|
||||||
WorkingText="@(SmartTranslateService.Translate("Sending"))"
|
|
||||||
CssClasses="btn-primary ms-2"
|
|
||||||
OnClick="Send">
|
|
||||||
</WButton>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-column flex-lg-row-auto w-100 mw-lg-300px mw-xxl-350px">
|
|
||||||
<div class="card p-10 mb-15 pb-8">
|
|
||||||
<h2 class="text-dark fw-bold mb-2">
|
|
||||||
<TL>User information</TL>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="d-flex align-items-center mb-1">
|
|
||||||
<span class="fw-semibold text-gray-800 fs-5 m-0">
|
|
||||||
<TL>Name</TL>: @(User.FirstName) @User.LastName
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-items-center mb-2">
|
|
||||||
<span class="fw-semibold text-gray-800 fs-5 m-0">
|
|
||||||
<TL>Email</TL>: <a href="/admin/users/view/@User.Id">@(User.Email)</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="align-items-center mt-3">
|
|
||||||
<span class="fw-semibold text-gray-800 fs-5 m-0">
|
|
||||||
<WButton Text="@(SmartTranslateService.Translate("Close ticket"))"
|
|
||||||
WorkingText="@(SmartTranslateService.Translate("Closing"))"
|
|
||||||
CssClasses="btn-danger float-end"
|
|
||||||
OnClick="CloseTicket">
|
|
||||||
</WButton>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</LazyLoader>
|
|
||||||
|
|
||||||
@code
|
|
||||||
{
|
|
||||||
[Parameter]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
private User? User;
|
|
||||||
|
|
||||||
private List<SupportChatMessage> Messages = new();
|
|
||||||
private string[] Typing = Array.Empty<string>();
|
|
||||||
|
|
||||||
private string Content = "";
|
|
||||||
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
|
|
||||||
|
|
||||||
private SmartFileSelect SmartFileSelect;
|
|
||||||
|
|
||||||
private async Task Load(LazyLoader arg)
|
|
||||||
{
|
|
||||||
User = UserRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefault(x => x.Id == Id);
|
|
||||||
|
|
||||||
if (User != null)
|
|
||||||
{
|
|
||||||
AdminService.OnMessage += OnMessage;
|
|
||||||
AdminService.OnTypingChanged += OnTypingChanged;
|
|
||||||
|
|
||||||
await AdminService.Start(User);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadMessages(LazyLoader arg)
|
|
||||||
{
|
|
||||||
Messages = (await AdminService.GetMessages()).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnTypingChanged(string[] typing)
|
|
||||||
{
|
|
||||||
Typing = typing;
|
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnMessage(SupportChatMessage arg)
|
|
||||||
{
|
|
||||||
Messages.Insert(0, arg);
|
|
||||||
|
|
||||||
//TODO: Sound when message from system or admin
|
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Send()
|
|
||||||
{
|
|
||||||
if (SmartFileSelect.SelectedFile != null && string.IsNullOrEmpty(Content))
|
|
||||||
Content = "File upload";
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(Content))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var message = await AdminService.SendMessage(Content, SmartFileSelect.SelectedFile);
|
|
||||||
Content = "";
|
|
||||||
|
|
||||||
await SmartFileSelect.RemoveSelection();
|
|
||||||
|
|
||||||
Messages.Insert(0, message);
|
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CloseTicket()
|
|
||||||
{
|
|
||||||
await AdminService.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnTyping()
|
|
||||||
{
|
|
||||||
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
|
|
||||||
{
|
|
||||||
LastTypingTimestamp = DateTime.UtcNow;
|
|
||||||
|
|
||||||
await AdminService.SendTyping();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
AdminService?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,275 +0,0 @@
|
||||||
@page "/support"
|
|
||||||
@using Moonlight.App.Services
|
|
||||||
@using Moonlight.App.Database.Entities
|
|
||||||
@using Moonlight.App.Helpers
|
|
||||||
@using Moonlight.App.Services.SupportChat
|
|
||||||
@using System.Text.RegularExpressions
|
|
||||||
@using Moonlight.App.Services.Files
|
|
||||||
@using Moonlight.App.Services.Sessions
|
|
||||||
|
|
||||||
@inject ResourceService ResourceService
|
|
||||||
@inject SupportChatClientService ClientService
|
|
||||||
@inject SmartTranslateService SmartTranslateService
|
|
||||||
@inject IdentityService IdentityService
|
|
||||||
|
|
||||||
@implements IDisposable
|
|
||||||
|
|
||||||
<LazyLoader Load="Load">
|
|
||||||
<div class="row">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<LazyLoader Load="LoadMessages">
|
|
||||||
<div class="scroll-y me-n5 pe-5" style="max-height: 55vh; display: flex; flex-direction: column-reverse;">
|
|
||||||
@foreach (var message in Messages.ToArray())
|
|
||||||
{
|
|
||||||
if (message.Sender == null || message.Sender.Id != IdentityService.User.Id)
|
|
||||||
{
|
|
||||||
<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="Logo" src="@(ResourceService.Image("logo.svg"))">
|
|
||||||
</div>
|
|
||||||
<div class="ms-3">
|
|
||||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
|
|
||||||
@if (message.Sender != null)
|
|
||||||
{
|
|
||||||
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span>
|
|
||||||
<TL>System</TL>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</a>
|
|
||||||
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
|
|
||||||
@if (message.Sender == null)
|
|
||||||
{
|
|
||||||
<TL>@(message.Content)</TL>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
int i = 0;
|
|
||||||
var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
||||||
foreach (var line in arr)
|
|
||||||
{
|
|
||||||
@line
|
|
||||||
if (i++ != arr.Length - 1)
|
|
||||||
{
|
|
||||||
<br />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.Attachment != "")
|
|
||||||
{
|
|
||||||
<div class="mt-3">
|
|
||||||
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
|
||||||
{
|
|
||||||
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
|
|
||||||
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="d-flex justify-content-end mb-10 ">
|
|
||||||
<div class="d-flex flex-column align-items-end">
|
|
||||||
<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 class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">
|
|
||||||
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="symbol symbol-35px symbol-circle ">
|
|
||||||
<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">
|
|
||||||
@foreach (var line in message.Content.Split("\n"))
|
|
||||||
{
|
|
||||||
@(line)<br/>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (message.Attachment != "")
|
|
||||||
{
|
|
||||||
<div class="mt-3">
|
|
||||||
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
|
||||||
{
|
|
||||||
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
|
|
||||||
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<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="Logo" src="@(ResourceService.Image("logo.svg"))">
|
|
||||||
</div>
|
|
||||||
<div class="ms-3">
|
|
||||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
|
|
||||||
<span>
|
|
||||||
<TL>System</TL>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
|
|
||||||
<TL>Welcome to the support chat. Ask your question here and we will help you</TL>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</LazyLoader>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
@if (Typing.Any())
|
|
||||||
{
|
|
||||||
<span class="mb-5 fs-5 d-flex flex-row">
|
|
||||||
<div class="wave me-1">
|
|
||||||
<div class="dot h-5px w-5px" style="margin-right: 1px;"></div>
|
|
||||||
<div class="dot h-5px w-5px" style="margin-right: 1px;"></div>
|
|
||||||
<div class="dot h-5px w-5px"></div>
|
|
||||||
</div>
|
|
||||||
@if (Typing.Length > 1)
|
|
||||||
{
|
|
||||||
<span>
|
|
||||||
@(Typing.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span>
|
|
||||||
@(Typing.First()) <TL>is typing</TL>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="d-flex flex-stack">
|
|
||||||
<table class="w-100">
|
|
||||||
<tr>
|
|
||||||
<td class="align-top">
|
|
||||||
<SmartFileSelect @ref="SmartFileSelect"></SmartFileSelect>
|
|
||||||
</td>
|
|
||||||
<td class="w-100">
|
|
||||||
<textarea @bind="Content" @oninput="OnTyping" class="form-control mb-3 form-control-flush" rows="1" placeholder="Type a message">
|
|
||||||
</textarea>
|
|
||||||
</td>
|
|
||||||
<td class="align-top">
|
|
||||||
<WButton Text="@(SmartTranslateService.Translate("Send"))"
|
|
||||||
WorkingText="@(SmartTranslateService.Translate("Sending"))"
|
|
||||||
CssClasses="btn-primary ms-2"
|
|
||||||
OnClick="Send">
|
|
||||||
</WButton>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</LazyLoader>
|
|
||||||
|
|
||||||
@code
|
|
||||||
{
|
|
||||||
private List<SupportChatMessage> Messages = new();
|
|
||||||
private string[] Typing = Array.Empty<string>();
|
|
||||||
|
|
||||||
private string Content = "";
|
|
||||||
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
|
|
||||||
|
|
||||||
private SmartFileSelect SmartFileSelect;
|
|
||||||
|
|
||||||
private async Task Load(LazyLoader lazyLoader)
|
|
||||||
{
|
|
||||||
await lazyLoader.SetText("Starting chat client");
|
|
||||||
|
|
||||||
ClientService.OnMessage += OnMessage;
|
|
||||||
ClientService.OnTypingChanged += OnTypingChanged;
|
|
||||||
|
|
||||||
await ClientService.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadMessages(LazyLoader arg)
|
|
||||||
{
|
|
||||||
Messages = (await ClientService.GetMessages()).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnTypingChanged(string[] typing)
|
|
||||||
{
|
|
||||||
Typing = typing;
|
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnMessage(SupportChatMessage message)
|
|
||||||
{
|
|
||||||
Messages.Insert(0, message);
|
|
||||||
|
|
||||||
//TODO: Sound when message from system or admin
|
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Send()
|
|
||||||
{
|
|
||||||
if (SmartFileSelect.SelectedFile != null && string.IsNullOrEmpty(Content))
|
|
||||||
Content = "File upload";
|
|
||||||
|
|
||||||
var message = await ClientService.SendMessage(Content, SmartFileSelect.SelectedFile);
|
|
||||||
Content = "";
|
|
||||||
|
|
||||||
await SmartFileSelect.RemoveSelection();
|
|
||||||
|
|
||||||
Messages.Insert(0, message);
|
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnTyping()
|
|
||||||
{
|
|
||||||
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
|
|
||||||
{
|
|
||||||
LastTypingTimestamp = DateTime.UtcNow;
|
|
||||||
await ClientService.SendTyping();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
ClientService?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnFileChange(InputFileChangeEventArgs obj)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
468
Moonlight/Shared/Views/Support/Index.razor
Normal file
468
Moonlight/Shared/Views/Support/Index.razor
Normal file
|
@ -0,0 +1,468 @@
|
||||||
|
@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.Services.Sessions
|
||||||
|
@using Moonlight.Shared.Components.Tickets
|
||||||
|
|
||||||
|
@inject TicketClientService ClientService
|
||||||
|
@inject Repository<Server> ServerRepository
|
||||||
|
@inject Repository<WebSpace> WebSpaceRepository
|
||||||
|
@inject Repository<Domain> DomainRepository
|
||||||
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
@inject IdentityService IdentityService
|
||||||
|
@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">
|
||||||
|
<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"))"
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
<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>
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
private Dictionary<Ticket, TicketMessage?> Tickets;
|
||||||
|
private List<TicketMessage> Messages = new();
|
||||||
|
private CreateTicketDataModel Model = new();
|
||||||
|
private string MessageText;
|
||||||
|
private SmartFileSelect FileSelect;
|
||||||
|
|
||||||
|
private Server[] Servers;
|
||||||
|
private WebSpace[] WebSpaces;
|
||||||
|
private Domain[] Domains;
|
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
await Unsubscribe();
|
||||||
|
await ReloadTickets();
|
||||||
|
await Subscribe();
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task LoadTicketCreate(LazyLoader _)
|
||||||
|
{
|
||||||
|
Servers = ServerRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Owner)
|
||||||
|
.Where(x => x.Owner.Id == IdentityService.User.Id)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
WebSpaces = WebSpaceRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Owner)
|
||||||
|
.Where(x => x.Owner.Id == IdentityService.User.Id)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Domains = DomainRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.SharedDomain)
|
||||||
|
.Include(x => x.Owner)
|
||||||
|
.Where(x => x.Owner.Id == IdentityService.User.Id)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnValidSubmit()
|
||||||
|
{
|
||||||
|
var ticket = await ClientService.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue