From cf3cb50e1a56aa0cb3521d0f5ee3825d124cc9e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:37:38 +0300 Subject: [PATCH] =?UTF-8?q?Classroom=20:=20B=C3=BCy=C3=BCk=20g=C3=BCncelle?= =?UTF-8?q?mee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ClassChatDto.cs => ClassroomChatDto.cs} | 5 +- .../Classroom/ClassroomDto.cs | 2 +- ...ipantDto.cs => ClassroomParticipantDto.cs} | 4 +- .../Classroom/IClassroomAppService.cs | 2 +- .../Classroom/ClassroomAppService.cs | 17 +- .../Classroom/ClassroomAutoMapperProfile.cs | 6 +- .../Classroom/Classroom.cs | 18 +- ...ssAttandance.cs => ClassroomAttandance.cs} | 6 +- .../{ClassChat.cs => ClassroomChat.cs} | 17 +- ...Participant.cs => ClassroomParticipant.cs} | 15 +- .../EntityFrameworkCore/PlatformDbContext.cs | 19 +- ....cs => 20250829093104_Initial.Designer.cs} | 442 +++--- ...3_Initial.cs => 20250829093104_Initial.cs} | 123 +- .../PlatformDbContextModelSnapshot.cs | 440 +++--- .../Classroom/ClassroomHub.cs | 367 +++-- ui/dev-dist/sw.js | 2 +- .../classroom/Panels/AttendancePanel.tsx | 4 +- .../components/classroom/Panels/ChatPanel.tsx | 4 +- .../components/classroom/ParticipantGrid.tsx | 10 +- ui/src/proxy/classroom/models.ts | 15 +- ui/src/services/classroom/signalr.ts | 92 +- ui/src/utils/hooks/useClassroomLogic.ts | 2 +- ui/src/views/classroom/ClassList.tsx | 1219 +++++++++-------- ui/src/views/classroom/Dashboard.tsx | 105 +- ui/src/views/classroom/RoomDetail.tsx | 1076 ++++++++------- 25 files changed, 2156 insertions(+), 1856 deletions(-) rename api/src/Kurs.Platform.Application.Contracts/Classroom/{ClassChatDto.cs => ClassroomChatDto.cs} (67%) rename api/src/Kurs.Platform.Application.Contracts/Classroom/{ClassParticipantDto.cs => ClassroomParticipantDto.cs} (82%) rename api/src/Kurs.Platform.Domain/Classroom/{ClassAttandance.cs => ClassroomAttandance.cs} (89%) rename api/src/Kurs.Platform.Domain/Classroom/{ClassChat.cs => ClassroomChat.cs} (62%) rename api/src/Kurs.Platform.Domain/Classroom/{ClassParticipant.cs => ClassroomParticipant.cs} (78%) rename api/src/Kurs.Platform.EntityFrameworkCore/Migrations/{20250828112303_Initial.Designer.cs => 20250829093104_Initial.Designer.cs} (99%) rename api/src/Kurs.Platform.EntityFrameworkCore/Migrations/{20250828112303_Initial.cs => 20250829093104_Initial.cs} (99%) diff --git a/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassChatDto.cs b/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomChatDto.cs similarity index 67% rename from api/src/Kurs.Platform.Application.Contracts/Classroom/ClassChatDto.cs rename to api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomChatDto.cs index 4f184df4..a5321fc1 100644 --- a/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassChatDto.cs +++ b/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomChatDto.cs @@ -2,7 +2,7 @@ using System; namespace Kurs.Platform.Classrooms; -public class ClassChatDto +public class ClassroomChatDto { public Guid Id { get; set; } public Guid SessionId { get; set; } @@ -10,5 +10,8 @@ public class ClassChatDto public string SenderName { get; set; } public string Message { get; set; } public DateTime Timestamp { get; set; } + public Guid? RecipientId { get; set; } + public string? RecipientName { get; set; } public bool IsTeacher { get; set; } + public string MessageType { get; set; } } \ No newline at end of file diff --git a/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomDto.cs b/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomDto.cs index 6ea46063..3d2891b8 100644 --- a/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomDto.cs +++ b/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomDto.cs @@ -53,7 +53,7 @@ public class GetClassroomListDto : PagedAndSortedResultRequestDto public Guid? TeacherId { get; set; } } -public class ClassAttendanceDto : EntityDto +public class ClassroomAttendanceDto : EntityDto { public Guid SessionId { get; set; } public Guid StudentId { get; set; } diff --git a/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassParticipantDto.cs b/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomParticipantDto.cs similarity index 82% rename from api/src/Kurs.Platform.Application.Contracts/Classroom/ClassParticipantDto.cs rename to api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomParticipantDto.cs index fbc748a0..e6923318 100644 --- a/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassParticipantDto.cs +++ b/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomParticipantDto.cs @@ -2,15 +2,15 @@ using System; namespace Kurs.Platform.Classrooms; -public class ClassParticipantDto +public class ClassroomParticipantDto { public Guid Id { get; set; } public Guid SessionId { get; set; } public Guid UserId { get; set; } public string UserName { get; set; } - public string UserEmail { get; set; } public bool IsTeacher { get; set; } public bool IsAudioMuted { get; set; } public bool IsVideoMuted { get; set; } + public bool IsHandRaised { get; set; } public DateTime JoinTime { get; set; } } diff --git a/api/src/Kurs.Platform.Application.Contracts/Classroom/IClassroomAppService.cs b/api/src/Kurs.Platform.Application.Contracts/Classroom/IClassroomAppService.cs index 010a6bdf..15b0e245 100644 --- a/api/src/Kurs.Platform.Application.Contracts/Classroom/IClassroomAppService.cs +++ b/api/src/Kurs.Platform.Application.Contracts/Classroom/IClassroomAppService.cs @@ -17,5 +17,5 @@ public interface IClassroomAppService : IApplicationService Task EndClassAsync(Guid id); Task JoinClassAsync(Guid id); Task LeaveClassAsync(Guid id); - Task> GetAttendanceAsync(Guid sessionId); + Task> GetAttendanceAsync(Guid sessionId); } \ No newline at end of file diff --git a/api/src/Kurs.Platform.Application/Classroom/ClassroomAppService.cs b/api/src/Kurs.Platform.Application/Classroom/ClassroomAppService.cs index af493eaa..900a2e9b 100644 --- a/api/src/Kurs.Platform.Application/Classroom/ClassroomAppService.cs +++ b/api/src/Kurs.Platform.Application/Classroom/ClassroomAppService.cs @@ -15,13 +15,13 @@ namespace Kurs.Platform.Classrooms; public class ClassroomAppService : PlatformAppService, IClassroomAppService { private readonly IRepository _classSessionRepository; - private readonly IRepository _participantRepository; - private readonly IRepository _attendanceRepository; + private readonly IRepository _participantRepository; + private readonly IRepository _attendanceRepository; public ClassroomAppService( IRepository classSessionRepository, - IRepository participantRepository, - IRepository attendanceRepository) + IRepository participantRepository, + IRepository attendanceRepository) { _classSessionRepository = classSessionRepository; _participantRepository = participantRepository; @@ -198,19 +198,18 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService if (existingParticipant == null) { // Add participant - var participant = new ClassParticipant( + var participant = new ClassroomParticipant( GuidGenerator.Create(), id, CurrentUser.Id, CurrentUser.Name, - CurrentUser.Email, false // isTeacher ); await _participantRepository.InsertAsync(participant); // Create attendance record - var attendance = new ClassAttandance( + var attendance = new ClassroomAttandance( GuidGenerator.Create(), id, CurrentUser.Id, @@ -257,7 +256,7 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService } } - public async Task> GetAttendanceAsync(Guid sessionId) + public async Task> GetAttendanceAsync(Guid sessionId) { var classSession = await _classSessionRepository.GetAsync(sessionId); @@ -270,6 +269,6 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService x => x.SessionId == sessionId ); - return ObjectMapper.Map, List>(attendanceRecords); + return ObjectMapper.Map, List>(attendanceRecords); } } \ No newline at end of file diff --git a/api/src/Kurs.Platform.Application/Classroom/ClassroomAutoMapperProfile.cs b/api/src/Kurs.Platform.Application/Classroom/ClassroomAutoMapperProfile.cs index 59d62e87..64b9fa25 100644 --- a/api/src/Kurs.Platform.Application/Classroom/ClassroomAutoMapperProfile.cs +++ b/api/src/Kurs.Platform.Application/Classroom/ClassroomAutoMapperProfile.cs @@ -8,8 +8,8 @@ public class ClassroomAutoMapperProfile : Profile public ClassroomAutoMapperProfile() { CreateMap(); - CreateMap(); - CreateMap(); - CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); } } diff --git a/api/src/Kurs.Platform.Domain/Classroom/Classroom.cs b/api/src/Kurs.Platform.Domain/Classroom/Classroom.cs index e03dc70c..4c1ce9a3 100644 --- a/api/src/Kurs.Platform.Domain/Classroom/Classroom.cs +++ b/api/src/Kurs.Platform.Domain/Classroom/Classroom.cs @@ -20,15 +20,15 @@ public class Classroom : FullAuditedEntity public int ParticipantCount { get; set; } public string SettingsJson { get; set; } - public virtual ICollection Participants { get; set; } - public virtual ICollection AttendanceRecords { get; set; } - public virtual ICollection ChatMessages { get; set; } + public virtual ICollection Participants { get; set; } + public virtual ICollection AttendanceRecords { get; set; } + public virtual ICollection ChatMessages { get; set; } protected Classroom() { - Participants = new HashSet(); - AttendanceRecords = new HashSet(); - ChatMessages = new HashSet(); + Participants = new HashSet(); + AttendanceRecords = new HashSet(); + ChatMessages = new HashSet(); } public Classroom( @@ -56,8 +56,8 @@ public class Classroom : FullAuditedEntity MaxParticipants = maxParticipants; SettingsJson = settingsJson; - Participants = new HashSet(); - AttendanceRecords = new HashSet(); - ChatMessages = new HashSet(); + Participants = new HashSet(); + AttendanceRecords = new HashSet(); + ChatMessages = new HashSet(); } } diff --git a/api/src/Kurs.Platform.Domain/Classroom/ClassAttandance.cs b/api/src/Kurs.Platform.Domain/Classroom/ClassroomAttandance.cs similarity index 89% rename from api/src/Kurs.Platform.Domain/Classroom/ClassAttandance.cs rename to api/src/Kurs.Platform.Domain/Classroom/ClassroomAttandance.cs index 4dc50c63..270394e1 100644 --- a/api/src/Kurs.Platform.Domain/Classroom/ClassAttandance.cs +++ b/api/src/Kurs.Platform.Domain/Classroom/ClassroomAttandance.cs @@ -3,7 +3,7 @@ using Volo.Abp.Domain.Entities.Auditing; namespace Kurs.Platform.Entities; -public class ClassAttandance : FullAuditedEntity +public class ClassroomAttandance : FullAuditedEntity { public Guid SessionId { get; set; } public Guid? StudentId { get; set; } @@ -15,11 +15,11 @@ public class ClassAttandance : FullAuditedEntity // Navigation properties public virtual Classroom Session { get; set; } - protected ClassAttandance() + protected ClassroomAttandance() { } - public ClassAttandance( + public ClassroomAttandance( Guid id, Guid sessionId, Guid? studentId, diff --git a/api/src/Kurs.Platform.Domain/Classroom/ClassChat.cs b/api/src/Kurs.Platform.Domain/Classroom/ClassroomChat.cs similarity index 62% rename from api/src/Kurs.Platform.Domain/Classroom/ClassChat.cs rename to api/src/Kurs.Platform.Domain/Classroom/ClassroomChat.cs index eede4a86..4efbc63d 100644 --- a/api/src/Kurs.Platform.Domain/Classroom/ClassChat.cs +++ b/api/src/Kurs.Platform.Domain/Classroom/ClassroomChat.cs @@ -3,36 +3,45 @@ using Volo.Abp.Domain.Entities.Auditing; namespace Kurs.Platform.Entities; -public class ClassChat : FullAuditedEntity +public class ClassroomChat : FullAuditedEntity { public Guid SessionId { get; set; } public Guid? SenderId { get; set; } public string SenderName { get; set; } public string Message { get; set; } public DateTime Timestamp { get; set; } + public Guid? RecipientId { get; set; } + public string? RecipientName { get; set; } public bool IsTeacher { get; set; } + public string MessageType { get; set; } // Navigation properties public virtual Classroom Session { get; set; } - protected ClassChat() + protected ClassroomChat() { } - public ClassChat( + public ClassroomChat( Guid id, Guid sessionId, Guid? senderId, string senderName, string message, - bool isTeacher + Guid? recipientId, + string recipientName, + bool isTeacher, + string messageType ) : base(id) { SessionId = sessionId; SenderId = senderId; SenderName = senderName; Message = message; + RecipientId = recipientId; + RecipientName = recipientName; IsTeacher = isTeacher; Timestamp = DateTime.UtcNow; + MessageType = messageType; } } diff --git a/api/src/Kurs.Platform.Domain/Classroom/ClassParticipant.cs b/api/src/Kurs.Platform.Domain/Classroom/ClassroomParticipant.cs similarity index 78% rename from api/src/Kurs.Platform.Domain/Classroom/ClassParticipant.cs rename to api/src/Kurs.Platform.Domain/Classroom/ClassroomParticipant.cs index 54847285..0afd26ad 100644 --- a/api/src/Kurs.Platform.Domain/Classroom/ClassParticipant.cs +++ b/api/src/Kurs.Platform.Domain/Classroom/ClassroomParticipant.cs @@ -3,41 +3,40 @@ using Volo.Abp.Domain.Entities.Auditing; namespace Kurs.Platform.Entities; -public class ClassParticipant : FullAuditedEntity +public class ClassroomParticipant : FullAuditedEntity { public Guid SessionId { get; set; } public Guid? UserId { get; set; } public string UserName { get; set; } - public string UserEmail { get; set; } public bool IsTeacher { get; set; } - public bool IsAudioMuted { get; set; } - public bool IsVideoMuted { get; set; } + public bool IsAudioMuted { get; set; } = false; + public bool IsVideoMuted { get; set; } = false; + public bool IsHandRaised { get; set; } = false; public DateTime JoinTime { get; set; } public string ConnectionId { get; set; } // Navigation properties public virtual Classroom Session { get; set; } - protected ClassParticipant() + protected ClassroomParticipant() { } - public ClassParticipant( + public ClassroomParticipant( Guid id, Guid sessionId, Guid? userId, string userName, - string userEmail, bool isTeacher ) : base(id) { SessionId = sessionId; UserId = userId; UserName = userName; - UserEmail = userEmail; IsTeacher = isTeacher; IsAudioMuted = false; IsVideoMuted = false; + IsHandRaised = false; JoinTime = DateTime.UtcNow; } diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Kurs.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index 21ae8773..83383037 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -98,9 +98,9 @@ public class PlatformDbContext : public DbSet Services { get; set; } public DbSet ClassSessions { get; set; } - public DbSet Participants { get; set; } - public DbSet AttendanceRecords { get; set; } - public DbSet ChatMessages { get; set; } + public DbSet Participants { get; set; } + public DbSet AttendanceRecords { get; set; } + public DbSet ChatMessages { get; set; } #region Entities from the modules @@ -913,13 +913,12 @@ public class PlatformDbContext : }); // Participant - builder.Entity(b => + builder.Entity(b => { - b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassParticipant), PlatformConsts.DbSchema); + b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassroomParticipant), PlatformConsts.DbSchema); b.ConfigureByConvention(); b.Property(x => x.UserName).IsRequired().HasMaxLength(100); - b.Property(x => x.UserEmail).HasMaxLength(200); b.Property(x => x.ConnectionId).HasMaxLength(100); b.HasIndex(x => x.SessionId); @@ -928,9 +927,9 @@ public class PlatformDbContext : }); // AttendanceRecord - builder.Entity(b => + builder.Entity(b => { - b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassAttandance), PlatformConsts.DbSchema); + b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassroomAttandance), PlatformConsts.DbSchema); b.ConfigureByConvention(); b.Property(x => x.StudentName).IsRequired().HasMaxLength(100); @@ -941,9 +940,9 @@ public class PlatformDbContext : }); // ChatMessage - builder.Entity(b => + builder.Entity(b => { - b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassChat), PlatformConsts.DbSchema); + b.ToTable(PlatformConsts.DbTablePrefix + nameof(ClassroomChat), PlatformConsts.DbSchema); b.ConfigureByConvention(); b.Property(x => x.SenderName).IsRequired().HasMaxLength(100); diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250828112303_Initial.Designer.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250829093104_Initial.Designer.cs similarity index 99% rename from api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250828112303_Initial.Designer.cs rename to api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250829093104_Initial.Designer.cs index 9940db03..231a2544 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250828112303_Initial.Designer.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250829093104_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Kurs.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20250828112303_Initial")] + [Migration("20250829093104_Initial")] partial class Initial { /// @@ -1559,219 +1559,6 @@ namespace Kurs.Platform.Migrations b.ToTable("PCity", (string)null); }); - modelBuilder.Entity("Kurs.Platform.Entities.ClassAttandance", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uniqueidentifier") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("datetime2") - .HasColumnName("DeletionTime"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("bit") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("JoinTime") - .HasColumnType("datetime2"); - - b.Property("LastModificationTime") - .HasColumnType("datetime2") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uniqueidentifier") - .HasColumnName("LastModifierId"); - - b.Property("LeaveTime") - .HasColumnType("datetime2"); - - b.Property("SessionId") - .HasColumnType("uniqueidentifier"); - - b.Property("StudentId") - .HasColumnType("uniqueidentifier"); - - b.Property("StudentName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("nvarchar(100)"); - - b.Property("TotalDurationMinutes") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.HasIndex("JoinTime"); - - b.HasIndex("SessionId"); - - b.HasIndex("StudentId"); - - b.ToTable("PClassAttandance", (string)null); - }); - - modelBuilder.Entity("Kurs.Platform.Entities.ClassChat", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uniqueidentifier") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("datetime2") - .HasColumnName("DeletionTime"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("bit") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("IsTeacher") - .HasColumnType("bit"); - - b.Property("LastModificationTime") - .HasColumnType("datetime2") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uniqueidentifier") - .HasColumnName("LastModifierId"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("nvarchar(2000)"); - - b.Property("SenderId") - .HasColumnType("uniqueidentifier"); - - b.Property("SenderName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("nvarchar(100)"); - - b.Property("SessionId") - .HasColumnType("uniqueidentifier"); - - b.Property("Timestamp") - .HasColumnType("datetime2"); - - b.HasKey("Id"); - - b.HasIndex("SenderId"); - - b.HasIndex("SessionId"); - - b.HasIndex("Timestamp"); - - b.ToTable("PClassChat", (string)null); - }); - - modelBuilder.Entity("Kurs.Platform.Entities.ClassParticipant", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("ConnectionId") - .HasMaxLength(100) - .HasColumnType("nvarchar(100)"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uniqueidentifier") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("datetime2") - .HasColumnName("DeletionTime"); - - b.Property("IsAudioMuted") - .HasColumnType("bit"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("bit") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("IsTeacher") - .HasColumnType("bit"); - - b.Property("IsVideoMuted") - .HasColumnType("bit"); - - b.Property("JoinTime") - .HasColumnType("datetime2"); - - b.Property("LastModificationTime") - .HasColumnType("datetime2") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uniqueidentifier") - .HasColumnName("LastModifierId"); - - b.Property("SessionId") - .HasColumnType("uniqueidentifier"); - - b.Property("UserEmail") - .HasMaxLength(200) - .HasColumnType("nvarchar(200)"); - - b.Property("UserId") - .HasColumnType("uniqueidentifier"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("nvarchar(100)"); - - b.HasKey("Id"); - - b.HasIndex("SessionId"); - - b.HasIndex("UserId"); - - b.HasIndex("SessionId", "UserId") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("PClassParticipant", (string)null); - }); - modelBuilder.Entity("Kurs.Platform.Entities.Classroom", b => { b.Property("Id") @@ -1861,6 +1648,227 @@ namespace Kurs.Platform.Migrations b.ToTable("PClassroom", (string)null); }); + modelBuilder.Entity("Kurs.Platform.Entities.ClassroomAttandance", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("JoinTime") + .HasColumnType("datetime2"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("LeaveTime") + .HasColumnType("datetime2"); + + b.Property("SessionId") + .HasColumnType("uniqueidentifier"); + + b.Property("StudentId") + .HasColumnType("uniqueidentifier"); + + b.Property("StudentName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("TotalDurationMinutes") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("JoinTime"); + + b.HasIndex("SessionId"); + + b.HasIndex("StudentId"); + + b.ToTable("PClassroomAttandance", (string)null); + }); + + modelBuilder.Entity("Kurs.Platform.Entities.ClassroomChat", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsTeacher") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipientId") + .HasColumnType("uniqueidentifier"); + + b.Property("RecipientName") + .HasColumnType("nvarchar(max)"); + + b.Property("SenderId") + .HasColumnType("uniqueidentifier"); + + b.Property("SenderName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("SessionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Timestamp") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("SenderId"); + + b.HasIndex("SessionId"); + + b.HasIndex("Timestamp"); + + b.ToTable("PClassroomChat", (string)null); + }); + + modelBuilder.Entity("Kurs.Platform.Entities.ClassroomParticipant", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("ConnectionId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsAudioMuted") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsHandRaised") + .HasColumnType("bit"); + + b.Property("IsTeacher") + .HasColumnType("bit"); + + b.Property("IsVideoMuted") + .HasColumnType("bit"); + + b.Property("JoinTime") + .HasColumnType("datetime2"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("SessionId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("SessionId"); + + b.HasIndex("UserId"); + + b.HasIndex("SessionId", "UserId") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("PClassroomParticipant", (string)null); + }); + modelBuilder.Entity("Kurs.Platform.Entities.Contact", b => { b.Property("Id") @@ -6558,7 +6566,7 @@ namespace Kurs.Platform.Migrations b.Navigation("Country"); }); - modelBuilder.Entity("Kurs.Platform.Entities.ClassAttandance", b => + modelBuilder.Entity("Kurs.Platform.Entities.ClassroomAttandance", b => { b.HasOne("Kurs.Platform.Entities.Classroom", "Session") .WithMany("AttendanceRecords") @@ -6569,7 +6577,7 @@ namespace Kurs.Platform.Migrations b.Navigation("Session"); }); - modelBuilder.Entity("Kurs.Platform.Entities.ClassChat", b => + modelBuilder.Entity("Kurs.Platform.Entities.ClassroomChat", b => { b.HasOne("Kurs.Platform.Entities.Classroom", "Session") .WithMany("ChatMessages") @@ -6580,7 +6588,7 @@ namespace Kurs.Platform.Migrations b.Navigation("Session"); }); - modelBuilder.Entity("Kurs.Platform.Entities.ClassParticipant", b => + modelBuilder.Entity("Kurs.Platform.Entities.ClassroomParticipant", b => { b.HasOne("Kurs.Platform.Entities.Classroom", "Session") .WithMany("Participants") diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250828112303_Initial.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250829093104_Initial.cs similarity index 99% rename from api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250828112303_Initial.cs rename to api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250829093104_Initial.cs index 77da4cd7..33df3eee 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250828112303_Initial.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250829093104_Initial.cs @@ -1922,7 +1922,7 @@ namespace Kurs.Platform.Migrations }); migrationBuilder.CreateTable( - name: "PClassAttandance", + name: "PClassroomAttandance", columns: table => new { Id = table.Column(type: "uniqueidentifier", nullable: false), @@ -1942,9 +1942,9 @@ namespace Kurs.Platform.Migrations }, constraints: table => { - table.PrimaryKey("PK_PClassAttandance", x => x.Id); + table.PrimaryKey("PK_PClassroomAttandance", x => x.Id); table.ForeignKey( - name: "FK_PClassAttandance_PClassroom_SessionId", + name: "FK_PClassroomAttandance_PClassroom_SessionId", column: x => x.SessionId, principalTable: "PClassroom", principalColumn: "Id", @@ -1952,7 +1952,7 @@ namespace Kurs.Platform.Migrations }); migrationBuilder.CreateTable( - name: "PClassChat", + name: "PClassroomChat", columns: table => new { Id = table.Column(type: "uniqueidentifier", nullable: false), @@ -1961,7 +1961,10 @@ namespace Kurs.Platform.Migrations SenderName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), Message = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: false), Timestamp = table.Column(type: "datetime2", nullable: false), + RecipientId = table.Column(type: "uniqueidentifier", nullable: true), + RecipientName = table.Column(type: "nvarchar(max)", nullable: true), IsTeacher = table.Column(type: "bit", nullable: false), + MessageType = table.Column(type: "nvarchar(max)", nullable: true), CreationTime = table.Column(type: "datetime2", nullable: false), CreatorId = table.Column(type: "uniqueidentifier", nullable: true), LastModificationTime = table.Column(type: "datetime2", nullable: true), @@ -1972,9 +1975,9 @@ namespace Kurs.Platform.Migrations }, constraints: table => { - table.PrimaryKey("PK_PClassChat", x => x.Id); + table.PrimaryKey("PK_PClassroomChat", x => x.Id); table.ForeignKey( - name: "FK_PClassChat_PClassroom_SessionId", + name: "FK_PClassroomChat_PClassroom_SessionId", column: x => x.SessionId, principalTable: "PClassroom", principalColumn: "Id", @@ -1982,17 +1985,17 @@ namespace Kurs.Platform.Migrations }); migrationBuilder.CreateTable( - name: "PClassParticipant", + name: "PClassroomParticipant", columns: table => new { Id = table.Column(type: "uniqueidentifier", nullable: false), SessionId = table.Column(type: "uniqueidentifier", nullable: false), UserId = table.Column(type: "uniqueidentifier", nullable: true), UserName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), - UserEmail = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), IsTeacher = table.Column(type: "bit", nullable: false), IsAudioMuted = table.Column(type: "bit", nullable: false), IsVideoMuted = table.Column(type: "bit", nullable: false), + IsHandRaised = table.Column(type: "bit", nullable: false), JoinTime = table.Column(type: "datetime2", nullable: false), ConnectionId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), CreationTime = table.Column(type: "datetime2", nullable: false), @@ -2005,9 +2008,9 @@ namespace Kurs.Platform.Migrations }, constraints: table => { - table.PrimaryKey("PK_PClassParticipant", x => x.Id); + table.PrimaryKey("PK_PClassroomParticipant", x => x.Id); table.ForeignKey( - name: "FK_PClassParticipant_PClassroom_SessionId", + name: "FK_PClassroomParticipant_PClassroom_SessionId", column: x => x.SessionId, principalTable: "PClassroom", principalColumn: "Id", @@ -2991,53 +2994,6 @@ namespace Kurs.Platform.Migrations columns: new[] { "CountryCode", "Code" }, unique: true); - migrationBuilder.CreateIndex( - name: "IX_PClassAttandance_JoinTime", - table: "PClassAttandance", - column: "JoinTime"); - - migrationBuilder.CreateIndex( - name: "IX_PClassAttandance_SessionId", - table: "PClassAttandance", - column: "SessionId"); - - migrationBuilder.CreateIndex( - name: "IX_PClassAttandance_StudentId", - table: "PClassAttandance", - column: "StudentId"); - - migrationBuilder.CreateIndex( - name: "IX_PClassChat_SenderId", - table: "PClassChat", - column: "SenderId"); - - migrationBuilder.CreateIndex( - name: "IX_PClassChat_SessionId", - table: "PClassChat", - column: "SessionId"); - - migrationBuilder.CreateIndex( - name: "IX_PClassChat_Timestamp", - table: "PClassChat", - column: "Timestamp"); - - migrationBuilder.CreateIndex( - name: "IX_PClassParticipant_SessionId", - table: "PClassParticipant", - column: "SessionId"); - - migrationBuilder.CreateIndex( - name: "IX_PClassParticipant_SessionId_UserId", - table: "PClassParticipant", - columns: new[] { "SessionId", "UserId" }, - unique: true, - filter: "[UserId] IS NOT NULL"); - - migrationBuilder.CreateIndex( - name: "IX_PClassParticipant_UserId", - table: "PClassParticipant", - column: "UserId"); - migrationBuilder.CreateIndex( name: "IX_PClassroom_ScheduledStartTime", table: "PClassroom", @@ -3048,6 +3004,53 @@ namespace Kurs.Platform.Migrations table: "PClassroom", column: "TeacherId"); + migrationBuilder.CreateIndex( + name: "IX_PClassroomAttandance_JoinTime", + table: "PClassroomAttandance", + column: "JoinTime"); + + migrationBuilder.CreateIndex( + name: "IX_PClassroomAttandance_SessionId", + table: "PClassroomAttandance", + column: "SessionId"); + + migrationBuilder.CreateIndex( + name: "IX_PClassroomAttandance_StudentId", + table: "PClassroomAttandance", + column: "StudentId"); + + migrationBuilder.CreateIndex( + name: "IX_PClassroomChat_SenderId", + table: "PClassroomChat", + column: "SenderId"); + + migrationBuilder.CreateIndex( + name: "IX_PClassroomChat_SessionId", + table: "PClassroomChat", + column: "SessionId"); + + migrationBuilder.CreateIndex( + name: "IX_PClassroomChat_Timestamp", + table: "PClassroomChat", + column: "Timestamp"); + + migrationBuilder.CreateIndex( + name: "IX_PClassroomParticipant_SessionId", + table: "PClassroomParticipant", + column: "SessionId"); + + migrationBuilder.CreateIndex( + name: "IX_PClassroomParticipant_SessionId_UserId", + table: "PClassroomParticipant", + columns: new[] { "SessionId", "UserId" }, + unique: true, + filter: "[UserId] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_PClassroomParticipant_UserId", + table: "PClassroomParticipant", + column: "UserId"); + migrationBuilder.CreateIndex( name: "IX_PCountry_Code", table: "PCountry", @@ -3303,13 +3306,13 @@ namespace Kurs.Platform.Migrations name: "PChart"); migrationBuilder.DropTable( - name: "PClassAttandance"); + name: "PClassroomAttandance"); migrationBuilder.DropTable( - name: "PClassChat"); + name: "PClassroomChat"); migrationBuilder.DropTable( - name: "PClassParticipant"); + name: "PClassroomParticipant"); migrationBuilder.DropTable( name: "PContact"); diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index 56b127d6..bf37c225 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -1556,219 +1556,6 @@ namespace Kurs.Platform.Migrations b.ToTable("PCity", (string)null); }); - modelBuilder.Entity("Kurs.Platform.Entities.ClassAttandance", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uniqueidentifier") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("datetime2") - .HasColumnName("DeletionTime"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("bit") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("JoinTime") - .HasColumnType("datetime2"); - - b.Property("LastModificationTime") - .HasColumnType("datetime2") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uniqueidentifier") - .HasColumnName("LastModifierId"); - - b.Property("LeaveTime") - .HasColumnType("datetime2"); - - b.Property("SessionId") - .HasColumnType("uniqueidentifier"); - - b.Property("StudentId") - .HasColumnType("uniqueidentifier"); - - b.Property("StudentName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("nvarchar(100)"); - - b.Property("TotalDurationMinutes") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.HasIndex("JoinTime"); - - b.HasIndex("SessionId"); - - b.HasIndex("StudentId"); - - b.ToTable("PClassAttandance", (string)null); - }); - - modelBuilder.Entity("Kurs.Platform.Entities.ClassChat", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uniqueidentifier") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("datetime2") - .HasColumnName("DeletionTime"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("bit") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("IsTeacher") - .HasColumnType("bit"); - - b.Property("LastModificationTime") - .HasColumnType("datetime2") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uniqueidentifier") - .HasColumnName("LastModifierId"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("nvarchar(2000)"); - - b.Property("SenderId") - .HasColumnType("uniqueidentifier"); - - b.Property("SenderName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("nvarchar(100)"); - - b.Property("SessionId") - .HasColumnType("uniqueidentifier"); - - b.Property("Timestamp") - .HasColumnType("datetime2"); - - b.HasKey("Id"); - - b.HasIndex("SenderId"); - - b.HasIndex("SessionId"); - - b.HasIndex("Timestamp"); - - b.ToTable("PClassChat", (string)null); - }); - - modelBuilder.Entity("Kurs.Platform.Entities.ClassParticipant", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("ConnectionId") - .HasMaxLength(100) - .HasColumnType("nvarchar(100)"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uniqueidentifier") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("datetime2") - .HasColumnName("DeletionTime"); - - b.Property("IsAudioMuted") - .HasColumnType("bit"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("bit") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("IsTeacher") - .HasColumnType("bit"); - - b.Property("IsVideoMuted") - .HasColumnType("bit"); - - b.Property("JoinTime") - .HasColumnType("datetime2"); - - b.Property("LastModificationTime") - .HasColumnType("datetime2") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uniqueidentifier") - .HasColumnName("LastModifierId"); - - b.Property("SessionId") - .HasColumnType("uniqueidentifier"); - - b.Property("UserEmail") - .HasMaxLength(200) - .HasColumnType("nvarchar(200)"); - - b.Property("UserId") - .HasColumnType("uniqueidentifier"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("nvarchar(100)"); - - b.HasKey("Id"); - - b.HasIndex("SessionId"); - - b.HasIndex("UserId"); - - b.HasIndex("SessionId", "UserId") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("PClassParticipant", (string)null); - }); - modelBuilder.Entity("Kurs.Platform.Entities.Classroom", b => { b.Property("Id") @@ -1858,6 +1645,227 @@ namespace Kurs.Platform.Migrations b.ToTable("PClassroom", (string)null); }); + modelBuilder.Entity("Kurs.Platform.Entities.ClassroomAttandance", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("JoinTime") + .HasColumnType("datetime2"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("LeaveTime") + .HasColumnType("datetime2"); + + b.Property("SessionId") + .HasColumnType("uniqueidentifier"); + + b.Property("StudentId") + .HasColumnType("uniqueidentifier"); + + b.Property("StudentName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("TotalDurationMinutes") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("JoinTime"); + + b.HasIndex("SessionId"); + + b.HasIndex("StudentId"); + + b.ToTable("PClassroomAttandance", (string)null); + }); + + modelBuilder.Entity("Kurs.Platform.Entities.ClassroomChat", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsTeacher") + .HasColumnType("bit"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("MessageType") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipientId") + .HasColumnType("uniqueidentifier"); + + b.Property("RecipientName") + .HasColumnType("nvarchar(max)"); + + b.Property("SenderId") + .HasColumnType("uniqueidentifier"); + + b.Property("SenderName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("SessionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Timestamp") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("SenderId"); + + b.HasIndex("SessionId"); + + b.HasIndex("Timestamp"); + + b.ToTable("PClassroomChat", (string)null); + }); + + modelBuilder.Entity("Kurs.Platform.Entities.ClassroomParticipant", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("ConnectionId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsAudioMuted") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsHandRaised") + .HasColumnType("bit"); + + b.Property("IsTeacher") + .HasColumnType("bit"); + + b.Property("IsVideoMuted") + .HasColumnType("bit"); + + b.Property("JoinTime") + .HasColumnType("datetime2"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("SessionId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("SessionId"); + + b.HasIndex("UserId"); + + b.HasIndex("SessionId", "UserId") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("PClassroomParticipant", (string)null); + }); + modelBuilder.Entity("Kurs.Platform.Entities.Contact", b => { b.Property("Id") @@ -6555,7 +6563,7 @@ namespace Kurs.Platform.Migrations b.Navigation("Country"); }); - modelBuilder.Entity("Kurs.Platform.Entities.ClassAttandance", b => + modelBuilder.Entity("Kurs.Platform.Entities.ClassroomAttandance", b => { b.HasOne("Kurs.Platform.Entities.Classroom", "Session") .WithMany("AttendanceRecords") @@ -6566,7 +6574,7 @@ namespace Kurs.Platform.Migrations b.Navigation("Session"); }); - modelBuilder.Entity("Kurs.Platform.Entities.ClassChat", b => + modelBuilder.Entity("Kurs.Platform.Entities.ClassroomChat", b => { b.HasOne("Kurs.Platform.Entities.Classroom", "Session") .WithMany("ChatMessages") @@ -6577,7 +6585,7 @@ namespace Kurs.Platform.Migrations b.Navigation("Session"); }); - modelBuilder.Entity("Kurs.Platform.Entities.ClassParticipant", b => + modelBuilder.Entity("Kurs.Platform.Entities.ClassroomParticipant", b => { b.HasOne("Kurs.Platform.Entities.Classroom", "Session") .WithMany("Participants") diff --git a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs index 642ec251..fc44f474 100644 --- a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs +++ b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs @@ -14,16 +14,18 @@ namespace Kurs.Platform.SignalR.Hubs; public class ClassroomHub : Hub { private readonly IRepository _classSessionRepository; - private readonly IRepository _participantRepository; - private readonly IRepository _chatMessageRepository; + private readonly IRepository _participantRepository; + private readonly IRepository _chatMessageRepository; + private readonly IRepository _attendanceRepository; private readonly ILogger _logger; private readonly IGuidGenerator _guidGenerator; private readonly ICurrentUser _currentUser; public ClassroomHub( IRepository classSessionRepository, - IRepository participantRepository, - IRepository chatMessageRepository, + IRepository participantRepository, + IRepository chatMessageRepository, + IRepository attendanceRepository, ILogger logger, IGuidGenerator guidGenerator, ICurrentUser currentUser) @@ -31,91 +33,93 @@ public class ClassroomHub : Hub _classSessionRepository = classSessionRepository; _participantRepository = participantRepository; _chatMessageRepository = chatMessageRepository; + _attendanceRepository = attendanceRepository; _logger = logger; _guidGenerator = guidGenerator; _currentUser = currentUser; } [HubMethodName("JoinClass")] - public async Task JoinClassAsync(Guid sessionId, string userName) + public async Task JoinClassAsync(Guid sessionId, Guid userId, string userName, bool isTeacher) { - var classSession = await _classSessionRepository.GetAsync(sessionId); - - // Add to SignalR group - await Groups.AddToGroupAsync(Context.ConnectionId, sessionId.ToString()); - - // Update participant connection var participant = await _participantRepository.FirstOrDefaultAsync( - x => x.SessionId == sessionId && x.UserId == _currentUser.Id + x => x.SessionId == sessionId && x.UserId == userId ); - if (participant != null) + if (participant == null) + { + participant = new ClassroomParticipant( + _guidGenerator.Create(), + sessionId, + userId, + userName, + isTeacher + ); + participant.UpdateConnectionId(Context.ConnectionId); + await _participantRepository.InsertAsync(participant, autoSave: true); + + // 🔑 Katılımcı sayısını güncelle + var classroom = await _classSessionRepository.GetAsync(sessionId); + var participantCount = await _participantRepository.CountAsync(x => x.SessionId == sessionId); + classroom.ParticipantCount = participantCount; + await _classSessionRepository.UpdateAsync(classroom, autoSave: true); + } + else { participant.UpdateConnectionId(Context.ConnectionId); - await _participantRepository.UpdateAsync(participant); + await _participantRepository.UpdateAsync(participant, autoSave: true); } - // Notify others + // 🔑 Attendance kaydı aç + var attendance = new ClassroomAttandance( + _guidGenerator.Create(), + sessionId, + userId, + userName, + DateTime.UtcNow + ); + await _attendanceRepository.InsertAsync(attendance, autoSave: true); + + await Groups.AddToGroupAsync(Context.ConnectionId, sessionId.ToString()); + await Clients.Group(sessionId.ToString()) - .SendAsync("ParticipantJoined", _currentUser.Id, userName); - _logger.LogInformation($"User {userName} joined class {sessionId}"); + .SendAsync("ParticipantJoined", userId, userName); } + [HubMethodName("LeaveClass")] public async Task LeaveClassAsync(Guid sessionId) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, sessionId.ToString()); + + var userId = _currentUser.Id; + if (userId.HasValue) + { + var attendance = await _attendanceRepository.FirstOrDefaultAsync( + x => x.SessionId == sessionId && x.StudentId == userId.Value && x.LeaveTime == null + ); + + if (attendance != null) + { + attendance.LeaveTime = DateTime.UtcNow; + attendance.TotalDurationMinutes = (int)Math.Max( + 1, + (attendance.LeaveTime.Value - attendance.JoinTime).TotalMinutes + ); + await _attendanceRepository.UpdateAsync(attendance, autoSave: true); + + await Clients.Group(sessionId.ToString()) + .SendAsync("AttendanceUpdated", attendance); + } + } + await Clients.Group(sessionId.ToString()) .SendAsync("ParticipantLeft", _currentUser); _logger.LogInformation($"User {_currentUser} left class {sessionId}"); } - public async Task SendSignalingMessageAsync(SignalingMessageDto message) - { - // Forward WebRTC signaling messages - await Clients.User(message.ToUserId) - .SendAsync("ReceiveSignalingMessage", message); - _logger.LogInformation($"Signaling message sent from {message.FromUserId} to {message.ToUserId}"); - } - - public async Task SendChatMessageAsync(Guid sessionId, string message) - { - var userName = _currentUser.UserName; - var userId = _currentUser.Id; - - // Check if user is teacher - var participant = await _participantRepository.FirstOrDefaultAsync( - x => x.SessionId == sessionId && x.UserId == userId - ); - - var isTeacher = participant?.IsTeacher ?? false; - - // Save message to database - var chatMessage = new ClassChat( - _guidGenerator.Create(), - sessionId, - userId, - userName, - message, - isTeacher - ); - - await _chatMessageRepository.InsertAsync(chatMessage); - - // Send to all participants - await Clients.Group(sessionId.ToString()) - .SendAsync("ChatMessage", new - { - Id = chatMessage.Id, - SenderId = chatMessage.SenderId, - SenderName = chatMessage.SenderName, - Message = chatMessage.Message, - Timestamp = chatMessage.Timestamp, - IsTeacher = chatMessage.IsTeacher - }); - } - - public async Task MuteParticipantAsync(Guid sessionId, Guid participantId, bool isMuted) + [HubMethodName("MuteParticipant")] + public async Task MuteParticipantAsync(Guid sessionId, Guid userId, bool isMuted, bool isTeacher) { var teacherParticipant = await _participantRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.UserId == _currentUser.Id @@ -128,59 +132,264 @@ public class ClassroomHub : Hub } var participant = await _participantRepository.FirstOrDefaultAsync( - x => x.SessionId == sessionId && x.UserId == participantId + x => x.SessionId == sessionId && x.UserId == userId ); if (participant != null) { - if (isMuted) - participant.MuteAudio(); - else - participant.UnmuteAudio(); + if (isMuted) participant.MuteAudio(); + else participant.UnmuteAudio(); - await _participantRepository.UpdateAsync(participant); + await _participantRepository.UpdateAsync(participant, autoSave: true); - // Notify the participant and others await Clients.Group(sessionId.ToString()) - .SendAsync("ParticipantMuted", participantId, isMuted); + .SendAsync("ParticipantMuted", userId, isMuted); } } + [HubMethodName("SendChatMessage")] + public async Task SendChatMessageAsync( + Guid sessionId, + Guid senderId, + string senderName, + string message, + bool isTeacher, + string messageType) + { + // Save message to DB + var chatMessage = new ClassroomChat( + _guidGenerator.Create(), + sessionId, + senderId, + senderName, + message, + null, + null, + isTeacher, + messageType + ); + + await _chatMessageRepository.InsertAsync(chatMessage, autoSave: true); + + // Broadcast to group + await Clients.Group(sessionId.ToString()).SendAsync("ChatMessage", new + { + Id = chatMessage.Id, + SenderId = senderId, + SenderName = senderName, + Message = chatMessage.Message, + Timestamp = chatMessage.Timestamp, + IsTeacher = isTeacher, + MessageType = messageType + }); + } + + [HubMethodName("SendPrivateMessage")] + public async Task SendPrivateMessageAsync( + Guid sessionId, + Guid senderId, + string senderName, + string message, + Guid recipientId, + string recipientName, + bool isTeacher, + string messageType) + { + // Save message to DB + var chatMessage = new ClassroomChat( + _guidGenerator.Create(), + sessionId, + senderId, + senderName, + message, + recipientId, + recipientName, + isTeacher, + "private" + ); + + await _chatMessageRepository.InsertAsync(chatMessage, autoSave: true); + + await Clients.User(recipientId.ToString()).SendAsync("ChatMessage", new + { + Id = Guid.NewGuid(), + SenderId = senderId, + SenderName = senderName, + Message = message, + Timestamp = DateTime.UtcNow, + IsTeacher = isTeacher, + RecipientId = recipientId, + RecipientName = recipientName, + MessageType = "private" + }); + + await Clients.Caller.SendAsync("ChatMessage", new + { + Id = Guid.NewGuid(), + SenderId = senderId, + SenderName = senderName, + Message = message, + Timestamp = DateTime.UtcNow, + IsTeacher = isTeacher, + RecipientId = recipientId, + RecipientName = recipientName, + MessageType = "private" + }); + } + + [HubMethodName("SendAnnouncement")] + public async Task SendAnnouncementAsync(Guid sessionId, Guid senderId, string senderName, string message, bool isTeacher) + { + // Save message to DB + var chatMessage = new ClassroomChat( + _guidGenerator.Create(), + sessionId, + senderId, + senderName, + message, + null, + null, + isTeacher, + "announcement" + ); + + await _chatMessageRepository.InsertAsync(chatMessage, autoSave: true); + + await Clients.Group(sessionId.ToString()).SendAsync("ChatMessage", new + { + Id = Guid.NewGuid(), + SenderId = senderId, + SenderName = senderName, + Message = message, + Timestamp = DateTime.UtcNow, + IsTeacher = isTeacher, + MessageType = "announcement" + }); + } + + [HubMethodName("RaiseHand")] + public async Task RaiseHandAsync(Guid sessionId, Guid studentId, string studentName) + { + await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseReceived", new + { + Id = Guid.NewGuid(), + StudentId = studentId, + StudentName = studentName, + Timestamp = DateTime.UtcNow, + IsActive = true + }); + } + + [HubMethodName("KickParticipant")] + public async Task KickParticipantAsync(Guid sessionId, Guid participantId) + { + // Attendance kapat + var attendance = await _attendanceRepository.FirstOrDefaultAsync( + x => x.SessionId == sessionId && x.StudentId == participantId && x.LeaveTime == null + ); + + if (attendance != null) + { + attendance.LeaveTime = DateTime.UtcNow; + attendance.TotalDurationMinutes = (int)Math.Max( + 1, + (attendance.LeaveTime.Value - attendance.JoinTime).TotalMinutes + ); + + await _attendanceRepository.UpdateAsync(attendance, autoSave: true); + + // Katılım güncellemesini yayınla + await Clients.Group(sessionId.ToString()).SendAsync("AttendanceUpdated", new + { + attendance.Id, + attendance.SessionId, + attendance.StudentId, + attendance.StudentName, + attendance.JoinTime, + attendance.LeaveTime, + attendance.TotalDurationMinutes + }); + } + + // Katılımcı çıkışını bildir + await Clients.Group(sessionId.ToString()).SendAsync("ParticipantLeft", participantId); + } + + [HubMethodName("ApproveHandRaise")] + public async Task ApproveHandRaiseAsync(Guid sessionId, Guid handRaiseId) + { + await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseDismissed", handRaiseId); + } + + [HubMethodName("DismissHandRaise")] + public async Task DismissHandRaiseAsync(Guid sessionId, Guid handRaiseId) + { + await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseDismissed", handRaiseId); + } + public override async Task OnDisconnectedAsync(Exception exception) { try { - // bağlantı gerçekten iptal edilmişse DB sorgusu çalıştırma if (Context.ConnectionAborted.IsCancellationRequested) return; var userId = _currentUser.Id; if (userId.HasValue) { + // 🔑 1. Katılımcı listesi var participants = await _participantRepository - .GetListAsync(x => x.UserId == userId.Value && x.ConnectionId == Context.ConnectionId) - .ConfigureAwait(false); + .GetListAsync(x => x.UserId == userId.Value && x.ConnectionId == Context.ConnectionId); foreach (var participant in participants) { + // 🔑 2. Attendance kaydını kapat + var attendance = await _attendanceRepository.FirstOrDefaultAsync( + x => x.SessionId == participant.SessionId && + x.StudentId == userId.Value && + x.LeaveTime == null + ); + + if (attendance != null) + { + attendance.LeaveTime = DateTime.UtcNow; + attendance.TotalDurationMinutes = (int)Math.Max( + 1, + (attendance.LeaveTime.Value - attendance.JoinTime).TotalMinutes + ); + + await _attendanceRepository.UpdateAsync(attendance, autoSave: true); + + // Frontend’e bildir + await Clients.Group(participant.SessionId.ToString()) + .SendAsync("AttendanceUpdated", new + { + attendance.Id, + attendance.SessionId, + attendance.StudentId, + attendance.StudentName, + attendance.JoinTime, + attendance.LeaveTime, + attendance.TotalDurationMinutes + }); + } + + // 🔑 3. ParticipantLeft event’i await Clients.Group(participant.SessionId.ToString()) - .SendAsync("ParticipantLeft", userId.Value) - .ConfigureAwait(false); + .SendAsync("ParticipantLeft", userId.Value); } } } catch (TaskCanceledException) { - // bağlantı kapandığında doğal, error yerine debug seviyesinde loglayın _logger.LogDebug("OnDisconnectedAsync iptal edildi (connection aborted)."); } catch (Exception ex) { - // beklenmeyen hataları error olarak loglayın _logger.LogError(ex, "OnDisconnectedAsync hata"); } - await base.OnDisconnectedAsync(exception).ConfigureAwait(false); + await base.OnDisconnectedAsync(exception); } } diff --git a/ui/dev-dist/sw.js b/ui/dev-dist/sw.js index 372d19ea..22f08234 100644 --- a/ui/dev-dist/sw.js +++ b/ui/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.b9bfk61okp" + "revision": "0.48gll4p3s3o" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/ui/src/components/classroom/Panels/AttendancePanel.tsx b/ui/src/components/classroom/Panels/AttendancePanel.tsx index a84d93ed..6c18da72 100644 --- a/ui/src/components/classroom/Panels/AttendancePanel.tsx +++ b/ui/src/components/classroom/Panels/AttendancePanel.tsx @@ -1,9 +1,9 @@ -import { ClassAttendanceDto } from '@/proxy/classroom/models' +import { ClassroomAttendanceDto } from '@/proxy/classroom/models' import React, { useEffect, useState } from 'react' import { FaClock, FaUsers } from 'react-icons/fa' interface AttendancePanelProps { - attendanceRecords: ClassAttendanceDto[] + attendanceRecords: ClassroomAttendanceDto[] isOpen: boolean onClose: () => void } diff --git a/ui/src/components/classroom/Panels/ChatPanel.tsx b/ui/src/components/classroom/Panels/ChatPanel.tsx index bbcc62e8..a043d228 100644 --- a/ui/src/components/classroom/Panels/ChatPanel.tsx +++ b/ui/src/components/classroom/Panels/ChatPanel.tsx @@ -1,10 +1,10 @@ -import { ClassChatDto } from '@/proxy/classroom/models' +import { ClassroomChatDto } from '@/proxy/classroom/models' import { useStoreState } from '@/store/store' import React, { useState, useRef, useEffect } from 'react' import { FaPaperPlane, FaComments, FaTimes, FaUsers, FaUser, FaBullhorn } from 'react-icons/fa' interface ChatPanelProps { - messages: ClassChatDto[] + messages: ClassroomChatDto[] isTeacher: boolean isOpen: boolean onClose: () => void diff --git a/ui/src/components/classroom/ParticipantGrid.tsx b/ui/src/components/classroom/ParticipantGrid.tsx index 9634d9e6..501156cd 100644 --- a/ui/src/components/classroom/ParticipantGrid.tsx +++ b/ui/src/components/classroom/ParticipantGrid.tsx @@ -1,10 +1,10 @@ import React from 'react' import { FaMicrophoneSlash, FaExpand, FaUserTimes } from 'react-icons/fa' import { VideoPlayer } from './VideoPlayer' -import { ClassParticipantDto, VideoLayoutDto } from '@/proxy/classroom/models' +import { ClassroomParticipantDto, VideoLayoutDto } from '@/proxy/classroom/models' interface ParticipantGridProps { - participants: ClassParticipantDto[] + participants: ClassroomParticipantDto[] localStream?: MediaStream currentUserId: string currentUserName: string @@ -14,7 +14,7 @@ interface ParticipantGridProps { onToggleAudio: () => void onToggleVideo: () => void onLeaveCall: () => void - onMuteParticipant?: (participantId: string, isMuted: boolean) => void + onMuteParticipant?: (participantId: string, isMuted: boolean, isTeacher: boolean) => void layout: VideoLayoutDto focusedParticipant?: string onParticipantFocus?: (participantId: string | undefined) => void @@ -211,7 +211,7 @@ export const ParticipantGrid: React.FC = ({ } const renderParticipant = ( - participant: ClassParticipantDto, + participant: ClassroomParticipantDto, isMain: boolean = false, isSmall: boolean = false, ) => ( @@ -243,7 +243,7 @@ export const ParticipantGrid: React.FC = ({ - )} - -
- {classList.length === 0 ? ( -
- -

Henüz programlanmış sınıf bulunmamaktadır.

-
- ) : ( -
- {classList.map((classSession, index) => { - const { status, className, showButtons, title, classes, event } = - getClassProps(classSession) - return ( - -
-
-

- {classSession.name} -

- - {status} - -
- - {/* Sağ kısım: buton */} - {showButtons && ( -
- {user.role === 'teacher' && classSession.teacherId === user.id && ( - <> - - - - - )} - - -
- )} -
- -
-

{classSession.subject}

-
- -
- - {classSession.description} - -
- -
-
-
- - - {showDbDateAsIs(classSession.scheduledStartTime)} - -
- -
- - {classSession.duration} dakika -
- -
- {classSession.scheduledEndTime && ( - <> - - - {showDbDateAsIs(classSession.scheduledEndTime!)} - - - )} -
- -
- - - {classSession.participantCount}/{classSession.maxParticipants} - -
-
-
-
- ) - })} -
- )} -
- - - - {/* Class Modal (Create/Edit) */} - {(showCreateModal || (showEditModal && classroom)) && ( -
- -
-

- {showCreateModal ? 'Yeni Sınıf Oluştur' : 'Sınıfı Düzenle'} -

-
- -
+ + + {/* Main Content */} +
+ {/* Stats Cards */} +
+ -
- +
+
+ +
+
+

Toplam Sınıf

+

+ {widgets().totalCount}{' '} +

+
+
+ + + {/* Aktif Sınıf */} + +
+
+ +
+
+

Aktif Sınıf

+

+ {widgets().activeCount} +

+
+
+
+ + {/* Katılıma Açık */} + +
+
+ +
+
+

Katılıma Açık

+

+ {widgets().openCount} +

+
+
+
+ + {/* Pasif Sınıf */} + +
+
+ +
+
+

Pasif Sınıf

+

+ {widgets().passiveCount} +

+
+
+
+ + {/* Toplam Katılımcı */} + +
+
+ +
+
+

Toplam Katılımcı

+

+ {classList.reduce((sum, c) => sum + c.participantCount, 0)} +

+
+
+
+
+ + {/* Filter Bar */} +
+
+
+ setClassroom({ ...classroom, name: e.target.value })} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Örn: Matematik 101 - Diferansiyel Denklemler" + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Search class" + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} />
+
+ + +
+
+
-
- -