diff --git a/src/Analysim.Core/Entities/Project.cs b/src/Analysim.Core/Entities/Project.cs index 1cbed8b2..3778c170 100644 --- a/src/Analysim.Core/Entities/Project.cs +++ b/src/Analysim.Core/Entities/Project.cs @@ -45,6 +45,7 @@ public class Project // Logs public ICollection ProjectLogs { get; set; } = new List(); public ICollection Publications { get; set; } = new List(); + public ICollection ProjectRecommendations { get; set; } = new List(); public ICollection ProjectMembershipRequests { get; set; } = new List(); public int ForkedFromProjectID { get; set; } diff --git a/src/Analysim.Core/Entities/ProjectRecommendation.cs b/src/Analysim.Core/Entities/ProjectRecommendation.cs new file mode 100644 index 00000000..e681038c --- /dev/null +++ b/src/Analysim.Core/Entities/ProjectRecommendation.cs @@ -0,0 +1,29 @@ +#nullable enable + +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Core.Entities +{ + public class ProjectRecommendation + { + [KeyAttribute] + public int ProjectRecommendationID { get; set; } + + [ForeignKey("Project")] + public int ProjectID { get; set; } + public Project Project { get; set; } = null!; + + [ForeignKey("User")] + public int UserID { get; set; } + public User User { get; set; } = null!; + + [Required(ErrorMessage = "Recommendation comment is a required field.")] + [MaxLength(1000, ErrorMessage = "Maximum length for Recommendation Comment is 1000 characters.")] + public string Comment { get; set; } = string.Empty; + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/src/Analysim.Core/Entities/User.cs b/src/Analysim.Core/Entities/User.cs index fde6c0f2..35430c9c 100644 --- a/src/Analysim.Core/Entities/User.cs +++ b/src/Analysim.Core/Entities/User.cs @@ -29,6 +29,7 @@ public class User : IdentityUser // Logs public ICollection ProjectLogs { get; set; } = new List(); + public ICollection ProjectRecommendations { get; set; } = new List(); public ICollection RefreshTokens { get; set; } = new List(); diff --git a/src/Analysim.Infrastructure/Data/ApplicationDbContext.cs b/src/Analysim.Infrastructure/Data/ApplicationDbContext.cs index 6d348234..375c1ba4 100644 --- a/src/Analysim.Infrastructure/Data/ApplicationDbContext.cs +++ b/src/Analysim.Infrastructure/Data/ApplicationDbContext.cs @@ -309,6 +309,23 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(pp => pp.ProjectID) .OnDelete(DeleteBehavior.Cascade); + // One recommendation per user per project + modelBuilder.Entity() + .HasIndex(pr => new { pr.UserID, pr.ProjectID }) + .IsUnique(); + + modelBuilder.Entity() + .HasMany(p => p.ProjectRecommendations) + .WithOne(pr => pr.Project) + .HasForeignKey(pr => pr.ProjectID) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasMany(u => u.ProjectRecommendations) + .WithOne(pr => pr.User) + .HasForeignKey(pr => pr.UserID) + .OnDelete(DeleteBehavior.Cascade); + // One To Many Relationship (Project -> ProjectMembershipRequest) modelBuilder.Entity() .HasMany(p => p.ProjectMembershipRequests) @@ -360,6 +377,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) public DbSet ProjectLogs { get; set; } public DbSet Publications { get; set; } + public DbSet ProjectRecommendations { get; set; } public DbSet ProjectMembershipRequests { get; set; } public DbSet Notifications { get; set; } public DbSet UserNotificationPreferences { get; set; } diff --git a/src/Analysim.Infrastructure/Infrastructure/Migrations/20260906231118_AddProjectRecommendations.Designer.cs b/src/Analysim.Infrastructure/Infrastructure/Migrations/20260906231118_AddProjectRecommendations.Designer.cs new file mode 100644 index 00000000..ab46dcff --- /dev/null +++ b/src/Analysim.Infrastructure/Infrastructure/Migrations/20260906231118_AddProjectRecommendations.Designer.cs @@ -0,0 +1,1246 @@ +// +using System; +using Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260906231118_AddProjectRecommendations")] + partial class AddProjectRecommendations + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Analysim.Core.Entities.BlobFileContent", b => + { + b.Property("BlobFileID") + .HasColumnType("integer"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("BlobFileID"); + + b.ToTable("BlobFileContent"); + }); + + modelBuilder.Entity("Analysim.Core.Entities.NotebookContent", b => + { + b.Property("NotebookID") + .HasColumnType("integer") + .HasColumnOrder(1); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnOrder(2); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Size") + .HasColumnType("integer"); + + b.HasKey("NotebookID", "Version"); + + b.ToTable("NotebookContent"); + }); + + modelBuilder.Entity("Core.Entities.BlobFile", b => + { + b.Property("BlobFileID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BlobFileID")); + + b.Property("Container") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Directory") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extension") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("Size") + .HasColumnType("integer"); + + b.Property("Uri") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.HasKey("BlobFileID"); + + b.HasIndex("ProjectID"); + + b.HasIndex("UserID"); + + b.ToTable("BlobFiles"); + }); + + modelBuilder.Entity("Core.Entities.Notebook", b => + { + b.Property("NotebookID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("NotebookID")); + + b.Property("Container") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Directory") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extension") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("Route") + .IsRequired() + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("integer"); + + b.Property("Uri") + .IsRequired() + .HasColumnType("text"); + + b.Property("type") + .HasColumnType("text"); + + b.HasKey("NotebookID"); + + b.HasIndex("Directory"); + + b.HasIndex("ProjectID"); + + b.ToTable("Notebook"); + }); + + modelBuilder.Entity("Core.Entities.ObservableNotebookDataset", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); + + b.Property("BlobFileID") + .HasColumnType("integer"); + + b.Property("NotebookID") + .HasColumnType("integer"); + + b.Property("datasetName") + .HasColumnType("text"); + + b.Property("datasetURL") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("NotebookID"); + + b.ToTable("ObservableNotebookDataset"); + }); + + modelBuilder.Entity("Core.Entities.Project", b => + { + b.Property("ProjectID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ProjectID")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ForkedFromProjectID") + .HasColumnType("integer"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Route") + .IsRequired() + .HasColumnType("text"); + + b.Property("Visibility") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ProjectID"); + + b.HasIndex("Route") + .IsUnique(); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("Core.Entities.ProjectComment", b => + { + b.Property("CommentID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CommentID")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsPendingReview") + .HasColumnType("boolean"); + + b.Property("ParentCommentID") + .HasColumnType("integer"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("ProjectLogID") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.HasKey("CommentID"); + + b.HasIndex("ParentCommentID"); + + b.HasIndex("ProjectID"); + + b.HasIndex("ProjectLogID"); + + b.HasIndex("UserID"); + + b.ToTable("ProjectComments"); + }); + + modelBuilder.Entity("Core.Entities.ProjectCommentFlag", b => + { + b.Property("FlagID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("FlagID")); + + b.Property("CommentContentSnapshot") + .HasColumnType("text"); + + b.Property("CommentID") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.HasKey("FlagID"); + + b.HasIndex("UserID"); + + b.HasIndex("CommentID", "UserID") + .IsUnique(); + + b.ToTable("ProjectCommentFlags"); + }); + + modelBuilder.Entity("Core.Entities.ProjectCommentLike", b => + { + b.Property("CommentID") + .HasColumnType("integer"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CommentID", "UserID"); + + b.HasIndex("UserID"); + + b.ToTable("ProjectCommentLikes"); + }); + + modelBuilder.Entity("Core.Entities.ProjectLog", b => + { + b.Property("LogID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("LogID")); + + b.Property("BlobFileID") + .HasColumnType("integer"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("ReferencedNotebookID") + .HasColumnType("integer"); + + b.Property("ReferencedNotebookVersion") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.HasKey("LogID"); + + b.HasIndex("BlobFileID"); + + b.HasIndex("ProjectID"); + + b.HasIndex("ReferencedNotebookID"); + + b.HasIndex("UserID"); + + b.ToTable("ProjectLogs"); + }); + + modelBuilder.Entity("Core.Entities.ProjectRecommendation", b => + { + b.Property("ProjectRecommendationID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ProjectRecommendationID")); + + b.Property("Comment") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.HasKey("ProjectRecommendationID"); + + b.HasIndex("ProjectID"); + + b.HasIndex("UserID", "ProjectID") + .IsUnique(); + + b.ToTable("ProjectRecommendations"); + }); + + modelBuilder.Entity("Core.Entities.ProjectTag", b => + { + b.Property("ProjectID") + .HasColumnType("integer") + .HasColumnOrder(2); + + b.Property("TagID") + .HasColumnType("integer") + .HasColumnOrder(1); + + b.HasKey("ProjectID", "TagID"); + + b.HasIndex("TagID"); + + b.ToTable("ProjectTags"); + }); + + modelBuilder.Entity("Core.Entities.ProjectUser", b => + { + b.Property("UserID") + .HasColumnType("integer") + .HasColumnOrder(1); + + b.Property("ProjectID") + .HasColumnType("integer") + .HasColumnOrder(2); + + b.Property("IsFollowing") + .HasColumnType("boolean"); + + b.Property("UserRole") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("UserID", "ProjectID"); + + b.HasIndex("ProjectID"); + + b.ToTable("ProjectUsers"); + }); + + modelBuilder.Entity("Core.Entities.Publication", b => + { + b.Property("PublicationID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PublicationID")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Doi") + .HasColumnType("text"); + + b.Property("Issue") + .HasColumnType("text"); + + b.Property("Journal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Pages") + .HasColumnType("text"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("SourceAuthor") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("Volume") + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("PublicationID"); + + b.HasIndex("ProjectID"); + + b.ToTable("Publications"); + }); + + modelBuilder.Entity("Core.Entities.RefreshToken", b => + { + b.Property("RefreshTokenID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RefreshTokenID")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByIp") + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplacedByTokenHash") + .HasColumnType("text"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedByIp") + .HasColumnType("text"); + + b.Property("RevokedReason") + .HasColumnType("text"); + + b.Property("TokenFamilyID") + .HasColumnType("uuid"); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.HasKey("RefreshTokenID"); + + b.HasIndex("TokenFamilyID"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("UserID"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Core.Entities.Tag", b => + { + b.Property("TagID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TagID")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("TagID"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("Core.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("Bio") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastOnline") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("ReceiveCommentReplyEmails") + .HasColumnType("boolean"); + + b.Property("RegistrationSurvey") + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Core.Entities.UserUser", b => + { + b.Property("UserID") + .HasColumnType("integer") + .HasColumnOrder(1); + + b.Property("FollowerID") + .HasColumnType("integer") + .HasColumnOrder(2); + + b.HasKey("UserID", "FollowerID"); + + b.HasIndex("FollowerID"); + + b.ToTable("UserUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + + b.HasData( + new + { + Id = 1, + ConcurrencyStamp = "d7c6278e-3f78-4db6-8832-aa41ea9fd62c", + Name = "Admin", + NormalizedName = "ADMIN" + }, + new + { + Id = 2, + ConcurrencyStamp = "7383e31e-a884-4cd6-82f7-a6287b4e30a5", + Name = "Customer", + NormalizedName = "CUSTOMER" + }, + new + { + Id = 3, + ConcurrencyStamp = "398ef1f2-6b82-45f2-9601-ed2129f197e9", + Name = "Moderator", + NormalizedName = "MODERATOR" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Analysim.Core.Entities.BlobFileContent", b => + { + b.HasOne("Core.Entities.BlobFile", "BlobFile") + .WithMany("BlobFileContents") + .HasForeignKey("BlobFileID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlobFile"); + }); + + modelBuilder.Entity("Analysim.Core.Entities.NotebookContent", b => + { + b.HasOne("Core.Entities.Notebook", "Notebook") + .WithMany("NotebookContents") + .HasForeignKey("NotebookID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notebook"); + }); + + modelBuilder.Entity("Core.Entities.BlobFile", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("BlobFiles") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Core.Entities.User", "User") + .WithMany("BlobFiles") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.Notebook", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("Notebooks") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Core.Entities.ObservableNotebookDataset", b => + { + b.HasOne("Core.Entities.Notebook", "notebook") + .WithMany("observableNotebookDatasets") + .HasForeignKey("NotebookID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("notebook"); + }); + + modelBuilder.Entity("Core.Entities.ProjectComment", b => + { + b.HasOne("Core.Entities.ProjectComment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentID") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Core.Entities.Project", "Project") + .WithMany("ProjectComments") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.ProjectLog", "ProjectLog") + .WithMany("Comments") + .HasForeignKey("ProjectLogID") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Core.Entities.User", "User") + .WithMany("ProjectComments") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ParentComment"); + + b.Navigation("Project"); + + b.Navigation("ProjectLog"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.ProjectCommentFlag", b => + { + b.HasOne("Core.Entities.ProjectComment", "ProjectComment") + .WithMany("CommentFlags") + .HasForeignKey("CommentID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("CommentFlags") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProjectComment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.ProjectCommentLike", b => + { + b.HasOne("Core.Entities.ProjectComment", "ProjectComment") + .WithMany("CommentLikes") + .HasForeignKey("CommentID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProjectComment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.ProjectLog", b => + { + b.HasOne("Core.Entities.BlobFile", "BlobFile") + .WithMany() + .HasForeignKey("BlobFileID") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Core.Entities.Project", "Project") + .WithMany("ProjectLogs") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.Notebook", "ReferencedNotebook") + .WithMany() + .HasForeignKey("ReferencedNotebookID") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Core.Entities.User", "User") + .WithMany("ProjectLogs") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BlobFile"); + + b.Navigation("Project"); + + b.Navigation("ReferencedNotebook"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.ProjectRecommendation", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("ProjectRecommendations") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("ProjectRecommendations") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.ProjectTag", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("ProjectTags") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.Tag", "Tag") + .WithMany("ProjectTags") + .HasForeignKey("TagID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Core.Entities.ProjectUser", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("ProjectUsers") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("ProjectUsers") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.Publication", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("Publications") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Core.Entities.RefreshToken", b => + { + b.HasOne("Core.Entities.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.UserUser", b => + { + b.HasOne("Core.Entities.User", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("Followers") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Follower"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Core.Entities.BlobFile", b => + { + b.Navigation("BlobFileContents"); + }); + + modelBuilder.Entity("Core.Entities.Notebook", b => + { + b.Navigation("NotebookContents"); + + b.Navigation("observableNotebookDatasets"); + }); + + modelBuilder.Entity("Core.Entities.Project", b => + { + b.Navigation("BlobFiles"); + + b.Navigation("Notebooks"); + + b.Navigation("ProjectComments"); + + b.Navigation("ProjectLogs"); + + b.Navigation("ProjectRecommendations"); + + b.Navigation("ProjectTags"); + + b.Navigation("ProjectUsers"); + + b.Navigation("Publications"); + }); + + modelBuilder.Entity("Core.Entities.ProjectComment", b => + { + b.Navigation("CommentFlags"); + + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("Core.Entities.ProjectLog", b => + { + b.Navigation("Comments"); + }); + + modelBuilder.Entity("Core.Entities.Tag", b => + { + b.Navigation("ProjectTags"); + }); + + modelBuilder.Entity("Core.Entities.User", b => + { + b.Navigation("BlobFiles"); + + b.Navigation("CommentFlags"); + + b.Navigation("CommentLikes"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("ProjectComments"); + + b.Navigation("ProjectLogs"); + + b.Navigation("ProjectRecommendations"); + + b.Navigation("ProjectUsers"); + + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Analysim.Infrastructure/Infrastructure/Migrations/20260906231118_AddProjectRecommendations.cs b/src/Analysim.Infrastructure/Infrastructure/Migrations/20260906231118_AddProjectRecommendations.cs new file mode 100644 index 00000000..e200916a --- /dev/null +++ b/src/Analysim.Infrastructure/Infrastructure/Migrations/20260906231118_AddProjectRecommendations.cs @@ -0,0 +1,63 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class AddProjectRecommendations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ProjectRecommendations", + columns: table => new + { + ProjectRecommendationID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectID = table.Column(type: "integer", nullable: false), + UserID = table.Column(type: "integer", nullable: false), + Comment = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProjectRecommendations", x => x.ProjectRecommendationID); + table.ForeignKey( + name: "FK_ProjectRecommendations_AspNetUsers_UserID", + column: x => x.UserID, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ProjectRecommendations_Projects_ProjectID", + column: x => x.ProjectID, + principalTable: "Projects", + principalColumn: "ProjectID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ProjectRecommendations_ProjectID", + table: "ProjectRecommendations", + column: "ProjectID"); + + migrationBuilder.CreateIndex( + name: "IX_ProjectRecommendations_UserID_ProjectID", + table: "ProjectRecommendations", + columns: new[] { "UserID", "ProjectID" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ProjectRecommendations"); + } + } +} diff --git a/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 16a22f52..cfadeaf8 100644 --- a/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -468,6 +468,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ProjectLogs"); }); + modelBuilder.Entity("Core.Entities.ProjectRecommendation", b => + { + b.Property("ProjectRecommendationID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ProjectRecommendationID")); + + b.Property("Comment") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.HasKey("ProjectRecommendationID"); + + b.HasIndex("ProjectID"); + + b.HasIndex("UserID", "ProjectID") + .IsUnique(); + + b.ToTable("ProjectRecommendations"); + }); + modelBuilder.Entity("Core.Entities.ProjectTag", b => { b.Property("ProjectID") @@ -1164,6 +1199,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Core.Entities.ProjectRecommendation", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("ProjectRecommendations") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("ProjectRecommendations") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Core.Entities.ProjectTag", b => { b.HasOne("Core.Entities.Project", "Project") @@ -1362,6 +1416,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("ProjectLogs"); + b.Navigation("ProjectRecommendations"); + b.Navigation("ProjectMembershipRequests"); b.Navigation("ProjectTags"); @@ -1406,6 +1462,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("ProjectLogs"); + b.Navigation("ProjectRecommendations"); + b.Navigation("ProjectUsers"); b.Navigation("RefreshTokens"); diff --git a/src/Analysim.Web/ClientApp/src/app/interfaces/project-recommendation.ts b/src/Analysim.Web/ClientApp/src/app/interfaces/project-recommendation.ts new file mode 100644 index 00000000..a2cf78ad --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/interfaces/project-recommendation.ts @@ -0,0 +1,9 @@ +export interface ProjectRecommendation { + projectRecommendationID: number; + userID: number; + authorName: string; + projectID: number; + comment: string; + createdAt: Date; + updatedAt: Date; +} diff --git a/src/Analysim.Web/ClientApp/src/app/interfaces/project.ts b/src/Analysim.Web/ClientApp/src/app/interfaces/project.ts index f1b3c355..ed474ae2 100644 --- a/src/Analysim.Web/ClientApp/src/app/interfaces/project.ts +++ b/src/Analysim.Web/ClientApp/src/app/interfaces/project.ts @@ -2,6 +2,7 @@ import { BlobFile } from 'src/app/interfaces/blob-file'; import { ProjectTag } from 'src/app/interfaces/project-tag'; import { ProjectUser } from './project-user'; import { Notebook } from './notebook'; +import { ProjectRecommendation } from './project-recommendation'; export interface Project { projectID: number; @@ -15,5 +16,6 @@ export interface Project { projectUsers: Array; blobFiles: Array; projectTags: Array + projectRecommendations: Array forkedFromProjectID: number; -} \ No newline at end of file +} diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project/modal-recommend/modal-recommend.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project/modal-recommend/modal-recommend.component.html new file mode 100644 index 00000000..7e4a1575 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/projects/project/modal-recommend/modal-recommend.component.html @@ -0,0 +1,67 @@ + diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project/modal-recommend/modal-recommend.component.scss b/src/Analysim.Web/ClientApp/src/app/projects/project/modal-recommend/modal-recommend.component.scss new file mode 100644 index 00000000..e0a7ed49 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/projects/project/modal-recommend/modal-recommend.component.scss @@ -0,0 +1,26 @@ +.recommend-modal { + .modal-body textarea { + resize: vertical; + } + + .recommendation-list { + max-height: 220px; + overflow-y: auto; + } + + .recommendation-item { + margin-bottom: var(--space-3); + } + + .recommendation-author { + font-weight: 600; + } + + .recommendation-comment { + white-space: pre-wrap; + } + + .recommendation-empty { + color: var(--gray); + } +} diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project/modal-recommend/modal-recommend.component.ts b/src/Analysim.Web/ClientApp/src/app/projects/project/modal-recommend/modal-recommend.component.ts new file mode 100644 index 00000000..e59f2b1f --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/projects/project/modal-recommend/modal-recommend.component.ts @@ -0,0 +1,112 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { Project } from 'src/app/interfaces/project'; +import { ProjectRecommendation } from 'src/app/interfaces/project-recommendation'; +import { User } from 'src/app/interfaces/user'; +import { ProjectService } from 'src/app/services/project.service'; + +@Component({ + selector: 'app-modal-recommend', + templateUrl: './modal-recommend.component.html', + styleUrls: ['./modal-recommend.component.scss'] +}) +export class ModalRecommendComponent implements OnInit { + constructor(private projectService: ProjectService) { } + + @Input() recommendModalRef: BsModalRef; + @Input() project: Project; + @Input() currentUser: User; + @Input() recommendation: ProjectRecommendation; + @Input() isOwner: boolean; + @Input() allowAction: boolean = true; + + @Output() recommendationSaved = new EventEmitter(); + @Output() recommendationRemoved = new EventEmitter(); + + comment: string = ''; + recommendations: ProjectRecommendation[] = []; + isLoading: boolean = false; + errorResult: string = ''; + errorStatusAlert: boolean = false; + + ngOnInit(): void { + this.comment = this.recommendation?.comment || ''; + this.loadRecommendations(); + } + + get canEditRecommendation(): boolean { + return this.allowAction && !this.isOwner; + } + + closeModal() { + this.recommendModalRef.hide(); + } + + saveRecommendation() { + if (!this.canEditRecommendation) return; + + const comment = this.comment.trim(); + if (!comment) { + this.handleError('Recommendation comment is required.'); + return; + } + + this.isLoading = true; + this.errorStatusAlert = false; + + const request = this.recommendation + ? this.projectService.updateRecommendation(this.recommendation.projectRecommendationID, comment) + : this.projectService.recommendProject(this.project.projectID, comment); + + request.subscribe(result => { + this.recommendationSaved.emit(result); + this.upsertRecommendation(result); + this.isLoading = false; + this.closeModal(); + }, error => { + console.log(error); + this.handleError(error?.error?.message || 'Unable to save recommendation.'); + }); + } + + removeRecommendation() { + if (!this.canEditRecommendation) return; + if (!this.recommendation) return; + + this.isLoading = true; + this.errorStatusAlert = false; + + this.projectService.unrecommendProject(this.project.projectID).subscribe(result => { + this.recommendationRemoved.emit(result); + this.recommendations = this.recommendations.filter(x => x.projectRecommendationID != result.projectRecommendationID); + this.isLoading = false; + this.closeModal(); + }, error => { + console.log(error); + this.handleError(error?.error?.message || 'Unable to remove recommendation.'); + }); + } + + handleError(text: string) { + this.errorResult = text; + this.errorStatusAlert = true; + this.isLoading = false; + } + + loadRecommendations() { + this.projectService.getProjectRecommendations(this.project.projectID).subscribe(result => { + this.recommendations = result; + }, error => { + console.log(error); + }); + } + + upsertRecommendation(recommendation: ProjectRecommendation) { + let index = this.recommendations.findIndex(x => x.projectRecommendationID == recommendation.projectRecommendationID); + if (index > -1) { + this.recommendations[index] = recommendation; + } else { + this.recommendations.unshift(recommendation); + } + } +} diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project/project-stat-counter/project-stat-counter.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project/project-stat-counter/project-stat-counter.component.html index 1f79dd0a..947da68a 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project/project-stat-counter/project-stat-counter.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project/project-stat-counter/project-stat-counter.component.html @@ -7,10 +7,17 @@
-
Recommendation
+ +
0 new
-
0
+
{{ project.projectRecommendations?.length || 0 }}
@@ -48,4 +55,15 @@ [currentUser]="currentUser" > - \ No newline at end of file + + + + + + diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project/project-stat-counter/project-stat-counter.component.ts b/src/Analysim.Web/ClientApp/src/app/projects/project/project-stat-counter/project-stat-counter.component.ts index 0230a224..3e0cc3be 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project/project-stat-counter/project-stat-counter.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/projects/project/project-stat-counter/project-stat-counter.component.ts @@ -1,6 +1,7 @@ import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; import { Project } from 'src/app/interfaces/project'; +import { ProjectRecommendation } from 'src/app/interfaces/project-recommendation'; import { User } from 'src/app/interfaces/user'; @Component({ @@ -13,8 +14,10 @@ export class ProjectStatCounterComponent implements OnInit { @Input() currentUser: User; @ViewChild('memberListModal') memberListModal: TemplateRef + @ViewChild('recommendationListModal') recommendationListModal: TemplateRef memberListModalRef: BsModalRef; + recommendationListModalRef: BsModalRef; constructor(private modalService: BsModalService) { } @@ -25,4 +28,20 @@ export class ProjectStatCounterComponent implements OnInit { this.memberListModalRef = this.modalService.show(this.memberListModal) } + toggleModalRecommendationList() { + this.recommendationListModalRef = this.modalService.show(this.recommendationListModal) + } + + get currentRecommendation(): ProjectRecommendation { + if (this.currentUser == null) return null + if (this.project == null || this.project.projectRecommendations == null) return null + return this.project.projectRecommendations.find(x => x.userID == this.currentUser.id) || null + } + + get isOwner(): boolean { + if (this.currentUser == null || this.project == null || this.project.projectUsers == null) return false + + const projectUser = this.project.projectUsers.find(x => x.userID == this.currentUser.id) + return projectUser?.userRole == 'owner' + } } diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project/project.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project/project.component.html index c0d89cf2..cd77ba30 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project/project.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project/project.component.html @@ -113,7 +113,16 @@