Started implementing the ticket system. Implemented user site tickets without attachments atm. Added db models

This commit is contained in:
Marcel Baumgartner 2023-11-01 17:35:12 +01:00
parent 9e182768f6
commit 1c96f9d13c
16 changed files with 1424 additions and 153 deletions

View file

@ -12,8 +12,6 @@ public class DataContext : DbContext
private readonly ConfigService ConfigService;
public DbSet<User> Users { get; set; }
//public DbSet<Ticket> Tickets { get; set; }
//public DbSet<TicketMessage> TicketMessages { get; set; }
// Store
public DbSet<Category> Categories { get; set; }
@ -32,6 +30,10 @@ public class DataContext : DbContext
public DbSet<PostComment> PostComments { get; set; }
public DbSet<PostLike> PostLikes { 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)
{

View file

@ -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<TicketMessage> Messages = new();
public List<TicketMessage> Messages { get; set; } = new();
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View 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
}
}
}

View file

@ -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");
}
}
}

View file

@ -357,6 +357,82 @@ namespace Moonlight.App.Database.Migrations
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")
@ -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");

View 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; }
}

View file

@ -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<Post> OnPostLiked;
public static EventHandler<PostComment> OnPostCommentCreated;
public static EventHandler<PostComment> OnPostCommentDeleted;
public static EventHandler<Ticket> OnTicketCreated;
public static EventHandler<TicketMessageEventArgs> OnTicketMessage;
public static EventHandler<Ticket> OnTicketUpdated;
}

View 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; }
}

View file

@ -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<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 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();
}
}

View 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;
}
}

View 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;
}
}

View file

@ -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<AutoMailSendService>();
builder.Services.AddScoped<ServiceService>();
builder.Services.AddSingleton<ServiceAdminService>();
// Services / Ticketing
builder.Services.AddScoped<TicketService>();
builder.Services.AddScoped<TicketChatService>();
builder.Services.AddScoped<TicketCreateService>();
// Services
builder.Services.AddScoped<IdentityService>();
builder.Services.AddSingleton<ConfigService>();

View file

@ -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="() => LiveChatMain.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 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);
}
}

View file

@ -1,163 +1,47 @@
<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++)
@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)
{
<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, were just writing to let you know that youve been subscribed to a repository on GitHub.
</div>
</div>
</div>
<LiveChatOverview />
}
else if (ViewIndex == 2)
{
<LiveChatView />
}
else if (ViewIndex == 3)
{
<LiveChatCreate />
}
</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>
}
}
</CascadingValue>
</div>
@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);
}
}

View file

@ -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<Ticket> TicketRepository
@inject IdentityService IdentityService
<div class="card-header">
<span class="card-title fs-5">Your tickets</span>
<div class="card-toolbar">
<button @onclick="() => LiveChatMain.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="() => LiveChatMain.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="() => LiveChatMain.OpenTicket(ticket)" @onclick:preventDefault class="d-flex flex-stack py-4">
<div class="d-flex align-items-center">
<div class="symbol symbol-45px symbol-circle">
@{
string color = "";
switch (ticket.Priority)
{
case TicketPriority.Critical:
color = "danger";
break;
case TicketPriority.High:
color = "warning";
break;
case TicketPriority.Medium:
color = "primary";
break;
case TicketPriority.Low:
color = "secondary";
break;
}
}
<span class="symbol-label bg-@(color) text-white fs-6 fw-bolder">@(ticket.Priority.ToString().First())</span>
</div>
<div class="ms-5">
<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 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;
}
}

View file

@ -0,0 +1,99 @@
@using Moonlight.App.Services.Ticketing
@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="() => LiveChatMain.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)">
@(message.Content)
</div>
</div>
</div>
}
else
{
@* System msgs here *@
}
}
</div>
</LazyLoader>
</div>
<div class="card-footer">
<div class="input-group">
<input @bind="MyMessageContent" class="form-control" placeholder="Type a message"/>
<WButton OnClick="SendMessage" Text="Send" CssClasses="btn btn-secondary"></WButton>
</div>
</div>
@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();
}
}