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 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; }

View file

@ -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);

View file

@ -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>();

View file

@ -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
));
}

View file

@ -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,

View file

@ -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; }

View file

@ -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;
}
}

View file

@ -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)

View file

@ -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");

View file

@ -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",

View file

@ -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");

View file

@ -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);
}
}

View file

@ -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"), {

View file

@ -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 {

View file

@ -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
}

View file

@ -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 ı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>

View file

@ -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) => {