virtual classroom
This commit is contained in:
parent
e96faabd76
commit
0b60f006d0
18 changed files with 356 additions and 324 deletions
|
|
@ -13,14 +13,12 @@ public class ClassroomDto : FullAuditedEntityDto<Guid>
|
|||
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; }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ public class ClassroomAutoMapperProfile : Profile
|
|||
{
|
||||
public ClassroomAutoMapperProfile()
|
||||
{
|
||||
CreateMap<Classroom, ClassroomDto>()
|
||||
.ForMember(dest => dest.CanJoin, opt => opt.MapFrom(src => src.CanJoin()));
|
||||
|
||||
CreateMap<Classroom, ClassroomDto>();
|
||||
CreateMap<ClassAttandance, ClassAttendanceDto>();
|
||||
CreateMap<ClassParticipant, ClassParticipantDto>();
|
||||
CreateMap<ClassChat, ClassChatDto>();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -12,12 +12,11 @@ public class Classroom : FullAuditedEntity<Guid>
|
|||
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>
|
|||
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<Guid>
|
|||
TeacherId = teacherId;
|
||||
TeacherName = teacherName;
|
||||
ScheduledStartTime = scheduledStartTime;
|
||||
ScheduledEndTime = scheduledEndTime;
|
||||
Duration = duration;
|
||||
MaxParticipants = maxParticipants;
|
||||
IsActive = isActive;
|
||||
IsScheduled = isScheduled;
|
||||
SettingsJson = settingsJson;
|
||||
|
||||
Participants = new HashSet<ClassParticipant>();
|
||||
AttendanceRecords = new HashSet<ClassAttandance>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Kurs.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20250826203853_Initial")]
|
||||
[Migration("20250828112303_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -1668,7 +1668,7 @@ namespace Kurs.Platform.Migrations
|
|||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
b.Property<Guid?>("SenderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("SenderName")
|
||||
|
|
@ -1777,6 +1777,9 @@ namespace Kurs.Platform.Migrations
|
|||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("ActualEndTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ActualStartTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
|
|
@ -1803,21 +1806,12 @@ namespace Kurs.Platform.Migrations
|
|||
b.Property<int>("Duration")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("EndTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<bool>("IsScheduled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
|
@ -1837,6 +1831,9 @@ namespace Kurs.Platform.Migrations
|
|||
b.Property<int>("ParticipantCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("ScheduledEndTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("ScheduledStartTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
|
|
@ -1857,8 +1854,6 @@ namespace Kurs.Platform.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsActive");
|
||||
|
||||
b.HasIndex("ScheduledStartTime");
|
||||
|
||||
b.HasIndex("TeacherId");
|
||||
|
|
@ -797,12 +797,11 @@ namespace Kurs.Platform.Migrations
|
|||
TeacherId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
TeacherName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
ScheduledStartTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ActualStartTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
EndTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
ScheduledEndTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
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),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||
IsScheduled = table.Column<bool>(type: "bit", nullable: false),
|
||||
ParticipantCount = table.Column<int>(type: "int", nullable: false),
|
||||
SettingsJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
|
|
@ -1958,7 +1957,7 @@ namespace Kurs.Platform.Migrations
|
|||
{
|
||||
Id = 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),
|
||||
Message = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false),
|
||||
Timestamp = table.Column<DateTime>(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",
|
||||
|
|
@ -1665,7 +1665,7 @@ namespace Kurs.Platform.Migrations
|
|||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<Guid>("SenderId")
|
||||
b.Property<Guid?>("SenderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("SenderName")
|
||||
|
|
@ -1774,6 +1774,9 @@ namespace Kurs.Platform.Migrations
|
|||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("ActualEndTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ActualStartTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
|
|
@ -1800,21 +1803,12 @@ namespace Kurs.Platform.Migrations
|
|||
b.Property<int>("Duration")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("EndTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<bool>("IsScheduled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
|
@ -1834,6 +1828,9 @@ namespace Kurs.Platform.Migrations
|
|||
b.Property<int>("ParticipantCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("ScheduledEndTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("ScheduledStartTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
|
|
@ -1854,8 +1851,6 @@ namespace Kurs.Platform.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsActive");
|
||||
|
||||
b.HasIndex("ScheduledStartTime");
|
||||
|
||||
b.HasIndex("TeacherId");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"), {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* 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
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -212,11 +256,14 @@ const ClassList: React.FC = () => {
|
|||
</div>
|
||||
<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-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>
|
||||
</motion.div>
|
||||
|
||||
{/* Aktif Sınıf */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -230,12 +277,51 @@ const ClassList: React.FC = () => {
|
|||
<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-xl sm:text-2xl font-bold text-gray-900">
|
||||
{classList.filter((c) => c.isActive).length}
|
||||
{widgets().activeCount}
|
||||
</p>
|
||||
</div>
|
||||
</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 Açı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
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -279,128 +365,118 @@ const ClassList: React.FC = () => {
|
|||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:gap-6">
|
||||
{classList.map((classSession, index) => (
|
||||
<motion.div
|
||||
key={classSession.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="border border-gray-200 rounded-lg p-4 sm:p-6 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<h3 className="text-base sm:text-lg font-semibold text-gray-900 break-words">
|
||||
{classSession.name}
|
||||
</h3>
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
classSession.isActive
|
||||
? 'bg-green-100 text-green-800'
|
||||
: canJoinClass(classSession.scheduledStartTime)
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{classSession.isActive
|
||||
? 'Aktif'
|
||||
: canJoinClass(classSession.scheduledStartTime)
|
||||
? 'Katılım Açık'
|
||||
: 'Beklemede'}
|
||||
</span>
|
||||
</div>
|
||||
{classList.map((classSession, index) => {
|
||||
const { status, className, showButtons, title, classes, event } =
|
||||
getClassProps(classSession)
|
||||
return (
|
||||
<motion.div
|
||||
key={classSession.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="border border-gray-200 rounded-lg p-4 sm:p-6 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<h3 className="text-base sm:text-lg font-semibold text-gray-900 break-words">
|
||||
{classSession.name}
|
||||
</h3>
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full text-xs font-medium ${className}`}
|
||||
>
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Sağ kısım: buton */}
|
||||
{canJoinClass(classSession.scheduledStartTime) && (
|
||||
<div className="flex space-x-2">
|
||||
{user.role === 'teacher' && classSession.teacherId === user.id && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => openEditModal(classSession)}
|
||||
disabled={classSession.isActive}
|
||||
className="flex px-3 sm:px-4 py-2 rounded-lg bg-blue-600 text-white
|
||||
{/* Sağ kısım: buton */}
|
||||
{showButtons && (
|
||||
<div className="flex space-x-2">
|
||||
{user.role === 'teacher' && classSession.teacherId === user.id && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => openEditModal(classSession)}
|
||||
disabled={classSession.actualStartTime ? true : false}
|
||||
className="flex px-3 sm:px-4 py-2 rounded-lg bg-blue-600 text-white
|
||||
hover:bg-blue-700
|
||||
disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400"
|
||||
title="Sınıfı Düzenle"
|
||||
>
|
||||
<FaEdit size={14} />
|
||||
Düzenle
|
||||
</button>
|
||||
title="Sınıfı Düzenle"
|
||||
>
|
||||
<FaEdit size={14} />
|
||||
Düzenle
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => openDeleteModal(classSession)}
|
||||
disabled={classSession.isActive}
|
||||
className="flex px-3 sm:px-4 py-2 rounded-lg bg-red-600 text-white
|
||||
<button
|
||||
onClick={() => openDeleteModal(classSession)}
|
||||
disabled={classSession.actualStartTime ? true : false}
|
||||
className="flex px-3 sm:px-4 py-2 rounded-lg bg-red-600 text-white
|
||||
hover:bg-red-700
|
||||
disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400"
|
||||
title="Sınıfı Sil"
|
||||
>
|
||||
<FaTrash size={14} />
|
||||
Sil
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
title="Sınıfı Sil"
|
||||
>
|
||||
<FaTrash size={14} />
|
||||
Sil
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
user.role === 'teacher' && classSession.teacherId === user.id
|
||||
? classSession.isActive
|
||||
? handleJoinClass(classSession)
|
||||
: handleStartClass(classSession)
|
||||
: handleJoinClass(classSession)
|
||||
}
|
||||
className={`px-3 sm:px-4 py-2 rounded-lg transition-colors ${
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
{user.role === 'teacher' && classSession.teacherId === user.id
|
||||
? classSession.isActive
|
||||
? 'Sınıfa Git'
|
||||
: 'Dersi Başlat'
|
||||
: 'Katıl'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={event}
|
||||
className={`px-3 sm:px-4 py-2 rounded-lg transition-colors ${
|
||||
classes
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{classSession.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||
<p className="text-gray-600 text-sm sm:text-base">{classSession.subject}</p>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div className="col-span-1 flex items-center gap-2 px-3 py-2 rounded-lg">
|
||||
<FaCalendarAlt size={14} className="text-gray-500" />
|
||||
<span className="truncate">
|
||||
{showDbDateAsIs(classSession.scheduledStartTime)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||
<sub className="text-gray-500 mb-3 text-xs sm:text-sm">
|
||||
{classSession.description}
|
||||
</sub>
|
||||
</div>
|
||||
|
||||
<div className="col-span-1 flex items-center gap-2 px-3 py-2 rounded-lg">
|
||||
<FaClock size={14} className="text-gray-500" />
|
||||
<span>{classSession.duration} dakika</span>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||
<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">
|
||||
<div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
|
||||
<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">
|
||||
<FaUsers size={14} className="text-gray-500" />
|
||||
<span>
|
||||
{classSession.participantCount}/{classSession.maxParticipants}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
|
||||
<FaClock size={14} className="text-gray-500" />
|
||||
<span>{classSession.duration} dakika</span>
|
||||
</div>
|
||||
|
||||
<div className="col-span-1 flex items-center gap-2 px-3 py-2 rounded-lg">
|
||||
<FaEye size={14} className="text-gray-500" />
|
||||
<span className="truncate">
|
||||
{getTimeUntilClass(classSession.scheduledStartTime)}
|
||||
</span>
|
||||
<div className="col-span-1 flex items-center gap-2 p-1 rounded-lg">
|
||||
{classSession.scheduledEndTime && (
|
||||
<>
|
||||
<FaEye size={14} className="text-gray-500" />
|
||||
<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>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<ClassroomDto>(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) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue