From 0b60f006d0af2607f7c144da6e2b63e91a2aa05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Thu, 28 Aug 2025 14:53:47 +0300 Subject: [PATCH] virtual classroom --- .../Classroom/ClassroomDto.cs | 8 +- .../Classroom/ClassroomAppService.cs | 41 +- .../Classroom/ClassroomAutoMapperProfile.cs | 4 +- .../Seeds/PlatformDataSeeder.cs | 3 +- .../Seeds/SeederData.json | 15 +- .../Seeds/SeederDto.cs | 3 +- .../Classroom/Classroom.cs | 22 +- .../EntityFrameworkCore/PlatformDbContext.cs | 1 - ....cs => 20250828112303_Initial.Designer.cs} | 21 +- ...3_Initial.cs => 20250828112303_Initial.cs} | 14 +- .../PlatformDbContextModelSnapshot.cs | 19 +- .../Classroom/ClassroomHub.cs | 44 ++- ui/dev-dist/sw.js | 2 +- ui/src/proxy/classroom/models.ts | 9 +- ui/src/services/classroom/signalr.ts | 83 ++-- ui/src/views/classroom/ClassList.tsx | 360 +++++++++++------- ui/src/views/classroom/Dashboard.tsx | 2 +- ui/src/views/classroom/RoomDetail.tsx | 29 +- 18 files changed, 356 insertions(+), 324 deletions(-) rename api/src/Kurs.Platform.EntityFrameworkCore/Migrations/{20250826203853_Initial.Designer.cs => 20250828112303_Initial.Designer.cs} (99%) rename api/src/Kurs.Platform.EntityFrameworkCore/Migrations/{20250826203853_Initial.cs => 20250828112303_Initial.cs} (99%) diff --git a/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomDto.cs b/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomDto.cs index 43290614..6ea46063 100644 --- a/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomDto.cs +++ b/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomDto.cs @@ -13,14 +13,12 @@ public class ClassroomDto : FullAuditedEntityDto public Guid TeacherId { get; set; } public string TeacherName { get; set; } public DateTime ScheduledStartTime { get; set; } - public DateTime? ActualStartTime { get; set; } - public DateTime? EndTime { get; set; } + public DateTime? ScheduledEndTime { get; set; } public int Duration { get; set; } + public DateTime? ActualStartTime { get; set; } + public DateTime? ActualEndTime { get; set; } public int MaxParticipants { get; set; } - public bool IsActive { get; set; } - public bool IsScheduled { get; set; } public int ParticipantCount { get; set; } - public bool CanJoin { get; set; } [JsonIgnore] public string SettingsJson { get; set; } diff --git a/api/src/Kurs.Platform.Application/Classroom/ClassroomAppService.cs b/api/src/Kurs.Platform.Application/Classroom/ClassroomAppService.cs index c565f1e4..03a2097d 100644 --- a/api/src/Kurs.Platform.Application/Classroom/ClassroomAppService.cs +++ b/api/src/Kurs.Platform.Application/Classroom/ClassroomAppService.cs @@ -60,10 +60,9 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService CurrentUser.Id, CurrentUser.Name, input.ScheduledStartTime, + input.ScheduledStartTime.AddMinutes(input.Duration), input.Duration, input.MaxParticipants, - false, - true, input.SettingsJson = JsonSerializer.Serialize(input.SettingsDto) ); @@ -81,22 +80,15 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService throw new UnauthorizedAccessException("Only the teacher can update this class"); } - if (classSession.IsActive) - { - throw new InvalidOperationException("Cannot update an active class"); - } - classSession.Name = input.Name; classSession.Description = input.Description; classSession.Subject = input.Subject; classSession.TeacherId = input.TeacherId; classSession.TeacherName = input.TeacherName; classSession.ScheduledStartTime = input.ScheduledStartTime; - classSession.ActualStartTime = input.ActualStartTime; + classSession.ScheduledEndTime = input.ScheduledStartTime.AddMinutes(input.Duration); classSession.Duration = input.Duration; classSession.MaxParticipants = input.MaxParticipants; - classSession.IsActive = input.IsActive; - classSession.IsScheduled = input.IsScheduled; classSession.SettingsJson = JsonSerializer.Serialize(input.SettingsDto); await _classSessionRepository.UpdateAsync(classSession); @@ -112,11 +104,6 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService throw new UnauthorizedAccessException("Only the teacher can delete this class"); } - if (classSession.IsActive) - { - throw new InvalidOperationException("Cannot delete an active class"); - } - await _classSessionRepository.DeleteAsync(id); } @@ -130,15 +117,6 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService throw new UnauthorizedAccessException("Only the teacher can start this class"); } - if (!classSession.CanJoin()) - { - throw new InvalidOperationException("Class cannot be started at this time"); - } - - if (classSession.IsActive) - throw new InvalidOperationException("Class is already active"); - - classSession.IsActive = true; classSession.ActualStartTime = DateTime.Now; await _classSessionRepository.UpdateAsync(classSession); @@ -156,11 +134,7 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService throw new UnauthorizedAccessException("Only the teacher can end this class"); } - if (!classSession.IsActive) - throw new InvalidOperationException("Class is not active"); - - classSession.IsActive = false; - classSession.EndTime = DateTime.Now; + classSession.ActualEndTime = DateTime.Now; await _classSessionRepository.UpdateAsync(classSession); @@ -171,7 +145,7 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService foreach (var attendance in activeAttendances) { - attendance.LeaveTime = DateTime.UtcNow; + attendance.LeaveTime = DateTime.Now; attendance.CalculateDuration(); await _attendanceRepository.UpdateAsync(attendance); } @@ -181,11 +155,6 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService { var classSession = await _classSessionRepository.GetAsync(id); - if (!classSession.CanJoin()) - { - throw new InvalidOperationException("Cannot join this class at this time"); - } - if (classSession.ParticipantCount >= classSession.MaxParticipants) { throw new InvalidOperationException("Class is full"); @@ -216,7 +185,7 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService id, CurrentUser.Id, CurrentUser.Name, - DateTime.UtcNow + DateTime.Now ); await _attendanceRepository.InsertAsync(attendance); diff --git a/api/src/Kurs.Platform.Application/Classroom/ClassroomAutoMapperProfile.cs b/api/src/Kurs.Platform.Application/Classroom/ClassroomAutoMapperProfile.cs index 40227e06..59d62e87 100644 --- a/api/src/Kurs.Platform.Application/Classroom/ClassroomAutoMapperProfile.cs +++ b/api/src/Kurs.Platform.Application/Classroom/ClassroomAutoMapperProfile.cs @@ -7,9 +7,7 @@ public class ClassroomAutoMapperProfile : Profile { public ClassroomAutoMapperProfile() { - CreateMap() - .ForMember(dest => dest.CanJoin, opt => opt.MapFrom(src => src.CanJoin())); - + CreateMap(); CreateMap(); CreateMap(); CreateMap(); diff --git a/api/src/Kurs.Platform.DbMigrator/Seeds/PlatformDataSeeder.cs b/api/src/Kurs.Platform.DbMigrator/Seeds/PlatformDataSeeder.cs index 62244a73..bef6beff 100644 --- a/api/src/Kurs.Platform.DbMigrator/Seeds/PlatformDataSeeder.cs +++ b/api/src/Kurs.Platform.DbMigrator/Seeds/PlatformDataSeeder.cs @@ -1046,10 +1046,9 @@ public class PlatformDataSeeder : IDataSeedContributor, ITransientDependency item.TeacherId, item.TeacherName, item.ScheduledStartTime, + item.ScheduledEndTime, item.Duration, item.MaxParticipants, - item.IsActive, - item.IsScheduled, item.SettingsJson )); } diff --git a/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json b/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json index b079fbcf..b666c884 100644 --- a/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json +++ b/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json @@ -17683,9 +17683,10 @@ "teacherId": "995220ff-2751-afd6-3d99-3a1bfc55f78e", "teacherName": "Prof. Dr. Mehmet Özkan", "scheduledStartTime": "2025-08-27T10:00:00Z", - "actualStartTime": "", - "endTime": "", + "scheduledEndTime": "2025-08-28T11:30:00Z", "duration": 90, + "actualStartTime": "", + "actualEndTime": "", "maxParticipants": 30, "isActive": false, "isScheduled": true, @@ -17699,9 +17700,10 @@ "teacherId": "995220ff-2751-afd6-3d99-3a1bfc55f78e", "teacherName": "Dr. Ayşe Kaya", "scheduledStartTime": "2025-08-26T10:00:00Z", - "actualStartTime": "", - "endTime": "", + "scheduledEndTime": "2025-08-28T12:00:00Z", "duration": 120, + "actualStartTime": "", + "actualEndTime": "", "maxParticipants": 25, "isActive": false, "isScheduled": true, @@ -17715,9 +17717,10 @@ "teacherId": "995220ff-2751-afd6-3d99-3a1bfc55f78e", "teacherName": "Dr. Ali Veli", "scheduledStartTime": "2025-08-28T10:00:00Z", - "actualStartTime": "", - "endTime": "", + "scheduledEndTime": "2025-08-28T11:15:00Z", "duration": 75, + "actualStartTime": "", + "actualEndTime": "", "maxParticipants": 20, "isActive": false, "isScheduled": true, diff --git a/api/src/Kurs.Platform.DbMigrator/Seeds/SeederDto.cs b/api/src/Kurs.Platform.DbMigrator/Seeds/SeederDto.cs index 4731f972..77835f31 100644 --- a/api/src/Kurs.Platform.DbMigrator/Seeds/SeederDto.cs +++ b/api/src/Kurs.Platform.DbMigrator/Seeds/SeederDto.cs @@ -335,8 +335,7 @@ public class ClassroomSeedDto public Guid? TeacherId { get; set; } public string TeacherName { get; set; } public DateTime ScheduledStartTime { get; set; } - public DateTime? ActualStartTime { get; set; } - public DateTime? EndTime { get; set; } + public DateTime ScheduledEndTime { get; set; } public int Duration { get; set; } public int MaxParticipants { get; set; } public bool IsActive { get; set; } diff --git a/api/src/Kurs.Platform.Domain/Classroom/Classroom.cs b/api/src/Kurs.Platform.Domain/Classroom/Classroom.cs index 86410337..e03dc70c 100644 --- a/api/src/Kurs.Platform.Domain/Classroom/Classroom.cs +++ b/api/src/Kurs.Platform.Domain/Classroom/Classroom.cs @@ -12,12 +12,11 @@ public class Classroom : FullAuditedEntity public Guid? TeacherId { get; set; } public string TeacherName { get; set; } public DateTime ScheduledStartTime { get; set; } - public DateTime? ActualStartTime { get; set; } - public DateTime? EndTime { get; set; } + public DateTime? ScheduledEndTime { get; set; } public int Duration { get; set; } + public DateTime? ActualStartTime { get; set; } + public DateTime? ActualEndTime { get; set; } public int MaxParticipants { get; set; } - public bool IsActive { get; set; } - public bool IsScheduled { get; set; } public int ParticipantCount { get; set; } public string SettingsJson { get; set; } @@ -40,10 +39,9 @@ public class Classroom : FullAuditedEntity Guid? teacherId, string teacherName, DateTime scheduledStartTime, + DateTime? scheduledEndTime, int duration, int maxParticipants, - bool isActive, - bool isScheduled, string settingsJson ) : base(id) { @@ -53,23 +51,13 @@ public class Classroom : FullAuditedEntity TeacherId = teacherId; TeacherName = teacherName; ScheduledStartTime = scheduledStartTime; + ScheduledEndTime = scheduledEndTime; Duration = duration; MaxParticipants = maxParticipants; - IsActive = isActive; - IsScheduled = isScheduled; SettingsJson = settingsJson; Participants = new HashSet(); AttendanceRecords = new HashSet(); ChatMessages = new HashSet(); } - - public bool CanJoin() - { - var now = DateTime.Now; - var tenMinutesBefore = ScheduledStartTime.AddMinutes(-10); - var twoHoursAfter = ScheduledStartTime.AddHours(2); - - return now >= tenMinutesBefore && now <= twoHoursAfter && ParticipantCount < MaxParticipants; - } } diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Kurs.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index 53decdc3..21ae8773 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -894,7 +894,6 @@ public class PlatformDbContext : b.HasIndex(x => x.TeacherId); b.HasIndex(x => x.ScheduledStartTime); - b.HasIndex(x => x.IsActive); // Relationships b.HasMany(x => x.Participants) diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250826203853_Initial.Designer.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250828112303_Initial.Designer.cs similarity index 99% rename from api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250826203853_Initial.Designer.cs rename to api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250828112303_Initial.Designer.cs index 1ca23822..9940db03 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250826203853_Initial.Designer.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250828112303_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Kurs.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20250826203853_Initial")] + [Migration("20250828112303_Initial")] partial class Initial { /// @@ -1668,7 +1668,7 @@ namespace Kurs.Platform.Migrations .HasMaxLength(2000) .HasColumnType("nvarchar(2000)"); - b.Property("SenderId") + b.Property("SenderId") .HasColumnType("uniqueidentifier"); b.Property("SenderName") @@ -1777,6 +1777,9 @@ namespace Kurs.Platform.Migrations b.Property("Id") .HasColumnType("uniqueidentifier"); + b.Property("ActualEndTime") + .HasColumnType("datetime2"); + b.Property("ActualStartTime") .HasColumnType("datetime2"); @@ -1803,21 +1806,12 @@ namespace Kurs.Platform.Migrations b.Property("Duration") .HasColumnType("int"); - b.Property("EndTime") - .HasColumnType("datetime2"); - - b.Property("IsActive") - .HasColumnType("bit"); - b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") .HasDefaultValue(false) .HasColumnName("IsDeleted"); - b.Property("IsScheduled") - .HasColumnType("bit"); - b.Property("LastModificationTime") .HasColumnType("datetime2") .HasColumnName("LastModificationTime"); @@ -1837,6 +1831,9 @@ namespace Kurs.Platform.Migrations b.Property("ParticipantCount") .HasColumnType("int"); + b.Property("ScheduledEndTime") + .HasColumnType("datetime2"); + b.Property("ScheduledStartTime") .HasColumnType("datetime2"); @@ -1857,8 +1854,6 @@ namespace Kurs.Platform.Migrations b.HasKey("Id"); - b.HasIndex("IsActive"); - b.HasIndex("ScheduledStartTime"); b.HasIndex("TeacherId"); diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250826203853_Initial.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250828112303_Initial.cs similarity index 99% rename from api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250826203853_Initial.cs rename to api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250828112303_Initial.cs index 721c2fa9..77da4cd7 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250826203853_Initial.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250828112303_Initial.cs @@ -797,12 +797,11 @@ namespace Kurs.Platform.Migrations TeacherId = table.Column(type: "uniqueidentifier", nullable: true), TeacherName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), ScheduledStartTime = table.Column(type: "datetime2", nullable: false), - ActualStartTime = table.Column(type: "datetime2", nullable: true), - EndTime = table.Column(type: "datetime2", nullable: true), + ScheduledEndTime = table.Column(type: "datetime2", nullable: true), Duration = table.Column(type: "int", nullable: false), + ActualStartTime = table.Column(type: "datetime2", nullable: true), + ActualEndTime = table.Column(type: "datetime2", nullable: true), MaxParticipants = table.Column(type: "int", nullable: false), - IsActive = table.Column(type: "bit", nullable: false), - IsScheduled = table.Column(type: "bit", nullable: false), ParticipantCount = table.Column(type: "int", nullable: false), SettingsJson = table.Column(type: "nvarchar(max)", nullable: true), CreationTime = table.Column(type: "datetime2", nullable: false), @@ -1958,7 +1957,7 @@ namespace Kurs.Platform.Migrations { Id = table.Column(type: "uniqueidentifier", nullable: false), SessionId = table.Column(type: "uniqueidentifier", nullable: false), - SenderId = table.Column(type: "uniqueidentifier", nullable: false), + SenderId = table.Column(type: "uniqueidentifier", nullable: true), 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), @@ -3039,11 +3038,6 @@ namespace Kurs.Platform.Migrations table: "PClassParticipant", column: "UserId"); - migrationBuilder.CreateIndex( - name: "IX_PClassroom_IsActive", - table: "PClassroom", - column: "IsActive"); - migrationBuilder.CreateIndex( name: "IX_PClassroom_ScheduledStartTime", table: "PClassroom", diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index 2e34960c..56b127d6 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -1665,7 +1665,7 @@ namespace Kurs.Platform.Migrations .HasMaxLength(2000) .HasColumnType("nvarchar(2000)"); - b.Property("SenderId") + b.Property("SenderId") .HasColumnType("uniqueidentifier"); b.Property("SenderName") @@ -1774,6 +1774,9 @@ namespace Kurs.Platform.Migrations b.Property("Id") .HasColumnType("uniqueidentifier"); + b.Property("ActualEndTime") + .HasColumnType("datetime2"); + b.Property("ActualStartTime") .HasColumnType("datetime2"); @@ -1800,21 +1803,12 @@ namespace Kurs.Platform.Migrations b.Property("Duration") .HasColumnType("int"); - b.Property("EndTime") - .HasColumnType("datetime2"); - - b.Property("IsActive") - .HasColumnType("bit"); - b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") .HasDefaultValue(false) .HasColumnName("IsDeleted"); - b.Property("IsScheduled") - .HasColumnType("bit"); - b.Property("LastModificationTime") .HasColumnType("datetime2") .HasColumnName("LastModificationTime"); @@ -1834,6 +1828,9 @@ namespace Kurs.Platform.Migrations b.Property("ParticipantCount") .HasColumnType("int"); + b.Property("ScheduledEndTime") + .HasColumnType("datetime2"); + b.Property("ScheduledStartTime") .HasColumnType("datetime2"); @@ -1854,8 +1851,6 @@ namespace Kurs.Platform.Migrations b.HasKey("Id"); - b.HasIndex("IsActive"); - b.HasIndex("ScheduledStartTime"); b.HasIndex("TeacherId"); diff --git a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs index aa4cf320..642ec251 100644 --- a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs +++ b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs @@ -41,12 +41,6 @@ public class ClassroomHub : Hub { var classSession = await _classSessionRepository.GetAsync(sessionId); - if (!classSession.CanJoin()) - { - await Clients.Caller.SendAsync("Error", "Cannot join this class at this time"); - return; - } - // Add to SignalR group await Groups.AddToGroupAsync(Context.ConnectionId, sessionId.ToString()); @@ -154,21 +148,39 @@ public class ClassroomHub : Hub public override async Task OnDisconnectedAsync(Exception exception) { - // Handle cleanup when user disconnects - var userId = _currentUser.Id; - if (userId.HasValue) + try { - var participants = await _participantRepository.GetListAsync( - x => x.UserId == userId.Value && x.ConnectionId == Context.ConnectionId - ); - foreach (var participant in participants) + // bağlantı gerçekten iptal edilmişse DB sorgusu çalıştırma + if (Context.ConnectionAborted.IsCancellationRequested) + return; + + var userId = _currentUser.Id; + if (userId.HasValue) { - await Clients.Group(participant.SessionId.ToString()) - .SendAsync("ParticipantLeft", userId.Value); + var participants = await _participantRepository + .GetListAsync(x => x.UserId == userId.Value && x.ConnectionId == Context.ConnectionId) + .ConfigureAwait(false); + + foreach (var participant in participants) + { + await Clients.Group(participant.SessionId.ToString()) + .SendAsync("ParticipantLeft", userId.Value) + .ConfigureAwait(false); + } } } + 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); + await base.OnDisconnectedAsync(exception).ConfigureAwait(false); } } diff --git a/ui/dev-dist/sw.js b/ui/dev-dist/sw.js index b68ee707..372d19ea 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.cb06g8q0ck8" + "revision": "0.b9bfk61okp" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/ui/src/proxy/classroom/models.ts b/ui/src/proxy/classroom/models.ts index 2e038776..46631718 100644 --- a/ui/src/proxy/classroom/models.ts +++ b/ui/src/proxy/classroom/models.ts @@ -17,14 +17,12 @@ export interface ClassroomDto { teacherId: string teacherName: string scheduledStartTime: string - actualStartTime?: string - endTime?: string + scheduledEndTime: string duration?: number + actualStartTime?: string + actualEndTime?: string maxParticipants?: number - isActive: boolean - isScheduled: boolean participantCount: number - canJoin: boolean settingsDto?: ClassroomSettingsDto } @@ -106,7 +104,6 @@ export interface ScheduledClassDto { name: string scheduledTime: string duration: number - canJoin: boolean } export interface HandRaiseDto { diff --git a/ui/src/services/classroom/signalr.ts b/ui/src/services/classroom/signalr.ts index f1a8284a..e672f363 100644 --- a/ui/src/services/classroom/signalr.ts +++ b/ui/src/services/classroom/signalr.ts @@ -10,7 +10,6 @@ import * as signalR from '@microsoft/signalr' export class SignalRService { private connection!: signalR.HubConnection private isConnected: boolean = false - private demoMode: boolean = false // Start in demo mode by default private onSignalingMessage?: (message: SignalingMessageDto) => void private onAttendanceUpdate?: (record: ClassAttendanceDto) => void private onParticipantJoined?: (userId: string, name: string) => void @@ -24,22 +23,20 @@ export class SignalRService { const { auth } = store.getState() // Only initialize connection if not in demo mode - if (!this.demoMode) { - // In production, replace with your actual SignalR hub URL - this.connection = new signalR.HubConnectionBuilder() - .withUrl('https://localhost:44344/classroomhub', { - accessTokenFactory: () => auth.session.token || '', - }) - .withAutomaticReconnect() - .configureLogging(signalR.LogLevel.Information) - .build() + // In production, replace with your actual SignalR hub URL + this.connection = new signalR.HubConnectionBuilder() + .withUrl(`${import.meta.env.VITE_API_URL}/classroomhub`, { + accessTokenFactory: () => auth.session.token || '', + }) + .withAutomaticReconnect() + .configureLogging(signalR.LogLevel.Information) + .build() - this.setupEventHandlers() - } + this.setupEventHandlers() } private setupEventHandlers() { - if (this.demoMode || !this.connection) return + if (!this.connection) return this.connection.on('ReceiveSignalingMessage', (message: SignalingMessageDto) => { this.onSignalingMessage?.(message) @@ -87,11 +84,6 @@ export class SignalRService { } async start(): Promise { - if (this.demoMode) { - console.log('SignalR running in demo mode - no backend connection required') - return - } - try { await this.connection.start() this.isConnected = true @@ -99,15 +91,13 @@ export class SignalRService { } catch (error) { console.error('Error starting SignalR connection:', error) // Switch to demo mode if connection fails - this.demoMode = true this.isConnected = false - console.log('Switched to demo mode - SignalR simulation active') } } async joinClass(sessionId: string, userName: string): Promise { - if (this.demoMode || !this.isConnected) { - console.log('Demo mode: Simulating join class for', userName) + if (!this.isConnected) { + console.log('Error starting SignalR connection join class for', userName) // Simulate successful join in demo mode // Don't auto-add participants in demo mode - let manual simulation handle this return @@ -123,8 +113,8 @@ export class SignalRService { async leaveClass(sessionId: string): Promise { const { auth } = store.getState() - if (this.demoMode || !this.isConnected) { - console.log('Demo mode: Simulating leave class for user', auth.user.id) + if (!this.isConnected) { + console.log('Error starting SignalR connection simulating leave class for user', auth.user.id) // Simulate successful leave in demo mode setTimeout(() => { this.onParticipantLeft?.(auth.user.id) @@ -140,8 +130,8 @@ export class SignalRService { } async sendSignalingMessage(message: SignalingMessageDto): Promise { - if (this.demoMode || !this.isConnected) { - console.log('Demo mode: Simulating signaling message', message.type) + if (!this.isConnected) { + console.log('Error starting SignalR connection signaling message', message.type) // In demo mode, we can't send real signaling messages // WebRTC will need to work in local-only mode return @@ -161,8 +151,8 @@ export class SignalRService { message: string, isTeacher: boolean, ): Promise { - if (this.demoMode || !this.isConnected) { - console.log('Demo mode: Simulating chat message from', senderName) + if (!this.isConnected) { + console.log('Error starting SignalR connection simulating chat message from', senderName) const chatMessage: ClassChatDto = { id: `msg-${Date.now()}`, senderId, @@ -202,8 +192,13 @@ export class SignalRService { recipientName: string, isTeacher: boolean, ): Promise { - if (this.demoMode || !this.isConnected) { - console.log('Demo mode: Simulating private message from', senderName, 'to', recipientName) + if (!this.isConnected) { + console.log( + 'Error starting SignalR connection simulating private message from', + senderName, + 'to', + recipientName, + ) const chatMessage: ClassChatDto = { id: `msg-${Date.now()}`, senderId, @@ -243,8 +238,8 @@ export class SignalRService { senderName: string, message: string, ): Promise { - if (this.demoMode || !this.isConnected) { - console.log('Demo mode: Simulating announcement from', senderName) + if (!this.isConnected) { + console.log('Error starting SignalR connection simulating announcement from', senderName) const chatMessage: ClassChatDto = { id: `msg-${Date.now()}`, senderId, @@ -268,8 +263,8 @@ export class SignalRService { } async muteParticipant(sessionId: string, userId: string, isMuted: boolean): Promise { - if (this.demoMode || !this.isConnected) { - console.log('Demo mode: Simulating mute participant', userId, isMuted) + if (!this.isConnected) { + console.log('Error starting SignalR connection simulating mute participant', userId, isMuted) setTimeout(() => { this.onParticipantMuted?.(userId, isMuted) }, 100) @@ -284,8 +279,8 @@ export class SignalRService { } async raiseHand(sessionId: string, studentId: string, studentName: string): Promise { - if (this.demoMode || !this.isConnected) { - console.log('Demo mode: Simulating hand raise from', studentName) + if (!this.isConnected) { + console.log('Error starting SignalR connection simulating hand raise from', studentName) const handRaise: HandRaiseDto = { id: `hand-${Date.now()}`, studentId, @@ -307,8 +302,8 @@ export class SignalRService { } async kickParticipant(sessionId: string, participantId: string): Promise { - if (this.demoMode || !this.isConnected) { - console.log('Demo mode: Simulating kick participant', participantId) + if (!this.isConnected) { + console.log('Error starting SignalR connection simulating kick participant', participantId) setTimeout(() => { this.onParticipantLeft?.(participantId) }, 100) @@ -323,8 +318,8 @@ export class SignalRService { } async approveHandRaise(sessionId: string, handRaiseId: string): Promise { - if (this.demoMode || !this.isConnected) { - console.log('Demo mode: Simulating hand raise approval') + if (!this.isConnected) { + console.log('Error starting SignalR connection simulating hand raise approval') setTimeout(() => { this.onHandRaiseDismissed?.(handRaiseId) }, 100) @@ -339,8 +334,8 @@ export class SignalRService { } async dismissHandRaise(sessionId: string, handRaiseId: string): Promise { - if (this.demoMode || !this.isConnected) { - console.log('Demo mode: Simulating hand raise dismissal') + if (!this.isConnected) { + console.log('Error starting SignalR connection simulating hand raise dismissal') setTimeout(() => { this.onHandRaiseDismissed?.(handRaiseId) }, 100) @@ -393,10 +388,6 @@ export class SignalRService { } } - isInDemoMode(): boolean { - return this.demoMode - } - getConnectionState(): boolean { return this.isConnected } diff --git a/ui/src/views/classroom/ClassList.tsx b/ui/src/views/classroom/ClassList.tsx index eb8cec4d..b3b4040d 100644 --- a/ui/src/views/classroom/ClassList.tsx +++ b/ui/src/views/classroom/ClassList.tsx @@ -11,6 +11,8 @@ import { FaEdit, FaTrash, FaEye, + FaHourglassEnd, + FaDoorOpen, } from 'react-icons/fa' import { ClassroomDto } from '@/proxy/classroom/models' @@ -25,6 +27,15 @@ import { updateClassroom, } from '@/services/classroom.service' +export interface ClassProps { + status: string + className: string + showButtons: boolean + title: string + classes: string + event?: () => void +} + const ClassList: React.FC = () => { const navigate = useNavigate() const { user } = useStoreState((state) => state.auth) @@ -37,12 +48,10 @@ const ClassList: React.FC = () => { teacherId: user.id, teacherName: user.name, scheduledStartTime: '', + scheduledEndTime: '', duration: 60, maxParticipants: 30, - isActive: false, - isScheduled: true, participantCount: 0, - canJoin: false, settingsDto: { allowHandRaise: true, allowStudentChat: true, @@ -91,15 +100,11 @@ const ClassList: React.FC = () => { e.preventDefault() try { - await createClassroom(newClassEntity) + await createClassroom(classroom) getClassroomList() setShowCreateModal(false) setClassroom(newClassEntity) - - if (classroom.id) { - handleJoinClass(classroom) - } } catch (error) { console.error('Sınıf oluştururken hata oluştu:', error) } @@ -161,38 +166,77 @@ const ClassList: React.FC = () => { } } - const canJoinClass = (scheduledTime: string) => { - const scheduled = new Date(scheduledTime) + const canJoinClass = (actualStartTime: string) => { + const actualed = new Date(actualStartTime) const now = new Date() - const tenMinutesBefore = new Date(scheduled.getTime() - 10 * 60 * 1000) - const twoHoursAfter = new Date(scheduled.getTime() + 2 * 60 * 60 * 1000) // 2 saat sonrasına kadar + const tenMinutesBefore = new Date(actualed.getTime() - 10 * 60 * 1000) //10 dakika öncesine kadar + const twoHoursAfter = new Date(actualed.getTime() + 2 * 60 * 60 * 1000) // 2 saat sonrasına kadar return now >= tenMinutesBefore && now <= twoHoursAfter } - const getTimeUntilClass = (scheduledTime: string) => { - const scheduled = new Date(scheduledTime) - const now = new Date() - const diff = scheduled.getTime() - now.getTime() + const widgets = () => { + return { + totalCount: classList.length, + activeCount: classList.filter((c) => !c.actualStartTime && !c.actualEndTime).length, + openCount: classList.filter( + (c) => c.actualStartTime && !c.actualEndTime, // && canJoinClass(c.actualStartTime), + ).length, + passiveCount: classList.filter((c) => c.actualStartTime && c.actualEndTime).length, + } + } - if (diff <= 0) { - // Sınıf başladıysa, ne kadar süredir devam ettiğini göster - const elapsed = Math.abs(diff) - const elapsedMinutes = Math.floor(elapsed / (1000 * 60)) - if (elapsedMinutes < 60) { - return `${elapsedMinutes} dakikadır devam ediyor` + const getClassProps = (classSession: ClassroomDto): ClassProps => { + //Aktif -> boş boş + if (!classSession.actualStartTime && !classSession.actualEndTime) { + return { + status: 'Aktif', + className: 'bg-blue-100 text-blue-800', + showButtons: true, + title: + user.role === 'teacher' && classSession.teacherId === user.id ? 'Dersi Başlat' : 'Katıl', + classes: + user.role === 'teacher' && classSession.teacherId === user.id + ? 'bg-green-600 text-white hover:bg-green-700' + : 'bg-blue-600 text-white hover:bg-blue-700', + event: () => { + user.role === 'teacher' && classSession.teacherId === user.id + ? handleStartClass(classSession) + : handleJoinClass(classSession) + }, } - const elapsedHours = Math.floor(elapsedMinutes / 60) - const remainingMinutes = elapsedMinutes % 60 - return `${elapsedHours}s ${remainingMinutes}d devam ediyor` } - const hours = Math.floor(diff / (1000 * 60 * 60)) - const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) - - if (hours > 0) { - return `${hours}s ${minutes}d kaldı` + //Katılıma Açık -> dolu boş + if ( + classSession.actualStartTime && + !classSession.actualEndTime + //&& canJoinClass(classSession.actualStartTime) + ) { + return { + status: 'Katılım Açık', + className: 'bg-yellow-100 text-yellow-800', + showButtons: true, + title: + user.role === 'teacher' && classSession.teacherId === user.id ? 'Sınıfa Git' : 'Katıl', + classes: + user.role === 'teacher' && classSession.teacherId === user.id + ? 'bg-green-600 text-white hover:bg-green-700' + : 'bg-blue-600 text-white hover:bg-blue-700', + event: () => { + handleJoinClass(classSession) + }, + } + } + + //Pasif + return { + status: 'Pasif', + className: 'bg-gray-100 text-gray-800', + showButtons: false, + title: '', + classes: '', + event: () => {}, } - return `${minutes}d kaldı` } return ( @@ -200,7 +244,7 @@ const ClassList: React.FC = () => { {/* Main Content */}
{/* Stats Cards */} -
+
{

Toplam Sınıf

-

{classList.length}

+

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

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

Aktif Sınıf

- {classList.filter((c) => c.isActive).length} + {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ı */} { ) : (
- {classList.map((classSession, index) => ( - -
-
-

- {classSession.name} -

- - {classSession.isActive - ? 'Aktif' - : canJoinClass(classSession.scheduledStartTime) - ? 'Katılım Açık' - : 'Beklemede'} - -
+ {classList.map((classSession, index) => { + const { status, className, showButtons, title, classes, event } = + getClassProps(classSession) + return ( + +
+
+

+ {classSession.name} +

+ + {status} + +
- {/* Sağ kısım: buton */} - {canJoinClass(classSession.scheduledStartTime) && ( -
- {user.role === 'teacher' && classSession.teacherId === user.id && ( - <> - + title="Sınıfı Düzenle" + > + + Düzenle + - - - )} + title="Sınıfı Sil" + > + + Sil + + + )} - -
- )} -
+ +
+ )} +
-
-

- {classSession.description} -

-
+
+

{classSession.subject}

+
-
-
-
- - - {showDbDateAsIs(classSession.scheduledStartTime)} - -
+
+ + {classSession.description} + +
-
- - {classSession.duration} dakika -
+
+
+
+ + + {showDbDateAsIs(classSession.scheduledStartTime)} + +
-
- - - {classSession.participantCount}/{classSession.maxParticipants} - -
+
+ + {classSession.duration} dakika +
-
- - - {getTimeUntilClass(classSession.scheduledStartTime)} - +
+ {classSession.scheduledEndTime && ( + <> + + + {showDbDateAsIs(classSession.scheduledEndTime!)} + + + )} +
+ +
+ + + {classSession.participantCount}/{classSession.maxParticipants} + +
-
- - ))} + + ) + })}
)}
diff --git a/ui/src/views/classroom/Dashboard.tsx b/ui/src/views/classroom/Dashboard.tsx index 288e3bc7..21f41675 100644 --- a/ui/src/views/classroom/Dashboard.tsx +++ b/ui/src/views/classroom/Dashboard.tsx @@ -74,4 +74,4 @@ const Dashboard: React.FC = () => { ) } -export default Dashboard \ No newline at end of file +export default Dashboard diff --git a/ui/src/views/classroom/RoomDetail.tsx b/ui/src/views/classroom/RoomDetail.tsx index 84042b59..7a567520 100644 --- a/ui/src/views/classroom/RoomDetail.tsx +++ b/ui/src/views/classroom/RoomDetail.tsx @@ -55,6 +55,9 @@ import { KickParticipantModal } from '@/components/classroom/KickParticipantModa import { useParams } from 'react-router-dom' import { getClassroomById } from '@/services/classroom.service' import { showDbDateAsIs } from '@/utils/dateUtils' +import { useNavigate } from 'react-router-dom' +import { endClassroom } from '@/services/classroom.service' +import { ROUTES_ENUM } from '@/routes/route.constant' type SidePanelType = | 'chat' @@ -71,17 +74,16 @@ const newClassSession: ClassroomDto = { teacherId: '', teacherName: '', scheduledStartTime: '', + scheduledEndTime: '', actualStartTime: '', - endTime: '', - isActive: false, - isScheduled: false, + actualEndTime: '', participantCount: 0, settingsDto: undefined, - canJoin: false, } const RoomDetail: React.FC = () => { const params = useParams() + const navigate = useNavigate() const { user } = useStoreState((state) => state.auth) const [classSession, setClassSession] = useState(newClassSession) @@ -223,6 +225,9 @@ const RoomDetail: React.FC = () => { signalRServiceRef.current.setParticipantJoinHandler((userId, name) => { console.log(`Participant joined: ${name}`) + // Eğer kendimsem, ekleme + if (userId === user.id) return + // Create WebRTC connection for new participant if (webRTCServiceRef.current) { webRTCServiceRef.current.createPeerConnection(userId) @@ -308,7 +313,21 @@ const RoomDetail: React.FC = () => { } const handleLeaveCall = async () => { - await cleanup() + try { + // Eğer teacher ise sınıfı kapat + if (user.role === 'teacher') { + await endClassroom(classSession.id) + } + + // Bağlantıları kapat + await cleanup() + + // Başka sayfaya yönlendir + navigate(ROUTES_ENUM.protected.admin.classroom.classes) + } catch (err) { + console.error('Leave işlemi sırasında hata:', err) + navigate(ROUTES_ENUM.protected.admin.classroom.classes) + } } const handleSendMessage = async (e: React.FormEvent) => {