From 1c96f9d13c22dca8dade98d3e53e279d1af3d80e Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Wed, 1 Nov 2023 17:35:12 +0100 Subject: [PATCH] Started implementing the ticket system. Implemented user site tickets without attachments atm. Added db models --- Moonlight/App/Database/DataContext.cs | 6 +- .../App/Database/Entities/Tickets/Ticket.cs | 6 +- ...231101161843_AddedTicketModels.Designer.cs | 665 ++++++++++++++++++ .../20231101161843_AddedTicketModels.cs | 104 +++ .../Migrations/DataContextModelSnapshot.cs | 111 +++ .../App/Event/Args/TicketMessageEventArgs.cs | 9 + Moonlight/App/Event/Events.cs | 4 + .../Forms/Ticketing/CreateTicketForm.cs | 24 + .../Services/Ticketing/TicketChatService.cs | 130 ++++ .../Services/Ticketing/TicketCreateService.cs | 41 ++ .../App/Services/Ticketing/TicketService.cs | 14 + Moonlight/Program.cs | 6 + .../Partials/LiveChat/LiveChatCreate.razor | 82 +++ .../Partials/LiveChat/LiveChatMain.razor | 182 +---- .../Partials/LiveChat/LiveChatOverview.razor | 94 +++ .../Partials/LiveChat/LiveChatView.razor | 99 +++ 16 files changed, 1424 insertions(+), 153 deletions(-) create mode 100644 Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.Designer.cs create mode 100644 Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.cs create mode 100644 Moonlight/App/Event/Args/TicketMessageEventArgs.cs create mode 100644 Moonlight/App/Models/Forms/Ticketing/CreateTicketForm.cs create mode 100644 Moonlight/App/Services/Ticketing/TicketChatService.cs create mode 100644 Moonlight/App/Services/Ticketing/TicketCreateService.cs create mode 100644 Moonlight/App/Services/Ticketing/TicketService.cs create mode 100644 Moonlight/Shared/Components/Partials/LiveChat/LiveChatCreate.razor create mode 100644 Moonlight/Shared/Components/Partials/LiveChat/LiveChatOverview.razor create mode 100644 Moonlight/Shared/Components/Partials/LiveChat/LiveChatView.razor diff --git a/Moonlight/App/Database/DataContext.cs b/Moonlight/App/Database/DataContext.cs index e4b3f60..a0ea380 100644 --- a/Moonlight/App/Database/DataContext.cs +++ b/Moonlight/App/Database/DataContext.cs @@ -12,8 +12,6 @@ public class DataContext : DbContext private readonly ConfigService ConfigService; public DbSet Users { get; set; } - //public DbSet Tickets { get; set; } - //public DbSet TicketMessages { get; set; } // Store public DbSet Categories { get; set; } @@ -32,6 +30,10 @@ public class DataContext : DbContext public DbSet PostComments { get; set; } public DbSet PostLikes { get; set; } public DbSet WordFilters { get; set; } + + // Tickets + public DbSet Tickets { get; set; } + public DbSet TicketMessages { get; set; } public DataContext(ConfigService configService) { diff --git a/Moonlight/App/Database/Entities/Tickets/Ticket.cs b/Moonlight/App/Database/Entities/Tickets/Ticket.cs index ef6d485..fb6e029 100644 --- a/Moonlight/App/Database/Entities/Tickets/Ticket.cs +++ b/Moonlight/App/Database/Entities/Tickets/Ticket.cs @@ -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; @@ -11,8 +12,9 @@ public class Ticket public string Tries { get; set; } = ""; public TicketPriority Priority { get; set; } = TicketPriority.Low; public bool Open { get; set; } = true; + public Service? Service { get; set; } - public List Messages = new(); + public List Messages { get; set; } = new(); public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } \ No newline at end of file diff --git a/Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.Designer.cs b/Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.Designer.cs new file mode 100644 index 0000000..1419a13 --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.Designer.cs @@ -0,0 +1,665 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Filter") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("WordFilters"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Percent") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CouponId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("GiftCodes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GiftCodeId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("MaxPerUser") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Stock") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConfigJsonOverride") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("RenewAt") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Open") + .HasColumnType("INTEGER"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsSupport") + .HasColumnType("INTEGER"); + + b.Property("SenderId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Avatar") + .HasColumnType("TEXT"); + + b.Property("Balance") + .HasColumnType("REAL"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Flags") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("INTEGER"); + + b.Property("TokenValidTimestamp") + .HasColumnType("TEXT"); + + b.Property("TotpKey") + .HasColumnType("TEXT"); + + b.Property("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 + } + } +} diff --git a/Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.cs b/Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.cs new file mode 100644 index 0000000..f76fc0d --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.cs @@ -0,0 +1,104 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + /// + public partial class AddedTicketModels : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Tickets", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + CreatorId = table.Column(type: "INTEGER", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: false), + Tries = table.Column(type: "TEXT", nullable: false), + Priority = table.Column(type: "INTEGER", nullable: false), + Open = table.Column(type: "INTEGER", nullable: false), + ServiceId = table.Column(type: "INTEGER", nullable: true), + CreatedAt = table.Column(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(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + SenderId = table.Column(type: "INTEGER", nullable: true), + IsSupport = table.Column(type: "INTEGER", nullable: false), + Content = table.Column(type: "TEXT", nullable: false), + Attachment = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + TicketId = table.Column(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"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TicketMessages"); + + migrationBuilder.DropTable( + name: "Tickets"); + } + } +} diff --git a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs index d6c7ed5..6e5d842 100644 --- a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs @@ -357,6 +357,82 @@ namespace Moonlight.App.Database.Migrations b.ToTable("Transaction"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Open") + .HasColumnType("INTEGER"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsSupport") + .HasColumnType("INTEGER"); + + b.Property("SenderId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") @@ -525,6 +601,36 @@ namespace Moonlight.App.Database.Migrations .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"); @@ -537,6 +643,11 @@ namespace Moonlight.App.Database.Migrations 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"); diff --git a/Moonlight/App/Event/Args/TicketMessageEventArgs.cs b/Moonlight/App/Event/Args/TicketMessageEventArgs.cs new file mode 100644 index 0000000..900be64 --- /dev/null +++ b/Moonlight/App/Event/Args/TicketMessageEventArgs.cs @@ -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; } +} \ No newline at end of file diff --git a/Moonlight/App/Event/Events.cs b/Moonlight/App/Event/Events.cs index d296728..4f59beb 100644 --- a/Moonlight/App/Event/Events.cs +++ b/Moonlight/App/Event/Events.cs @@ -1,6 +1,7 @@ using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities.Community; using Moonlight.App.Database.Entities.Store; +using Moonlight.App.Database.Entities.Tickets; using Moonlight.App.Event.Args; namespace Moonlight.App.Event; @@ -19,4 +20,7 @@ public class Events public static EventHandler OnPostLiked; public static EventHandler OnPostCommentCreated; public static EventHandler OnPostCommentDeleted; + public static EventHandler OnTicketCreated; + public static EventHandler OnTicketMessage; + public static EventHandler OnTicketUpdated; } \ No newline at end of file diff --git a/Moonlight/App/Models/Forms/Ticketing/CreateTicketForm.cs b/Moonlight/App/Models/Forms/Ticketing/CreateTicketForm.cs new file mode 100644 index 0000000..2c57b96 --- /dev/null +++ b/Moonlight/App/Models/Forms/Ticketing/CreateTicketForm.cs @@ -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; } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Ticketing/TicketChatService.cs b/Moonlight/App/Services/Ticketing/TicketChatService.cs new file mode 100644 index 0000000..f73cbcb --- /dev/null +++ b/Moonlight/App/Services/Ticketing/TicketChatService.cs @@ -0,0 +1,130 @@ +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database.Entities.Tickets; +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 TicketRepository; + private readonly BucketService BucketService; + private readonly List MessageCache = new(); + + public Ticket Ticket; + public bool IsSupporter; + public Func? OnUpdate; + public TicketMessage[] Messages => MessageCache.ToArray(); + + public TicketChatService( + IdentityService identityService, + Repository 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 Task Stop() + { + Events.OnTicketMessage -= OnTicketMessage; + Events.OnTicketUpdated -= OnTicketUpdated; + + MessageCache.Clear(); + + return Task.CompletedTask; + } + + public async Task SendMessage(string content, Stream? attachmentStream = null, string? attachmentString = null) + { + if(string.IsNullOrEmpty(content)) + return; + + string? attachmentName = null; + + // Check and download attachments + if (attachmentStream != null && attachmentName != null) + { + attachmentName = await BucketService.Store( + "ticketAttachments", + attachmentStream, + attachmentName + ); + } + + // Build the message model + var message = new TicketMessage() + { + Content = content, + Attachment = attachmentName, + CreatedAt = DateTime.UtcNow, + Sender = IdentityService.CurrentUser, + IsSupport = IsSupporter + }; + + // 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 + }); + } + + // 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(); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Ticketing/TicketCreateService.cs b/Moonlight/App/Services/Ticketing/TicketCreateService.cs new file mode 100644 index 0000000..0a005fe --- /dev/null +++ b/Moonlight/App/Services/Ticketing/TicketCreateService.cs @@ -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 TicketRepository; + private readonly IdentityService IdentityService; + + public TicketCreateService(Repository ticketRepository, IdentityService identityService) + { + TicketRepository = ticketRepository; + IdentityService = identityService; + } + + public async Task 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; + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Ticketing/TicketService.cs b/Moonlight/App/Services/Ticketing/TicketService.cs new file mode 100644 index 0000000..b3b16ca --- /dev/null +++ b/Moonlight/App/Services/Ticketing/TicketService.cs @@ -0,0 +1,14 @@ +namespace Moonlight.App.Services.Ticketing; + +public class TicketService +{ + private readonly IServiceProvider ServiceProvider; + + public TicketChatService Chat => ServiceProvider.GetRequiredService(); + public TicketCreateService Create => ServiceProvider.GetRequiredService(); + + public TicketService(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } +} \ No newline at end of file diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 2300633..4f00ccd 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -12,6 +12,7 @@ using Moonlight.App.Services.Community; using Moonlight.App.Services.Interop; using Moonlight.App.Services.ServiceManage; using Moonlight.App.Services.Store; +using Moonlight.App.Services.Ticketing; using Moonlight.App.Services.Users; using Moonlight.App.Services.Utils; using Serilog; @@ -75,6 +76,11 @@ builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddSingleton(); +// Services / Ticketing +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + // Services builder.Services.AddScoped(); builder.Services.AddSingleton(); diff --git a/Moonlight/Shared/Components/Partials/LiveChat/LiveChatCreate.razor b/Moonlight/Shared/Components/Partials/LiveChat/LiveChatCreate.razor new file mode 100644 index 0000000..f2d9306 --- /dev/null +++ b/Moonlight/Shared/Components/Partials/LiveChat/LiveChatCreate.razor @@ -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 ServiceRepository +@inject TicketService TicketService + +
+ Create a new ticket +
+ +
+
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ +@code +{ + [CascadingParameter] + public LiveChatMain LiveChatMain { 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 LiveChatMain.OpenTicket(ticket); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/Partials/LiveChat/LiveChatMain.razor b/Moonlight/Shared/Components/Partials/LiveChat/LiveChatMain.razor index 8a1d08b..70e6781 100644 --- a/Moonlight/Shared/Components/Partials/LiveChat/LiveChatMain.razor +++ b/Moonlight/Shared/Components/Partials/LiveChat/LiveChatMain.razor @@ -1,163 +1,47 @@ -
- @if (ViewIndex == 1) - { -
-
- Chats -
- -
-
-
-
-
- -
- - -
-
- M -
-
- Melody Macy -
melody@altbox.com
-
-
-
- 2 weeks -
- -
-
-
-
- Pic -
-
- Max Smith -
max@kt.com
-
-
-
- 1 day -
-
-
-
-
- } - else if (ViewIndex == 2) - { -
-
- Some chat -
- -
-
-
-
- @for (int i = 0; i < 9; i++) +@using Moonlight.App.Database.Entities.Tickets +
+ + @if (ViewIndex == 0) + { + + } + else + { +
+ @if (ViewIndex == 1) { -
-
-
-
- Pic -
-
- Brian Cox - 2 mins -
-
-
- How likely are you to recommend our company to your friends and family ? -
-
-
-
-
-
-
- 5 mins - You -
-
- Pic -
-
-
- Hey there, we’re just writing to let you know that you’ve been subscribed to a repository on GitHub. -
-
-
+ + } + else if (ViewIndex == 2) + { + + } + else if (ViewIndex == 3) + { + }
-
- -
- } - else if (ViewIndex == 3) - { -
-
- Create ticket -
- -
-
-
-
- - -
-
- - -
-
- - -
- -
- -
-
-
- } - else - { - - } + } +
@code { private int ViewIndex = 0; + + public Ticket CurrentTicket { get; private set; } - private async Task SetViewIndex(int index) + public async Task SetViewIndex(int index) { ViewIndex = index; await InvokeAsync(StateHasChanged); } + + public async Task OpenTicket(Ticket ticket) + { + CurrentTicket = ticket; + await SetViewIndex(2); + } } \ No newline at end of file diff --git a/Moonlight/Shared/Components/Partials/LiveChat/LiveChatOverview.razor b/Moonlight/Shared/Components/Partials/LiveChat/LiveChatOverview.razor new file mode 100644 index 0000000..3d9ee2b --- /dev/null +++ b/Moonlight/Shared/Components/Partials/LiveChat/LiveChatOverview.razor @@ -0,0 +1,94 @@ +@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 TicketRepository +@inject IdentityService IdentityService + +
+ Your tickets +
+ +
+
+
+
+
+

Need help? Create a ticket

+
+ +
+
+ + + @if (Tickets.Any()) + { + foreach (var ticket in Tickets) + { + + +
+ @(Formatter.FormatAgoFromDateTime(ticket.CreatedAt)) +
+ + } + } + else + { +
No open tickets found
+ } +
+
+
+ +@code +{ + [CascadingParameter] + public LiveChatMain LiveChatMain { 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; + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/Partials/LiveChat/LiveChatView.razor b/Moonlight/Shared/Components/Partials/LiveChat/LiveChatView.razor new file mode 100644 index 0000000..cfcebac --- /dev/null +++ b/Moonlight/Shared/Components/Partials/LiveChat/LiveChatView.razor @@ -0,0 +1,99 @@ +@using Moonlight.App.Services.Ticketing + +@implements IDisposable + +@inject TicketService TicketService + +
+ @(HasStarted ? TicketService.Chat.Ticket.Name : "Loading") +
+ +
+
+
+ +
+ @foreach (var message in TicketService.Chat.Messages.OrderByDescending(x => x.CreatedAt)) + { + var orientation = message.IsSupport ? "start" : "end"; + + @if (message.Sender != null) + { +
+
+
+
+ Avatar +
+
+
@(message.Sender.Username)
+ @(Formatter.FormatAgoFromDateTime(message.CreatedAt)) +
+
+
+ @(message.Content) +
+
+
+ } + else + { + @* System msgs here *@ + } + } +
+
+
+ + +@code +{ + [CascadingParameter] + public LiveChatMain LiveChatMain { get; set; } + + private bool HasStarted = false; + private string MyMessageContent = ""; + + 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(LiveChatMain.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(MyMessageContent)) + return; + + if (!HasStarted) + return; + + await TicketService.Chat.SendMessage(MyMessageContent); + MyMessageContent = ""; + await InvokeAsync(StateHasChanged); + } + + public async void Dispose() + { + await TicketService.Chat.Stop(); + } +} \ No newline at end of file