Merge pull request #337 from Moonlight-Panel/AddTicketSystem
Added ticket system
This commit is contained in:
@@ -12,8 +12,6 @@ public class DataContext : DbContext
|
|||||||
private readonly ConfigService ConfigService;
|
private readonly ConfigService ConfigService;
|
||||||
|
|
||||||
public DbSet<User> Users { get; set; }
|
public DbSet<User> Users { get; set; }
|
||||||
//public DbSet<Ticket> Tickets { get; set; }
|
|
||||||
//public DbSet<TicketMessage> TicketMessages { get; set; }
|
|
||||||
|
|
||||||
// Store
|
// Store
|
||||||
public DbSet<Category> Categories { get; set; }
|
public DbSet<Category> Categories { get; set; }
|
||||||
@@ -32,6 +30,10 @@ public class DataContext : DbContext
|
|||||||
public DbSet<PostComment> PostComments { get; set; }
|
public DbSet<PostComment> PostComments { get; set; }
|
||||||
public DbSet<PostLike> PostLikes { get; set; }
|
public DbSet<PostLike> PostLikes { get; set; }
|
||||||
public DbSet<WordFilter> WordFilters { get; set; }
|
public DbSet<WordFilter> WordFilters { get; set; }
|
||||||
|
|
||||||
|
// Tickets
|
||||||
|
public DbSet<Ticket> Tickets { get; set; }
|
||||||
|
public DbSet<TicketMessage> TicketMessages { get; set; }
|
||||||
|
|
||||||
public DataContext(ConfigService configService)
|
public DataContext(ConfigService configService)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Moonlight.App.Database.Enums;
|
using Moonlight.App.Database.Entities.Store;
|
||||||
|
using Moonlight.App.Database.Enums;
|
||||||
|
|
||||||
namespace Moonlight.App.Database.Entities.Tickets;
|
namespace Moonlight.App.Database.Entities.Tickets;
|
||||||
|
|
||||||
@@ -11,8 +12,9 @@ public class Ticket
|
|||||||
public string Tries { get; set; } = "";
|
public string Tries { get; set; } = "";
|
||||||
public TicketPriority Priority { get; set; } = TicketPriority.Low;
|
public TicketPriority Priority { get; set; } = TicketPriority.Low;
|
||||||
public bool Open { get; set; } = true;
|
public bool Open { get; set; } = true;
|
||||||
|
public Service? Service { get; set; }
|
||||||
|
|
||||||
public List<TicketMessage> Messages = new();
|
public List<TicketMessage> Messages { get; set; } = new();
|
||||||
|
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
665
Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.Designer.cs
generated
Normal file
665
Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.Designer.cs
generated
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Moonlight.App.Database;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Moonlight.App.Database.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DataContext))]
|
||||||
|
[Migration("20231101161843_AddedTicketModels")]
|
||||||
|
partial class AddedTicketModels
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "7.0.2");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AuthorId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AuthorId");
|
||||||
|
|
||||||
|
b.ToTable("Posts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("AuthorId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("PostId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AuthorId");
|
||||||
|
|
||||||
|
b.HasIndex("PostId");
|
||||||
|
|
||||||
|
b.ToTable("PostComments");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("PostId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PostId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("PostLikes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.WordFilter", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Filter")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("WordFilters");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Categories");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Amount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Percent")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Coupons");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("CouponId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CouponId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("CouponUses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCode", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Amount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<double>("Value")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("GiftCodes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("GiftCodeId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("GiftCodeId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("GiftCodeUses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("CategoryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConfigJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Duration")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("MaxPerUser")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<double>("Price")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Stock")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CategoryId");
|
||||||
|
|
||||||
|
b.ToTable("Products");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConfigJsonOverride")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Nickname")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("OwnerId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ProductId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RenewAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Suspended")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OwnerId");
|
||||||
|
|
||||||
|
b.HasIndex("ProductId");
|
||||||
|
|
||||||
|
b.ToTable("Services");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("ServiceId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ServiceId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("ServiceShares");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<double>("Price")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("UserId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Transaction");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("CreatorId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Open")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("ServiceId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Tries")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatorId");
|
||||||
|
|
||||||
|
b.HasIndex("ServiceId");
|
||||||
|
|
||||||
|
b.ToTable("Tickets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.TicketMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Attachment")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSupport")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("SenderId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("TicketId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SenderId");
|
||||||
|
|
||||||
|
b.HasIndex("TicketId");
|
||||||
|
|
||||||
|
b.ToTable("TicketMessages");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Avatar")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<double>("Balance")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Flags")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Permissions")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("TokenValidTimestamp")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("TotpKey")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "Author")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AuthorId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Author");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "Author")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AuthorId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Community.Post", null)
|
||||||
|
.WithMany("Comments")
|
||||||
|
.HasForeignKey("PostId");
|
||||||
|
|
||||||
|
b.Navigation("Author");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Community.Post", null)
|
||||||
|
.WithMany("Likes")
|
||||||
|
.HasForeignKey("PostId");
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CouponId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", null)
|
||||||
|
.WithMany("CouponUses")
|
||||||
|
.HasForeignKey("UserId");
|
||||||
|
|
||||||
|
b.Navigation("Coupon");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Store.GiftCode", "GiftCode")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("GiftCodeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", null)
|
||||||
|
.WithMany("GiftCodeUses")
|
||||||
|
.HasForeignKey("UserId");
|
||||||
|
|
||||||
|
b.Navigation("GiftCode");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CategoryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Category");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OwnerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Store.Product", "Product")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProductId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Owner");
|
||||||
|
|
||||||
|
b.Navigation("Product");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Store.Service", null)
|
||||||
|
.WithMany("Shares")
|
||||||
|
.HasForeignKey("ServiceId");
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", null)
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("UserId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "Creator")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CreatorId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Store.Service", "Service")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ServiceId");
|
||||||
|
|
||||||
|
b.Navigation("Creator");
|
||||||
|
|
||||||
|
b.Navigation("Service");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.TicketMessage", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "Sender")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SenderId");
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Tickets.Ticket", null)
|
||||||
|
.WithMany("Messages")
|
||||||
|
.HasForeignKey("TicketId");
|
||||||
|
|
||||||
|
b.Navigation("Sender");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Comments");
|
||||||
|
|
||||||
|
b.Navigation("Likes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Shares");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Messages");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CouponUses");
|
||||||
|
|
||||||
|
b.Navigation("GiftCodeUses");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Moonlight.App.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddedTicketModels : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Tickets",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
CreatorId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Tries = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Priority = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Open = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
ServiceId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Tickets", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Tickets_Services_ServiceId",
|
||||||
|
column: x => x.ServiceId,
|
||||||
|
principalTable: "Services",
|
||||||
|
principalColumn: "Id");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Tickets_Users_CreatorId",
|
||||||
|
column: x => x.CreatorId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TicketMessages",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
SenderId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
IsSupport = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
Content = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Attachment = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
TicketId = table.Column<int>(type: "INTEGER", 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");
|
||||||
|
});
|
||||||
|
|
||||||
|
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_CreatorId",
|
||||||
|
table: "Tickets",
|
||||||
|
column: "CreatorId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Tickets_ServiceId",
|
||||||
|
table: "Tickets",
|
||||||
|
column: "ServiceId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TicketMessages");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Tickets");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -357,6 +357,82 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
b.ToTable("Transaction");
|
b.ToTable("Transaction");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("CreatorId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Open")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("ServiceId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Tries")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatorId");
|
||||||
|
|
||||||
|
b.HasIndex("ServiceId");
|
||||||
|
|
||||||
|
b.ToTable("Tickets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.TicketMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Attachment")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSupport")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("SenderId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("TicketId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
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")
|
||||||
@@ -525,6 +601,36 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
.HasForeignKey("UserId");
|
.HasForeignKey("UserId");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "Creator")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CreatorId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Store.Service", "Service")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ServiceId");
|
||||||
|
|
||||||
|
b.Navigation("Creator");
|
||||||
|
|
||||||
|
b.Navigation("Service");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.TicketMessage", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.User", "Sender")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SenderId");
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Tickets.Ticket", null)
|
||||||
|
.WithMany("Messages")
|
||||||
|
.HasForeignKey("TicketId");
|
||||||
|
|
||||||
|
b.Navigation("Sender");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Comments");
|
b.Navigation("Comments");
|
||||||
@@ -537,6 +643,11 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
b.Navigation("Shares");
|
b.Navigation("Shares");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Messages");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("CouponUses");
|
b.Navigation("CouponUses");
|
||||||
|
|||||||
9
Moonlight/App/Event/Args/TicketMessageEventArgs.cs
Normal file
9
Moonlight/App/Event/Args/TicketMessageEventArgs.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Moonlight.App.Database.Entities.Tickets;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Event.Args;
|
||||||
|
|
||||||
|
public class TicketMessageEventArgs
|
||||||
|
{
|
||||||
|
public Ticket Ticket { get; set; }
|
||||||
|
public TicketMessage TicketMessage { get; set; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Moonlight.App.Database.Entities;
|
using Moonlight.App.Database.Entities;
|
||||||
using Moonlight.App.Database.Entities.Community;
|
using Moonlight.App.Database.Entities.Community;
|
||||||
using Moonlight.App.Database.Entities.Store;
|
using Moonlight.App.Database.Entities.Store;
|
||||||
|
using Moonlight.App.Database.Entities.Tickets;
|
||||||
using Moonlight.App.Event.Args;
|
using Moonlight.App.Event.Args;
|
||||||
|
|
||||||
namespace Moonlight.App.Event;
|
namespace Moonlight.App.Event;
|
||||||
@@ -19,5 +20,8 @@ public class Events
|
|||||||
public static EventHandler<Post> OnPostLiked;
|
public static EventHandler<Post> OnPostLiked;
|
||||||
public static EventHandler<PostComment> OnPostCommentCreated;
|
public static EventHandler<PostComment> OnPostCommentCreated;
|
||||||
public static EventHandler<PostComment> OnPostCommentDeleted;
|
public static EventHandler<PostComment> OnPostCommentDeleted;
|
||||||
|
public static EventHandler<Ticket> OnTicketCreated;
|
||||||
|
public static EventHandler<TicketMessageEventArgs> OnTicketMessage;
|
||||||
|
public static EventHandler<Ticket> OnTicketUpdated;
|
||||||
public static EventHandler OnMoonlightRestart;
|
public static EventHandler OnMoonlightRestart;
|
||||||
}
|
}
|
||||||
24
Moonlight/App/Models/Forms/Ticketing/CreateTicketForm.cs
Normal file
24
Moonlight/App/Models/Forms/Ticketing/CreateTicketForm.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Moonlight.App.Database.Entities.Store;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Models.Forms.Ticketing;
|
||||||
|
|
||||||
|
public class CreateTicketForm
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "You need to enter a ticket name")]
|
||||||
|
[MinLength(8, ErrorMessage = "The title needs to be longer then 8 characters")]
|
||||||
|
[MaxLength(64, ErrorMessage = "The ticket name should not exceed 64 characters in lenght")]
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to enter a description")]
|
||||||
|
[MinLength(8, ErrorMessage = "The description needs to be longer then 8 characters")]
|
||||||
|
[MaxLength(256, ErrorMessage = "The description should not exceed 256 characters in lenght")]
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify what you have tried already")]
|
||||||
|
[MinLength(8, ErrorMessage = "The tries description needs to be longer then 8 characters")]
|
||||||
|
[MaxLength(256, ErrorMessage = "The tries description should not exceed 256 characters in lenght")]
|
||||||
|
public string Tries { get; set; } = "";
|
||||||
|
|
||||||
|
public Service? Service { get; set; }
|
||||||
|
}
|
||||||
@@ -63,4 +63,22 @@ public class BucketService
|
|||||||
else
|
else
|
||||||
throw new FileNotFoundException();
|
throw new FileNotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task Delete(string bucket, string file, bool ignoreNotFound = false)
|
||||||
|
{
|
||||||
|
var filePath = PathBuilder.File(BasePath, bucket, file);
|
||||||
|
|
||||||
|
if (File.Exists(filePath))
|
||||||
|
{
|
||||||
|
File.Delete(filePath);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This section will only be reached if the file does not exist
|
||||||
|
|
||||||
|
if (!ignoreNotFound)
|
||||||
|
throw new FileNotFoundException();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
179
Moonlight/App/Services/Ticketing/TicketChatService.cs
Normal file
179
Moonlight/App/Services/Ticketing/TicketChatService.cs
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.App.Database.Entities.Tickets;
|
||||||
|
using Moonlight.App.Database.Enums;
|
||||||
|
using Moonlight.App.Event;
|
||||||
|
using Moonlight.App.Event.Args;
|
||||||
|
using Moonlight.App.Extensions;
|
||||||
|
using Moonlight.App.Repositories;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Ticketing;
|
||||||
|
|
||||||
|
public class TicketChatService
|
||||||
|
{
|
||||||
|
private readonly IdentityService IdentityService;
|
||||||
|
private readonly Repository<Ticket> TicketRepository;
|
||||||
|
private readonly BucketService BucketService;
|
||||||
|
private readonly List<TicketMessage> MessageCache = new();
|
||||||
|
|
||||||
|
public Ticket Ticket;
|
||||||
|
public bool IsSupporter;
|
||||||
|
public Func<Task>? OnUpdate;
|
||||||
|
public TicketMessage[] Messages => MessageCache.ToArray();
|
||||||
|
|
||||||
|
public TicketChatService(
|
||||||
|
IdentityService identityService,
|
||||||
|
Repository<Ticket> ticketRepository,
|
||||||
|
BucketService bucketService)
|
||||||
|
{
|
||||||
|
IdentityService = identityService;
|
||||||
|
TicketRepository = ticketRepository;
|
||||||
|
BucketService = bucketService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Start(Ticket ticket, bool isSupporter = false)
|
||||||
|
{
|
||||||
|
IsSupporter = isSupporter;
|
||||||
|
|
||||||
|
// Load data into local cache
|
||||||
|
Ticket = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Messages)
|
||||||
|
.Include(x => x.Creator)
|
||||||
|
.Include(x => x.Service)
|
||||||
|
.First(x => x.Id == ticket.Id);
|
||||||
|
|
||||||
|
MessageCache.AddRange(Ticket.Messages);
|
||||||
|
|
||||||
|
// Register event handlers
|
||||||
|
Events.OnTicketMessage += OnTicketMessage;
|
||||||
|
Events.OnTicketUpdated += OnTicketUpdated;
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Update(bool open, TicketPriority priority) // Updated and syncs ticket states to all listeners
|
||||||
|
{
|
||||||
|
if (Ticket.Open != open)
|
||||||
|
{
|
||||||
|
Ticket.Open = open;
|
||||||
|
|
||||||
|
if(open)
|
||||||
|
await SendSystemMessage("Ticket has been opened");
|
||||||
|
else
|
||||||
|
await SendSystemMessage("Ticket has been closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Ticket.Priority != priority)
|
||||||
|
{
|
||||||
|
Ticket.Priority = priority;
|
||||||
|
|
||||||
|
await SendSystemMessage($"Ticket priority to {priority}");
|
||||||
|
}
|
||||||
|
|
||||||
|
TicketRepository.Update(Ticket);
|
||||||
|
|
||||||
|
await Events.OnTicketUpdated.InvokeAsync(Ticket);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Stop() // Clear cache and stop listeners
|
||||||
|
{
|
||||||
|
Events.OnTicketMessage -= OnTicketMessage;
|
||||||
|
Events.OnTicketUpdated -= OnTicketUpdated;
|
||||||
|
|
||||||
|
MessageCache.Clear();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Sending
|
||||||
|
|
||||||
|
public async Task SendSystemMessage(string content) // use this to send a message shown in a seperator
|
||||||
|
{
|
||||||
|
// Build the message model
|
||||||
|
var message = new TicketMessage()
|
||||||
|
{
|
||||||
|
Content = content,
|
||||||
|
Attachment = null,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Sender = null,
|
||||||
|
IsSupport = IsSupporter
|
||||||
|
};
|
||||||
|
|
||||||
|
await SyncMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendMessage(string content, Stream? attachmentStream = null, string? attachmentName = null) // Regular send method
|
||||||
|
{
|
||||||
|
if(string.IsNullOrEmpty(content))
|
||||||
|
return;
|
||||||
|
|
||||||
|
string? attachmentBucketName = null;
|
||||||
|
|
||||||
|
// Check and download attachments
|
||||||
|
if (attachmentStream != null && attachmentName != null)
|
||||||
|
{
|
||||||
|
attachmentBucketName = await BucketService.Store(
|
||||||
|
"ticketAttachments",
|
||||||
|
attachmentStream,
|
||||||
|
attachmentName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the message model
|
||||||
|
var message = new TicketMessage()
|
||||||
|
{
|
||||||
|
Content = content,
|
||||||
|
Attachment = attachmentBucketName,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Sender = IdentityService.CurrentUser,
|
||||||
|
IsSupport = IsSupporter
|
||||||
|
};
|
||||||
|
|
||||||
|
await SyncMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SyncMessage(TicketMessage message) // Use this function to save and sync function to others
|
||||||
|
{
|
||||||
|
// Save ticket to the db
|
||||||
|
var t = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.First(x => x.Id == Ticket.Id); // We do this to get a clean reference
|
||||||
|
|
||||||
|
t.Messages.Add(message);
|
||||||
|
TicketRepository.Update(t);
|
||||||
|
|
||||||
|
// Now emit the events
|
||||||
|
await Events.OnTicketMessage.InvokeAsync(new()
|
||||||
|
{
|
||||||
|
Ticket = t, // We use this reference as it has less data attached to it
|
||||||
|
TicketMessage = message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
private async void OnTicketUpdated(object? _, Ticket ticket)
|
||||||
|
{
|
||||||
|
if(Ticket.Id != ticket.Id) // Only listen to our ticket
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Update the possible values
|
||||||
|
Ticket.Open = ticket.Open;
|
||||||
|
Ticket.Priority = ticket.Priority;
|
||||||
|
|
||||||
|
if (OnUpdate != null)
|
||||||
|
await OnUpdate.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnTicketMessage(object? _, TicketMessageEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
if(Ticket.Id != eventArgs.Ticket.Id) // Only listen to our ticket
|
||||||
|
return;
|
||||||
|
|
||||||
|
MessageCache.Add(eventArgs.TicketMessage);
|
||||||
|
|
||||||
|
if (OnUpdate != null)
|
||||||
|
await OnUpdate.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Moonlight/App/Services/Ticketing/TicketCreateService.cs
Normal file
41
Moonlight/App/Services/Ticketing/TicketCreateService.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using Moonlight.App.Database.Entities.Store;
|
||||||
|
using Moonlight.App.Database.Entities.Tickets;
|
||||||
|
using Moonlight.App.Database.Enums;
|
||||||
|
using Moonlight.App.Event;
|
||||||
|
using Moonlight.App.Extensions;
|
||||||
|
using Moonlight.App.Repositories;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Ticketing;
|
||||||
|
|
||||||
|
public class TicketCreateService
|
||||||
|
{
|
||||||
|
private readonly Repository<Ticket> TicketRepository;
|
||||||
|
private readonly IdentityService IdentityService;
|
||||||
|
|
||||||
|
public TicketCreateService(Repository<Ticket> ticketRepository, IdentityService identityService)
|
||||||
|
{
|
||||||
|
TicketRepository = ticketRepository;
|
||||||
|
IdentityService = identityService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Ticket> Perform(string name, string description, string tries, Service? service)
|
||||||
|
{
|
||||||
|
var ticket = new Ticket()
|
||||||
|
{
|
||||||
|
Creator = IdentityService.CurrentUser,
|
||||||
|
Service = service,
|
||||||
|
Description = description,
|
||||||
|
Tries = tries,
|
||||||
|
Open = true,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Name = name,
|
||||||
|
Priority = TicketPriority.Low
|
||||||
|
};
|
||||||
|
|
||||||
|
var finalTicket = TicketRepository.Add(ticket);
|
||||||
|
|
||||||
|
await Events.OnTicketCreated.InvokeAsync(finalTicket);
|
||||||
|
|
||||||
|
return finalTicket;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Moonlight/App/Services/Ticketing/TicketService.cs
Normal file
14
Moonlight/App/Services/Ticketing/TicketService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Moonlight.App.Services.Ticketing;
|
||||||
|
|
||||||
|
public class TicketService
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider ServiceProvider;
|
||||||
|
|
||||||
|
public TicketChatService Chat => ServiceProvider.GetRequiredService<TicketChatService>();
|
||||||
|
public TicketCreateService Create => ServiceProvider.GetRequiredService<TicketCreateService>();
|
||||||
|
|
||||||
|
public TicketService(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
ServiceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using Moonlight.App.Database.Entities;
|
using Moonlight.App.Database.Entities;
|
||||||
using Moonlight.App.Database.Entities.Community;
|
using Moonlight.App.Database.Entities.Community;
|
||||||
using Moonlight.App.Database.Entities.Store;
|
using Moonlight.App.Database.Entities.Store;
|
||||||
|
using Moonlight.App.Database.Entities.Tickets;
|
||||||
using Moonlight.App.Repositories;
|
using Moonlight.App.Repositories;
|
||||||
using Moonlight.App.Services.Community;
|
using Moonlight.App.Services.Community;
|
||||||
using Moonlight.App.Services.ServiceManage;
|
using Moonlight.App.Services.ServiceManage;
|
||||||
@@ -16,6 +17,8 @@ public class UserDeleteService
|
|||||||
private readonly Repository<Transaction> TransactionRepository;
|
private readonly Repository<Transaction> TransactionRepository;
|
||||||
private readonly Repository<CouponUse> CouponUseRepository;
|
private readonly Repository<CouponUse> CouponUseRepository;
|
||||||
private readonly Repository<GiftCodeUse> GiftCodeUseRepository;
|
private readonly Repository<GiftCodeUse> GiftCodeUseRepository;
|
||||||
|
private readonly Repository<Ticket> TicketRepository;
|
||||||
|
private readonly Repository<TicketMessage> TicketMessageRepository;
|
||||||
private readonly ServiceService ServiceService;
|
private readonly ServiceService ServiceService;
|
||||||
private readonly PostService PostService;
|
private readonly PostService PostService;
|
||||||
|
|
||||||
@@ -27,7 +30,9 @@ public class UserDeleteService
|
|||||||
Repository<User> userRepository,
|
Repository<User> userRepository,
|
||||||
Repository<GiftCodeUse> giftCodeUseRepository,
|
Repository<GiftCodeUse> giftCodeUseRepository,
|
||||||
Repository<CouponUse> couponUseRepository,
|
Repository<CouponUse> couponUseRepository,
|
||||||
Repository<Transaction> transactionRepository)
|
Repository<Transaction> transactionRepository,
|
||||||
|
Repository<Ticket> ticketRepository,
|
||||||
|
Repository<TicketMessage> ticketMessageRepository)
|
||||||
{
|
{
|
||||||
ServiceRepository = serviceRepository;
|
ServiceRepository = serviceRepository;
|
||||||
ServiceService = serviceService;
|
ServiceService = serviceService;
|
||||||
@@ -37,6 +42,8 @@ public class UserDeleteService
|
|||||||
GiftCodeUseRepository = giftCodeUseRepository;
|
GiftCodeUseRepository = giftCodeUseRepository;
|
||||||
CouponUseRepository = couponUseRepository;
|
CouponUseRepository = couponUseRepository;
|
||||||
TransactionRepository = transactionRepository;
|
TransactionRepository = transactionRepository;
|
||||||
|
TicketRepository = ticketRepository;
|
||||||
|
TicketMessageRepository = ticketMessageRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Perform(User user)
|
public async Task Perform(User user)
|
||||||
@@ -103,6 +110,42 @@ public class UserDeleteService
|
|||||||
foreach (var transaction in transactions)
|
foreach (var transaction in transactions)
|
||||||
TransactionRepository.Delete(transaction);
|
TransactionRepository.Delete(transaction);
|
||||||
|
|
||||||
|
// Tickets and ticket messages
|
||||||
|
|
||||||
|
// First we need to fetch every message this user has sent and delete it as admin accounts can have messages
|
||||||
|
// in tickets they dont own
|
||||||
|
var messagesFromUser = TicketMessageRepository
|
||||||
|
.Get()
|
||||||
|
.Where(x => x.Sender.Id == user.Id)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var message in messagesFromUser)
|
||||||
|
{
|
||||||
|
TicketMessageRepository.Delete(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we can only delete the tickets the user actually owns
|
||||||
|
var tickets = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Messages)
|
||||||
|
.Where(x => x.Creator.Id == user.Id)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var ticket in tickets)
|
||||||
|
{
|
||||||
|
var messages = ticket.Messages.ToArray(); // Cache message models
|
||||||
|
|
||||||
|
ticket.Messages.Clear();
|
||||||
|
TicketRepository.Update(ticket);
|
||||||
|
|
||||||
|
foreach (var ticketMessage in messages)
|
||||||
|
{
|
||||||
|
TicketMessageRepository.Delete(ticketMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
TicketRepository.Delete(ticket);
|
||||||
|
}
|
||||||
|
|
||||||
// User
|
// User
|
||||||
|
|
||||||
// We need to use this in order to entity framework not crashing because of the previous deleted data
|
// We need to use this in order to entity framework not crashing because of the previous deleted data
|
||||||
|
|||||||
@@ -16,17 +16,25 @@ public class UserDetailsService
|
|||||||
|
|
||||||
public async Task UpdateAvatar(User user, Stream stream, string fileName)
|
public async Task UpdateAvatar(User user, Stream stream, string fileName)
|
||||||
{
|
{
|
||||||
|
if (user.Avatar != null)
|
||||||
|
{
|
||||||
|
await BucketService.Delete("avatars", user.Avatar, true);
|
||||||
|
}
|
||||||
|
|
||||||
var file = await BucketService.Store("avatars", stream, fileName);
|
var file = await BucketService.Store("avatars", stream, fileName);
|
||||||
|
|
||||||
user.Avatar = file;
|
user.Avatar = file;
|
||||||
UserRepository.Update(user);
|
UserRepository.Update(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task UpdateAvatar(User user) // Overload to reset avatar
|
public async Task UpdateAvatar(User user) // Overload to reset avatar
|
||||||
{
|
{
|
||||||
|
if (user.Avatar != null)
|
||||||
|
{
|
||||||
|
await BucketService.Delete("avatars", user.Avatar, true);
|
||||||
|
}
|
||||||
|
|
||||||
user.Avatar = null;
|
user.Avatar = null;
|
||||||
UserRepository.Update(user);
|
UserRepository.Update(user);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,9 +44,4 @@
|
|||||||
<PackageReference Include="Serilog" Version="3.1.0-dev-02078" />
|
<PackageReference Include="Serilog" Version="3.1.0-dev-02078" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0-dev-00923" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0-dev-00923" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<_ContentIncludedByDefault Remove="storage\config.json" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Moonlight</title>
|
<title>Moonlight</title>
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/img/logo.svg">
|
||||||
|
|
||||||
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered"/>
|
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered"/>
|
||||||
|
|
||||||
<link href="/css/theme.css" rel="stylesheet" type="text/css"/>
|
<link href="/css/theme.css" rel="stylesheet" type="text/css"/>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using Moonlight.App.Services.Community;
|
|||||||
using Moonlight.App.Services.Interop;
|
using Moonlight.App.Services.Interop;
|
||||||
using Moonlight.App.Services.ServiceManage;
|
using Moonlight.App.Services.ServiceManage;
|
||||||
using Moonlight.App.Services.Store;
|
using Moonlight.App.Services.Store;
|
||||||
|
using Moonlight.App.Services.Ticketing;
|
||||||
using Moonlight.App.Services.Sys;
|
using Moonlight.App.Services.Sys;
|
||||||
using Moonlight.App.Services.Users;
|
using Moonlight.App.Services.Users;
|
||||||
using Moonlight.App.Services.Utils;
|
using Moonlight.App.Services.Utils;
|
||||||
@@ -26,6 +27,7 @@ Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
|
|||||||
var logConfig = new LoggerConfiguration();
|
var logConfig = new LoggerConfiguration();
|
||||||
|
|
||||||
logConfig = logConfig.Enrich.FromLogContext()
|
logConfig = logConfig.Enrich.FromLogContext()
|
||||||
|
.MinimumLevel.Debug()
|
||||||
.WriteTo.Console(
|
.WriteTo.Console(
|
||||||
outputTemplate:
|
outputTemplate:
|
||||||
"{Timestamp:HH:mm:ss} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}");
|
"{Timestamp:HH:mm:ss} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}");
|
||||||
@@ -79,6 +81,11 @@ builder.Services.AddSingleton<AutoMailSendService>();
|
|||||||
builder.Services.AddScoped<ServiceService>();
|
builder.Services.AddScoped<ServiceService>();
|
||||||
builder.Services.AddSingleton<ServiceAdminService>();
|
builder.Services.AddSingleton<ServiceAdminService>();
|
||||||
|
|
||||||
|
// Services / Ticketing
|
||||||
|
builder.Services.AddScoped<TicketService>();
|
||||||
|
builder.Services.AddScoped<TicketChatService>();
|
||||||
|
builder.Services.AddScoped<TicketCreateService>();
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
builder.Services.AddScoped<IdentityService>();
|
builder.Services.AddScoped<IdentityService>();
|
||||||
builder.Services.AddSingleton(configService);
|
builder.Services.AddSingleton(configService);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<WButton OnClick="Send" Text="Continue" CssClasses="btn btn-primary me-2 flex-shrink-0" />
|
<WButton OnClick="Send" Text="Send verification email" CssClasses="btn btn-primary me-2 flex-shrink-0" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
56
Moonlight/Shared/Components/Forms/ChatFileSelect.razor
Normal file
56
Moonlight/Shared/Components/Forms/ChatFileSelect.razor
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
|
@{
|
||||||
|
var id = $"fileUpload{GetHashCode()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
<InputFile OnChange="OnFileChanged" type="file" id="@id" hidden=""/>
|
||||||
|
@if (SelectedFile != null)
|
||||||
|
{
|
||||||
|
<button @onclick="RemoveSelection" class="btn btn-icon btn-bg-light btn-color-danger rounded-start rounded-end">
|
||||||
|
<i class="bx bx-sm bx-x"></i>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<label for="@id" class="btn btn-icon btn-bg-light btn-color-primary rounded-start rounded-end">
|
||||||
|
<i class="bx bx-sm bx-upload"></i>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public IBrowserFile? SelectedFile { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int MaxFileSize { get; set; } = 1024 * 1024 * 5;
|
||||||
|
|
||||||
|
private async Task OnFileChanged(InputFileChangeEventArgs arg)
|
||||||
|
{
|
||||||
|
if (arg.FileCount > 0)
|
||||||
|
{
|
||||||
|
if (arg.File.Size < MaxFileSize)
|
||||||
|
{
|
||||||
|
SelectedFile = arg.File;
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ToastService.Danger($"The uploaded file should not be bigger than {Formatter.FormatSize(MaxFileSize)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedFile = null;
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveSelection()
|
||||||
|
{
|
||||||
|
SelectedFile = null;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
{
|
{
|
||||||
if (arg.FileCount > 0)
|
if (arg.FileCount > 0)
|
||||||
{
|
{
|
||||||
if (arg.File.Size < 1024 * 1024 * 5)
|
if (arg.File.Size < MaxFileSize)
|
||||||
{
|
{
|
||||||
SelectedFile = arg.File;
|
SelectedFile = arg.File;
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ToastService.Danger("The uploaded file should not be bigger than 5MB");
|
await ToastService.Danger($"The uploaded file should not be bigger than {Formatter.FormatSize(MaxFileSize)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectedFile = null;
|
SelectedFile = null;
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
<div class="mb-4 me-4 d-flex justify-content-end fixed-bottom" style="pointer-events: none;">
|
|
||||||
@if (ViewIndex == 1)
|
|
||||||
{
|
|
||||||
<div class="card border border-2 border-primary" style="pointer-events: all">
|
|
||||||
<div class="card-header">
|
|
||||||
<span class="card-title fs-5">Chats</span>
|
|
||||||
<div class="card-toolbar">
|
|
||||||
<button @onclick="() => SetViewIndex(0)" class="btn btn-rounded-circle btn-icon">
|
|
||||||
<i class="bx bx-sm bx-x"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body pt-5">
|
|
||||||
<div class="scroll-y me-n5 pe-5" style="height: 50vh; width: 40vh; display: flex; flex-direction: column;">
|
|
||||||
<div class="d-flex flex-stack py-4 justify-content-center">
|
|
||||||
<button @onclick="() => SetViewIndex(3)" class="btn btn-success">New ticket</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a href="#" @onclick="() => SetViewIndex(2)" class="d-flex flex-stack py-4">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="symbol symbol-45px symbol-circle ">
|
|
||||||
<span class="symbol-label bg-light-danger text-danger fs-6 fw-bolder ">M</span>
|
|
||||||
</div>
|
|
||||||
<div class="ms-5">
|
|
||||||
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary mb-2">Melody Macy</a>
|
|
||||||
<div class="fw-semibold text-muted">melody@altbox.com</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-column align-items-end ms-2">
|
|
||||||
<span class="text-muted fs-7 mb-1">2 weeks</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div class="separator separator-dashed d-none"></div>
|
|
||||||
<div class="d-flex flex-stack py-4">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="symbol symbol-45px symbol-circle ">
|
|
||||||
<img alt="Pic" src="/metronic8/demo38/assets/media/avatars/300-1.jpg">
|
|
||||||
</div>
|
|
||||||
<div class="ms-5">
|
|
||||||
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary mb-2">Max Smith</a>
|
|
||||||
<div class="fw-semibold text-muted">max@kt.com</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-column align-items-end ms-2">
|
|
||||||
<span class="text-muted fs-7 mb-1">1 day</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else if (ViewIndex == 2)
|
|
||||||
{
|
|
||||||
<div class="card border border-2 border-primary" style="pointer-events: all">
|
|
||||||
<div class="card-header">
|
|
||||||
<span class="card-title fs-5">Some chat</span>
|
|
||||||
<div class="card-toolbar">
|
|
||||||
<button @onclick="() => SetViewIndex(1)" class="btn btn-rounded-circle btn-icon">
|
|
||||||
<i class="bx bx-sm bx-chevron-left"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body bg-black">
|
|
||||||
<div class="scroll-y me-n5 pe-5" style="height: 50vh; width: 40vh; display: flex; flex-direction: column-reverse;">
|
|
||||||
@for (int i = 0; i < 9; i++)
|
|
||||||
{
|
|
||||||
<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="Pic" src="/metronic8/demo38/assets/media/avatars/300-25.jpg">
|
|
||||||
</div>
|
|
||||||
<div class="ms-3">
|
|
||||||
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">Brian Cox</a>
|
|
||||||
<span class="text-muted fs-7 mb-1">2 mins</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start" data-kt-element="message-text">
|
|
||||||
How likely are you to recommend our company to your friends and family ?
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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">5 mins</span>
|
|
||||||
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">You</a>
|
|
||||||
</div>
|
|
||||||
<div class="symbol symbol-35px symbol-circle ">
|
|
||||||
<img alt="Pic" src="/metronic8/demo38/assets/media/avatars/300-1.jpg">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end" data-kt-element="message-text">
|
|
||||||
Hey there, we’re just writing to let you know that you’ve been subscribed to a repository on GitHub.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<div class="input-group">
|
|
||||||
<input class="form-control" placeholder="Type a message"/>
|
|
||||||
<button class="btn btn-secondary">Send</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else if (ViewIndex == 3)
|
|
||||||
{
|
|
||||||
<div class="card border border-2 border-primary p-5" style="pointer-events: all">
|
|
||||||
<div class="card-header">
|
|
||||||
<span class="card-title fs-5">Create ticket</span>
|
|
||||||
<div class="card-toolbar">
|
|
||||||
<button @onclick="() => SetViewIndex(1)" class="btn btn-rounded-circle btn-icon">
|
|
||||||
<i class="bx bx-sm bx-chevron-left"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" style="width: 40vh;">
|
|
||||||
<div class="mb-5">
|
|
||||||
<label class="form-label">Name</label>
|
|
||||||
<input class="form-control" type="text"/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-5">
|
|
||||||
<label class="form-label">Description</label>
|
|
||||||
<textarea class="form-control"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="mb-5">
|
|
||||||
<label class="form-label">Tries</label>
|
|
||||||
<textarea class="form-control"></textarea>
|
|
||||||
</div>
|
|
||||||
<select class="mb-5 form-select">
|
|
||||||
<option>None</option>
|
|
||||||
<option>LOL</option>
|
|
||||||
<option>NEIN</option>
|
|
||||||
<option>OOF</option>
|
|
||||||
</select>
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
<button class="btn btn-primary" type="submit">Create</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<button @onclick="() => SetViewIndex(1)" class="btn btn-lg btn-icon btn-rounded-circle btn-white border border-primary" style="pointer-events: all">
|
|
||||||
<i class="bx bg-lg bx-chat"></i>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code
|
|
||||||
{
|
|
||||||
private int ViewIndex = 0;
|
|
||||||
|
|
||||||
private async Task SetViewIndex(int index)
|
|
||||||
{
|
|
||||||
ViewIndex = index;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
<a href="#" @onclick="Layout.ToggleMobileSidebar" @onclick:preventDefault class="btn btn-icon btn-active-color-primary w-35px h-35px me-2">
|
<a href="#" @onclick="Layout.ToggleMobileSidebar" @onclick:preventDefault class="btn btn-icon btn-active-color-primary w-35px h-35px me-2">
|
||||||
<i class="bx bx-sm bx-menu"></i>
|
<i class="bx bx-sm bx-menu"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="/metronic8/demo38/../demo38/index.html">
|
<a href="/">
|
||||||
<img alt="Logo" src="/metronic8/demo38/assets/media/logos/demo38-small.svg" class="h-30px">
|
<img alt="Logo" src="/img/logo.svg" class="h-30px">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="app-navbar flex-lg-grow-1">
|
<div class="app-navbar flex-lg-grow-1">
|
||||||
|
|||||||
@@ -106,6 +106,17 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-item">
|
||||||
|
<a class="menu-link " href="/admin/tickets">
|
||||||
|
<span class="menu-icon">
|
||||||
|
<i class="bx bx-sm bx-support"></i>
|
||||||
|
</span>
|
||||||
|
<span class="menu-title">
|
||||||
|
Tickets
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="menu-item">
|
<div class="menu-item">
|
||||||
<a class="menu-link " href="/admin/sys">
|
<a class="menu-link " href="/admin/sys">
|
||||||
<span class="menu-icon">
|
<span class="menu-icon">
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
@using Moonlight.App.Services
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.App.Database.Entities.Store
|
||||||
|
@using Moonlight.App.Models.Forms.Ticketing
|
||||||
|
@using Moonlight.App.Services.Ticketing
|
||||||
|
|
||||||
|
@inject IdentityService IdentityService
|
||||||
|
@inject Repository<Service> ServiceRepository
|
||||||
|
@inject TicketService TicketService
|
||||||
|
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title fs-5">Create a new ticket</span>
|
||||||
|
<div class="card-toolbar">
|
||||||
|
<button @onclick="() => TicketPopupMain.SetViewIndex(1)" class="btn btn-rounded-circle btn-icon">
|
||||||
|
<i class="bx bx-sm bx-chevron-left"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" style="width: 45vh; overflow-y: scroll">
|
||||||
|
<LazyLoader Load="LoadServices">
|
||||||
|
<SmartForm Model="Form" OnValidSubmit="CreateTicket">
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="form-label">Name</label>
|
||||||
|
<input @bind="Form.Name" class="form-control" type="text"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="form-label">Description</label>
|
||||||
|
<textarea @bind="Form.Description" class="form-control"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="form-label">Tries</label>
|
||||||
|
<textarea @bind="Form.Tries" class="form-control"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="form-label">Service</label>
|
||||||
|
<SmartSelect TField="Service"
|
||||||
|
@bind-Value="Form.Service"
|
||||||
|
Items="Services"
|
||||||
|
DisplayField="@(x => x.Nickname ?? $"Service {x.Id}")"
|
||||||
|
CanBeNull="true"/>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<button class="btn btn-primary" type="submit">Create</button>
|
||||||
|
</div>
|
||||||
|
</SmartForm>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[CascadingParameter]
|
||||||
|
public TicketPopupMain TicketPopupMain { get; set; }
|
||||||
|
|
||||||
|
private Service[] Services;
|
||||||
|
private CreateTicketForm Form = new();
|
||||||
|
|
||||||
|
private async Task LoadServices(LazyLoader lazyLoader)
|
||||||
|
{
|
||||||
|
await lazyLoader.SetText("Loading your services");
|
||||||
|
|
||||||
|
Services = ServiceRepository
|
||||||
|
.Get()
|
||||||
|
.Where(x => x.Owner.Id == IdentityService.CurrentUser.Id)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateTicket()
|
||||||
|
{
|
||||||
|
// Prevent some annoying users
|
||||||
|
if (Form.Description.Trim().ToLower() == Form.Tries.Trim().ToLower())
|
||||||
|
throw new DisplayException("Please fill out the form correctly");
|
||||||
|
|
||||||
|
var ticket = await TicketService.Create.Perform(
|
||||||
|
Form.Name,
|
||||||
|
Form.Description,
|
||||||
|
Form.Tries,
|
||||||
|
Form.Service
|
||||||
|
);
|
||||||
|
|
||||||
|
await TicketPopupMain.OpenTicket(ticket);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
@using Moonlight.App.Database.Entities.Tickets
|
||||||
|
<div class="mb-4 me-4 d-flex justify-content-end fixed-bottom" style="pointer-events: none;">
|
||||||
|
<CascadingValue Value="this">
|
||||||
|
@if (ViewIndex == 0)
|
||||||
|
{
|
||||||
|
<button @onclick="() => SetViewIndex(1)" class="btn btn-lg btn-icon btn-rounded-circle btn-white border border-warning" style="pointer-events: all">
|
||||||
|
<i class="bx bg-lg bx-chat"></i>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="card border border-2 border-warning" style="pointer-events: all; height: 70vh">
|
||||||
|
@if (ViewIndex == 1)
|
||||||
|
{
|
||||||
|
<TicketPopupOverview />
|
||||||
|
}
|
||||||
|
else if (ViewIndex == 2)
|
||||||
|
{
|
||||||
|
<TicketPopupView />
|
||||||
|
}
|
||||||
|
else if (ViewIndex == 3)
|
||||||
|
{
|
||||||
|
<TicketPopupCreate />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</CascadingValue>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private int ViewIndex = 0;
|
||||||
|
|
||||||
|
public Ticket CurrentTicket { get; private set; }
|
||||||
|
|
||||||
|
public async Task SetViewIndex(int index)
|
||||||
|
{
|
||||||
|
ViewIndex = index;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OpenTicket(Ticket ticket)
|
||||||
|
{
|
||||||
|
CurrentTicket = ticket;
|
||||||
|
await SetViewIndex(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
@using Moonlight.App.Services.Ticketing
|
||||||
|
@using Moonlight.App.Database.Entities.Tickets
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using Moonlight.App.Database.Enums
|
||||||
|
@using Moonlight.App.Services
|
||||||
|
|
||||||
|
@inject Repository<Ticket> TicketRepository
|
||||||
|
@inject IdentityService IdentityService
|
||||||
|
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title fs-5">Your tickets</span>
|
||||||
|
<div class="card-toolbar">
|
||||||
|
<button @onclick="() => TicketPopupMain.SetViewIndex(0)" class="btn btn-rounded-circle btn-icon">
|
||||||
|
<i class="bx bx-sm bx-x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-5">
|
||||||
|
<div class="scroll-y me-n5 pe-5" style="height: 50vh; width: 40vh; display: flex; flex-direction: column;">
|
||||||
|
<div class="d-flex flex-stack py-2 justify-content-center">
|
||||||
|
<h3 class="align-middle text-center">Need help? Create a <a @onclick="() => TicketPopupMain.SetViewIndex(3)" @onclick:preventDefault href="#" class="text-primary">ticket</a></h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-stack py-4">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LazyLoader Load="LoadTickets">
|
||||||
|
@if (Tickets.Any())
|
||||||
|
{
|
||||||
|
foreach (var ticket in Tickets)
|
||||||
|
{
|
||||||
|
<a href="#" @onclick="() => TicketPopupMain.OpenTicket(ticket)" @onclick:preventDefault class="d-flex flex-stack py-4">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div>
|
||||||
|
<a href="#" class="fs-5 fw-bold text-gray-900 text-hover-primary mb-2">@(ticket.Name)</a>
|
||||||
|
<div class="fw-semibold text-muted">@(ticket.Description.Length > 100 ? string.Concat(ticket.Description.Take(97)) : ticket.Description)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column align-items-end ms-2">
|
||||||
|
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(ticket.CreatedAt))</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="text-center text-muted fs-5">No open tickets found</div>
|
||||||
|
}
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[CascadingParameter]
|
||||||
|
public TicketPopupMain TicketPopupMain { get; set; }
|
||||||
|
|
||||||
|
private Ticket[] Tickets;
|
||||||
|
|
||||||
|
private Task LoadTickets(LazyLoader _)
|
||||||
|
{
|
||||||
|
Tickets = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Where(x => x.Creator.Id == IdentityService.CurrentUser.Id)
|
||||||
|
.Where(x => x.Open)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
135
Moonlight/Shared/Components/TicketPopup/TicketPopupView.razor
Normal file
135
Moonlight/Shared/Components/TicketPopup/TicketPopupView.razor
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
@using Moonlight.App.Services.Ticketing
|
||||||
|
@using System.Text.RegularExpressions
|
||||||
|
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
@inject TicketService TicketService
|
||||||
|
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title fs-5">@(HasStarted ? TicketService.Chat.Ticket.Name : "Loading")</span>
|
||||||
|
<div class="card-toolbar">
|
||||||
|
<button @onclick="() => TicketPopupMain.SetViewIndex(1)" class="btn btn-rounded-circle btn-icon">
|
||||||
|
<i class="bx bx-sm bx-chevron-left"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body bg-black py-0">
|
||||||
|
<LazyLoader Load="Load">
|
||||||
|
<div class="scroll-y me-n5 pe-5" style="height: 50vh; width: 40vh; display: flex; flex-direction: column-reverse;">
|
||||||
|
@foreach (var message in TicketService.Chat.Messages.OrderByDescending(x => x.CreatedAt))
|
||||||
|
{
|
||||||
|
var orientation = message.IsSupport ? "start" : "end";
|
||||||
|
|
||||||
|
@if (message.Sender != null)
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-@(orientation) mb-10 ">
|
||||||
|
<div class="d-flex flex-column align-items-@(orientation)">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<div class="symbol symbol-35px symbol-circle ">
|
||||||
|
<img alt="Avatar" src="/api/bucket/avatars/@(message.Sender.Avatar)">
|
||||||
|
</div>
|
||||||
|
<div class="ms-3">
|
||||||
|
<div class="fs-5 fw-bold text-gray-900 me-1">@(message.Sender.Username)</div>
|
||||||
|
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt))</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 rounded bg-light-@(message.IsSupport ? "info" : "primary") text-dark fw-semibold mw-lg-400px text-@(orientation)">
|
||||||
|
@(Formatter.FormatLineBreaks(message.Content))
|
||||||
|
|
||||||
|
@if (message.Attachment != null)
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="/api/bucket/ticketAttachments/@(message.Attachment)" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="/api/bucket/ticketAttachments/@(message.Attachment)" target="_blank" class="btn btn-secondary">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="separator separator-content my-15">@(message.Content)</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="row">
|
||||||
|
<div class="input-group">
|
||||||
|
<textarea @bind="MessageContent" class="form-control form-control-solid-bg rounded-end me-3" placeholder="Type a message" style="height: 1vh"></textarea>
|
||||||
|
<ChatFileSelect @ref="FileSelect"/>
|
||||||
|
<WButton OnClick="SendMessage" CssClasses="ms-2 btn btn-icon btn-bg-light btn-color-white">
|
||||||
|
<i class="bx bx-sm bx-send"></i>
|
||||||
|
</WButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[CascadingParameter]
|
||||||
|
public TicketPopupMain TicketPopupMain { get; set; }
|
||||||
|
|
||||||
|
private bool HasStarted = false;
|
||||||
|
|
||||||
|
private ChatFileSelect FileSelect;
|
||||||
|
private string MessageContent = "";
|
||||||
|
|
||||||
|
private async Task Load(LazyLoader lazyLoader)
|
||||||
|
{
|
||||||
|
await lazyLoader.SetText("Starting chat client");
|
||||||
|
|
||||||
|
// Initialize chat service and start it
|
||||||
|
TicketService.Chat.OnUpdate = OnUpdate;
|
||||||
|
await TicketService.Chat.Start(TicketPopupMain.CurrentTicket);
|
||||||
|
|
||||||
|
// Let the ui know that we are ready
|
||||||
|
HasStarted = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnUpdate() // This will be called to update the ui. Additional updates and check may be added here
|
||||||
|
{
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendMessage()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(MessageContent) && FileSelect.SelectedFile == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!HasStarted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (FileSelect.SelectedFile == null)
|
||||||
|
await TicketService.Chat.SendMessage(MessageContent);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await TicketService.Chat.SendMessage(
|
||||||
|
string.IsNullOrEmpty(MessageContent) ? $"Upload of {FileSelect.SelectedFile.Name}" : MessageContent,
|
||||||
|
FileSelect.SelectedFile.OpenReadStream(1024 * 1024 * 5),
|
||||||
|
FileSelect.SelectedFile.Name
|
||||||
|
);
|
||||||
|
|
||||||
|
await FileSelect.RemoveSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageContent = "";
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Dispose()
|
||||||
|
{
|
||||||
|
await TicketService.Chat.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@using Moonlight.Shared.Components.Partials.LiveChat
|
@using Moonlight.Shared.Components.TicketPopup
|
||||||
|
|
||||||
<div class="d-flex flex-column flex-root app-root">
|
<div class="d-flex flex-column flex-root app-root">
|
||||||
<div class="app-page flex-column flex-column-fluid">
|
<div class="app-page flex-column flex-column-fluid">
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="app-container container-fluid">
|
<div class="app-container container-fluid">
|
||||||
@ChildContent
|
@ChildContent
|
||||||
|
|
||||||
<LiveChatMain />
|
<TicketPopupMain />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
265
Moonlight/Shared/Views/Admin/Tickets/Index.razor
Normal file
265
Moonlight/Shared/Views/Admin/Tickets/Index.razor
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
@page "/admin/tickets"
|
||||||
|
|
||||||
|
@using Moonlight.App.Extensions.Attributes
|
||||||
|
@using Moonlight.App.Models.Enums
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.App.Database.Entities.Tickets
|
||||||
|
@using BlazorTable
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using Moonlight.App.Database.Enums
|
||||||
|
@using Moonlight.App.Event
|
||||||
|
@using Moonlight.App.Event.Args
|
||||||
|
|
||||||
|
@attribute [RequirePermission(Permission.AdminTickets)]
|
||||||
|
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
@inject Repository<Ticket> TicketRepository
|
||||||
|
|
||||||
|
<div class="row mb-5">
|
||||||
|
<LazyLoader @ref="StatisticsLazyLoader" Load="LoadStatistics" ShowAsCard="true">
|
||||||
|
<div class="col-md-4 col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center gx-0">
|
||||||
|
<div class="col">
|
||||||
|
<h6 class="text-uppercase text-muted mb-2">
|
||||||
|
<TL>Total Tickets</TL>
|
||||||
|
</h6>
|
||||||
|
<span class="h2 mb-0">
|
||||||
|
@(TotalTicketsCount)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="h2 text-muted mb-0">
|
||||||
|
<i class="text-primary bx bx-purchase-tag bx-lg"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center gx-0">
|
||||||
|
<div class="col">
|
||||||
|
<h6 class="text-uppercase text-muted mb-2">
|
||||||
|
<TL>Pending tickets</TL>
|
||||||
|
</h6>
|
||||||
|
<span class="h2 mb-0">
|
||||||
|
@(PendingTicketsCount)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="h2 text-muted mb-">
|
||||||
|
<i class="text-primary bx bx-hourglass bx-lg"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center gx-0">
|
||||||
|
<div class="col">
|
||||||
|
<h6 class="text-uppercase text-muted mb-2">
|
||||||
|
<TL>Closed tickets</TL>
|
||||||
|
</h6>
|
||||||
|
<span class="h2 mb-0">
|
||||||
|
@(ClosedTicketsCount)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="h2 text-muted mb-">
|
||||||
|
<i class="text-primary bx bx-lock bx-lg"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
<TL>Ticket overview</TL>
|
||||||
|
</span>
|
||||||
|
<div class="card-toolbar">
|
||||||
|
<div class="btn-group">
|
||||||
|
<WButton Text="Overview" CssClasses="btn-secondary" OnClick="() => UpdateFilter(0)" />
|
||||||
|
<WButton Text="Closed tickets" CssClasses="btn-secondary" OnClick="() => UpdateFilter(1)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<LazyLoader @ref="TicketLazyLoader" Load="LoadTickets" ShowAsCard="true">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<Table TableItem="Ticket" Items="AllTickets" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
|
||||||
|
<Column TableItem="Ticket" Title="Id" Field="@(x => x.Id)" Filterable="true" Sortable="true"/>
|
||||||
|
<Column TableItem="Ticket" Title="Name" Field="@(x => x.Name)" Filterable="true" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<a href="/admin/tickets/view/@(context.Id)">@(context.Name)</a>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Ticket" Title="User" Field="@(x => x.Id)" Filterable="false" Sortable="false">
|
||||||
|
<Template>
|
||||||
|
<a href="/admin/users/view/@(context.Creator.Id)">@(context.Creator.Username)</a>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Ticket" Title="Created at" Field="@(x => x.CreatedAt)" Filterable="true" Sortable="true">
|
||||||
|
<Template>
|
||||||
|
<span>@(Formatter.FormatDate(context.CreatedAt))</span>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Ticket" Title="Priority" Field="@(x => x.Priority)" Filterable="true" Sortable="true">
|
||||||
|
<Template>
|
||||||
|
@switch (context.Priority)
|
||||||
|
{
|
||||||
|
case TicketPriority.Low:
|
||||||
|
<span class="badge bg-success">@(context.Priority)</span>
|
||||||
|
break;
|
||||||
|
case TicketPriority.Medium:
|
||||||
|
<span class="badge bg-primary">@(context.Priority)</span>
|
||||||
|
break;
|
||||||
|
case TicketPriority.High:
|
||||||
|
<span class="badge bg-warning">@(context.Priority)</span>
|
||||||
|
break;
|
||||||
|
case TicketPriority.Critical:
|
||||||
|
<span class="badge bg-danger">@(context.Priority)</span>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Ticket" Title="Status" Field="@(x => x.Open)" Filterable="true" Sortable="true">
|
||||||
|
<Template>
|
||||||
|
@if (context.Open)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">Open</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-danger">Closed</span>
|
||||||
|
}
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Pager AlwaysShow="true" ShowPageNumber="true" ShowTotalCount="true"/>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
// Lazy loaders
|
||||||
|
private LazyLoader TicketLazyLoader;
|
||||||
|
private LazyLoader StatisticsLazyLoader;
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
private int TotalTicketsCount;
|
||||||
|
private int ClosedTicketsCount;
|
||||||
|
private int PendingTicketsCount;
|
||||||
|
|
||||||
|
// Data
|
||||||
|
private int Filter = 0;
|
||||||
|
private Ticket[] AllTickets;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Events.OnTicketCreated += OnTicketCreated;
|
||||||
|
Events.OnTicketUpdated += OnTicketUpdated;
|
||||||
|
Events.OnTicketMessage += OnTicketMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Lazyloaders
|
||||||
|
|
||||||
|
private Task LoadStatistics(LazyLoader lazyLoader)
|
||||||
|
{
|
||||||
|
TotalTicketsCount = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Count();
|
||||||
|
|
||||||
|
ClosedTicketsCount = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Count(x => !x.Open);
|
||||||
|
|
||||||
|
PendingTicketsCount = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Where(x => x.Open)
|
||||||
|
.Count(x => x.Messages.All(x => !x.IsSupport));
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task LoadTickets(LazyLoader lazyLoader)
|
||||||
|
{
|
||||||
|
if (Filter == 0)
|
||||||
|
{
|
||||||
|
AllTickets = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Creator)
|
||||||
|
.Include(x => x.Service)
|
||||||
|
.ThenInclude(x => x.Product)
|
||||||
|
.Where(x => x.Open)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
else if (Filter == 1)
|
||||||
|
{
|
||||||
|
AllTickets = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Creator)
|
||||||
|
.Include(x => x.Service)
|
||||||
|
.ThenInclude(x => x.Product)
|
||||||
|
.Where(x => !x.Open)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private async Task UpdateFilter(int filter)
|
||||||
|
{
|
||||||
|
Filter = filter;
|
||||||
|
|
||||||
|
await TicketLazyLoader.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Events
|
||||||
|
|
||||||
|
private async void OnTicketMessage(object? sender, TicketMessageEventArgs message)
|
||||||
|
{
|
||||||
|
if(!message.TicketMessage.IsSupport) // Only update if support has sent messages as the pending tickets depend on that
|
||||||
|
return;
|
||||||
|
|
||||||
|
await StatisticsLazyLoader.Reload();
|
||||||
|
await TicketLazyLoader.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnTicketUpdated(object? o, Ticket e)
|
||||||
|
{
|
||||||
|
await StatisticsLazyLoader.Reload();
|
||||||
|
await TicketLazyLoader.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnTicketCreated(object? o, Ticket e)
|
||||||
|
{
|
||||||
|
await StatisticsLazyLoader.Reload();
|
||||||
|
await TicketLazyLoader.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Dispose() // Unsubscribe to events
|
||||||
|
{
|
||||||
|
Events.OnTicketCreated -= OnTicketCreated;
|
||||||
|
Events.OnTicketUpdated -= OnTicketUpdated;
|
||||||
|
Events.OnTicketMessage -= OnTicketMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
304
Moonlight/Shared/Views/Admin/Tickets/View.razor
Normal file
304
Moonlight/Shared/Views/Admin/Tickets/View.razor
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
@page "/admin/tickets/view/{Id:int}"
|
||||||
|
|
||||||
|
@using Moonlight.App.Extensions.Attributes
|
||||||
|
@using Moonlight.App.Models.Enums
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.App.Services.Ticketing
|
||||||
|
@using Moonlight.App.Database.Entities.Tickets
|
||||||
|
@using System.Text.RegularExpressions
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using Moonlight.App.Database.Enums
|
||||||
|
|
||||||
|
@attribute [RequirePermission(Permission.AdminTickets)]
|
||||||
|
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
@inject TicketService TicketService
|
||||||
|
@inject ToastService ToastService
|
||||||
|
@inject Repository<Ticket> TicketRepository
|
||||||
|
|
||||||
|
<LazyLoader Load="LoadTicket" ShowAsCard="true">
|
||||||
|
@if (Ticket == null)
|
||||||
|
{
|
||||||
|
<NotFoundAlert/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 col-12 mb-5">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
|
||||||
|
<li class="nav-item mt-2">
|
||||||
|
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(ShowDetails ? "" : "active")" @onclick="() => SetShowDetails(false)" @onclick:preventDefault href="#">
|
||||||
|
General
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item mt-2">
|
||||||
|
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(ShowDetails ? "active" : "")" @onclick="() => SetShowDetails(true)" @onclick:preventDefault href="#">
|
||||||
|
Details
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@if (ShowDetails)
|
||||||
|
{
|
||||||
|
<div class="mb-4 pb-2 g-3">
|
||||||
|
<span class="fs-2 fw-bold">@(Ticket.Name)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="fs-4">
|
||||||
|
Description
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<p class="fs-5 text-muted">
|
||||||
|
@(Formatter.FormatLineBreaks(Ticket.Description))
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<span class="fs-4">
|
||||||
|
Tries:
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<p class="fs-5 text-muted">
|
||||||
|
@(Formatter.FormatLineBreaks(Ticket.Tries))
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-borderless align-middle mb-0 fs-5">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span>Ticket ID</span>
|
||||||
|
</th>
|
||||||
|
<td>@(Ticket.Id)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span>User</span>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/users/view/@(Ticket.Creator.Id)">@(Ticket.Creator.Username)</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span>Service</span>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
@if (Ticket.Service == null)
|
||||||
|
{
|
||||||
|
<span>None</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="/service/@(Ticket.Service.Id)">@(Ticket.Service.Nickname ?? $"Service {Ticket.Service.Id}")</a>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span>Open</span>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" @bind="EditOpen"/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span>Priority</span>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<SmartEnumSelect @bind-Value="EditPriority"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span>Created at</span>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<span>@(Formatter.FormatDate(Ticket.CreatedAt))</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<td>
|
||||||
|
<WButton OnClick="Save" Text="Save" CssClasses="btn-primary"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9 col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body bg-black p-8">
|
||||||
|
<LazyLoader Load="LoadChatClient">
|
||||||
|
<div class="scroll-y" style="display: flex; flex-direction: column-reverse; height: 70vh">
|
||||||
|
@foreach (var message in TicketService.Chat.Messages.OrderByDescending(x => x.CreatedAt))
|
||||||
|
{
|
||||||
|
var orientation = message.IsSupport ? "end" : "start";
|
||||||
|
|
||||||
|
if (message.Sender != null)
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-@(orientation) mb-10 ">
|
||||||
|
<div class="d-flex flex-column align-items-@(orientation)">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<div class="symbol symbol-35px symbol-circle ">
|
||||||
|
<img alt="Avatar" src="/api/bucket/avatars/@(message.Sender.Avatar)">
|
||||||
|
</div>
|
||||||
|
<div class="ms-3">
|
||||||
|
<div class="fs-5 fw-bold text-gray-900 me-1">@(message.Sender.Username)</div>
|
||||||
|
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt))</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 rounded bg-light-@(message.IsSupport ? "info" : "primary") text-dark fw-semibold mw-lg-400px text-@(orientation)">
|
||||||
|
@(Formatter.FormatLineBreaks(message.Content))
|
||||||
|
|
||||||
|
@if (message.Attachment != null)
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
|
||||||
|
{
|
||||||
|
<img src="/api/bucket/ticketAttachments/@(message.Attachment)" class="img-fluid" alt="Attachment"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="/api/bucket/ticketAttachments/@(message.Attachment)" target="_blank" class="btn btn-secondary">
|
||||||
|
<i class="me-2 bx bx-download"></i> @(message.Attachment)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="separator separator-content my-15">@(message.Content)</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="row">
|
||||||
|
<div class="input-group">
|
||||||
|
<textarea @bind="MessageContent" class="form-control form-control-solid-bg rounded-end me-3" placeholder="Type a message" style="height: 1vh"></textarea>
|
||||||
|
<ChatFileSelect @ref="FileSelect"/>
|
||||||
|
<WButton OnClick="SendMessage" CssClasses="ms-2 btn btn-icon btn-bg-light btn-color-white">
|
||||||
|
<i class="bx bx-sm bx-send"></i>
|
||||||
|
</WButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</LazyLoader>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
private Ticket? Ticket;
|
||||||
|
private bool HasStarted = false;
|
||||||
|
private bool ShowDetails = false;
|
||||||
|
|
||||||
|
// Message compose cache
|
||||||
|
private ChatFileSelect FileSelect;
|
||||||
|
private string MessageContent = "";
|
||||||
|
|
||||||
|
// Edit cache
|
||||||
|
private bool EditOpen;
|
||||||
|
private TicketPriority EditPriority;
|
||||||
|
|
||||||
|
private Task LoadTicket(LazyLoader _)
|
||||||
|
{
|
||||||
|
Ticket = TicketRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.Creator)
|
||||||
|
.Include(x => x.Service)
|
||||||
|
.FirstOrDefault(x => x.Id == Id);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadChatClient(LazyLoader lazyLoader)
|
||||||
|
{
|
||||||
|
if (Ticket != null)
|
||||||
|
{
|
||||||
|
await lazyLoader.SetText("Starting chat client");
|
||||||
|
|
||||||
|
TicketService.Chat.OnUpdate += OnUpdate;
|
||||||
|
await TicketService.Chat.Start(Ticket, true);
|
||||||
|
|
||||||
|
EditOpen = TicketService.Chat.Ticket.Open;
|
||||||
|
EditPriority = TicketService.Chat.Ticket.Priority;
|
||||||
|
|
||||||
|
HasStarted = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendMessage()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(MessageContent) && FileSelect.SelectedFile == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!HasStarted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (FileSelect.SelectedFile == null)
|
||||||
|
await TicketService.Chat.SendMessage(MessageContent);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await TicketService.Chat.SendMessage(
|
||||||
|
string.IsNullOrEmpty(MessageContent) ? $"Upload of {FileSelect.SelectedFile.Name}" : MessageContent,
|
||||||
|
FileSelect.SelectedFile.OpenReadStream(1024 * 1024 * 5),
|
||||||
|
FileSelect.SelectedFile.Name
|
||||||
|
);
|
||||||
|
|
||||||
|
await FileSelect.RemoveSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageContent = "";
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Save()
|
||||||
|
{
|
||||||
|
await TicketService.Chat.Update(EditOpen, EditPriority);
|
||||||
|
await ToastService.Success("Successfully updated ticket");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetShowDetails(bool b)
|
||||||
|
{
|
||||||
|
ShowDetails = b;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnUpdate()
|
||||||
|
{
|
||||||
|
// Overwrite current cached data
|
||||||
|
EditOpen = TicketService.Chat.Ticket.Open;
|
||||||
|
EditPriority = TicketService.Chat.Ticket.Priority;
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Dispose()
|
||||||
|
{
|
||||||
|
await TicketService.Chat.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Moonlight/wwwroot/img/logo.svg
vendored
Normal file
14
Moonlight/wwwroot/img/logo.svg
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="256px" height="301px" viewBox="0 0 256 301" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||||
|
<defs>
|
||||||
|
<linearGradient x1="2.17771739%" y1="34.7938955%" x2="92.7221942%" y2="91.3419405%" id="linearGradient-1">
|
||||||
|
<stop stop-color="#41A7EF" offset="0%"></stop>
|
||||||
|
<stop stop-color="#813DDE" offset="54.2186236%"></stop>
|
||||||
|
<stop stop-color="#8F2EE2" offset="74.4988788%"></stop>
|
||||||
|
<stop stop-color="#A11CE6" offset="100%"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<path d="M124.183681,101.699 C124.183681,66.515 136.256681,34.152 156.486681,8.525 C159.197681,5.092 156.787681,0.069 152.412681,0.012 C151.775681,0.004 151.136681,0 150.497681,0 C67.6206813,0 0.390681343,66.99 0.00168134279,149.775 C-0.386318657,232.369 66.4286813,300.195 149.019681,300.988 C189.884681,301.381 227.036681,285.484 254.376681,259.395 C257.519681,256.396 255.841681,251.082 251.548681,250.42 C179.413681,239.291 124.183681,176.949 124.183681,101.699" fill="url(#linearGradient-1)"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
Reference in New Issue
Block a user