virtual classroom

This commit is contained in:
Sedat ÖZTÜRK 2025-08-28 14:53:47 +03:00
parent e96faabd76
commit 0b60f006d0
18 changed files with 356 additions and 324 deletions

View file

@ -13,14 +13,12 @@ public class ClassroomDto : FullAuditedEntityDto<Guid>
public Guid TeacherId { get; set; } public Guid TeacherId { get; set; }
public string TeacherName { get; set; } public string TeacherName { get; set; }
public DateTime ScheduledStartTime { get; set; } public DateTime ScheduledStartTime { get; set; }
public DateTime? ActualStartTime { get; set; } public DateTime? ScheduledEndTime { get; set; }
public DateTime? EndTime { get; set; }
public int Duration { get; set; } public int Duration { get; set; }
public DateTime? ActualStartTime { get; set; }
public DateTime? ActualEndTime { get; set; }
public int MaxParticipants { get; set; } public int MaxParticipants { get; set; }
public bool IsActive { get; set; }
public bool IsScheduled { get; set; }
public int ParticipantCount { get; set; } public int ParticipantCount { get; set; }
public bool CanJoin { get; set; }
[JsonIgnore] [JsonIgnore]
public string SettingsJson { get; set; } public string SettingsJson { get; set; }

View file

@ -60,10 +60,9 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
CurrentUser.Id, CurrentUser.Id,
CurrentUser.Name, CurrentUser.Name,
input.ScheduledStartTime, input.ScheduledStartTime,
input.ScheduledStartTime.AddMinutes(input.Duration),
input.Duration, input.Duration,
input.MaxParticipants, input.MaxParticipants,
false,
true,
input.SettingsJson = JsonSerializer.Serialize(input.SettingsDto) 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"); 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.Name = input.Name;
classSession.Description = input.Description; classSession.Description = input.Description;
classSession.Subject = input.Subject; classSession.Subject = input.Subject;
classSession.TeacherId = input.TeacherId; classSession.TeacherId = input.TeacherId;
classSession.TeacherName = input.TeacherName; classSession.TeacherName = input.TeacherName;
classSession.ScheduledStartTime = input.ScheduledStartTime; classSession.ScheduledStartTime = input.ScheduledStartTime;
classSession.ActualStartTime = input.ActualStartTime; classSession.ScheduledEndTime = input.ScheduledStartTime.AddMinutes(input.Duration);
classSession.Duration = input.Duration; classSession.Duration = input.Duration;
classSession.MaxParticipants = input.MaxParticipants; classSession.MaxParticipants = input.MaxParticipants;
classSession.IsActive = input.IsActive;
classSession.IsScheduled = input.IsScheduled;
classSession.SettingsJson = JsonSerializer.Serialize(input.SettingsDto); classSession.SettingsJson = JsonSerializer.Serialize(input.SettingsDto);
await _classSessionRepository.UpdateAsync(classSession); await _classSessionRepository.UpdateAsync(classSession);
@ -112,11 +104,6 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
throw new UnauthorizedAccessException("Only the teacher can delete this class"); 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); await _classSessionRepository.DeleteAsync(id);
} }
@ -130,15 +117,6 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
throw new UnauthorizedAccessException("Only the teacher can start this class"); 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; classSession.ActualStartTime = DateTime.Now;
await _classSessionRepository.UpdateAsync(classSession); await _classSessionRepository.UpdateAsync(classSession);
@ -156,11 +134,7 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
throw new UnauthorizedAccessException("Only the teacher can end this class"); throw new UnauthorizedAccessException("Only the teacher can end this class");
} }
if (!classSession.IsActive) classSession.ActualEndTime = DateTime.Now;
throw new InvalidOperationException("Class is not active");
classSession.IsActive = false;
classSession.EndTime = DateTime.Now;
await _classSessionRepository.UpdateAsync(classSession); await _classSessionRepository.UpdateAsync(classSession);
@ -171,7 +145,7 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
foreach (var attendance in activeAttendances) foreach (var attendance in activeAttendances)
{ {
attendance.LeaveTime = DateTime.UtcNow; attendance.LeaveTime = DateTime.Now;
attendance.CalculateDuration(); attendance.CalculateDuration();
await _attendanceRepository.UpdateAsync(attendance); await _attendanceRepository.UpdateAsync(attendance);
} }
@ -181,11 +155,6 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
{ {
var classSession = await _classSessionRepository.GetAsync(id); var classSession = await _classSessionRepository.GetAsync(id);
if (!classSession.CanJoin())
{
throw new InvalidOperationException("Cannot join this class at this time");
}
if (classSession.ParticipantCount >= classSession.MaxParticipants) if (classSession.ParticipantCount >= classSession.MaxParticipants)
{ {
throw new InvalidOperationException("Class is full"); throw new InvalidOperationException("Class is full");
@ -216,7 +185,7 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
id, id,
CurrentUser.Id, CurrentUser.Id,
CurrentUser.Name, CurrentUser.Name,
DateTime.UtcNow DateTime.Now
); );
await _attendanceRepository.InsertAsync(attendance); await _attendanceRepository.InsertAsync(attendance);

View file

@ -7,9 +7,7 @@ public class ClassroomAutoMapperProfile : Profile
{ {
public ClassroomAutoMapperProfile() public ClassroomAutoMapperProfile()
{ {
CreateMap<Classroom, ClassroomDto>() CreateMap<Classroom, ClassroomDto>();
.ForMember(dest => dest.CanJoin, opt => opt.MapFrom(src => src.CanJoin()));
CreateMap<ClassAttandance, ClassAttendanceDto>(); CreateMap<ClassAttandance, ClassAttendanceDto>();
CreateMap<ClassParticipant, ClassParticipantDto>(); CreateMap<ClassParticipant, ClassParticipantDto>();
CreateMap<ClassChat, ClassChatDto>(); CreateMap<ClassChat, ClassChatDto>();

View file

@ -1046,10 +1046,9 @@ public class PlatformDataSeeder : IDataSeedContributor, ITransientDependency
item.TeacherId, item.TeacherId,
item.TeacherName, item.TeacherName,
item.ScheduledStartTime, item.ScheduledStartTime,
item.ScheduledEndTime,
item.Duration, item.Duration,
item.MaxParticipants, item.MaxParticipants,
item.IsActive,
item.IsScheduled,
item.SettingsJson item.SettingsJson
)); ));
} }

View file

@ -17683,9 +17683,10 @@
"teacherId": "995220ff-2751-afd6-3d99-3a1bfc55f78e", "teacherId": "995220ff-2751-afd6-3d99-3a1bfc55f78e",
"teacherName": "Prof. Dr. Mehmet Özkan", "teacherName": "Prof. Dr. Mehmet Özkan",
"scheduledStartTime": "2025-08-27T10:00:00Z", "scheduledStartTime": "2025-08-27T10:00:00Z",
"actualStartTime": "", "scheduledEndTime": "2025-08-28T11:30:00Z",
"endTime": "",
"duration": 90, "duration": 90,
"actualStartTime": "",
"actualEndTime": "",
"maxParticipants": 30, "maxParticipants": 30,
"isActive": false, "isActive": false,
"isScheduled": true, "isScheduled": true,
@ -17699,9 +17700,10 @@
"teacherId": "995220ff-2751-afd6-3d99-3a1bfc55f78e", "teacherId": "995220ff-2751-afd6-3d99-3a1bfc55f78e",
"teacherName": "Dr. Ayşe Kaya", "teacherName": "Dr. Ayşe Kaya",
"scheduledStartTime": "2025-08-26T10:00:00Z", "scheduledStartTime": "2025-08-26T10:00:00Z",
"actualStartTime": "", "scheduledEndTime": "2025-08-28T12:00:00Z",
"endTime": "",
"duration": 120, "duration": 120,
"actualStartTime": "",
"actualEndTime": "",
"maxParticipants": 25, "maxParticipants": 25,
"isActive": false, "isActive": false,
"isScheduled": true, "isScheduled": true,
@ -17715,9 +17717,10 @@
"teacherId": "995220ff-2751-afd6-3d99-3a1bfc55f78e", "teacherId": "995220ff-2751-afd6-3d99-3a1bfc55f78e",
"teacherName": "Dr. Ali Veli", "teacherName": "Dr. Ali Veli",
"scheduledStartTime": "2025-08-28T10:00:00Z", "scheduledStartTime": "2025-08-28T10:00:00Z",
"actualStartTime": "", "scheduledEndTime": "2025-08-28T11:15:00Z",
"endTime": "",
"duration": 75, "duration": 75,
"actualStartTime": "",
"actualEndTime": "",
"maxParticipants": 20, "maxParticipants": 20,
"isActive": false, "isActive": false,
"isScheduled": true, "isScheduled": true,

View file

@ -335,8 +335,7 @@ public class ClassroomSeedDto
public Guid? TeacherId { get; set; } public Guid? TeacherId { get; set; }
public string TeacherName { get; set; } public string TeacherName { get; set; }
public DateTime ScheduledStartTime { get; set; } public DateTime ScheduledStartTime { get; set; }
public DateTime? ActualStartTime { get; set; } public DateTime ScheduledEndTime { get; set; }
public DateTime? EndTime { get; set; }
public int Duration { get; set; } public int Duration { get; set; }
public int MaxParticipants { get; set; } public int MaxParticipants { get; set; }
public bool IsActive { get; set; } public bool IsActive { get; set; }

View file

@ -12,12 +12,11 @@ public class Classroom : FullAuditedEntity<Guid>
public Guid? TeacherId { get; set; } public Guid? TeacherId { get; set; }
public string TeacherName { get; set; } public string TeacherName { get; set; }
public DateTime ScheduledStartTime { get; set; } public DateTime ScheduledStartTime { get; set; }
public DateTime? ActualStartTime { get; set; } public DateTime? ScheduledEndTime { get; set; }
public DateTime? EndTime { get; set; }
public int Duration { get; set; } public int Duration { get; set; }
public DateTime? ActualStartTime { get; set; }
public DateTime? ActualEndTime { get; set; }
public int MaxParticipants { get; set; } public int MaxParticipants { get; set; }
public bool IsActive { get; set; }
public bool IsScheduled { get; set; }
public int ParticipantCount { get; set; } public int ParticipantCount { get; set; }
public string SettingsJson { get; set; } public string SettingsJson { get; set; }
@ -40,10 +39,9 @@ public class Classroom : FullAuditedEntity<Guid>
Guid? teacherId, Guid? teacherId,
string teacherName, string teacherName,
DateTime scheduledStartTime, DateTime scheduledStartTime,
DateTime? scheduledEndTime,
int duration, int duration,
int maxParticipants, int maxParticipants,
bool isActive,
bool isScheduled,
string settingsJson string settingsJson
) : base(id) ) : base(id)
{ {
@ -53,23 +51,13 @@ public class Classroom : FullAuditedEntity<Guid>
TeacherId = teacherId; TeacherId = teacherId;
TeacherName = teacherName; TeacherName = teacherName;
ScheduledStartTime = scheduledStartTime; ScheduledStartTime = scheduledStartTime;
ScheduledEndTime = scheduledEndTime;
Duration = duration; Duration = duration;
MaxParticipants = maxParticipants; MaxParticipants = maxParticipants;
IsActive = isActive;
IsScheduled = isScheduled;
SettingsJson = settingsJson; SettingsJson = settingsJson;
Participants = new HashSet<ClassParticipant>(); Participants = new HashSet<ClassParticipant>();
AttendanceRecords = new HashSet<ClassAttandance>(); AttendanceRecords = new HashSet<ClassAttandance>();
ChatMessages = new HashSet<ClassChat>(); ChatMessages = new HashSet<ClassChat>();
} }
public bool CanJoin()
{
var now = DateTime.Now;
var tenMinutesBefore = ScheduledStartTime.AddMinutes(-10);
var twoHoursAfter = ScheduledStartTime.AddHours(2);
return now >= tenMinutesBefore && now <= twoHoursAfter && ParticipantCount < MaxParticipants;
}
} }

View file

@ -894,7 +894,6 @@ public class PlatformDbContext :
b.HasIndex(x => x.TeacherId); b.HasIndex(x => x.TeacherId);
b.HasIndex(x => x.ScheduledStartTime); b.HasIndex(x => x.ScheduledStartTime);
b.HasIndex(x => x.IsActive);
// Relationships // Relationships
b.HasMany(x => x.Participants) b.HasMany(x => x.Participants)

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Kurs.Platform.Migrations namespace Kurs.Platform.Migrations
{ {
[DbContext(typeof(PlatformDbContext))] [DbContext(typeof(PlatformDbContext))]
[Migration("20250826203853_Initial")] [Migration("20250828112303_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -1668,7 +1668,7 @@ namespace Kurs.Platform.Migrations
.HasMaxLength(2000) .HasMaxLength(2000)
.HasColumnType("nvarchar(2000)"); .HasColumnType("nvarchar(2000)");
b.Property<Guid>("SenderId") b.Property<Guid?>("SenderId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<string>("SenderName") b.Property<string>("SenderName")
@ -1777,6 +1777,9 @@ namespace Kurs.Platform.Migrations
b.Property<Guid>("Id") b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<DateTime?>("ActualEndTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("ActualStartTime") b.Property<DateTime?>("ActualStartTime")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
@ -1803,21 +1806,12 @@ namespace Kurs.Platform.Migrations
b.Property<int>("Duration") b.Property<int>("Duration")
.HasColumnType("int"); .HasColumnType("int");
b.Property<DateTime?>("EndTime")
.HasColumnType("datetime2");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted") b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("bit") .HasColumnType("bit")
.HasDefaultValue(false) .HasDefaultValue(false)
.HasColumnName("IsDeleted"); .HasColumnName("IsDeleted");
b.Property<bool>("IsScheduled")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime") b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2") .HasColumnType("datetime2")
.HasColumnName("LastModificationTime"); .HasColumnName("LastModificationTime");
@ -1837,6 +1831,9 @@ namespace Kurs.Platform.Migrations
b.Property<int>("ParticipantCount") b.Property<int>("ParticipantCount")
.HasColumnType("int"); .HasColumnType("int");
b.Property<DateTime?>("ScheduledEndTime")
.HasColumnType("datetime2");
b.Property<DateTime>("ScheduledStartTime") b.Property<DateTime>("ScheduledStartTime")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
@ -1857,8 +1854,6 @@ namespace Kurs.Platform.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("IsActive");
b.HasIndex("ScheduledStartTime"); b.HasIndex("ScheduledStartTime");
b.HasIndex("TeacherId"); b.HasIndex("TeacherId");

View file

@ -797,12 +797,11 @@ namespace Kurs.Platform.Migrations
TeacherId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), TeacherId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
TeacherName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false), TeacherName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
ScheduledStartTime = table.Column<DateTime>(type: "datetime2", nullable: false), ScheduledStartTime = table.Column<DateTime>(type: "datetime2", nullable: false),
ActualStartTime = table.Column<DateTime>(type: "datetime2", nullable: true), ScheduledEndTime = table.Column<DateTime>(type: "datetime2", nullable: true),
EndTime = table.Column<DateTime>(type: "datetime2", nullable: true),
Duration = table.Column<int>(type: "int", nullable: false), Duration = table.Column<int>(type: "int", nullable: false),
ActualStartTime = table.Column<DateTime>(type: "datetime2", nullable: true),
ActualEndTime = table.Column<DateTime>(type: "datetime2", nullable: true),
MaxParticipants = table.Column<int>(type: "int", nullable: false), MaxParticipants = table.Column<int>(type: "int", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
IsScheduled = table.Column<bool>(type: "bit", nullable: false),
ParticipantCount = table.Column<int>(type: "int", nullable: false), ParticipantCount = table.Column<int>(type: "int", nullable: false),
SettingsJson = table.Column<string>(type: "nvarchar(max)", nullable: true), SettingsJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false), CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
@ -1958,7 +1957,7 @@ namespace Kurs.Platform.Migrations
{ {
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false), Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
SessionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false), SessionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
SenderId = table.Column<Guid>(type: "uniqueidentifier", nullable: false), SenderId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
SenderName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false), SenderName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Message = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false), Message = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false),
Timestamp = table.Column<DateTime>(type: "datetime2", nullable: false), Timestamp = table.Column<DateTime>(type: "datetime2", nullable: false),
@ -3039,11 +3038,6 @@ namespace Kurs.Platform.Migrations
table: "PClassParticipant", table: "PClassParticipant",
column: "UserId"); column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_PClassroom_IsActive",
table: "PClassroom",
column: "IsActive");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_PClassroom_ScheduledStartTime", name: "IX_PClassroom_ScheduledStartTime",
table: "PClassroom", table: "PClassroom",

View file

@ -1665,7 +1665,7 @@ namespace Kurs.Platform.Migrations
.HasMaxLength(2000) .HasMaxLength(2000)
.HasColumnType("nvarchar(2000)"); .HasColumnType("nvarchar(2000)");
b.Property<Guid>("SenderId") b.Property<Guid?>("SenderId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<string>("SenderName") b.Property<string>("SenderName")
@ -1774,6 +1774,9 @@ namespace Kurs.Platform.Migrations
b.Property<Guid>("Id") b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<DateTime?>("ActualEndTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("ActualStartTime") b.Property<DateTime?>("ActualStartTime")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
@ -1800,21 +1803,12 @@ namespace Kurs.Platform.Migrations
b.Property<int>("Duration") b.Property<int>("Duration")
.HasColumnType("int"); .HasColumnType("int");
b.Property<DateTime?>("EndTime")
.HasColumnType("datetime2");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted") b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("bit") .HasColumnType("bit")
.HasDefaultValue(false) .HasDefaultValue(false)
.HasColumnName("IsDeleted"); .HasColumnName("IsDeleted");
b.Property<bool>("IsScheduled")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime") b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2") .HasColumnType("datetime2")
.HasColumnName("LastModificationTime"); .HasColumnName("LastModificationTime");
@ -1834,6 +1828,9 @@ namespace Kurs.Platform.Migrations
b.Property<int>("ParticipantCount") b.Property<int>("ParticipantCount")
.HasColumnType("int"); .HasColumnType("int");
b.Property<DateTime?>("ScheduledEndTime")
.HasColumnType("datetime2");
b.Property<DateTime>("ScheduledStartTime") b.Property<DateTime>("ScheduledStartTime")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
@ -1854,8 +1851,6 @@ namespace Kurs.Platform.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("IsActive");
b.HasIndex("ScheduledStartTime"); b.HasIndex("ScheduledStartTime");
b.HasIndex("TeacherId"); b.HasIndex("TeacherId");

View file

@ -41,12 +41,6 @@ public class ClassroomHub : Hub
{ {
var classSession = await _classSessionRepository.GetAsync(sessionId); 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 // Add to SignalR group
await Groups.AddToGroupAsync(Context.ConnectionId, sessionId.ToString()); await Groups.AddToGroupAsync(Context.ConnectionId, sessionId.ToString());
@ -154,21 +148,39 @@ public class ClassroomHub : Hub
public override async Task OnDisconnectedAsync(Exception exception) public override async Task OnDisconnectedAsync(Exception exception)
{ {
// Handle cleanup when user disconnects try
var userId = _currentUser.Id;
if (userId.HasValue)
{ {
var participants = await _participantRepository.GetListAsync( // bağlantı gerçekten iptal edilmişse DB sorgusu çalıştırma
x => x.UserId == userId.Value && x.ConnectionId == Context.ConnectionId if (Context.ConnectionAborted.IsCancellationRequested)
); return;
foreach (var participant in participants)
var userId = _currentUser.Id;
if (userId.HasValue)
{ {
await Clients.Group(participant.SessionId.ToString()) var participants = await _participantRepository
.SendAsync("ParticipantLeft", userId.Value); .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);
} }
} }

View file

@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, { }, {
"url": "index.html", "url": "index.html",
"revision": "0.cb06g8q0ck8" "revision": "0.b9bfk61okp"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View file

@ -17,14 +17,12 @@ export interface ClassroomDto {
teacherId: string teacherId: string
teacherName: string teacherName: string
scheduledStartTime: string scheduledStartTime: string
actualStartTime?: string scheduledEndTime: string
endTime?: string
duration?: number duration?: number
actualStartTime?: string
actualEndTime?: string
maxParticipants?: number maxParticipants?: number
isActive: boolean
isScheduled: boolean
participantCount: number participantCount: number
canJoin: boolean
settingsDto?: ClassroomSettingsDto settingsDto?: ClassroomSettingsDto
} }
@ -106,7 +104,6 @@ export interface ScheduledClassDto {
name: string name: string
scheduledTime: string scheduledTime: string
duration: number duration: number
canJoin: boolean
} }
export interface HandRaiseDto { export interface HandRaiseDto {

View file

@ -10,7 +10,6 @@ import * as signalR from '@microsoft/signalr'
export class SignalRService { export class SignalRService {
private connection!: signalR.HubConnection private connection!: signalR.HubConnection
private isConnected: boolean = false private isConnected: boolean = false
private demoMode: boolean = false // Start in demo mode by default
private onSignalingMessage?: (message: SignalingMessageDto) => void private onSignalingMessage?: (message: SignalingMessageDto) => void
private onAttendanceUpdate?: (record: ClassAttendanceDto) => void private onAttendanceUpdate?: (record: ClassAttendanceDto) => void
private onParticipantJoined?: (userId: string, name: string) => void private onParticipantJoined?: (userId: string, name: string) => void
@ -24,22 +23,20 @@ export class SignalRService {
const { auth } = store.getState() const { auth } = store.getState()
// Only initialize connection if not in demo mode // Only initialize connection if not in demo mode
if (!this.demoMode) { // In production, replace with your actual SignalR hub URL
// In production, replace with your actual SignalR hub URL this.connection = new signalR.HubConnectionBuilder()
this.connection = new signalR.HubConnectionBuilder() .withUrl(`${import.meta.env.VITE_API_URL}/classroomhub`, {
.withUrl('https://localhost:44344/classroomhub', { accessTokenFactory: () => auth.session.token || '',
accessTokenFactory: () => auth.session.token || '', })
}) .withAutomaticReconnect()
.withAutomaticReconnect() .configureLogging(signalR.LogLevel.Information)
.configureLogging(signalR.LogLevel.Information) .build()
.build()
this.setupEventHandlers() this.setupEventHandlers()
}
} }
private setupEventHandlers() { private setupEventHandlers() {
if (this.demoMode || !this.connection) return if (!this.connection) return
this.connection.on('ReceiveSignalingMessage', (message: SignalingMessageDto) => { this.connection.on('ReceiveSignalingMessage', (message: SignalingMessageDto) => {
this.onSignalingMessage?.(message) this.onSignalingMessage?.(message)
@ -87,11 +84,6 @@ export class SignalRService {
} }
async start(): Promise<void> { async start(): Promise<void> {
if (this.demoMode) {
console.log('SignalR running in demo mode - no backend connection required')
return
}
try { try {
await this.connection.start() await this.connection.start()
this.isConnected = true this.isConnected = true
@ -99,15 +91,13 @@ export class SignalRService {
} catch (error) { } catch (error) {
console.error('Error starting SignalR connection:', error) console.error('Error starting SignalR connection:', error)
// Switch to demo mode if connection fails // Switch to demo mode if connection fails
this.demoMode = true
this.isConnected = false this.isConnected = false
console.log('Switched to demo mode - SignalR simulation active')
} }
} }
async joinClass(sessionId: string, userName: string): Promise<void> { async joinClass(sessionId: string, userName: string): Promise<void> {
if (this.demoMode || !this.isConnected) { if (!this.isConnected) {
console.log('Demo mode: Simulating join class for', userName) console.log('Error starting SignalR connection join class for', userName)
// Simulate successful join in demo mode // Simulate successful join in demo mode
// Don't auto-add participants in demo mode - let manual simulation handle this // Don't auto-add participants in demo mode - let manual simulation handle this
return return
@ -123,8 +113,8 @@ export class SignalRService {
async leaveClass(sessionId: string): Promise<void> { async leaveClass(sessionId: string): Promise<void> {
const { auth } = store.getState() const { auth } = store.getState()
if (this.demoMode || !this.isConnected) { if (!this.isConnected) {
console.log('Demo mode: Simulating leave class for user', auth.user.id) console.log('Error starting SignalR connection simulating leave class for user', auth.user.id)
// Simulate successful leave in demo mode // Simulate successful leave in demo mode
setTimeout(() => { setTimeout(() => {
this.onParticipantLeft?.(auth.user.id) this.onParticipantLeft?.(auth.user.id)
@ -140,8 +130,8 @@ export class SignalRService {
} }
async sendSignalingMessage(message: SignalingMessageDto): Promise<void> { async sendSignalingMessage(message: SignalingMessageDto): Promise<void> {
if (this.demoMode || !this.isConnected) { if (!this.isConnected) {
console.log('Demo mode: Simulating signaling message', message.type) console.log('Error starting SignalR connection signaling message', message.type)
// In demo mode, we can't send real signaling messages // In demo mode, we can't send real signaling messages
// WebRTC will need to work in local-only mode // WebRTC will need to work in local-only mode
return return
@ -161,8 +151,8 @@ export class SignalRService {
message: string, message: string,
isTeacher: boolean, isTeacher: boolean,
): Promise<void> { ): Promise<void> {
if (this.demoMode || !this.isConnected) { if (!this.isConnected) {
console.log('Demo mode: Simulating chat message from', senderName) console.log('Error starting SignalR connection simulating chat message from', senderName)
const chatMessage: ClassChatDto = { const chatMessage: ClassChatDto = {
id: `msg-${Date.now()}`, id: `msg-${Date.now()}`,
senderId, senderId,
@ -202,8 +192,13 @@ export class SignalRService {
recipientName: string, recipientName: string,
isTeacher: boolean, isTeacher: boolean,
): Promise<void> { ): Promise<void> {
if (this.demoMode || !this.isConnected) { if (!this.isConnected) {
console.log('Demo mode: Simulating private message from', senderName, 'to', recipientName) console.log(
'Error starting SignalR connection simulating private message from',
senderName,
'to',
recipientName,
)
const chatMessage: ClassChatDto = { const chatMessage: ClassChatDto = {
id: `msg-${Date.now()}`, id: `msg-${Date.now()}`,
senderId, senderId,
@ -243,8 +238,8 @@ export class SignalRService {
senderName: string, senderName: string,
message: string, message: string,
): Promise<void> { ): Promise<void> {
if (this.demoMode || !this.isConnected) { if (!this.isConnected) {
console.log('Demo mode: Simulating announcement from', senderName) console.log('Error starting SignalR connection simulating announcement from', senderName)
const chatMessage: ClassChatDto = { const chatMessage: ClassChatDto = {
id: `msg-${Date.now()}`, id: `msg-${Date.now()}`,
senderId, senderId,
@ -268,8 +263,8 @@ export class SignalRService {
} }
async muteParticipant(sessionId: string, userId: string, isMuted: boolean): Promise<void> { async muteParticipant(sessionId: string, userId: string, isMuted: boolean): Promise<void> {
if (this.demoMode || !this.isConnected) { if (!this.isConnected) {
console.log('Demo mode: Simulating mute participant', userId, isMuted) console.log('Error starting SignalR connection simulating mute participant', userId, isMuted)
setTimeout(() => { setTimeout(() => {
this.onParticipantMuted?.(userId, isMuted) this.onParticipantMuted?.(userId, isMuted)
}, 100) }, 100)
@ -284,8 +279,8 @@ export class SignalRService {
} }
async raiseHand(sessionId: string, studentId: string, studentName: string): Promise<void> { async raiseHand(sessionId: string, studentId: string, studentName: string): Promise<void> {
if (this.demoMode || !this.isConnected) { if (!this.isConnected) {
console.log('Demo mode: Simulating hand raise from', studentName) console.log('Error starting SignalR connection simulating hand raise from', studentName)
const handRaise: HandRaiseDto = { const handRaise: HandRaiseDto = {
id: `hand-${Date.now()}`, id: `hand-${Date.now()}`,
studentId, studentId,
@ -307,8 +302,8 @@ export class SignalRService {
} }
async kickParticipant(sessionId: string, participantId: string): Promise<void> { async kickParticipant(sessionId: string, participantId: string): Promise<void> {
if (this.demoMode || !this.isConnected) { if (!this.isConnected) {
console.log('Demo mode: Simulating kick participant', participantId) console.log('Error starting SignalR connection simulating kick participant', participantId)
setTimeout(() => { setTimeout(() => {
this.onParticipantLeft?.(participantId) this.onParticipantLeft?.(participantId)
}, 100) }, 100)
@ -323,8 +318,8 @@ export class SignalRService {
} }
async approveHandRaise(sessionId: string, handRaiseId: string): Promise<void> { async approveHandRaise(sessionId: string, handRaiseId: string): Promise<void> {
if (this.demoMode || !this.isConnected) { if (!this.isConnected) {
console.log('Demo mode: Simulating hand raise approval') console.log('Error starting SignalR connection simulating hand raise approval')
setTimeout(() => { setTimeout(() => {
this.onHandRaiseDismissed?.(handRaiseId) this.onHandRaiseDismissed?.(handRaiseId)
}, 100) }, 100)
@ -339,8 +334,8 @@ export class SignalRService {
} }
async dismissHandRaise(sessionId: string, handRaiseId: string): Promise<void> { async dismissHandRaise(sessionId: string, handRaiseId: string): Promise<void> {
if (this.demoMode || !this.isConnected) { if (!this.isConnected) {
console.log('Demo mode: Simulating hand raise dismissal') console.log('Error starting SignalR connection simulating hand raise dismissal')
setTimeout(() => { setTimeout(() => {
this.onHandRaiseDismissed?.(handRaiseId) this.onHandRaiseDismissed?.(handRaiseId)
}, 100) }, 100)
@ -393,10 +388,6 @@ export class SignalRService {
} }
} }
isInDemoMode(): boolean {
return this.demoMode
}
getConnectionState(): boolean { getConnectionState(): boolean {
return this.isConnected return this.isConnected
} }

View file

@ -11,6 +11,8 @@ import {
FaEdit, FaEdit,
FaTrash, FaTrash,
FaEye, FaEye,
FaHourglassEnd,
FaDoorOpen,
} from 'react-icons/fa' } from 'react-icons/fa'
import { ClassroomDto } from '@/proxy/classroom/models' import { ClassroomDto } from '@/proxy/classroom/models'
@ -25,6 +27,15 @@ import {
updateClassroom, updateClassroom,
} from '@/services/classroom.service' } from '@/services/classroom.service'
export interface ClassProps {
status: string
className: string
showButtons: boolean
title: string
classes: string
event?: () => void
}
const ClassList: React.FC = () => { const ClassList: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const { user } = useStoreState((state) => state.auth) const { user } = useStoreState((state) => state.auth)
@ -37,12 +48,10 @@ const ClassList: React.FC = () => {
teacherId: user.id, teacherId: user.id,
teacherName: user.name, teacherName: user.name,
scheduledStartTime: '', scheduledStartTime: '',
scheduledEndTime: '',
duration: 60, duration: 60,
maxParticipants: 30, maxParticipants: 30,
isActive: false,
isScheduled: true,
participantCount: 0, participantCount: 0,
canJoin: false,
settingsDto: { settingsDto: {
allowHandRaise: true, allowHandRaise: true,
allowStudentChat: true, allowStudentChat: true,
@ -91,15 +100,11 @@ const ClassList: React.FC = () => {
e.preventDefault() e.preventDefault()
try { try {
await createClassroom(newClassEntity) await createClassroom(classroom)
getClassroomList() getClassroomList()
setShowCreateModal(false) setShowCreateModal(false)
setClassroom(newClassEntity) setClassroom(newClassEntity)
if (classroom.id) {
handleJoinClass(classroom)
}
} catch (error) { } catch (error) {
console.error('Sınıf oluştururken hata oluştu:', 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 canJoinClass = (actualStartTime: string) => {
const scheduled = new Date(scheduledTime) const actualed = new Date(actualStartTime)
const now = new Date() const now = new Date()
const tenMinutesBefore = new Date(scheduled.getTime() - 10 * 60 * 1000) const tenMinutesBefore = new Date(actualed.getTime() - 10 * 60 * 1000) //10 dakika öncesine kadar
const twoHoursAfter = new Date(scheduled.getTime() + 2 * 60 * 60 * 1000) // 2 saat sonrasına kadar const twoHoursAfter = new Date(actualed.getTime() + 2 * 60 * 60 * 1000) // 2 saat sonrasına kadar
return now >= tenMinutesBefore && now <= twoHoursAfter return now >= tenMinutesBefore && now <= twoHoursAfter
} }
const getTimeUntilClass = (scheduledTime: string) => { const widgets = () => {
const scheduled = new Date(scheduledTime) return {
const now = new Date() totalCount: classList.length,
const diff = scheduled.getTime() - now.getTime() 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) { const getClassProps = (classSession: ClassroomDto): ClassProps => {
// Sınıf başladıysa, ne kadar süredir devam ettiğini göster //Aktif -> boş boş
const elapsed = Math.abs(diff) if (!classSession.actualStartTime && !classSession.actualEndTime) {
const elapsedMinutes = Math.floor(elapsed / (1000 * 60)) return {
if (elapsedMinutes < 60) { status: 'Aktif',
return `${elapsedMinutes} dakikadır devam ediyor` 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)) //Katılıma Açık -> dolu boş
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) if (
classSession.actualStartTime &&
if (hours > 0) { !classSession.actualEndTime
return `${hours}s ${minutes}d kaldı` //&& 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 ( return (
@ -200,7 +244,7 @@ const ClassList: React.FC = () => {
{/* Main Content */} {/* Main Content */}
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 mb-6 sm:mb-8"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 sm:gap-6 mb-6 sm:mb-8">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@ -212,11 +256,14 @@ const ClassList: React.FC = () => {
</div> </div>
<div className="ml-3 sm:ml-4"> <div className="ml-3 sm:ml-4">
<p className="text-xs sm:text-sm font-medium text-gray-600">Toplam Sınıf</p> <p className="text-xs sm:text-sm font-medium text-gray-600">Toplam Sınıf</p>
<p className="text-xl sm:text-2xl font-bold text-gray-900">{classList.length}</p> <p className="text-xl sm:text-2xl font-bold text-gray-900">
{widgets().totalCount}{' '}
</p>
</div> </div>
</div> </div>
</motion.div> </motion.div>
{/* Aktif Sınıf */}
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@ -230,12 +277,51 @@ const ClassList: React.FC = () => {
<div className="ml-3 sm:ml-4"> <div className="ml-3 sm:ml-4">
<p className="text-xs sm:text-sm font-medium text-gray-600">Aktif Sınıf</p> <p className="text-xs sm:text-sm font-medium text-gray-600">Aktif Sınıf</p>
<p className="text-xl sm:text-2xl font-bold text-gray-900"> <p className="text-xl sm:text-2xl font-bold text-gray-900">
{classList.filter((c) => c.isActive).length} {widgets().activeCount}
</p> </p>
</div> </div>
</div> </div>
</motion.div> </motion.div>
{/* Katılıma Açık */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.15 }}
className="bg-white rounded-lg shadow-md p-4 sm:p-6"
>
<div className="flex items-center">
<div className="p-2 sm:p-3 bg-blue-100 rounded-full">
<FaDoorOpen className="text-blue-600" size={20} />
</div>
<div className="ml-3 sm:ml-4">
<p className="text-xs sm:text-sm font-medium text-gray-600">Katılıma ık</p>
<p className="text-xl sm:text-2xl font-bold text-gray-900">{widgets().openCount}</p>
</div>
</div>
</motion.div>
{/* Pasif Sınıf */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-white rounded-lg shadow-md p-4 sm:p-6"
>
<div className="flex items-center">
<div className="p-2 sm:p-3 bg-gray-100 rounded-full">
<FaHourglassEnd className="text-gray-600" size={20} />
</div>
<div className="ml-3 sm:ml-4">
<p className="text-xs sm:text-sm font-medium text-gray-600">Pasif Sınıf</p>
<p className="text-xl sm:text-2xl font-bold text-gray-900">
{widgets().passiveCount}
</p>
</div>
</div>
</motion.div>
{/* Toplam Katılımcı */}
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@ -279,128 +365,118 @@ const ClassList: React.FC = () => {
</div> </div>
) : ( ) : (
<div className="grid gap-4 sm:gap-6"> <div className="grid gap-4 sm:gap-6">
{classList.map((classSession, index) => ( {classList.map((classSession, index) => {
<motion.div const { status, className, showButtons, title, classes, event } =
key={classSession.id} getClassProps(classSession)
initial={{ opacity: 0, x: -20 }} return (
animate={{ opacity: 1, x: 0 }} <motion.div
transition={{ delay: index * 0.1 }} key={classSession.id}
className="border border-gray-200 rounded-lg p-4 sm:p-6 hover:shadow-md transition-shadow" initial={{ opacity: 0, x: -20 }}
> animate={{ opacity: 1, x: 0 }}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-2"> transition={{ delay: index * 0.1 }}
<div className="flex items-center space-x-3"> className="border border-gray-200 rounded-lg p-4 sm:p-6 hover:shadow-md transition-shadow"
<h3 className="text-base sm:text-lg font-semibold text-gray-900 break-words"> >
{classSession.name} <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
</h3> <div className="flex items-center space-x-3">
<span <h3 className="text-base sm:text-lg font-semibold text-gray-900 break-words">
className={`px-2 py-1 rounded-full text-xs font-medium ${ {classSession.name}
classSession.isActive </h3>
? 'bg-green-100 text-green-800' <span
: canJoinClass(classSession.scheduledStartTime) className={`px-2 py-1 rounded-full text-xs font-medium ${className}`}
? 'bg-yellow-100 text-yellow-800' >
: 'bg-gray-100 text-gray-800' {status}
}`} </span>
> </div>
{classSession.isActive
? 'Aktif'
: canJoinClass(classSession.scheduledStartTime)
? 'Katılım Açık'
: 'Beklemede'}
</span>
</div>
{/* Sağ kısım: buton */} {/* Sağ kısım: buton */}
{canJoinClass(classSession.scheduledStartTime) && ( {showButtons && (
<div className="flex space-x-2"> <div className="flex space-x-2">
{user.role === 'teacher' && classSession.teacherId === user.id && ( {user.role === 'teacher' && classSession.teacherId === user.id && (
<> <>
<button <button
onClick={() => openEditModal(classSession)} onClick={() => openEditModal(classSession)}
disabled={classSession.isActive} disabled={classSession.actualStartTime ? true : false}
className="flex px-3 sm:px-4 py-2 rounded-lg bg-blue-600 text-white className="flex px-3 sm:px-4 py-2 rounded-lg bg-blue-600 text-white
hover:bg-blue-700 hover:bg-blue-700
disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400" disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400"
title="Sınıfı Düzenle" title="Sınıfı Düzenle"
> >
<FaEdit size={14} /> <FaEdit size={14} />
Düzenle Düzenle
</button> </button>
<button <button
onClick={() => openDeleteModal(classSession)} onClick={() => openDeleteModal(classSession)}
disabled={classSession.isActive} disabled={classSession.actualStartTime ? true : false}
className="flex px-3 sm:px-4 py-2 rounded-lg bg-red-600 text-white className="flex px-3 sm:px-4 py-2 rounded-lg bg-red-600 text-white
hover:bg-red-700 hover:bg-red-700
disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400" disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400"
title="Sınıfı Sil" title="Sınıfı Sil"
> >
<FaTrash size={14} /> <FaTrash size={14} />
Sil Sil
</button> </button>
</> </>
)} )}
<button <button
onClick={() => onClick={event}
user.role === 'teacher' && classSession.teacherId === user.id className={`px-3 sm:px-4 py-2 rounded-lg transition-colors ${
? classSession.isActive classes
? handleJoinClass(classSession) }`}
: handleStartClass(classSession) >
: handleJoinClass(classSession) {title}
} </button>
className={`px-3 sm:px-4 py-2 rounded-lg transition-colors ${ </div>
user.role === 'teacher' && classSession.teacherId === user.id )}
? 'bg-green-600 text-white hover:bg-green-700' </div>
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{user.role === 'teacher' && classSession.teacherId === user.id
? classSession.isActive
? 'Sınıfa Git'
: 'Dersi Başlat'
: 'Katıl'}
</button>
</div>
)}
</div>
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<p className="text-gray-600 mb-3 text-sm sm:text-base"> <p className="text-gray-600 text-sm sm:text-base">{classSession.subject}</p>
{classSession.description} </div>
</p>
</div>
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div className="grid grid-cols-4 gap-3 w-full text-xs sm:text-sm text-gray-600"> <sub className="text-gray-500 mb-3 text-xs sm:text-sm">
<div className="col-span-1 flex items-center gap-2 px-3 py-2 rounded-lg"> {classSession.description}
<FaCalendarAlt size={14} className="text-gray-500" /> </sub>
<span className="truncate"> </div>
{showDbDateAsIs(classSession.scheduledStartTime)}
</span>
</div>
<div className="col-span-1 flex items-center gap-2 px-3 py-2 rounded-lg"> <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<FaClock size={14} className="text-gray-500" /> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 md:gap-3 w-full text-xs sm:text-sm text-gray-600">
<span>{classSession.duration} dakika</span> <div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
</div> <FaCalendarAlt size={14} className="text-gray-500" />
<span className="truncate">
{showDbDateAsIs(classSession.scheduledStartTime)}
</span>
</div>
<div className="col-span-1 flex items-center gap-2 px-3 py-2 rounded-lg"> <div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
<FaUsers size={14} className="text-gray-500" /> <FaClock size={14} className="text-gray-500" />
<span> <span>{classSession.duration} dakika</span>
{classSession.participantCount}/{classSession.maxParticipants} </div>
</span>
</div>
<div className="col-span-1 flex items-center gap-2 px-3 py-2 rounded-lg"> <div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
<FaEye size={14} className="text-gray-500" /> {classSession.scheduledEndTime && (
<span className="truncate"> <>
{getTimeUntilClass(classSession.scheduledStartTime)} <FaEye size={14} className="text-gray-500" />
</span> <span className="truncate">
{showDbDateAsIs(classSession.scheduledEndTime!)}
</span>
</>
)}
</div>
<div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
<FaUsers size={14} className="text-gray-500" />
<span>
{classSession.participantCount}/{classSession.maxParticipants}
</span>
</div>
</div> </div>
</div> </div>
</div> </motion.div>
</motion.div> )
))} })}
</div> </div>
)} )}
</div> </div>

View file

@ -55,6 +55,9 @@ import { KickParticipantModal } from '@/components/classroom/KickParticipantModa
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { getClassroomById } from '@/services/classroom.service' import { getClassroomById } from '@/services/classroom.service'
import { showDbDateAsIs } from '@/utils/dateUtils' 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 = type SidePanelType =
| 'chat' | 'chat'
@ -71,17 +74,16 @@ const newClassSession: ClassroomDto = {
teacherId: '', teacherId: '',
teacherName: '', teacherName: '',
scheduledStartTime: '', scheduledStartTime: '',
scheduledEndTime: '',
actualStartTime: '', actualStartTime: '',
endTime: '', actualEndTime: '',
isActive: false,
isScheduled: false,
participantCount: 0, participantCount: 0,
settingsDto: undefined, settingsDto: undefined,
canJoin: false,
} }
const RoomDetail: React.FC = () => { const RoomDetail: React.FC = () => {
const params = useParams() const params = useParams()
const navigate = useNavigate()
const { user } = useStoreState((state) => state.auth) const { user } = useStoreState((state) => state.auth)
const [classSession, setClassSession] = useState<ClassroomDto>(newClassSession) const [classSession, setClassSession] = useState<ClassroomDto>(newClassSession)
@ -223,6 +225,9 @@ const RoomDetail: React.FC = () => {
signalRServiceRef.current.setParticipantJoinHandler((userId, name) => { signalRServiceRef.current.setParticipantJoinHandler((userId, name) => {
console.log(`Participant joined: ${name}`) console.log(`Participant joined: ${name}`)
// Eğer kendimsem, ekleme
if (userId === user.id) return
// Create WebRTC connection for new participant // Create WebRTC connection for new participant
if (webRTCServiceRef.current) { if (webRTCServiceRef.current) {
webRTCServiceRef.current.createPeerConnection(userId) webRTCServiceRef.current.createPeerConnection(userId)
@ -308,7 +313,21 @@ const RoomDetail: React.FC = () => {
} }
const handleLeaveCall = async () => { 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) => { const handleSendMessage = async (e: React.FormEvent) => {