Video Rooms
This commit is contained in:
parent
6fa266f23e
commit
bdc7f744aa
41 changed files with 6534 additions and 128 deletions
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Volo.Abp.Application.Services;
|
||||
|
||||
namespace Sozsoft.Platform.VideoRooms;
|
||||
|
||||
public interface IVideoroomAppService : IApplicationService
|
||||
{
|
||||
Task<VideoroomDto> GetAsync(Guid id);
|
||||
Task<PagedResultDto<VideoroomDto>> GetListAsync(VideoroomFilterInputDto input);
|
||||
Task<VideoroomDto> CreateAsync(VideoroomDto input);
|
||||
Task<VideoroomDto> UpdateAsync(Guid id, VideoroomDto input);
|
||||
Task DeleteAsync(Guid id);
|
||||
Task<VideoroomDto> StartClassAsync(Guid id);
|
||||
Task EndClassAsync(Guid id);
|
||||
Task<VideoroomDto> JoinClassAsync(Guid id);
|
||||
Task LeaveClassAsync(Guid id);
|
||||
Task<List<VideoroomAttendanceDto>> GetAttendanceAsync(Guid sessionId);
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
|
||||
namespace Sozsoft.Platform.VideoRooms;
|
||||
|
||||
public class VideoroomChatDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid SessionId { get; set; }
|
||||
public Guid SenderId { get; set; }
|
||||
public string SenderName { get; set; }
|
||||
public string Message { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
public Guid? RecipientId { get; set; }
|
||||
public string? RecipientName { get; set; }
|
||||
public bool IsTeacher { get; set; }
|
||||
public string MessageType { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
|
||||
namespace Sozsoft.Platform.VideoRooms;
|
||||
|
||||
public class VideoroomDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Subject { get; set; }
|
||||
public Guid TeacherId { get; set; }
|
||||
public string TeacherName { get; set; }
|
||||
public DateTime ScheduledStartTime { 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 int ParticipantCount { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string SettingsJson { get; set; }
|
||||
public VideoroomSettingsDto SettingsDto
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!string.IsNullOrEmpty(SettingsJson))
|
||||
return JsonSerializer.Deserialize<VideoroomSettingsDto>(SettingsJson);
|
||||
|
||||
return new VideoroomSettingsDto();
|
||||
}
|
||||
set { SettingsJson = JsonSerializer.Serialize(value); }
|
||||
}
|
||||
}
|
||||
|
||||
public class VideoroomSettingsDto
|
||||
{
|
||||
public bool AllowHandRaise { get; set; }
|
||||
public bool AllowStudentChat { get; set; }
|
||||
public bool AllowPrivateMessages { get; set; }
|
||||
public bool AllowStudentScreenShare { get; set; }
|
||||
public string DefaultMicrophoneState { get; set; } = "muted"; // 'muted' | 'unmuted'
|
||||
public string DefaultCameraState { get; set; } = "off"; // 'on' | 'off'
|
||||
public string DefaultLayout { get; set; } = "grid";
|
||||
public bool AutoMuteNewParticipants { get; set; }
|
||||
}
|
||||
|
||||
public class VideoroomListDto : PagedAndSortedResultRequestDto
|
||||
{
|
||||
public bool? IsActive { get; set; }
|
||||
public Guid? TeacherId { get; set; }
|
||||
}
|
||||
|
||||
public class VideoroomAttendanceDto : EntityDto<Guid>
|
||||
{
|
||||
public Guid SessionId { get; set; }
|
||||
public Guid StudentId { get; set; }
|
||||
public string StudentName { get; set; }
|
||||
public DateTime JoinTime { get; set; }
|
||||
public DateTime? LeaveTime { get; set; }
|
||||
public int TotalDurationMinutes { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using Volo.Abp.Application.Dtos;
|
||||
|
||||
namespace Sozsoft.Platform.VideoRooms;
|
||||
|
||||
public class VideoroomFilterInputDto : PagedAndSortedResultRequestDto
|
||||
{
|
||||
public string Search { get; set; }
|
||||
public string Status { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
|
||||
namespace Sozsoft.Platform.VideoRooms;
|
||||
|
||||
public class VideoroomParticipantDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid SessionId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public bool IsTeacher { get; set; }
|
||||
public bool IsAudioMuted { get; set; }
|
||||
public bool IsVideoMuted { get; set; }
|
||||
public bool IsHandRaised { get; set; }
|
||||
public bool IsKicked { get; set; }
|
||||
public DateTime JoinTime { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
|
|
@ -650,7 +650,7 @@ public class PublicAppService : PlatformAppService
|
|||
var features = new List<HomeFeatureDto>
|
||||
{
|
||||
new() { Icon = "FaUsers", TitleKey = "Public.features.reliable", DescriptionKey = "Public.features.reliable.desc" },
|
||||
new() { Icon = "FaCalendarAlt", TitleKey = "App.Coordinator.Classroom.Planning", DescriptionKey = "Public.features.rapid.desc" },
|
||||
new() { Icon = "FaCalendarAlt", TitleKey = "App.Videoroom.Planning", DescriptionKey = "Public.features.rapid.desc" },
|
||||
new() { Icon = "FaBookOpen", TitleKey = "Public.features.expert", DescriptionKey = "Public.features.expert.desc" },
|
||||
new() { Icon = "FaCreditCard", TitleKey = "Public.features.muhasebe", DescriptionKey = "Public.features.muhasebe.desc" },
|
||||
new() { Icon = "FaRegComment", TitleKey = "Public.features.iletisim", DescriptionKey = "Public.features.iletisim.desc" },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,303 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Sozsoft.Platform.Entities;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
|
||||
namespace Sozsoft.Platform.VideoRooms;
|
||||
|
||||
[Authorize]
|
||||
public class VideoroomAppService : PlatformAppService, IVideoroomAppService
|
||||
{
|
||||
private readonly IRepository<Videoroom, Guid> _classSessionRepository;
|
||||
private readonly IRepository<VideoroomParticipant, Guid> _participantRepository;
|
||||
private readonly IRepository<VideoroomAttandance, Guid> _attendanceRepository;
|
||||
private readonly IRepository<VideoroomChat, Guid> _chatRepository;
|
||||
|
||||
public VideoroomAppService(
|
||||
IRepository<Videoroom, Guid> classSessionRepository,
|
||||
IRepository<VideoroomParticipant, Guid> participantRepository,
|
||||
IRepository<VideoroomAttandance, Guid> attendanceRepository,
|
||||
IRepository<VideoroomChat, Guid> chatRepository)
|
||||
{
|
||||
_classSessionRepository = classSessionRepository;
|
||||
_participantRepository = participantRepository;
|
||||
_attendanceRepository = attendanceRepository;
|
||||
_chatRepository = chatRepository;
|
||||
}
|
||||
|
||||
public async Task<VideoroomDto> GetAsync(Guid id)
|
||||
{
|
||||
var classSession = await _classSessionRepository.GetAsync(id);
|
||||
return ObjectMapper.Map<Videoroom, VideoroomDto>(classSession);
|
||||
}
|
||||
|
||||
public async Task<PagedResultDto<VideoroomDto>> GetListAsync(VideoroomFilterInputDto input)
|
||||
{
|
||||
var query = await _classSessionRepository.GetQueryableAsync();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(input.Search))
|
||||
{
|
||||
query = query.Where(x =>
|
||||
x.Name.Contains(input.Search) ||
|
||||
x.Description.Contains(input.Search) ||
|
||||
x.Subject.Contains(input.Search) ||
|
||||
x.TeacherName.Contains(input.Search)
|
||||
);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(input.Status))
|
||||
{
|
||||
switch (input.Status)
|
||||
{
|
||||
case "Active":
|
||||
query = query.Where(x => x.ActualStartTime == null && x.ActualEndTime == null);
|
||||
break;
|
||||
|
||||
case "Open":
|
||||
query = query.Where(x => x.ActualStartTime != null && x.ActualEndTime == null);
|
||||
break;
|
||||
|
||||
case "Passive":
|
||||
query = query.Where(x => x.ActualEndTime != null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var totalCount = query.Count();
|
||||
var items = query
|
||||
.OrderBy(x => x.ScheduledStartTime)
|
||||
.Skip(input.SkipCount)
|
||||
.Take(input.MaxResultCount)
|
||||
.ToList();
|
||||
|
||||
return new PagedResultDto<VideoroomDto>(
|
||||
totalCount,
|
||||
ObjectMapper.Map<List<Videoroom>, List<VideoroomDto>>(items)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<VideoroomDto> CreateAsync(VideoroomDto input)
|
||||
{
|
||||
var classSession = new Videoroom
|
||||
{
|
||||
Name = input.Name,
|
||||
Description = input.Description,
|
||||
Subject = input.Subject,
|
||||
TeacherId = CurrentUser.Id,
|
||||
TeacherName = CurrentUser.Name,
|
||||
ScheduledStartTime = input.ScheduledStartTime,
|
||||
ScheduledEndTime = input.ScheduledStartTime.AddMinutes(input.Duration),
|
||||
Duration = input.Duration,
|
||||
MaxParticipants = input.MaxParticipants,
|
||||
SettingsJson = JsonSerializer.Serialize(input.SettingsDto)
|
||||
};
|
||||
|
||||
await _classSessionRepository.InsertAsync(classSession);
|
||||
|
||||
return ObjectMapper.Map<Videoroom, VideoroomDto>(classSession);
|
||||
}
|
||||
|
||||
public async Task<VideoroomDto> UpdateAsync(Guid id, VideoroomDto input)
|
||||
{
|
||||
var classSession = await _classSessionRepository.GetAsync(id);
|
||||
|
||||
if (classSession.TeacherId != CurrentUser.Id)
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only the teacher can update this 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.ScheduledEndTime = input.ScheduledStartTime.AddMinutes(input.Duration);
|
||||
classSession.Duration = input.Duration;
|
||||
classSession.MaxParticipants = input.MaxParticipants;
|
||||
classSession.SettingsJson = JsonSerializer.Serialize(input.SettingsDto);
|
||||
|
||||
await _classSessionRepository.UpdateAsync(classSession);
|
||||
return ObjectMapper.Map<Videoroom, VideoroomDto>(classSession);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Guid id)
|
||||
{
|
||||
var classSession = await _classSessionRepository.GetAsync(id);
|
||||
|
||||
if (classSession.TeacherId != CurrentUser.Id)
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only the teacher can delete this class");
|
||||
}
|
||||
|
||||
await _classSessionRepository.DeleteAsync(id);
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
public async Task<VideoroomDto> StartClassAsync(Guid id)
|
||||
{
|
||||
var classSession = await _classSessionRepository.GetAsync(id);
|
||||
|
||||
if (classSession.TeacherId != CurrentUser.Id)
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only the teacher can start this class");
|
||||
}
|
||||
|
||||
classSession.ActualStartTime = DateTime.Now;
|
||||
|
||||
await _classSessionRepository.UpdateAsync(classSession);
|
||||
|
||||
return ObjectMapper.Map<Videoroom, VideoroomDto>(classSession);
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
public async Task EndClassAsync(Guid id)
|
||||
{
|
||||
var classSession = await _classSessionRepository.GetAsync(id);
|
||||
|
||||
if (classSession.TeacherId != CurrentUser.Id)
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only the teacher can end this class");
|
||||
}
|
||||
|
||||
classSession.ActualEndTime = DateTime.Now;
|
||||
|
||||
await _classSessionRepository.UpdateAsync(classSession);
|
||||
|
||||
// Update attendance records
|
||||
var activeAttendances = await _attendanceRepository.GetListAsync(
|
||||
x => x.SessionId == id && x.LeaveTime == null
|
||||
);
|
||||
|
||||
foreach (var attendance in activeAttendances)
|
||||
{
|
||||
attendance.LeaveTime = DateTime.Now;
|
||||
attendance.CalculateDuration();
|
||||
await _attendanceRepository.UpdateAsync(attendance);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<VideoroomDto> JoinClassAsync(Guid id)
|
||||
{
|
||||
var classSession = await _classSessionRepository.GetAsync(id);
|
||||
if (classSession == null)
|
||||
{
|
||||
throw new InvalidOperationException("Class not found");
|
||||
}
|
||||
|
||||
var videoroomSettings = string.IsNullOrWhiteSpace(classSession.SettingsJson)
|
||||
? new VideoroomSettingsDto() // default ayarlar
|
||||
: JsonSerializer.Deserialize<VideoroomSettingsDto>(classSession.SettingsJson);
|
||||
|
||||
|
||||
if (classSession.ParticipantCount >= classSession.MaxParticipants)
|
||||
{
|
||||
throw new InvalidOperationException("Class is full");
|
||||
}
|
||||
|
||||
// Check if user is already in the class
|
||||
var existingParticipant = await _participantRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == id && x.UserId == CurrentUser.Id
|
||||
);
|
||||
|
||||
if (existingParticipant == null)
|
||||
{
|
||||
// Add participant
|
||||
var participant = new VideoroomParticipant(
|
||||
GuidGenerator.Create(),
|
||||
id,
|
||||
CurrentUser.Id,
|
||||
CurrentUser.Name,
|
||||
false, // isTeacher
|
||||
videoroomSettings?.DefaultMicrophoneState == "muted",
|
||||
videoroomSettings?.DefaultCameraState == "off",
|
||||
false, // HandRaised
|
||||
false, // isKicked
|
||||
true // isActive
|
||||
);
|
||||
|
||||
await _participantRepository.InsertAsync(participant);
|
||||
|
||||
// Create attendance record
|
||||
var attendance = new VideoroomAttandance(
|
||||
GuidGenerator.Create(),
|
||||
id,
|
||||
CurrentUser.Id,
|
||||
CurrentUser.Name,
|
||||
DateTime.Now
|
||||
);
|
||||
|
||||
await _attendanceRepository.InsertAsync(attendance);
|
||||
|
||||
// Update participant count
|
||||
classSession.ParticipantCount++;
|
||||
await _classSessionRepository.UpdateAsync(classSession);
|
||||
}
|
||||
|
||||
return ObjectMapper.Map<Videoroom, VideoroomDto>(classSession);
|
||||
}
|
||||
|
||||
public async Task LeaveClassAsync(Guid id)
|
||||
{
|
||||
var participant = await _participantRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == id && x.UserId == CurrentUser.Id
|
||||
);
|
||||
|
||||
if (participant != null)
|
||||
{
|
||||
await _participantRepository.DeleteAsync(participant);
|
||||
|
||||
// Update attendance record
|
||||
var attendance = await _attendanceRepository.FirstOrDefaultAsync(
|
||||
x => x.SessionId == id && x.StudentId == CurrentUser.Id && x.LeaveTime == null
|
||||
);
|
||||
|
||||
if (attendance != null)
|
||||
{
|
||||
attendance.LeaveTime = DateTime.UtcNow;
|
||||
attendance.CalculateDuration();
|
||||
await _attendanceRepository.UpdateAsync(attendance);
|
||||
}
|
||||
|
||||
// Update participant count
|
||||
var classSession = await _classSessionRepository.GetAsync(id);
|
||||
classSession.ParticipantCount = Math.Max(0, classSession.ParticipantCount - 1);
|
||||
await _classSessionRepository.UpdateAsync(classSession);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<VideoroomAttendanceDto>> GetAttendanceAsync(Guid sessionId)
|
||||
{
|
||||
var attendanceRecords = await _attendanceRepository.GetListAsync(
|
||||
x => x.SessionId == sessionId
|
||||
);
|
||||
|
||||
return ObjectMapper.Map<List<VideoroomAttandance>, List<VideoroomAttendanceDto>>(attendanceRecords);
|
||||
}
|
||||
|
||||
public async Task<List<VideoroomParticipantDto>> GetParticipantAsync(Guid sessionId)
|
||||
{
|
||||
var participantRecords = await _participantRepository.GetListAsync(
|
||||
x => x.SessionId == sessionId
|
||||
);
|
||||
|
||||
return ObjectMapper.Map<List<VideoroomParticipant>, List<VideoroomParticipantDto>>(participantRecords);
|
||||
}
|
||||
|
||||
public async Task<List<VideoroomChatDto>> GetChatAsync(Guid sessionId)
|
||||
{
|
||||
var chatRecords = await _chatRepository.GetListAsync(
|
||||
x => x.SessionId == sessionId
|
||||
);
|
||||
|
||||
return ObjectMapper.Map<List<VideoroomChat>, List<VideoroomChatDto>>(chatRecords);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
using AutoMapper;
|
||||
using Sozsoft.Platform.Entities;
|
||||
|
||||
namespace Sozsoft.Platform.VideoRooms;
|
||||
|
||||
public class VideoroomAutoMapperProfile : Profile
|
||||
{
|
||||
public VideoroomAutoMapperProfile()
|
||||
{
|
||||
CreateMap<Videoroom, VideoroomDto>();
|
||||
CreateMap<VideoroomAttandance, VideoroomAttendanceDto>();
|
||||
CreateMap<VideoroomParticipant, VideoroomParticipantDto>();
|
||||
CreateMap<VideoroomChat, VideoroomChatDto>();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3638,9 +3638,33 @@
|
|||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Intranet",
|
||||
"en": "Intranet",
|
||||
"tr": "Intranet"
|
||||
"key": "App.Videoroom",
|
||||
"en": "Video Rooms",
|
||||
"tr": "Video Odaları"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Videoroom.Dashboard",
|
||||
"en": "Video Rooms",
|
||||
"tr": "Video Odaları"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Videoroom.List",
|
||||
"en": "Video Rooms",
|
||||
"tr": "Video Odaları"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Videoroom.RoomDetail",
|
||||
"en": "Video Room Detail",
|
||||
"tr": "Video Oda Detayı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Videoroom.Planning",
|
||||
"en": "Video Room Planning",
|
||||
"tr": "Video Oda Planlama"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
|
|
@ -6090,6 +6114,12 @@
|
|||
"tr": "Eğitim Durumu",
|
||||
"en": "Education Status"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Intranet",
|
||||
"tr": "Intranet",
|
||||
"en": "Intranet"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Intranet.SocialComment",
|
||||
|
|
@ -9744,36 +9774,6 @@
|
|||
"tr": "İletişim",
|
||||
"en": "Contact"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Coordinator.Classroom",
|
||||
"tr": "Sınıf",
|
||||
"en": "Classroom"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Coordinator.Classroom.Dashboard",
|
||||
"tr": "Gösterge Paneli",
|
||||
"en": "Dashboard"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Coordinator.Classroom.List",
|
||||
"tr": "Sınıflar",
|
||||
"en": "Classes"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Coordinator.Classroom.RoomDetail",
|
||||
"tr": "Sanal Sınıf",
|
||||
"en": "Virtul Class"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Coordinator.Classroom.Planning",
|
||||
"tr": "Sınıf Planlama",
|
||||
"en": "Class Planning"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SupplyChain.PaymentTerm",
|
||||
|
|
|
|||
|
|
@ -453,6 +453,33 @@
|
|||
"authority": [
|
||||
"App.Contact"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "admin.videoroom.dashboard",
|
||||
"path": "/admin/videoroom/dashboard",
|
||||
"componentPath": "@/views/admin/videoroom/Dashboard",
|
||||
"routeType": "protected",
|
||||
"authority": [
|
||||
"App.Videoroom.Dashboard"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "admin.videoroom.list",
|
||||
"path": "/admin/videoroom/list",
|
||||
"componentPath": "@/views/admin/videoroom/RoomList",
|
||||
"routeType": "protected",
|
||||
"authority": [
|
||||
"App.Videoroom.List"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "admin.videoroom.roomdetail",
|
||||
"path": "/admin/videoroom/room/:id",
|
||||
"componentPath": "@/views/admin/videoroom/RoomDetail",
|
||||
"routeType": "protected",
|
||||
"authority": [
|
||||
"App.Videoroom.RoomDetail"
|
||||
]
|
||||
}
|
||||
],
|
||||
"MenuGroups": [
|
||||
|
|
@ -1096,11 +1123,21 @@
|
|||
"RequiredPermissionName": "App.Intranet.Events.Event",
|
||||
"IsDisabled": false
|
||||
},
|
||||
{
|
||||
"ParentCode": "App.Administration",
|
||||
"Code": "App.Videoroom.Dashboard",
|
||||
"DisplayName": "App.Videoroom.Dashboard",
|
||||
"Order": 4,
|
||||
"Url": "/admin/videoroom/dashboard",
|
||||
"Icon": "FcVideoCall",
|
||||
"RequiredPermissionName": "App.Videoroom.Dashboard",
|
||||
"IsDisabled": false
|
||||
},
|
||||
{
|
||||
"ParentCode": "App.Administration",
|
||||
"Code": "App.Administration.Restrictions",
|
||||
"DisplayName": "App.Restrictions",
|
||||
"Order": 4,
|
||||
"Order": 5,
|
||||
"Url": null,
|
||||
"Icon": "FaLock",
|
||||
"RequiredPermissionName": null,
|
||||
|
|
@ -1130,7 +1167,7 @@
|
|||
"ParentCode": "App.Administration",
|
||||
"Code": "Abp.Identity",
|
||||
"DisplayName": "Abp.Identity",
|
||||
"Order": 5,
|
||||
"Order": 6,
|
||||
"Url": null,
|
||||
"Icon": "FcConferenceCall",
|
||||
"RequiredPermissionName": null,
|
||||
|
|
@ -1220,7 +1257,7 @@
|
|||
"ParentCode": "App.Administration",
|
||||
"Code": "App.Reports.Management",
|
||||
"DisplayName": "App.Reports.Management",
|
||||
"Order": 6,
|
||||
"Order": 7,
|
||||
"Url": null,
|
||||
"Icon": "FcDocument",
|
||||
"RequiredPermissionName": null,
|
||||
|
|
@ -1250,7 +1287,7 @@
|
|||
"ParentCode": "App.Administration",
|
||||
"Code": "App.Files",
|
||||
"DisplayName": "App.Files",
|
||||
"Order": 7,
|
||||
"Order": 8,
|
||||
"Url": "/admin/files",
|
||||
"Icon": "FcFolder",
|
||||
"RequiredPermissionName": "App.Files",
|
||||
|
|
@ -1260,7 +1297,7 @@
|
|||
"ParentCode": "App.Administration",
|
||||
"Code": "App.Forum",
|
||||
"DisplayName": "App.Forum",
|
||||
"Order": 8,
|
||||
"Order": 9,
|
||||
"Url": "/admin/forum",
|
||||
"Icon": "FcLink",
|
||||
"RequiredPermissionName": "App.ForumManagement.Publish",
|
||||
|
|
|
|||
|
|
@ -4071,6 +4071,43 @@
|
|||
"IsEnabled": true,
|
||||
"MultiTenancySide": 3,
|
||||
"MenuGroup": "Erp"
|
||||
},
|
||||
|
||||
{
|
||||
"GroupName": "App.Administration",
|
||||
"Name": "App.Videoroom",
|
||||
"ParentName": null,
|
||||
"DisplayName": "App.Videoroom",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Administration",
|
||||
"Name": "App.Videoroom.Dashboard",
|
||||
"ParentName": "App.Videoroom",
|
||||
"DisplayName": "App.Videoroom.Dashboard",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Administration",
|
||||
"Name": "App.Videoroom.List",
|
||||
"ParentName": "App.Videoroom",
|
||||
"DisplayName": "App.Videoroom.List",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Kurs"
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Administration",
|
||||
"Name": "App.Videoroom.RoomDetail",
|
||||
"ParentName": "App.Videoroom",
|
||||
"DisplayName": "App.Videoroom.RoomDetail",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2,
|
||||
"MenuGroup": "Kurs"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,5 +80,9 @@ public enum TableNameEnum
|
|||
EventType,
|
||||
Event,
|
||||
EventPhoto,
|
||||
EventComment
|
||||
EventComment,
|
||||
Videoroom,
|
||||
VideoroomParticipant,
|
||||
VideoroomAttandance,
|
||||
VideoroomChat
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ public static class TableNameResolver
|
|||
{ nameof(TableNameEnum.Note), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
||||
{ nameof(TableNameEnum.ReportCategory), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
||||
{ nameof(TableNameEnum.ReportTemplate), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
||||
{ nameof(TableNameEnum.Videoroom), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
||||
{ nameof(TableNameEnum.VideoroomParticipant), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
||||
{ nameof(TableNameEnum.VideoroomAttandance), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
||||
{ nameof(TableNameEnum.VideoroomChat), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
||||
|
||||
// 🔹 INTRANET TABLOLARI
|
||||
{ nameof(TableNameEnum.Announcement), (TablePrefix.TenantByName, MenuPrefix.Administration) },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
namespace Sozsoft.Platform.Entities;
|
||||
|
||||
public class Videoroom : FullAuditedEntity<Guid>, IMultiTenant
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
public Guid? BranchId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Subject { get; set; }
|
||||
public Guid? TeacherId { get; set; }
|
||||
public string TeacherName { get; set; }
|
||||
public DateTime ScheduledStartTime { 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 int ParticipantCount { get; set; }
|
||||
public string SettingsJson { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public virtual ICollection<VideoroomParticipant> Participants { get; set; } = new HashSet<VideoroomParticipant>();
|
||||
public virtual ICollection<VideoroomAttandance> AttendanceRecords { get; set; } = new HashSet<VideoroomAttandance>();
|
||||
public virtual ICollection<VideoroomChat> ChatMessages { get; set; } = new HashSet<VideoroomChat>();
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
namespace Sozsoft.Platform.Entities;
|
||||
|
||||
public class VideoroomAttandance : FullAuditedEntity<Guid>, IMultiTenant
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
public Guid? BranchId { get; set; }
|
||||
|
||||
public Guid SessionId { get; set; }
|
||||
public Guid? StudentId { get; set; }
|
||||
public string StudentName { get; set; }
|
||||
public DateTime JoinTime { get; set; }
|
||||
public DateTime? LeaveTime { get; set; }
|
||||
public int TotalDurationMinutes { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public virtual Videoroom Session { get; set; }
|
||||
|
||||
protected VideoroomAttandance()
|
||||
{
|
||||
}
|
||||
|
||||
public VideoroomAttandance(
|
||||
Guid id,
|
||||
Guid sessionId,
|
||||
Guid? studentId,
|
||||
string studentName,
|
||||
DateTime joinTime
|
||||
) : base(id)
|
||||
{
|
||||
SessionId = sessionId;
|
||||
StudentId = studentId;
|
||||
StudentName = studentName;
|
||||
JoinTime = joinTime;
|
||||
TotalDurationMinutes = 0;
|
||||
}
|
||||
|
||||
public void CalculateDuration()
|
||||
{
|
||||
if (LeaveTime.HasValue)
|
||||
{
|
||||
var duration = LeaveTime.Value - JoinTime;
|
||||
TotalDurationMinutes = (int)duration.TotalMinutes;
|
||||
}
|
||||
else
|
||||
{
|
||||
var duration = DateTime.UtcNow - JoinTime;
|
||||
TotalDurationMinutes = (int)duration.TotalMinutes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
namespace Sozsoft.Platform.Entities;
|
||||
|
||||
public class VideoroomChat : FullAuditedEntity<Guid>, IMultiTenant
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
public Guid? BranchId { get; set; }
|
||||
|
||||
public Guid SessionId { get; set; }
|
||||
public Guid? SenderId { get; set; }
|
||||
public string SenderName { get; set; }
|
||||
public string Message { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
public Guid? RecipientId { get; set; }
|
||||
public string? RecipientName { get; set; }
|
||||
public bool IsTeacher { get; set; }
|
||||
public string MessageType { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public virtual Videoroom Session { get; set; }
|
||||
|
||||
protected VideoroomChat()
|
||||
{
|
||||
}
|
||||
|
||||
public VideoroomChat(
|
||||
Guid id,
|
||||
Guid sessionId,
|
||||
Guid? senderId,
|
||||
string senderName,
|
||||
string message,
|
||||
Guid? recipientId,
|
||||
string recipientName,
|
||||
bool isTeacher,
|
||||
string messageType
|
||||
) : base(id)
|
||||
{
|
||||
SessionId = sessionId;
|
||||
SenderId = senderId;
|
||||
SenderName = senderName;
|
||||
Message = message;
|
||||
RecipientId = recipientId;
|
||||
RecipientName = recipientName;
|
||||
IsTeacher = isTeacher;
|
||||
Timestamp = DateTime.UtcNow;
|
||||
MessageType = messageType;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
namespace Sozsoft.Platform.Entities;
|
||||
|
||||
public class VideoroomParticipant : FullAuditedEntity<Guid>, IMultiTenant
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
public Guid? BranchId { get; set; }
|
||||
|
||||
public Guid SessionId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public bool IsTeacher { get; set; }
|
||||
public bool IsAudioMuted { get; set; } = false;
|
||||
public bool IsVideoMuted { get; set; } = false;
|
||||
public bool IsHandRaised { get; set; } = false;
|
||||
public bool IsKicked { get; set; } = false;
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime JoinTime { get; set; }
|
||||
public string ConnectionId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public virtual Videoroom Session { get; set; }
|
||||
|
||||
protected VideoroomParticipant()
|
||||
{
|
||||
}
|
||||
|
||||
public VideoroomParticipant(
|
||||
Guid id,
|
||||
Guid sessionId,
|
||||
Guid? userId,
|
||||
string userName,
|
||||
bool isTeacher,
|
||||
bool isAudioMuted,
|
||||
bool isVideoMuted,
|
||||
bool isHandRaised,
|
||||
bool isKicked,
|
||||
bool isActive
|
||||
) : base(id)
|
||||
{
|
||||
SessionId = sessionId;
|
||||
UserId = userId;
|
||||
UserName = userName;
|
||||
IsTeacher = isTeacher;
|
||||
IsAudioMuted = isAudioMuted;
|
||||
IsVideoMuted = isVideoMuted;
|
||||
IsHandRaised = isHandRaised;
|
||||
IsActive = isActive;
|
||||
IsKicked = isKicked;
|
||||
JoinTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void MuteAudio()
|
||||
{
|
||||
IsAudioMuted = true;
|
||||
}
|
||||
|
||||
public void UnmuteAudio()
|
||||
{
|
||||
IsAudioMuted = false;
|
||||
}
|
||||
|
||||
public void MuteVideo()
|
||||
{
|
||||
IsVideoMuted = true;
|
||||
}
|
||||
|
||||
public void UnmuteVideo()
|
||||
{
|
||||
IsVideoMuted = false;
|
||||
}
|
||||
|
||||
public void UpdateConnectionId(string connectionId)
|
||||
{
|
||||
ConnectionId = connectionId;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,6 +131,13 @@ public class PlatformDbContext :
|
|||
public DbSet<SocialLike> SocialLikes { get; set; }
|
||||
#endregion
|
||||
|
||||
#region VideoCall
|
||||
public DbSet<Videoroom> ClassSessions { get; set; }
|
||||
public DbSet<VideoroomParticipant> Participants { get; set; }
|
||||
public DbSet<VideoroomAttandance> AttendanceRecords { get; set; }
|
||||
public DbSet<VideoroomChat> ChatMessages { get; set; }
|
||||
#endregion
|
||||
|
||||
public PlatformDbContext(DbContextOptions<PlatformDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
|
|
@ -1267,5 +1274,77 @@ public class PlatformDbContext :
|
|||
.HasForeignKey(x => x.EventId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
//Videoroom
|
||||
builder.Entity<Videoroom>(b =>
|
||||
{
|
||||
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Videoroom)), Prefix.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.Name).IsRequired().HasMaxLength(256);
|
||||
b.Property(x => x.Description).HasMaxLength(1024);
|
||||
b.Property(x => x.Subject).HasMaxLength(128);
|
||||
b.Property(x => x.TeacherName).IsRequired().HasMaxLength(128);
|
||||
|
||||
b.HasIndex(x => x.TeacherId);
|
||||
b.HasIndex(x => x.ScheduledStartTime);
|
||||
|
||||
b.HasMany(x => x.Participants)
|
||||
.WithOne(x => x.Session)
|
||||
.HasForeignKey(x => x.SessionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasMany(x => x.AttendanceRecords)
|
||||
.WithOne(x => x.Session)
|
||||
.HasForeignKey(x => x.SessionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasMany(x => x.ChatMessages)
|
||||
.WithOne(x => x.Session)
|
||||
.HasForeignKey(x => x.SessionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
builder.Entity<VideoroomParticipant>(b =>
|
||||
{
|
||||
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.VideoroomParticipant)), Prefix.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.UserName).IsRequired().HasMaxLength(128);
|
||||
b.Property(x => x.ConnectionId).HasMaxLength(128);
|
||||
b.Property(x => x.IsActive).HasDefaultValue(true);
|
||||
b.Property(x => x.IsKicked).HasDefaultValue(false);
|
||||
|
||||
b.HasIndex(x => x.SessionId);
|
||||
b.HasIndex(x => x.UserId);
|
||||
b.HasIndex(x => new { x.SessionId, x.UserId }).IsUnique();
|
||||
});
|
||||
|
||||
builder.Entity<VideoroomAttandance>(b =>
|
||||
{
|
||||
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.VideoroomAttandance)), Prefix.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.StudentName).IsRequired().HasMaxLength(128);
|
||||
|
||||
b.HasIndex(x => x.SessionId);
|
||||
b.HasIndex(x => x.StudentId);
|
||||
b.HasIndex(x => x.JoinTime);
|
||||
});
|
||||
|
||||
builder.Entity<VideoroomChat>(b =>
|
||||
{
|
||||
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.VideoroomChat)), Prefix.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.SenderName).IsRequired().HasMaxLength(128);
|
||||
b.Property(x => x.Message).IsRequired().HasMaxLength(2048);
|
||||
b.Property(x => x.MessageType).HasMaxLength(64);
|
||||
b.Property(x => x.RecipientName).HasMaxLength(256);
|
||||
|
||||
b.HasIndex(x => x.SessionId);
|
||||
b.HasIndex(x => x.SenderId);
|
||||
b.HasIndex(x => x.Timestamp);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Sozsoft.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20260507140651_Initial")]
|
||||
[Migration("20260507202053_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -4894,6 +4894,356 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.ToTable("Sas_H_UomCategory", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.Videoroom", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("ActualEndTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ActualStartTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("BranchId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.Property<int>("Duration")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<int>("MaxParticipants")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<int>("ParticipantCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("ScheduledEndTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("ScheduledStartTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("SettingsJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<Guid?>("TeacherId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("TeacherName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ScheduledStartTime");
|
||||
|
||||
b.HasIndex("TeacherId");
|
||||
|
||||
b.ToTable("Adm_T_Videoroom", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.VideoroomAttandance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("BranchId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<DateTime>("JoinTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<DateTime?>("LeaveTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid>("SessionId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("StudentId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("StudentName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<int>("TotalDurationMinutes")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JoinTime");
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.HasIndex("StudentId");
|
||||
|
||||
b.ToTable("Adm_T_VideoroomAttandance", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.VideoroomChat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("BranchId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<bool>("IsTeacher")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("nvarchar(2048)");
|
||||
|
||||
b.Property<string>("MessageType")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<Guid?>("RecipientId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("RecipientName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<Guid?>("SenderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("SenderName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<Guid>("SessionId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SenderId");
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.ToTable("Adm_T_VideoroomChat", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.VideoroomParticipant", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("BranchId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("ConnectionId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsAudioMuted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<bool>("IsHandRaised")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsKicked")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("IsTeacher")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsVideoMuted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime>("JoinTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<Guid>("SessionId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("SessionId", "UserId")
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Adm_T_VideoroomParticipant", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.WorkHour", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -7599,6 +7949,39 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.Navigation("UomCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.VideoroomAttandance", b =>
|
||||
{
|
||||
b.HasOne("Sozsoft.Platform.Entities.Videoroom", "Session")
|
||||
.WithMany("AttendanceRecords")
|
||||
.HasForeignKey("SessionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Session");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.VideoroomChat", b =>
|
||||
{
|
||||
b.HasOne("Sozsoft.Platform.Entities.Videoroom", "Session")
|
||||
.WithMany("ChatMessages")
|
||||
.HasForeignKey("SessionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Session");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.VideoroomParticipant", b =>
|
||||
{
|
||||
b.HasOne("Sozsoft.Platform.Entities.Videoroom", "Session")
|
||||
.WithMany("Participants")
|
||||
.HasForeignKey("SessionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Session");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Forum.ForumPost", b =>
|
||||
{
|
||||
b.HasOne("Sozsoft.Platform.Forum.ForumPost", "ParentPost")
|
||||
|
|
@ -7885,6 +8268,15 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.Navigation("Uoms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.Videoroom", b =>
|
||||
{
|
||||
b.Navigation("AttendanceRecords");
|
||||
|
||||
b.Navigation("ChatMessages");
|
||||
|
||||
b.Navigation("Participants");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Forum.ForumCategory", b =>
|
||||
{
|
||||
b.Navigation("Topics");
|
||||
|
|
@ -691,6 +691,39 @@ namespace Sozsoft.Platform.Migrations
|
|||
table.PrimaryKey("PK_Adm_T_Survey", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Adm_T_Videoroom",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
BranchId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
|
||||
Subject = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
TeacherId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
TeacherName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
ScheduledStartTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
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),
|
||||
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),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Adm_T_Videoroom", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Adm_T_WorkHour",
|
||||
columns: table => new
|
||||
|
|
@ -2205,6 +2238,110 @@ namespace Sozsoft.Platform.Migrations
|
|||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Adm_T_VideoroomAttandance",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
BranchId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
SessionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
StudentId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
StudentName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
JoinTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
LeaveTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
TotalDurationMinutes = table.Column<int>(type: "int", nullable: false),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Adm_T_VideoroomAttandance", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Adm_T_VideoroomAttandance_Adm_T_Videoroom_SessionId",
|
||||
column: x => x.SessionId,
|
||||
principalTable: "Adm_T_Videoroom",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Adm_T_VideoroomChat",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
BranchId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
SessionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
SenderId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
SenderName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
Message = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: false),
|
||||
Timestamp = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
RecipientId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
RecipientName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
IsTeacher = table.Column<bool>(type: "bit", nullable: false),
|
||||
MessageType = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Adm_T_VideoroomChat", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Adm_T_VideoroomChat_Adm_T_Videoroom_SessionId",
|
||||
column: x => x.SessionId,
|
||||
principalTable: "Adm_T_Videoroom",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Adm_T_VideoroomParticipant",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
BranchId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
SessionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
UserName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
IsTeacher = table.Column<bool>(type: "bit", nullable: false),
|
||||
IsAudioMuted = table.Column<bool>(type: "bit", nullable: false),
|
||||
IsVideoMuted = table.Column<bool>(type: "bit", nullable: false),
|
||||
IsHandRaised = table.Column<bool>(type: "bit", nullable: false),
|
||||
IsKicked = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
|
||||
JoinTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ConnectionId = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Adm_T_VideoroomParticipant", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Adm_T_VideoroomParticipant_Adm_T_Videoroom_SessionId",
|
||||
column: x => x.SessionId,
|
||||
principalTable: "Adm_T_Videoroom",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OpenIddictAuthorizations",
|
||||
columns: table => new
|
||||
|
|
@ -3306,6 +3443,63 @@ namespace Sozsoft.Platform.Migrations
|
|||
table: "Adm_T_SurveyResponse",
|
||||
column: "SurveyId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Adm_T_Videoroom_ScheduledStartTime",
|
||||
table: "Adm_T_Videoroom",
|
||||
column: "ScheduledStartTime");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Adm_T_Videoroom_TeacherId",
|
||||
table: "Adm_T_Videoroom",
|
||||
column: "TeacherId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Adm_T_VideoroomAttandance_JoinTime",
|
||||
table: "Adm_T_VideoroomAttandance",
|
||||
column: "JoinTime");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Adm_T_VideoroomAttandance_SessionId",
|
||||
table: "Adm_T_VideoroomAttandance",
|
||||
column: "SessionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Adm_T_VideoroomAttandance_StudentId",
|
||||
table: "Adm_T_VideoroomAttandance",
|
||||
column: "StudentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Adm_T_VideoroomChat_SenderId",
|
||||
table: "Adm_T_VideoroomChat",
|
||||
column: "SenderId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Adm_T_VideoroomChat_SessionId",
|
||||
table: "Adm_T_VideoroomChat",
|
||||
column: "SessionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Adm_T_VideoroomChat_Timestamp",
|
||||
table: "Adm_T_VideoroomChat",
|
||||
column: "Timestamp");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Adm_T_VideoroomParticipant_SessionId",
|
||||
table: "Adm_T_VideoroomParticipant",
|
||||
column: "SessionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Adm_T_VideoroomParticipant_SessionId_UserId",
|
||||
table: "Adm_T_VideoroomParticipant",
|
||||
columns: new[] { "SessionId", "UserId" },
|
||||
unique: true,
|
||||
filter: "[UserId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Adm_T_VideoroomParticipant_UserId",
|
||||
table: "Adm_T_VideoroomParticipant",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OpenIddictApplications_ClientId",
|
||||
table: "OpenIddictApplications",
|
||||
|
|
@ -3629,6 +3823,15 @@ namespace Sozsoft.Platform.Migrations
|
|||
migrationBuilder.DropTable(
|
||||
name: "Adm_T_SurveyQuestionOption");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Adm_T_VideoroomAttandance");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Adm_T_VideoroomChat");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Adm_T_VideoroomParticipant");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Adm_T_WorkHour");
|
||||
|
||||
|
|
@ -3782,6 +3985,9 @@ namespace Sozsoft.Platform.Migrations
|
|||
migrationBuilder.DropTable(
|
||||
name: "Adm_T_SurveyQuestion");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Adm_T_Videoroom");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OpenIddictAuthorizations");
|
||||
|
||||
|
|
@ -4891,6 +4891,356 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.ToTable("Sas_H_UomCategory", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.Videoroom", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("ActualEndTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ActualStartTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("BranchId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.Property<int>("Duration")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<int>("MaxParticipants")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<int>("ParticipantCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("ScheduledEndTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("ScheduledStartTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("SettingsJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<Guid?>("TeacherId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("TeacherName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ScheduledStartTime");
|
||||
|
||||
b.HasIndex("TeacherId");
|
||||
|
||||
b.ToTable("Adm_T_Videoroom", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.VideoroomAttandance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("BranchId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<DateTime>("JoinTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<DateTime?>("LeaveTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid>("SessionId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("StudentId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("StudentName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<int>("TotalDurationMinutes")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JoinTime");
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.HasIndex("StudentId");
|
||||
|
||||
b.ToTable("Adm_T_VideoroomAttandance", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.VideoroomChat", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("BranchId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<bool>("IsTeacher")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("nvarchar(2048)");
|
||||
|
||||
b.Property<string>("MessageType")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<Guid?>("RecipientId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("RecipientName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<Guid?>("SenderId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("SenderName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<Guid>("SessionId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SenderId");
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.ToTable("Adm_T_VideoroomChat", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.VideoroomParticipant", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("BranchId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("ConnectionId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsAudioMuted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<bool>("IsHandRaised")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsKicked")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("IsTeacher")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsVideoMuted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime>("JoinTime")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<Guid>("SessionId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("SessionId", "UserId")
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Adm_T_VideoroomParticipant", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.WorkHour", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -7596,6 +7946,39 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.Navigation("UomCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.VideoroomAttandance", b =>
|
||||
{
|
||||
b.HasOne("Sozsoft.Platform.Entities.Videoroom", "Session")
|
||||
.WithMany("AttendanceRecords")
|
||||
.HasForeignKey("SessionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Session");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.VideoroomChat", b =>
|
||||
{
|
||||
b.HasOne("Sozsoft.Platform.Entities.Videoroom", "Session")
|
||||
.WithMany("ChatMessages")
|
||||
.HasForeignKey("SessionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Session");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.VideoroomParticipant", b =>
|
||||
{
|
||||
b.HasOne("Sozsoft.Platform.Entities.Videoroom", "Session")
|
||||
.WithMany("Participants")
|
||||
.HasForeignKey("SessionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Session");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Forum.ForumPost", b =>
|
||||
{
|
||||
b.HasOne("Sozsoft.Platform.Forum.ForumPost", "ParentPost")
|
||||
|
|
@ -7882,6 +8265,15 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.Navigation("Uoms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.Videoroom", b =>
|
||||
{
|
||||
b.Navigation("AttendanceRecords");
|
||||
|
||||
b.Navigation("ChatMessages");
|
||||
|
||||
b.Navigation("Participants");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Forum.ForumCategory", b =>
|
||||
{
|
||||
b.Navigation("Topics");
|
||||
|
|
|
|||
|
|
@ -82,8 +82,8 @@ server {
|
|||
large_client_header_buffers 4 16k;
|
||||
|
||||
# SignalR için özel ayar
|
||||
location /classroomhub {
|
||||
proxy_pass http://127.0.0.1:8080/classroomhub;
|
||||
location /videoroomhub {
|
||||
proxy_pass http://127.0.0.1:8080/videoroomhub;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
|
|
|||
111
ui/src/proxy/videoroom/models.ts
Normal file
111
ui/src/proxy/videoroom/models.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { PagedAndSortedResultRequestDto } from '../abp'
|
||||
|
||||
export type Role = 'teacher' | 'student' | 'observer'
|
||||
export type RoleState = 'role-selection' | 'dashboard' | 'videoroom'
|
||||
|
||||
export type MessageType = 'public' | 'private' | 'announcement'
|
||||
|
||||
export type VideoLayoutType = 'grid' | 'sidebar' | 'teacher-focus'
|
||||
|
||||
export interface VideoroomDto {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
subject?: string
|
||||
teacherId: string
|
||||
teacherName: string
|
||||
scheduledStartTime: string
|
||||
scheduledEndTime: string
|
||||
duration?: number
|
||||
actualStartTime?: string
|
||||
actualEndTime?: string
|
||||
maxParticipants?: number
|
||||
participantCount: number
|
||||
settingsDto?: VideoroomSettingsDto
|
||||
}
|
||||
|
||||
export interface VideoroomSettingsDto {
|
||||
allowHandRaise: boolean
|
||||
allowStudentChat: boolean
|
||||
allowPrivateMessages: boolean
|
||||
allowStudentScreenShare: boolean
|
||||
defaultMicrophoneState: 'muted' | 'unmuted'
|
||||
defaultCameraState: 'on' | 'off'
|
||||
defaultLayout: string
|
||||
autoMuteNewParticipants: boolean
|
||||
}
|
||||
|
||||
export interface VideoroomAttendanceDto {
|
||||
id: string
|
||||
sessionId: string
|
||||
studentId: string
|
||||
studentName: string
|
||||
joinTime: string
|
||||
leaveTime?: string
|
||||
totalDurationMinutes: number
|
||||
}
|
||||
|
||||
export interface VideoroomParticipantDto {
|
||||
id: string
|
||||
name: string
|
||||
sessionId: string
|
||||
isTeacher: boolean
|
||||
isObserver?: boolean
|
||||
isAudioMuted?: boolean
|
||||
isVideoMuted?: boolean
|
||||
isHandRaised?: boolean
|
||||
isKicked?: boolean
|
||||
isActive?: boolean
|
||||
stream?: MediaStream
|
||||
screenStream?: MediaStream
|
||||
isScreenSharing?: boolean
|
||||
peerConnection?: RTCPeerConnection
|
||||
}
|
||||
|
||||
export interface VideoroomChatDto {
|
||||
id: string
|
||||
sessionId: string
|
||||
senderId: string
|
||||
senderName: string
|
||||
message: string
|
||||
timestamp: string
|
||||
isTeacher: boolean
|
||||
recipientId?: string
|
||||
recipientName?: string
|
||||
messageType: MessageType
|
||||
}
|
||||
|
||||
export interface VideoroomLayoutDto {
|
||||
id: string
|
||||
name: string
|
||||
type: VideoLayoutType
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface VideoroomDocumentDto {
|
||||
id: string
|
||||
name: string
|
||||
url: string
|
||||
type: string
|
||||
size: number
|
||||
uploadedAt: string
|
||||
uploadedBy: string
|
||||
isPresentation?: boolean
|
||||
totalPages?: number
|
||||
}
|
||||
|
||||
export interface Videoroom {
|
||||
id: string
|
||||
name: string
|
||||
layoutType: string
|
||||
rows: number
|
||||
columns: number
|
||||
capacity: number
|
||||
creationTime: string
|
||||
lastModificationTime: string
|
||||
}
|
||||
|
||||
export interface VideoroomFilterInputDto extends PagedAndSortedResultRequestDto {
|
||||
search: string
|
||||
status: string
|
||||
}
|
||||
|
|
@ -73,6 +73,13 @@ export const ROUTES_ENUM = {
|
|||
},
|
||||
forum: '/admin/forum',
|
||||
|
||||
videoroom: {
|
||||
dashboard: '/admin/videoroom/dashboard',
|
||||
roomList: '/admin/videoroom/list',
|
||||
roomDetail: '/admin/videoroom/room/:id',
|
||||
planning: '/admin/videoroom/planning/:id',
|
||||
},
|
||||
|
||||
list: '/admin/list/:listFormCode',
|
||||
formNew: '/admin/form/:listFormCode',
|
||||
formView: '/admin/form/:listFormCode/:id',
|
||||
|
|
@ -86,11 +93,11 @@ export const ROUTES_ENUM = {
|
|||
accessDenied: '/admin/access-denied',
|
||||
|
||||
coordinator: {
|
||||
classroom: {
|
||||
dashboard: '/admin/coordinator/classroom/dashboard',
|
||||
classes: '/admin/coordinator/classroom/classes',
|
||||
roomDetail: '/admin/coordinator/classroom/room/:id',
|
||||
planning: '/admin/coordinator/classroom/planning/:id',
|
||||
videoroom: {
|
||||
dashboard: '/admin/coordinator/videoroom/dashboard',
|
||||
roomList: '/admin/coordinator/videoroom/rooms',
|
||||
roomDetail: '/admin/coordinator/videoroom/room/:id',
|
||||
planning: '/admin/coordinator/videoroom/planning/:id',
|
||||
},
|
||||
exams: '/admin/coordinator/exams',
|
||||
examDetail: '/admin/coordinator/exam/:id',
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
import {
|
||||
ClassroomAttendanceDto,
|
||||
ClassroomChatDto,
|
||||
ClassroomDto,
|
||||
ClassroomFilterInputDto,
|
||||
ClassroomParticipantDto,
|
||||
} from '@/proxy/classroom/models'
|
||||
import apiService from './api.service'
|
||||
import { PagedAndSortedResultRequestDto, PagedResultDto } from '@/proxy'
|
||||
|
||||
export const getClassroomById = (id: string) =>
|
||||
apiService.fetchData<ClassroomDto>({
|
||||
method: 'GET',
|
||||
url: `/api/app/classroom/${id}`,
|
||||
})
|
||||
|
||||
export const getClassrooms = (input: ClassroomFilterInputDto) =>
|
||||
apiService.fetchData<PagedResultDto<ClassroomDto>>({
|
||||
method: 'GET',
|
||||
url: `/api/app/classroom`,
|
||||
params: input,
|
||||
})
|
||||
|
||||
export const createClassroom = (input: ClassroomDto) =>
|
||||
apiService.fetchData<ClassroomDto>({
|
||||
method: 'POST',
|
||||
url: `/api/app/classroom`,
|
||||
data: input as any,
|
||||
})
|
||||
|
||||
export const updateClassroom = (input: ClassroomDto) =>
|
||||
apiService.fetchData({
|
||||
method: 'PUT',
|
||||
url: `/api/app/classroom/${input.id}`,
|
||||
data: input,
|
||||
})
|
||||
|
||||
export const deleteClassroom = (id: string) =>
|
||||
apiService.fetchData({
|
||||
method: 'DELETE',
|
||||
url: `/api/app/classroom/${id}`,
|
||||
})
|
||||
|
||||
export const startClassroom = (id: string) =>
|
||||
apiService.fetchData({
|
||||
method: 'PUT',
|
||||
url: `/api/app/classroom/${id}/start-class`,
|
||||
})
|
||||
|
||||
export const endClassroom = (id: string) =>
|
||||
apiService.fetchData({
|
||||
method: 'PUT',
|
||||
url: `/api/app/classroom/${id}/end-class`,
|
||||
})
|
||||
|
||||
export const getClassroomAttandances = (id: string) =>
|
||||
apiService.fetchData<ClassroomAttendanceDto[]>({
|
||||
method: 'GET',
|
||||
url: `/api/app/classroom/attendance/${id}`,
|
||||
})
|
||||
|
||||
export const getClassroomParticipants = (id: string) =>
|
||||
apiService.fetchData<ClassroomParticipantDto[]>({
|
||||
method: 'GET',
|
||||
url: `/api/app/classroom/participant/${id}`,
|
||||
})
|
||||
|
||||
export const getClassroomChats = (id: string) =>
|
||||
apiService.fetchData<ClassroomChatDto[]>({
|
||||
method: 'GET',
|
||||
url: `/api/app/classroom/chat/${id}`,
|
||||
})
|
||||
72
ui/src/services/videoroom.service.ts
Normal file
72
ui/src/services/videoroom.service.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import {
|
||||
VideoroomAttendanceDto,
|
||||
VideoroomChatDto,
|
||||
VideoroomDto,
|
||||
VideoroomFilterInputDto,
|
||||
VideoroomParticipantDto,
|
||||
} from '@/proxy/videoroom/models'
|
||||
import apiService from './api.service'
|
||||
import { PagedResultDto } from '@/proxy'
|
||||
|
||||
export const getVideoroomById = (id: string) =>
|
||||
apiService.fetchData<VideoroomDto>({
|
||||
method: 'GET',
|
||||
url: `/api/app/videoroom/${id}`,
|
||||
})
|
||||
|
||||
export const getVideorooms = (input: VideoroomFilterInputDto) =>
|
||||
apiService.fetchData<PagedResultDto<VideoroomDto>>({
|
||||
method: 'GET',
|
||||
url: `/api/app/videoroom`,
|
||||
params: input,
|
||||
})
|
||||
|
||||
export const createVideoroom = (input: VideoroomDto) =>
|
||||
apiService.fetchData<VideoroomDto>({
|
||||
method: 'POST',
|
||||
url: `/api/app/videoroom`,
|
||||
data: input as any,
|
||||
})
|
||||
|
||||
export const updateVideoroom = (input: VideoroomDto) =>
|
||||
apiService.fetchData({
|
||||
method: 'PUT',
|
||||
url: `/api/app/videoroom/${input.id}`,
|
||||
data: input,
|
||||
})
|
||||
|
||||
export const deleteVideoroom = (id: string) =>
|
||||
apiService.fetchData({
|
||||
method: 'DELETE',
|
||||
url: `/api/app/videoroom/${id}`,
|
||||
})
|
||||
|
||||
export const startVideoroom = (id: string) =>
|
||||
apiService.fetchData({
|
||||
method: 'PUT',
|
||||
url: `/api/app/videoroom/${id}/start-class`,
|
||||
})
|
||||
|
||||
export const endVideoroom = (id: string) =>
|
||||
apiService.fetchData({
|
||||
method: 'PUT',
|
||||
url: `/api/app/videoroom/${id}/end-class`,
|
||||
})
|
||||
|
||||
export const getVideoroomAttandances = (id: string) =>
|
||||
apiService.fetchData<VideoroomAttendanceDto[]>({
|
||||
method: 'GET',
|
||||
url: `/api/app/videoroom/attendance/${id}`,
|
||||
})
|
||||
|
||||
export const getVideoroomParticipants = (id: string) =>
|
||||
apiService.fetchData<VideoroomParticipantDto[]>({
|
||||
method: 'GET',
|
||||
url: `/api/app/videoroom/participant/${id}`,
|
||||
})
|
||||
|
||||
export const getVideoroomChats = (id: string) =>
|
||||
apiService.fetchData<VideoroomChatDto[]>({
|
||||
method: 'GET',
|
||||
url: `/api/app/videoroom/chat/${id}`,
|
||||
})
|
||||
573
ui/src/services/videoroom/signalr.tsx
Normal file
573
ui/src/services/videoroom/signalr.tsx
Normal file
|
|
@ -0,0 +1,573 @@
|
|||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { store } from '@/store/store'
|
||||
import * as signalR from '@microsoft/signalr'
|
||||
import { toast } from '@/components/ui'
|
||||
import Notification from '@/components/ui/Notification'
|
||||
import { VideoroomAttendanceDto, VideoroomChatDto } from '@/proxy/videoroom/models'
|
||||
|
||||
export class SignalRService {
|
||||
private connection!: signalR.HubConnection
|
||||
private isConnected: boolean = false
|
||||
private currentSessionId?: string
|
||||
private isKicked: boolean = false
|
||||
|
||||
private onAttendanceUpdate?: (record: VideoroomAttendanceDto) => void
|
||||
private onParticipantJoined?: (
|
||||
userId: string,
|
||||
name: string,
|
||||
isTeacher: boolean,
|
||||
isActive: boolean,
|
||||
) => void
|
||||
private onParticipantLeft?: (payload: {
|
||||
userId: string
|
||||
sessionId: string
|
||||
userName: string
|
||||
}) => void
|
||||
private onChatMessage?: (message: VideoroomChatDto) => void
|
||||
private onParticipantMuted?: (userId: string, isMuted: boolean) => void
|
||||
private onHandRaiseReceived?: (studentId: string) => void
|
||||
private onHandRaiseDismissed?: (studentId: string) => void
|
||||
private onOfferReceived?: (fromUserId: string, offer: RTCSessionDescriptionInit) => void
|
||||
private onAnswerReceived?: (fromUserId: string, answer: RTCSessionDescriptionInit) => void
|
||||
private onIceCandidateReceived?: (fromUserId: string, candidate: RTCIceCandidateInit) => void
|
||||
private onForceCleanup?: () => void
|
||||
|
||||
constructor() {
|
||||
const { auth } = store.getState()
|
||||
|
||||
this.connection = new signalR.HubConnectionBuilder()
|
||||
.withUrl(`${import.meta.env.VITE_API_URL}/videoroomhub`, {
|
||||
accessTokenFactory: () => auth.session.token || '',
|
||||
})
|
||||
.configureLogging(signalR.LogLevel.Information)
|
||||
.build()
|
||||
|
||||
this.setupEventHandlers()
|
||||
}
|
||||
|
||||
private setupEventHandlers() {
|
||||
if (!this.connection) return
|
||||
|
||||
this.connection.on('AttendanceUpdated', (record: VideoroomAttendanceDto) => {
|
||||
this.onAttendanceUpdate?.(record)
|
||||
})
|
||||
|
||||
this.connection.on(
|
||||
'ParticipantJoined',
|
||||
(userId: string, name: string, isTeacher: boolean, isActive: boolean) => {
|
||||
this.onParticipantJoined?.(userId, name, isTeacher, isActive)
|
||||
},
|
||||
)
|
||||
|
||||
this.connection.on(
|
||||
'ParticipantLeft',
|
||||
(payload: { userId: string; sessionId: string; userName: string }) => {
|
||||
this.onParticipantLeft?.(payload)
|
||||
},
|
||||
)
|
||||
|
||||
this.connection.on('ChatMessage', (message: any) => {
|
||||
this.onChatMessage?.(message)
|
||||
})
|
||||
|
||||
this.connection.on('ParticipantMuted', (userId: string, isMuted: boolean) => {
|
||||
this.onParticipantMuted?.(userId, isMuted)
|
||||
})
|
||||
|
||||
this.connection.on('HandRaiseReceived', (payload: any) => {
|
||||
this.onHandRaiseReceived?.(payload.studentId)
|
||||
})
|
||||
|
||||
this.connection.on('HandRaiseDismissed', (payload: any) => {
|
||||
this.onHandRaiseDismissed?.(payload.studentId)
|
||||
})
|
||||
|
||||
this.connection.on('ReceiveOffer', (fromUserId: string, offer: RTCSessionDescriptionInit) => {
|
||||
this.onOfferReceived?.(fromUserId, offer)
|
||||
})
|
||||
|
||||
this.connection.on('ReceiveAnswer', (fromUserId: string, answer: RTCSessionDescriptionInit) => {
|
||||
this.onAnswerReceived?.(fromUserId, answer)
|
||||
})
|
||||
|
||||
this.connection.on(
|
||||
'ReceiveIceCandidate',
|
||||
(fromUserId: string, candidate: RTCIceCandidateInit) => {
|
||||
this.onIceCandidateReceived?.(fromUserId, candidate)
|
||||
},
|
||||
)
|
||||
|
||||
this.connection.onreconnected(async () => {
|
||||
this.isConnected = true
|
||||
toast.push(<Notification title="🔄 Bağlantı tekrar kuruldu" type="success" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
|
||||
if (this.currentSessionId && store.getState().auth.user) {
|
||||
const u = store.getState().auth.user
|
||||
await this.joinClass(this.currentSessionId, u.id, u.name, u.role === 'teacher', true)
|
||||
}
|
||||
})
|
||||
|
||||
this.connection.onclose(async () => {
|
||||
if (this.isKicked) {
|
||||
toast.push(
|
||||
<Notification title="⚠️ Bağlantı koptu, yeniden bağlanılıyor..." type="warning" />,
|
||||
{ placement: 'top-end' },
|
||||
)
|
||||
this.isConnected = false
|
||||
this.currentSessionId = undefined
|
||||
return
|
||||
}
|
||||
|
||||
this.isConnected = false
|
||||
try {
|
||||
if (this.currentSessionId) {
|
||||
await this.connection.invoke('LeaveClass', this.currentSessionId)
|
||||
}
|
||||
} finally {
|
||||
this.currentSessionId = undefined
|
||||
}
|
||||
})
|
||||
|
||||
this.connection.on('Error', (message: string) => {
|
||||
toast.push(<Notification title={`❌ Hata: ${message}`} type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
})
|
||||
|
||||
this.connection.on('Warning', (message: string) => {
|
||||
toast.push(<Notification title={`⚠️ Uyarı: ${message}`} type="warning" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
})
|
||||
|
||||
this.connection.on('Info', (message: string) => {
|
||||
toast.push(<Notification title={`ℹ️ Bilgi: ${message}`} type="info" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
})
|
||||
|
||||
this.connection.onreconnecting(() => {
|
||||
if (this.isKicked) {
|
||||
toast.push(
|
||||
<Notification
|
||||
title="❌ Sınıftan çıkarıldığınız için yeniden bağlanma engellendi"
|
||||
type="danger"
|
||||
/>,
|
||||
)
|
||||
this.connection.stop()
|
||||
throw new Error('Reconnect blocked after kick')
|
||||
}
|
||||
})
|
||||
|
||||
this.connection.on('ForceDisconnect', async (message: string) => {
|
||||
this.isKicked = true
|
||||
toast.push(<Notification title={`❌ Sınıftan çıkarıldınız: ${message}`} type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
|
||||
if (this.onForceCleanup) {
|
||||
this.onForceCleanup()
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.stop()
|
||||
} catch {}
|
||||
|
||||
this.isConnected = false
|
||||
|
||||
if (this.currentSessionId && store.getState().auth.user) {
|
||||
this.onParticipantLeft?.({
|
||||
userId: store.getState().auth.user.id,
|
||||
sessionId: this.currentSessionId,
|
||||
userName: store.getState().auth.user.name,
|
||||
})
|
||||
}
|
||||
|
||||
this.currentSessionId = undefined
|
||||
window.location.href = ROUTES_ENUM.protected.admin.videoroom.roomList
|
||||
})
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
try {
|
||||
const startPromise = this.connection.start()
|
||||
const timeout = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Bağlantı zaman aşımına uğradı')), 10000),
|
||||
)
|
||||
|
||||
await Promise.race([startPromise, timeout])
|
||||
this.isConnected = true
|
||||
toast.push(<Notification title="✅ Bağlantı kuruldu" type="success" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
} catch {
|
||||
toast.push(
|
||||
<Notification
|
||||
title="⚠️ Sunucuya bağlanılamadı. Lütfen sayfayı yenileyin veya internet bağlantınızı kontrol edin."
|
||||
type="danger"
|
||||
/>,
|
||||
{ placement: 'top-end' },
|
||||
)
|
||||
this.isConnected = false
|
||||
}
|
||||
}
|
||||
|
||||
async joinClass(
|
||||
sessionId: string,
|
||||
userId: string,
|
||||
userName: string,
|
||||
isTeacher: boolean,
|
||||
isActive: boolean,
|
||||
): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
toast.push(
|
||||
<Notification
|
||||
title="⚠️ Bağlantı yok. Sınıfa katılmadan önce bağlantıyı kontrol edin."
|
||||
type="warning"
|
||||
/>,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
this.currentSessionId = sessionId
|
||||
try {
|
||||
await this.connection.invoke('JoinClass', sessionId, userId, userName, isTeacher, isActive)
|
||||
} catch {
|
||||
toast.push(<Notification title="❌ Sınıfa katılamadı" type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async leaveClass(sessionId: string): Promise<void> {
|
||||
const { auth } = store.getState()
|
||||
|
||||
if (!this.isConnected) {
|
||||
this.onParticipantLeft?.({ userId: auth.user.id, sessionId, userName: auth.user.name })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.invoke('LeaveClass', sessionId)
|
||||
this.currentSessionId = undefined
|
||||
} catch {
|
||||
toast.push(<Notification title="⚠️ Çıkış başarısız" type="warning" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async sendChatMessage(
|
||||
sessionId: string,
|
||||
senderId: string,
|
||||
senderName: string,
|
||||
message: string,
|
||||
isTeacher: boolean,
|
||||
): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
const chatMessage: VideoroomChatDto = {
|
||||
id: crypto.randomUUID(),
|
||||
sessionId,
|
||||
senderId,
|
||||
senderName,
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
isTeacher,
|
||||
messageType: 'public',
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.onChatMessage?.(chatMessage)
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.invoke(
|
||||
'SendChatMessage',
|
||||
sessionId,
|
||||
senderId,
|
||||
senderName,
|
||||
message,
|
||||
isTeacher,
|
||||
'public',
|
||||
)
|
||||
} catch {
|
||||
toast.push(<Notification title="❌ Mesaj gönderilemedi" type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async sendPrivateMessage(
|
||||
sessionId: string,
|
||||
senderId: string,
|
||||
senderName: string,
|
||||
message: string,
|
||||
recipientId: string,
|
||||
recipientName: string,
|
||||
isTeacher: boolean,
|
||||
): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
const chatMessage: VideoroomChatDto = {
|
||||
id: crypto.randomUUID(),
|
||||
sessionId,
|
||||
senderId,
|
||||
senderName,
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
isTeacher,
|
||||
recipientId,
|
||||
recipientName,
|
||||
messageType: 'private',
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.onChatMessage?.(chatMessage)
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.invoke(
|
||||
'SendPrivateMessage',
|
||||
sessionId,
|
||||
senderId,
|
||||
senderName,
|
||||
message,
|
||||
recipientId,
|
||||
recipientName,
|
||||
isTeacher,
|
||||
'private',
|
||||
)
|
||||
} catch {
|
||||
toast.push(<Notification title="❌ Özel mesaj gönderilemedi" type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async sendAnnouncement(
|
||||
sessionId: string,
|
||||
senderId: string,
|
||||
senderName: string,
|
||||
message: string,
|
||||
isTeacher: boolean,
|
||||
): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
const chatMessage: VideoroomChatDto = {
|
||||
id: crypto.randomUUID(),
|
||||
sessionId,
|
||||
senderId,
|
||||
senderName,
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
isTeacher,
|
||||
messageType: 'announcement',
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.onChatMessage?.(chatMessage)
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.invoke(
|
||||
'SendAnnouncement',
|
||||
sessionId,
|
||||
senderId,
|
||||
senderName,
|
||||
message,
|
||||
isTeacher,
|
||||
)
|
||||
} catch {
|
||||
toast.push(<Notification title="❌ Duyuru gönderilemedi" type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async muteParticipant(
|
||||
sessionId: string,
|
||||
userId: string,
|
||||
isMuted: boolean,
|
||||
isTeacher: boolean,
|
||||
): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
setTimeout(() => {
|
||||
this.onParticipantMuted?.(userId, isMuted)
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted, isTeacher)
|
||||
} catch {
|
||||
toast.push(<Notification title="⚠️ Katılımcı susturulamadı" type="warning" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async raiseHand(sessionId: string, studentId: string, studentName: string): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
setTimeout(() => {
|
||||
this.onHandRaiseReceived?.(studentId)
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.invoke('RaiseHand', sessionId, studentId, studentName)
|
||||
} catch {
|
||||
toast.push(<Notification title="❌ El kaldırma başarısız" type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async kickParticipant(sessionId: string, participantId: string, userName: string): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
setTimeout(() => {
|
||||
this.onParticipantLeft?.({ userId: participantId, sessionId, userName })
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.invoke('KickParticipant', sessionId, participantId)
|
||||
} catch {
|
||||
toast.push(<Notification title="❌ Katılımcı atılamadı" type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async approveHandRaise(sessionId: string, studentId: string): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
setTimeout(() => {
|
||||
this.onHandRaiseDismissed?.(studentId)
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.invoke('ApproveHandRaise', sessionId, studentId)
|
||||
} catch {
|
||||
toast.push(<Notification title="⚠️ El kaldırma onayı başarısız" type="warning" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async dismissHandRaise(sessionId: string, studentId: string): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
setTimeout(() => {
|
||||
this.onHandRaiseDismissed?.(studentId)
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.invoke('DismissHandRaise', sessionId, studentId)
|
||||
} catch {
|
||||
toast.push(<Notification title="⚠️ El indirme başarısız" type="warning" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async sendOffer(sessionId: string, targetUserId: string, offer: RTCSessionDescriptionInit) {
|
||||
if (!this.isConnected) return
|
||||
await this.connection.invoke('SendOffer', sessionId, targetUserId, offer)
|
||||
}
|
||||
|
||||
async sendAnswer(sessionId: string, targetUserId: string, answer: RTCSessionDescriptionInit) {
|
||||
if (!this.isConnected) return
|
||||
await this.connection.invoke('SendAnswer', sessionId, targetUserId, answer)
|
||||
}
|
||||
|
||||
async sendIceCandidate(sessionId: string, targetUserId: string, candidate: RTCIceCandidateInit) {
|
||||
if (!this.isConnected) return
|
||||
await this.connection.invoke('SendIceCandidate', sessionId, targetUserId, candidate)
|
||||
}
|
||||
|
||||
setExistingParticipantsHandler(callback: (participants: any[]) => void) {
|
||||
this.connection.on('ExistingParticipants', callback)
|
||||
}
|
||||
|
||||
setAttendanceUpdatedHandler(callback: (record: VideoroomAttendanceDto) => void) {
|
||||
this.onAttendanceUpdate = callback
|
||||
}
|
||||
|
||||
setParticipantJoinHandler(
|
||||
callback: (userId: string, name: string, isTeacher: boolean, isActive: boolean) => void,
|
||||
) {
|
||||
this.onParticipantJoined = callback
|
||||
}
|
||||
|
||||
setParticipantLeaveHandler(
|
||||
callback: (payload: { userId: string; sessionId: string; userName: string }) => void,
|
||||
) {
|
||||
this.onParticipantLeft = callback
|
||||
}
|
||||
|
||||
setChatMessageReceivedHandler(callback: (message: VideoroomChatDto) => void) {
|
||||
this.onChatMessage = callback
|
||||
}
|
||||
|
||||
setParticipantMutedHandler(callback: (userId: string, isMuted: boolean) => void) {
|
||||
this.onParticipantMuted = callback
|
||||
}
|
||||
|
||||
setHandRaiseReceivedHandler(callback: (studentId: string) => void) {
|
||||
this.onHandRaiseReceived = callback
|
||||
}
|
||||
|
||||
setHandRaiseDismissedHandler(callback: (studentId: string) => void) {
|
||||
this.onHandRaiseDismissed = callback
|
||||
}
|
||||
|
||||
setOfferReceivedHandler(
|
||||
callback: (fromUserId: string, offer: RTCSessionDescriptionInit) => void,
|
||||
) {
|
||||
this.onOfferReceived = callback
|
||||
}
|
||||
|
||||
setAnswerReceivedHandler(
|
||||
callback: (fromUserId: string, answer: RTCSessionDescriptionInit) => void,
|
||||
) {
|
||||
this.onAnswerReceived = callback
|
||||
}
|
||||
|
||||
setIceCandidateReceivedHandler(
|
||||
callback: (fromUserId: string, candidate: RTCIceCandidateInit) => void,
|
||||
) {
|
||||
this.onIceCandidateReceived = callback
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.isConnected && this.currentSessionId) {
|
||||
try {
|
||||
await this.connection.invoke('LeaveClass', this.currentSessionId)
|
||||
} catch {
|
||||
toast.push(<Notification title="⚠️ Bağlantı koparılırken hata" type="warning" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}
|
||||
if (this.connection) {
|
||||
await this.connection.stop()
|
||||
}
|
||||
this.isConnected = false
|
||||
this.currentSessionId = undefined
|
||||
}
|
||||
|
||||
getConnectionState(): boolean {
|
||||
return this.isConnected
|
||||
}
|
||||
|
||||
setForceCleanupHandler(callback: () => void) {
|
||||
this.onForceCleanup = callback
|
||||
}
|
||||
}
|
||||
358
ui/src/services/videoroom/webrtc.tsx
Normal file
358
ui/src/services/videoroom/webrtc.tsx
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
import { toast } from '@/components/ui'
|
||||
import Notification from '@/components/ui/Notification'
|
||||
|
||||
export class WebRTCService {
|
||||
private peerConnections: Map<string, RTCPeerConnection> = new Map()
|
||||
private retryCounts: Map<string, number> = new Map()
|
||||
private maxRetries = 3
|
||||
private signalRService: any
|
||||
private sessionId: string = ''
|
||||
|
||||
private localStream: MediaStream | null = null
|
||||
private onRemoteStream?: (userId: string, stream: MediaStream) => void
|
||||
private onIceCandidate?: (userId: string, candidate: RTCIceCandidateInit) => void
|
||||
private candidateBuffer: Map<string, RTCIceCandidateInit[]> = new Map()
|
||||
|
||||
private rtcConfiguration: RTCConfiguration = {
|
||||
iceServers: [
|
||||
{
|
||||
urls: [
|
||||
'stun:turn.sozsoft.com:3478',
|
||||
'turn:turn.sozsoft.com:3478?transport=udp',
|
||||
'turn:turn.sozsoft.com:3478?transport=tcp',
|
||||
'turns:turn.sozsoft.com:5349?transport=tcp',
|
||||
],
|
||||
username: 'webrtc',
|
||||
credential: 'strongpassword123',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
async initializeLocalStream(enableAudio: boolean, enableVideo: boolean): Promise<MediaStream> {
|
||||
try {
|
||||
this.localStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 },
|
||||
frameRate: { ideal: 30 },
|
||||
},
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true,
|
||||
},
|
||||
})
|
||||
|
||||
this.localStream.getAudioTracks().forEach((track) => (track.enabled = enableAudio))
|
||||
this.localStream.getVideoTracks().forEach((track) => (track.enabled = enableVideo))
|
||||
|
||||
return this.localStream
|
||||
} catch {
|
||||
toast.push(
|
||||
<Notification
|
||||
title="❌ Kamera/Mikrofon erişilemedi. Tarayıcı ayarlarınızı veya izinleri kontrol edin."
|
||||
type="danger"
|
||||
/>,
|
||||
{ placement: 'top-end' },
|
||||
)
|
||||
throw new Error('Media devices access failed')
|
||||
}
|
||||
}
|
||||
|
||||
async createPeerConnection(userId: string): Promise<RTCPeerConnection> {
|
||||
const peerConnection = new RTCPeerConnection(this.rtcConfiguration)
|
||||
this.peerConnections.set(userId, peerConnection)
|
||||
this.retryCounts.set(userId, 0)
|
||||
|
||||
if (this.localStream) {
|
||||
this.localStream.getTracks().forEach((track) => {
|
||||
peerConnection.addTrack(track, this.localStream!)
|
||||
})
|
||||
}
|
||||
|
||||
peerConnection.ontrack = (event) => {
|
||||
const [remoteStream] = event.streams
|
||||
this.onRemoteStream?.(userId, remoteStream)
|
||||
}
|
||||
|
||||
peerConnection.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
this.onIceCandidate?.(userId, event.candidate)
|
||||
}
|
||||
}
|
||||
|
||||
peerConnection.onconnectionstatechange = async () => {
|
||||
const state = peerConnection.connectionState
|
||||
|
||||
if (state === 'closed') {
|
||||
this.closePeerConnection(userId)
|
||||
}
|
||||
|
||||
if (state === 'failed') {
|
||||
let retries = this.retryCounts.get(userId) ?? 0
|
||||
if (retries < this.maxRetries) {
|
||||
toast.push(
|
||||
<Notification
|
||||
title={`⚠️ Bağlantı başarısız, yeniden deneniyor (${retries + 1}/${this.maxRetries})`}
|
||||
type="warning"
|
||||
/>,
|
||||
)
|
||||
this.retryCounts.set(userId, retries + 1)
|
||||
await this.restartIce(peerConnection, userId)
|
||||
} else {
|
||||
toast.push(
|
||||
<Notification
|
||||
title={`❌ Bağlantı kurulamadı (${this.maxRetries} deneme başarısız).`}
|
||||
type="danger"
|
||||
/>,
|
||||
{ placement: 'top-end' },
|
||||
)
|
||||
this.closePeerConnection(userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.candidateBuffer.has(userId)) {
|
||||
for (const cand of this.candidateBuffer.get(userId)!) {
|
||||
try {
|
||||
await peerConnection.addIceCandidate(cand)
|
||||
} catch {
|
||||
toast.push(
|
||||
<Notification
|
||||
title={`⚠️ ICE candidate eklenemedi. Kullanıcı: ${userId}`}
|
||||
type="warning"
|
||||
/>,
|
||||
)
|
||||
}
|
||||
}
|
||||
this.candidateBuffer.delete(userId)
|
||||
}
|
||||
|
||||
return peerConnection
|
||||
}
|
||||
|
||||
setSignalRService(signalRService: any, sessionId: string) {
|
||||
this.signalRService = signalRService
|
||||
this.sessionId = sessionId
|
||||
}
|
||||
|
||||
setIceCandidateHandler(callback: (userId: string, candidate: RTCIceCandidateInit) => void) {
|
||||
this.onIceCandidate = callback
|
||||
}
|
||||
|
||||
async createOffer(userId: string): Promise<RTCSessionDescriptionInit> {
|
||||
const pc = this.peerConnections.get(userId)
|
||||
if (!pc) throw new Error('Peer connection not found')
|
||||
|
||||
try {
|
||||
const offer = await pc.createOffer()
|
||||
await pc.setLocalDescription(offer)
|
||||
return offer
|
||||
} catch {
|
||||
toast.push(<Notification title="❌ Offer oluşturulamadı" type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
throw new Error('Offer creation failed')
|
||||
}
|
||||
}
|
||||
|
||||
async createAnswer(
|
||||
userId: string,
|
||||
offer: RTCSessionDescriptionInit,
|
||||
): Promise<RTCSessionDescriptionInit> {
|
||||
const pc = this.peerConnections.get(userId)
|
||||
if (!pc) throw new Error('Peer connection not found')
|
||||
|
||||
try {
|
||||
await pc.setRemoteDescription(offer)
|
||||
const answer = await pc.createAnswer()
|
||||
await pc.setLocalDescription(answer)
|
||||
return answer
|
||||
} catch {
|
||||
toast.push(<Notification title="❌ Answer oluşturulamadı" type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
throw new Error('Answer creation failed')
|
||||
}
|
||||
}
|
||||
|
||||
async handleAnswer(userId: string, answer: RTCSessionDescriptionInit): Promise<void> {
|
||||
const peerConnection = this.peerConnections.get(userId)
|
||||
if (!peerConnection) throw new Error('Peer connection not found')
|
||||
await peerConnection.setRemoteDescription(answer)
|
||||
}
|
||||
|
||||
async addIceCandidate(userId: string, candidate: RTCIceCandidateInit): Promise<void> {
|
||||
const pc = this.peerConnections.get(userId)
|
||||
if (!pc) {
|
||||
if (!this.candidateBuffer.has(userId)) {
|
||||
this.candidateBuffer.set(userId, [])
|
||||
}
|
||||
this.candidateBuffer.get(userId)!.push(candidate)
|
||||
return
|
||||
}
|
||||
|
||||
if (pc.signalingState === 'stable' || pc.signalingState === 'have-remote-offer') {
|
||||
try {
|
||||
await pc.addIceCandidate(candidate)
|
||||
} catch {
|
||||
toast.push(
|
||||
<Notification
|
||||
title={`⚠️ ICE candidate eklenemedi. Kullanıcı: ${userId}`}
|
||||
type="warning"
|
||||
/>,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (!this.candidateBuffer.has(userId)) {
|
||||
this.candidateBuffer.set(userId, [])
|
||||
}
|
||||
this.candidateBuffer.get(userId)!.push(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
onRemoteStreamReceived(callback: (userId: string, stream: MediaStream) => void) {
|
||||
this.onRemoteStream = callback
|
||||
}
|
||||
|
||||
async toggleVideo(enabled: boolean): Promise<void> {
|
||||
if (!this.localStream) return
|
||||
let videoTrack = this.localStream.getVideoTracks()[0]
|
||||
|
||||
if (videoTrack) {
|
||||
videoTrack.enabled = enabled
|
||||
} else if (enabled) {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ video: true })
|
||||
const newTrack = stream.getVideoTracks()[0]
|
||||
if (newTrack) {
|
||||
this.localStream!.addTrack(newTrack)
|
||||
this.peerConnections.forEach((pc) => {
|
||||
const sender = pc.getSenders().find((s) => s.track?.kind === newTrack.kind)
|
||||
if (sender) {
|
||||
sender.replaceTrack(newTrack)
|
||||
} else {
|
||||
pc.addTrack(newTrack, this.localStream!)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
toast.push(<Notification title="❌ Kamera açılamadı" type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async toggleAudio(enabled: boolean): Promise<void> {
|
||||
if (!this.localStream) return
|
||||
let audioTrack = this.localStream.getAudioTracks()[0]
|
||||
|
||||
if (audioTrack) {
|
||||
audioTrack.enabled = enabled
|
||||
} else if (enabled) {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||
const newTrack = stream.getAudioTracks()[0]
|
||||
if (newTrack) {
|
||||
this.localStream!.addTrack(newTrack)
|
||||
this.peerConnections.forEach((pc) => {
|
||||
const sender = pc.getSenders().find((s) => s.track?.kind === newTrack.kind)
|
||||
if (sender) {
|
||||
sender.replaceTrack(newTrack)
|
||||
} else {
|
||||
pc.addTrack(newTrack, this.localStream!)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
toast.push(<Notification title="❌ Mikrofon açılamadı" type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getLocalStream(): MediaStream | null {
|
||||
return this.localStream
|
||||
}
|
||||
|
||||
private async restartIce(peerConnection: RTCPeerConnection, userId: string) {
|
||||
try {
|
||||
const offer = await peerConnection.createOffer({ iceRestart: true })
|
||||
await peerConnection.setLocalDescription(offer)
|
||||
|
||||
if (this.signalRService) {
|
||||
await this.signalRService.sendOffer(this.sessionId, userId, offer)
|
||||
} else {
|
||||
toast.push(<Notification title="⚠️ Tekrar bağlanma başarısız" type="warning" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
toast.push(<Notification title="❌ ICE restart başarısız" type="danger" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
closePeerConnection(userId: string): void {
|
||||
const peerConnection = this.peerConnections.get(userId)
|
||||
if (peerConnection) {
|
||||
peerConnection.getSenders().forEach((sender) => sender.track?.stop())
|
||||
peerConnection.close()
|
||||
this.peerConnections.delete(userId)
|
||||
this.retryCounts.delete(userId)
|
||||
}
|
||||
}
|
||||
|
||||
getPeerConnection(userId: string): RTCPeerConnection | undefined {
|
||||
return this.peerConnections.get(userId)
|
||||
}
|
||||
|
||||
closeAllConnections(): void {
|
||||
this.peerConnections.forEach((pc) => {
|
||||
pc.getSenders().forEach((sender) => sender.track?.stop())
|
||||
pc.close()
|
||||
})
|
||||
this.peerConnections.clear()
|
||||
|
||||
if (this.localStream) {
|
||||
this.localStream.getTracks().forEach((track) => track.stop())
|
||||
this.localStream = null
|
||||
}
|
||||
}
|
||||
|
||||
addStreamToPeers(stream: MediaStream) {
|
||||
this.peerConnections.forEach((pc) => {
|
||||
stream.getTracks().forEach((track) => {
|
||||
const alreadyHas = pc.getSenders().some((s) => s.track?.id === track.id)
|
||||
if (!alreadyHas) {
|
||||
pc.addTrack(track, stream)
|
||||
track.onended = () => {
|
||||
this.removeTrackFromPeers(track)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
removeTrackFromPeers(track: MediaStreamTrack) {
|
||||
this.peerConnections.forEach((pc) => {
|
||||
pc.getSenders().forEach((sender) => {
|
||||
if (sender.track === track) {
|
||||
try {
|
||||
pc.removeTrack(sender)
|
||||
} catch {
|
||||
toast.push(<Notification title="⚠️ Track silinemedi" type="warning" />, {
|
||||
placement: 'top-end',
|
||||
})
|
||||
}
|
||||
if (sender.track?.readyState !== 'ended') {
|
||||
sender.track?.stop()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import { ClassroomDto, Role, RoleState } from '@/proxy/classroom/models'
|
||||
import { Role, RoleState, VideoroomDto } from '@/proxy/videoroom/models'
|
||||
import { useStoreActions, useStoreState } from '@/store/store'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function useClassroomLogic() {
|
||||
export function useVideoroomLogic() {
|
||||
const { user } = useStoreState((state) => state.auth)
|
||||
const { setUser } = useStoreActions((actions) => actions.auth.user)
|
||||
|
||||
const [roleState, setRoleState] = useState<RoleState>('role-selection')
|
||||
const [currentClass, setCurrentClass] = useState<ClassroomDto | null>(null)
|
||||
const [allClasses, setAllClasses] = useState<ClassroomDto[]>([])
|
||||
const [currentClass, setCurrentClass] = useState<VideoroomDto | null>(null)
|
||||
const [allClasses, setAllClasses] = useState<VideoroomDto[]>([])
|
||||
|
||||
const handleRoleSelect = (role: Role) => {
|
||||
setUser({
|
||||
|
|
@ -18,7 +18,7 @@ export function useClassroomLogic() {
|
|||
setRoleState('dashboard')
|
||||
}
|
||||
|
||||
const handleCreateClass = (classData: Partial<ClassroomDto>) => {
|
||||
const handleCreateClass = (classData: Partial<VideoroomDto>) => {
|
||||
const newClass = {
|
||||
...classData,
|
||||
id: crypto.randomUUID(),
|
||||
|
|
@ -27,11 +27,11 @@ export function useClassroomLogic() {
|
|||
isActive: false,
|
||||
isScheduled: true,
|
||||
participantCount: 0,
|
||||
} as ClassroomDto
|
||||
} as VideoroomDto
|
||||
setAllClasses((prev) => [...prev, newClass])
|
||||
}
|
||||
|
||||
const handleEditClass = (classId: string, classData: Partial<ClassroomDto>) => {
|
||||
const handleEditClass = (classId: string, classData: Partial<VideoroomDto>) => {
|
||||
setAllClasses((prev) => prev.map((c) => (c.id === classId ? { ...c, ...classData } : c)))
|
||||
}
|
||||
|
||||
|
|
|
|||
196
ui/src/views/admin/videoroom/ChatPanel.tsx
Normal file
196
ui/src/views/admin/videoroom/ChatPanel.tsx
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
import {
|
||||
VideoroomChatDto,
|
||||
VideoroomParticipantDto,
|
||||
VideoroomSettingsDto,
|
||||
MessageType,
|
||||
} from '@/proxy/videoroom/models'
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import { FaTimes, FaUsers, FaUser, FaBullhorn, FaPaperPlane } from 'react-icons/fa'
|
||||
|
||||
interface ChatPanelProps {
|
||||
user: { id: string; name: string; role: string }
|
||||
participants: VideoroomParticipantDto[]
|
||||
chatMessages: VideoroomChatDto[]
|
||||
newMessage: string
|
||||
setNewMessage: (msg: string) => void
|
||||
messageMode: MessageType
|
||||
setMessageMode: (mode: MessageType) => void
|
||||
selectedRecipient: { id: string; name: string } | null
|
||||
setSelectedRecipient: (recipient: { id: string; name: string } | null) => void
|
||||
onSendMessage: (e: React.FormEvent) => void
|
||||
onClose: () => void
|
||||
formatTime: (timestamp: string) => string
|
||||
classSettings: VideoroomSettingsDto
|
||||
}
|
||||
|
||||
const ChatPanel: React.FC<ChatPanelProps> = ({
|
||||
user,
|
||||
participants,
|
||||
chatMessages,
|
||||
newMessage,
|
||||
setNewMessage,
|
||||
messageMode,
|
||||
setMessageMode,
|
||||
selectedRecipient,
|
||||
setSelectedRecipient,
|
||||
onSendMessage,
|
||||
onClose,
|
||||
formatTime,
|
||||
classSettings,
|
||||
}) => {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [chatMessages])
|
||||
|
||||
const availableRecipients = participants.filter((p) => p.id !== user.id)
|
||||
|
||||
return (
|
||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
||||
{/* Header */}
|
||||
<div className="p-3 sm:p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h3 className="text-base sm:text-lg font-semibold">Sohbet</h3>
|
||||
<button onClick={onClose}>
|
||||
<FaTimes className="text-gray-500" size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mesaj Modu */}
|
||||
<div className="p-3 border-b border-gray-200 bg-gray-50">
|
||||
<div className="flex space-x-2 mb-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setMessageMode('public')
|
||||
setSelectedRecipient(null)
|
||||
}}
|
||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
|
||||
messageMode === 'public'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<FaUsers size={12} />
|
||||
<span>Herkese</span>
|
||||
</button>
|
||||
|
||||
{classSettings.allowPrivateMessages && (
|
||||
<button
|
||||
onClick={() => setMessageMode('private')}
|
||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
|
||||
messageMode === 'private'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<FaUser size={12} />
|
||||
<span>Özel</span>
|
||||
</button>
|
||||
)}
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setMessageMode('announcement')
|
||||
setSelectedRecipient(null)
|
||||
}}
|
||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
|
||||
messageMode === 'announcement'
|
||||
? 'bg-red-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<FaBullhorn size={12} />
|
||||
<span>Duyuru</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{messageMode === 'private' && (
|
||||
<select
|
||||
value={selectedRecipient?.id || ''}
|
||||
onChange={(e) => {
|
||||
const recipient = availableRecipients.find((p) => p.id === e.target.value)
|
||||
setSelectedRecipient(recipient ? { id: recipient.id, name: recipient.name } : null)
|
||||
}}
|
||||
className="w-full px-2 py-1 text-xs border border-gray-300 rounded"
|
||||
>
|
||||
<option value="">Kişi seçin...</option>
|
||||
{availableRecipients.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name} {p.isTeacher ? '(Öğretmen)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mesaj Listesi */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
{chatMessages.length === 0 ? (
|
||||
<div className="text-center text-gray-500 text-sm">Henüz mesaj yok.</div>
|
||||
) : (
|
||||
chatMessages.map((m) => (
|
||||
<div
|
||||
key={m.id}
|
||||
className={`${
|
||||
m.messageType === 'announcement'
|
||||
? 'w-full'
|
||||
: m.senderId === user.id
|
||||
? 'flex justify-end'
|
||||
: 'flex justify-start'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-xs px-3 py-2 rounded-lg ${
|
||||
m.messageType === 'announcement'
|
||||
? 'bg-red-100 text-red-800 border border-red-200 w-full text-center'
|
||||
: m.messageType === 'private'
|
||||
? m.senderId === user.id
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-green-100 text-green-800 border'
|
||||
: m.senderId === user.id
|
||||
? 'bg-blue-600 text-white'
|
||||
: m.isTeacher
|
||||
? 'bg-yellow-100 text-yellow-800 border'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{m.senderId !== user.id && (
|
||||
<div className="text-xs font-semibold mb-1">
|
||||
{m.senderName}
|
||||
{m.isTeacher && ' (Öğretmen)'}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm">{m.message}</div>
|
||||
<div className="text-xs mt-1 opacity-75">{formatTime(m.timestamp)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Mesaj Gönderme */}
|
||||
<form onSubmit={onSendMessage} className="p-4 border-t border-gray-200">
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
placeholder="Mesaj yaz..."
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!newMessage.trim()}
|
||||
className="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
|
||||
>
|
||||
<FaPaperPlane size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatPanel
|
||||
87
ui/src/views/admin/videoroom/Dashboard.tsx
Normal file
87
ui/src/views/admin/videoroom/Dashboard.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { FaGraduationCap, FaUserCheck, FaEye } from 'react-icons/fa'
|
||||
import { useStoreActions, useStoreState } from '@/store/store'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Role } from '@/proxy/videoroom/models'
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { translate } = useLocalization()
|
||||
const { user } = useStoreState((state) => state.auth)
|
||||
const { setUser } = useStoreActions((actions) => actions.auth.user)
|
||||
|
||||
const handleRoleSelect = (role: Role) => {
|
||||
setUser({
|
||||
...user,
|
||||
role,
|
||||
})
|
||||
|
||||
navigate(ROUTES_ENUM.protected.admin.videoroom.roomList, { replace: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet
|
||||
titleTemplate="%s | Erp Platform"
|
||||
title={translate('::' + 'App.Videoroom.Dashboard')}
|
||||
defaultTitle="Erp Platform"
|
||||
></Helmet>
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="text-center w-full max-w-4xl"
|
||||
>
|
||||
<p className="text-lg sm:text-xl text-gray-600 mb-8 sm:mb-12">Lütfen rolünüzü seçin</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => handleRoleSelect('teacher')}
|
||||
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-blue-500"
|
||||
>
|
||||
<FaGraduationCap size={48} className="mx-auto text-blue-600 mb-4 sm:mb-4" />
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Öğretmen</h2>
|
||||
<p className="text-gray-600 text-sm sm:text-base">
|
||||
Ders başlatın, öğrencilerle iletişim kurun ve katılım raporlarını görün
|
||||
</p>
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => handleRoleSelect('student')}
|
||||
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-green-500"
|
||||
>
|
||||
<FaUserCheck size={48} className="mx-auto text-green-600 mb-4 sm:mb-4" />
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Öğrenci</h2>
|
||||
<p className="text-gray-600 text-sm sm:text-base">
|
||||
Aktif derslere katılın, öğretmeniniz ve diğer öğrencilerle etkileşim kurun
|
||||
</p>
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => handleRoleSelect('observer')}
|
||||
className="bg-white rounded-lg shadow-lg p-6 sm:p-8 hover:shadow-xl transition-all duration-300 border-2 border-transparent hover:border-purple-500 md:col-span-2 lg:col-span-1"
|
||||
>
|
||||
<FaEye size={48} className="mx-auto text-purple-600 mb-4 sm:mb-4" />
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Gözlemci</h2>
|
||||
<p className="text-gray-600 text-sm sm:text-base">
|
||||
Sınıfı gözlemleyin, eğitim sürecini takip edin (ses/video paylaşımı yok)
|
||||
</p>
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Dashboard
|
||||
153
ui/src/views/admin/videoroom/DocumentsPanel.tsx
Normal file
153
ui/src/views/admin/videoroom/DocumentsPanel.tsx
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { VideoroomDocumentDto } from '@/proxy/videoroom/models';
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { FaTimes, FaFile, FaEye, FaDownload, FaTrash } from 'react-icons/fa'
|
||||
|
||||
interface DocumentsPanelProps {
|
||||
user: { role: string; name: string }
|
||||
documents: VideoroomDocumentDto[]
|
||||
onUpload: (file: File) => void
|
||||
onDelete: (id: string) => void
|
||||
onView: (doc: VideoroomDocumentDto) => void
|
||||
onClose: () => void
|
||||
formatFileSize: (bytes: number) => string
|
||||
getFileIcon: (type: string) => JSX.Element
|
||||
}
|
||||
|
||||
const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||
user,
|
||||
documents,
|
||||
onUpload,
|
||||
onDelete,
|
||||
onView,
|
||||
onClose,
|
||||
formatFileSize,
|
||||
getFileIcon,
|
||||
}) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [dragOver, setDragOver] = useState(false)
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setDragOver(false)
|
||||
if (user.role !== 'teacher') return
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
files.forEach((file) => onUpload(file))
|
||||
}
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (user.role !== 'teacher') return
|
||||
const files = Array.from(e.target.files || [])
|
||||
files.forEach((file) => onUpload(file))
|
||||
if (fileInputRef.current) fileInputRef.current.value = ''
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Sınıf Dokümanları</h3>
|
||||
<button onClick={onClose}>
|
||||
<FaTimes className="text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{/* Upload Area (Teacher Only) */}
|
||||
{user.role === 'teacher' && (
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg p-6 mb-4 text-center transition-colors ${
|
||||
dragOver ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
|
||||
}`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault()
|
||||
setDragOver(true)
|
||||
}}
|
||||
onDragLeave={() => setDragOver(false)}
|
||||
>
|
||||
<FaFile size={32} className="mx-auto text-gray-400 mb-2" />
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">Doküman Yükle</p>
|
||||
<p className="text-xs text-gray-500 mb-3">Dosyaları buraya sürükleyin veya seçin</p>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Dosya Seç
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
accept=".pdf,.doc,.docx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.odp"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Documents List */}
|
||||
{documents.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<FaFile size={32} className="mx-auto mb-4 text-gray-300" />
|
||||
<p className="text-sm">Henüz doküman yüklenmemiş.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{documents.map((doc) => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="flex items-center justify-between p-3 border border-gray-200 rounded-lg hover:shadow-sm transition-shadow"
|
||||
>
|
||||
<div className="flex items-center space-x-3 min-w-0 flex-1">
|
||||
<div className="text-lg flex-shrink-0">{getFileIcon(doc.type)}</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="font-medium text-gray-800 text-sm truncate">{doc.name}</h4>
|
||||
<p className="text-xs text-gray-600">
|
||||
{formatFileSize(doc.size)} •{' '}
|
||||
{new Date(doc.uploadedAt).toLocaleDateString('tr-TR')}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">{doc.uploadedBy}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => onView(doc)}
|
||||
className="p-1 text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||
title="Görüntüle"
|
||||
>
|
||||
<FaEye size={12} />
|
||||
</button>
|
||||
|
||||
<a
|
||||
href={doc.url}
|
||||
download={doc.name}
|
||||
className="p-1 text-green-600 hover:bg-green-50 rounded transition-colors"
|
||||
title="İndir"
|
||||
>
|
||||
<FaDownload size={12} />
|
||||
</a>
|
||||
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
onClick={() => onDelete(doc.id)}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||
title="Sil"
|
||||
>
|
||||
<FaTrash size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DocumentsPanel
|
||||
82
ui/src/views/admin/videoroom/KickParticipantModal.tsx
Normal file
82
ui/src/views/admin/videoroom/KickParticipantModal.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { FaUserTimes, FaExclamationTriangle } from 'react-icons/fa'
|
||||
|
||||
interface KickParticipantModalProps {
|
||||
participant: { id: string; name: string } | null
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onConfirm: (participantId: string, participantName: string) => void
|
||||
}
|
||||
|
||||
export const KickParticipantModal: React.FC<KickParticipantModalProps> = ({
|
||||
participant,
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}) => {
|
||||
if (!isOpen || !participant) return null
|
||||
|
||||
const handleConfirm = () => {
|
||||
onConfirm(participant.id, participant.name)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="bg-white rounded-lg max-w-md w-full mx-4"
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="p-3 bg-red-100 rounded-full mr-4">
|
||||
<FaExclamationTriangle className="text-red-600" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">Katılımcıyı Çıkar</h3>
|
||||
<p className="text-gray-600">Bu işlem geri alınamaz</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-700 mb-2">
|
||||
<strong>"{participant.name}"</strong> adlı katılımcıyı sınıftan çıkarmak
|
||||
istediğinizden emin misiniz?
|
||||
</p>
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||||
<div className="flex items-start">
|
||||
<FaExclamationTriangle className="text-yellow-600 mt-0.5 mr-2" size={16} />
|
||||
<div className="text-sm text-yellow-800">
|
||||
<p className="font-medium">Dikkat:</p>
|
||||
<ul className="mt-1 list-disc list-inside space-y-1">
|
||||
<li>Katılımcı anında sınıftan çıkarılacak</li>
|
||||
<li>Tekrar katılım için davet gerekebilir</li>
|
||||
<li>Katılım süresi kaydedilecek</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end space-x-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<FaUserTimes size={16} />
|
||||
<span>Çıkar</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
112
ui/src/views/admin/videoroom/LayoutPanel.tsx
Normal file
112
ui/src/views/admin/videoroom/LayoutPanel.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { VideoroomLayoutDto } from '@/proxy/videoroom/models'
|
||||
import React from 'react'
|
||||
import { FaTimes, FaTh, FaExpand, FaDesktop, FaUsers } from 'react-icons/fa'
|
||||
|
||||
interface LayoutPanelProps {
|
||||
layouts: VideoroomLayoutDto[]
|
||||
currentLayout: VideoroomLayoutDto
|
||||
onChangeLayout: (layout: VideoroomLayoutDto) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const getLayoutIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'grid':
|
||||
return <FaTh size={24} />
|
||||
case 'speaker':
|
||||
return <FaExpand size={24} />
|
||||
case 'presentation':
|
||||
return <FaDesktop size={24} />
|
||||
case 'sidebar':
|
||||
return <FaUsers size={24} />
|
||||
case 'teacher-focus':
|
||||
return <FaDesktop size={24} />
|
||||
default:
|
||||
return <FaTh size={24} />
|
||||
}
|
||||
}
|
||||
|
||||
const LayoutPanel: React.FC<LayoutPanelProps> = ({
|
||||
layouts,
|
||||
currentLayout,
|
||||
onChangeLayout,
|
||||
onClose,
|
||||
}) => {
|
||||
return (
|
||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Video Layout Seçin</h3>
|
||||
<button onClick={onClose}>
|
||||
<FaTimes className="text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
<div className="space-y-3">
|
||||
{layouts.map((layout) => (
|
||||
<button
|
||||
key={layout.id}
|
||||
onClick={() => onChangeLayout(layout)}
|
||||
className={`w-full p-4 rounded-lg border-2 transition-all text-left ${
|
||||
currentLayout.id === layout.id
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-blue-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<div
|
||||
className={`p-2 rounded-full ${
|
||||
currentLayout.id === layout.id
|
||||
? 'bg-blue-100 text-blue-600'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{getLayoutIcon(layout.type)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 text-sm">{layout.name}</h4>
|
||||
<p className="text-xs text-gray-600">{layout.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Layout Preview */}
|
||||
<div className="bg-gray-100 rounded p-3 h-16 flex items-center justify-center">
|
||||
{layout.type === 'grid' && (
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="w-4 h-3 bg-blue-300 rounded"></div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{layout.type === 'sidebar' && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-6 bg-blue-500 rounded"></div>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="w-1 h-1 bg-blue-300 rounded"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{layout.type === 'teacher-focus' && (
|
||||
<div className="space-y-1">
|
||||
<div className="w-12 h-4 bg-green-500 rounded"></div>
|
||||
<div className="flex space-x-1">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="w-1 h-1 bg-blue-300 rounded"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LayoutPanel
|
||||
226
ui/src/views/admin/videoroom/ParticipantsPanel.tsx
Normal file
226
ui/src/views/admin/videoroom/ParticipantsPanel.tsx
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
import { VideoroomAttendanceDto, VideoroomParticipantDto } from '@/proxy/videoroom/models'
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
FaTimes,
|
||||
FaUsers,
|
||||
FaClipboardList,
|
||||
FaHandPaper,
|
||||
FaMicrophone,
|
||||
FaMicrophoneSlash,
|
||||
FaVideoSlash,
|
||||
FaUserTimes,
|
||||
} from 'react-icons/fa'
|
||||
|
||||
interface ParticipantsPanelProps {
|
||||
user: { id: string; name: string; role: string }
|
||||
participants: VideoroomParticipantDto[]
|
||||
attendanceRecords: VideoroomAttendanceDto[]
|
||||
onMuteParticipant: (participantId: string, isMuted: boolean, isTeacher: boolean) => void
|
||||
onKickParticipant: (participantId: string, participantName: string) => void
|
||||
onApproveHandRaise: (participantId: string) => void
|
||||
onDismissHandRaise: (participantId: string) => void
|
||||
onClose: () => void
|
||||
formatTime: (timestamp: string) => string
|
||||
formatDuration: (minutes: number) => string
|
||||
}
|
||||
|
||||
const ParticipantsPanel: React.FC<ParticipantsPanelProps> = ({
|
||||
user,
|
||||
participants,
|
||||
attendanceRecords,
|
||||
onMuteParticipant,
|
||||
onKickParticipant,
|
||||
onClose,
|
||||
onApproveHandRaise,
|
||||
onDismissHandRaise,
|
||||
formatTime,
|
||||
formatDuration,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'participants' | 'attendance'>('participants')
|
||||
|
||||
// El kaldıranları bul
|
||||
const handRaised = participants.filter((p) => p.isHandRaised)
|
||||
|
||||
return (
|
||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Katılımcılar ({participants.length + 1})
|
||||
</h3>
|
||||
<button onClick={onClose}>
|
||||
<FaTimes className="text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* El kaldıranlar göstergesi */}
|
||||
{user.role === 'teacher' && handRaised.length > 0 && (
|
||||
<div className="mb-2 flex items-center space-x-2 p-2 bg-yellow-50 rounded">
|
||||
<FaHandPaper className="text-yellow-600" />
|
||||
<span className="font-medium text-yellow-800">
|
||||
{handRaised.length} kişi el kaldırdı:
|
||||
</span>
|
||||
<span className="text-yellow-900 text-sm truncate">
|
||||
{handRaised.map((p) => p.name).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex space-x-1 bg-gray-100 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('participants')}
|
||||
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all ${
|
||||
activeTab === 'participants'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<FaUsers className="inline mr-1" size={14} />
|
||||
Katılımcılar
|
||||
</button>
|
||||
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
onClick={() => setActiveTab('attendance')}
|
||||
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md transition-all ${
|
||||
activeTab === 'attendance'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<FaClipboardList className="inline mr-1" size={14} />
|
||||
Katılım Raporu
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Participants Tab */}
|
||||
{activeTab === 'participants' && (
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
<div className="space-y-2">
|
||||
{/* Current User */}
|
||||
<div className="flex items-center justify-between p-2 rounded-lg bg-blue-50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-semibold">
|
||||
{user.name.charAt(0)}
|
||||
</div>
|
||||
<span className="text-gray-900">{user.name} (Siz)</span>
|
||||
</div>
|
||||
{user.role === 'teacher' && (
|
||||
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
|
||||
Öğretmen
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Other Participants */}
|
||||
{participants.map((participant) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className="flex items-center justify-between p-2 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-gray-500 rounded-full flex items-center justify-center text-white text-sm font-semibold">
|
||||
{participant.name.charAt(0)}
|
||||
</div>
|
||||
<span className="text-gray-900">{participant.name}</span>
|
||||
|
||||
{/* Hand Raise Indicator & Teacher Control */}
|
||||
{participant.isHandRaised &&
|
||||
(user.role === 'teacher' && !participant.isTeacher ? (
|
||||
<button
|
||||
onClick={() => onDismissHandRaise(participant.id)}
|
||||
className="ml-2 p-1 rounded bg-yellow-100 hover:bg-yellow-200"
|
||||
title="El kaldırmayı kaldır"
|
||||
>
|
||||
<FaHandPaper className="text-yellow-600" />
|
||||
</button>
|
||||
) : (
|
||||
<FaHandPaper className="text-yellow-600 ml-2" title="Parmak kaldırdı" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
{/* Hand Raise Controls kaldırıldı, kontrol yukarıya taşındı */}
|
||||
|
||||
{/* Mute / Unmute Button */}
|
||||
{user.role === 'teacher' && !participant.isTeacher && (
|
||||
<button
|
||||
onClick={() =>
|
||||
onMuteParticipant(participant.id, !participant.isAudioMuted, true)
|
||||
}
|
||||
className={`p-1 rounded transition-colors ${
|
||||
participant.isAudioMuted
|
||||
? 'text-green-600 hover:bg-green-50'
|
||||
: 'text-yellow-600 hover:bg-yellow-50'
|
||||
}`}
|
||||
title={participant.isAudioMuted ? 'Sesi Aç' : 'Sesi Kapat'}
|
||||
>
|
||||
{participant.isAudioMuted ? <FaMicrophone /> : <FaMicrophoneSlash />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Video muted indicator */}
|
||||
{participant.isVideoMuted && <FaVideoSlash className="text-red-500 text-sm" />}
|
||||
|
||||
{participant.isTeacher && (
|
||||
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
|
||||
Öğretmen
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Kick Button (Teacher Only) */}
|
||||
{user.role === 'teacher' && !participant.isTeacher && (
|
||||
<button
|
||||
onClick={() => onKickParticipant(participant.id, participant.name)}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||
title="Sınıftan Çıkar"
|
||||
>
|
||||
<FaUserTimes size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Attendance Tab */}
|
||||
{activeTab === 'attendance' && (
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{attendanceRecords.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<FaClipboardList size={32} className="mx-auto mb-4 text-gray-300" />
|
||||
<p className="text-sm text-gray-600">Henüz katılım kaydı bulunmamaktadır.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{attendanceRecords.map((record) => (
|
||||
<div key={record.id} className="p-3 border border-gray-200 rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-medium text-gray-800">{record.studentName}</h4>
|
||||
<span className="text-sm font-semibold text-blue-600">
|
||||
{formatDuration(record.totalDurationMinutes)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 space-y-1">
|
||||
<div>Giriş: {formatTime(record.joinTime)}</div>
|
||||
<div>
|
||||
Çıkış: {record.leaveTime ? formatTime(record.leaveTime) : 'Devam ediyor'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ParticipantsPanel
|
||||
1320
ui/src/views/admin/videoroom/RoomDetail.tsx
Normal file
1320
ui/src/views/admin/videoroom/RoomDetail.tsx
Normal file
File diff suppressed because it is too large
Load diff
918
ui/src/views/admin/videoroom/RoomList.tsx
Normal file
918
ui/src/views/admin/videoroom/RoomList.tsx
Normal file
|
|
@ -0,0 +1,918 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { showDbDateAsIs } from '@/utils/dateUtils'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
import {
|
||||
FaPlus,
|
||||
FaCalendarAlt,
|
||||
FaClock,
|
||||
FaUsers,
|
||||
FaPlay,
|
||||
FaEdit,
|
||||
FaTrash,
|
||||
FaEye,
|
||||
FaHourglassEnd,
|
||||
FaDoorOpen,
|
||||
FaSearch,
|
||||
FaFilter,
|
||||
} from 'react-icons/fa'
|
||||
|
||||
import { useStoreState } from '@/store/store'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { Container } from '@/components/shared'
|
||||
import {
|
||||
createVideoroom,
|
||||
deleteVideoroom,
|
||||
getVideorooms,
|
||||
startVideoroom,
|
||||
updateVideoroom,
|
||||
} from '@/services/videoroom.service'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { VideoroomDto } from '@/proxy/videoroom/models'
|
||||
|
||||
export interface RoomProps {
|
||||
status: string
|
||||
className: string
|
||||
showButtons: boolean
|
||||
title: string
|
||||
classes: string
|
||||
event?: () => void
|
||||
}
|
||||
|
||||
const RoomList: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { user } = useStoreState((state) => state.auth)
|
||||
const { translate } = useLocalization()
|
||||
|
||||
const newClassEntity: VideoroomDto = {
|
||||
id: crypto.randomUUID(),
|
||||
name: '',
|
||||
description: '',
|
||||
subject: '',
|
||||
teacherId: user.id,
|
||||
teacherName: user.name,
|
||||
scheduledStartTime: '',
|
||||
scheduledEndTime: '',
|
||||
duration: 60,
|
||||
maxParticipants: 30,
|
||||
participantCount: 0,
|
||||
settingsDto: {
|
||||
allowHandRaise: true,
|
||||
allowStudentChat: true,
|
||||
allowPrivateMessages: true,
|
||||
allowStudentScreenShare: false,
|
||||
defaultMicrophoneState: 'muted',
|
||||
defaultCameraState: 'on',
|
||||
defaultLayout: 'grid',
|
||||
autoMuteNewParticipants: true,
|
||||
},
|
||||
}
|
||||
const [videoList, setVideoist] = useState<VideoroomDto[]>([])
|
||||
const [videoroom, setVideoroom] = useState<VideoroomDto>(newClassEntity)
|
||||
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [showEditModal, setShowEditModal] = useState(false)
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
|
||||
// Filter/search state
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState('')
|
||||
|
||||
const getVideoroomList = async (
|
||||
skipCount = 0,
|
||||
maxResultCount = 1000,
|
||||
sorting = '',
|
||||
search = '',
|
||||
status = '',
|
||||
) => {
|
||||
try {
|
||||
const result = await getVideorooms({
|
||||
sorting,
|
||||
skipCount,
|
||||
maxResultCount,
|
||||
search,
|
||||
status,
|
||||
})
|
||||
|
||||
const items = (result.data.items || []).map((item) => ({
|
||||
...item,
|
||||
scheduledStartTime: item.scheduledStartTime
|
||||
? showDbDateAsIs(item.scheduledStartTime)
|
||||
: null,
|
||||
actualStartTime: item.actualStartTime ? showDbDateAsIs(item.actualStartTime) : null,
|
||||
})) as VideoroomDto[]
|
||||
|
||||
setVideoist(items)
|
||||
} catch (error) {
|
||||
console.error('Error fetching videorooms:', error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getVideoroomList(0, 1000, '', searchTerm, statusFilter)
|
||||
}, [searchTerm, statusFilter])
|
||||
|
||||
const handleCreateClass = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
await createVideoroom(videoroom)
|
||||
|
||||
getVideoroomList()
|
||||
setShowCreateModal(false)
|
||||
setVideoroom(newClassEntity)
|
||||
} catch (error) {
|
||||
console.error('Sınıf oluştururken hata oluştu:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditClass = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!videoroom) return
|
||||
|
||||
try {
|
||||
await updateVideoroom(videoroom)
|
||||
|
||||
getVideoroomList()
|
||||
setShowEditModal(false)
|
||||
setVideoroom(newClassEntity)
|
||||
resetForm()
|
||||
} catch (error) {
|
||||
console.error('Sınıf oluştururken hata oluştu:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const openEditModal = (classSession: VideoroomDto) => {
|
||||
setVideoroom(classSession)
|
||||
setShowEditModal(true)
|
||||
}
|
||||
|
||||
const openDeleteModal = (classSession: VideoroomDto) => {
|
||||
setVideoroom(classSession)
|
||||
setShowDeleteModal(true)
|
||||
}
|
||||
|
||||
const handleDeleteClass = async () => {
|
||||
if (!videoroom) return
|
||||
|
||||
try {
|
||||
await deleteVideoroom(videoroom.id!)
|
||||
getVideoroomList()
|
||||
setShowDeleteModal(false)
|
||||
setVideoroom(newClassEntity)
|
||||
} catch (error) {
|
||||
console.error('Sınıf silinirken hata oluştu:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setVideoroom(newClassEntity)
|
||||
}
|
||||
|
||||
const handleStartClass = async (classSession: VideoroomDto) => {
|
||||
await startVideoroom(classSession.id!)
|
||||
getVideoroomList()
|
||||
|
||||
handleJoinClass(classSession)
|
||||
}
|
||||
|
||||
const handleJoinClass = (classSession: VideoroomDto) => {
|
||||
if (classSession.id) {
|
||||
navigate(ROUTES_ENUM.protected.admin.videoroom.roomDetail.replace(':id', classSession.id))
|
||||
}
|
||||
}
|
||||
|
||||
const handlePlanningClass = (classSession: VideoroomDto) => {
|
||||
if (classSession.id) {
|
||||
navigate(ROUTES_ENUM.protected.admin.videoroom.planning.replace(':id', classSession.id))
|
||||
}
|
||||
}
|
||||
|
||||
const widgets = () => {
|
||||
return {
|
||||
totalCount: videoList.length,
|
||||
activeCount: videoList.filter((c) => !c.actualStartTime && !c.actualEndTime).length,
|
||||
openCount: videoList.filter(
|
||||
(c) => c.actualStartTime && !c.actualEndTime, // && canJoinClass(c.actualStartTime),
|
||||
).length,
|
||||
passiveCount: videoList.filter((c) => c.actualStartTime && c.actualEndTime).length,
|
||||
}
|
||||
}
|
||||
|
||||
const getClassProps = (classSession: VideoroomDto): RoomProps => {
|
||||
//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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//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 (
|
||||
<>
|
||||
<Helmet
|
||||
titleTemplate="%s | Erp Platform"
|
||||
title={translate('::' + 'App.Coordinator.Videoroom.List')}
|
||||
defaultTitle="Erp Platform"
|
||||
></Helmet>
|
||||
<Container>
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 sm:gap-6 mb-3">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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">
|
||||
<FaCalendarAlt 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">Toplam Sınıf</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 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
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-green-100 rounded-full">
|
||||
<FaPlay className="text-green-600" size={20} />
|
||||
</div>
|
||||
<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">
|
||||
{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 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="bg-white rounded-lg shadow-md p-4 sm:p-6 sm:col-span-2 lg:col-span-1"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="p-2 sm:p-3 bg-purple-100 rounded-full">
|
||||
<FaUsers className="text-purple-600" size={20} />
|
||||
</div>
|
||||
<div className="ml-3 sm:ml-4">
|
||||
<p className="text-xs sm:text-sm font-medium text-gray-600">Toplam Katılımcı</p>
|
||||
<p className="text-xl sm:text-2xl font-bold text-gray-900">
|
||||
{videoList.reduce((sum, c) => sum + c.participantCount, 0)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Filter Bar */}
|
||||
<div className="bg-white rounded-lg border border-slate-200 p-6 mb-6 shadow-sm">
|
||||
<div className="flex flex-col lg:flex-row gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Search class"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<FaFilter className="w-5 h-5 text-slate-500" />
|
||||
<select
|
||||
className="ml-2 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
style={{ minWidth: 120 }}
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="Active">Aktif</option>
|
||||
<option value="Open">Katılıma Açık</option>
|
||||
<option value="Passive">Pasif</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scheduled Classes */}
|
||||
<div className="bg-white rounded-lg shadow-md">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 p-4 sm:px-6 border-b border-gray-200">
|
||||
<h2 className="text-lg sm:text-xl font-semibold text-gray-900">Programlı Sınıflar</h2>
|
||||
{user.role === 'teacher' && (
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="flex items-center justify-center space-x-2 bg-blue-600 text-white px-3 sm:px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors sm:w-auto"
|
||||
>
|
||||
<FaPlus size={15} />
|
||||
<span className="hidden sm:inline">Yeni Sınıf Oluştur</span>
|
||||
<span className="sm:hidden">Yeni Sınıf</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 sm:p-6">
|
||||
{videoList.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" />
|
||||
<p className="text-gray-500">Henüz programlanmış sınıf bulunmamaktadır.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:gap-6">
|
||||
{videoList.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 */}
|
||||
{showButtons && (
|
||||
<div className="flex space-x-2">
|
||||
{/* {user.role === 'teacher' && classSession.teacherId === user.id && ( */}
|
||||
{user.role === 'teacher' && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handlePlanningClass(classSession)}
|
||||
disabled={classSession.actualStartTime ? true : false}
|
||||
className="flex px-3 sm:px-4 py-2 rounded-lg bg-yellow-600 text-white
|
||||
hover:bg-yellow-700
|
||||
disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-gray-400"
|
||||
title="Sınıfı Planla"
|
||||
>
|
||||
<FaUsers size={14} />
|
||||
Planlama
|
||||
</button>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={event}
|
||||
disabled={status === 'Katılıma Açık' ? true : false}
|
||||
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 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">
|
||||
<sub className="text-gray-500 mb-3 text-xs sm:text-sm">
|
||||
{classSession.description}
|
||||
</sub>
|
||||
</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 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 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>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Class Modal (Create/Edit) */}
|
||||
{(showCreateModal || (showEditModal && videoroom)) && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="bg-white rounded-lg max-w-2xl w-full max-h-[95vh] overflow-y-auto"
|
||||
>
|
||||
<div className="p-3 sm:p-3 border-b border-gray-200">
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-gray-900">
|
||||
{showCreateModal ? 'Yeni Sınıf Oluştur' : 'Sınıfı Düzenle'}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={showCreateModal ? handleCreateClass : handleEditClass}
|
||||
className="p-4 sm:p-6 space-y-4 sm:space-y-6"
|
||||
>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Sınıf Adı *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
autoFocus={showCreateModal}
|
||||
value={videoroom.name}
|
||||
onChange={(e) => setVideoroom({ ...videoroom, name: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Örn: Matematik 101 - Diferansiyel Denklemler"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Açıklama</label>
|
||||
<textarea
|
||||
value={videoroom.description}
|
||||
onChange={(e) => setVideoroom({ ...videoroom, description: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Ders hakkında kısa açıklama..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ders Konusu
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={videoroom.subject}
|
||||
onChange={(e) => setVideoroom({ ...videoroom, subject: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Örn: Matematik, Fizik, Kimya"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Başlangıç Tarihi ve Saati *
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
required
|
||||
value={
|
||||
videoroom.scheduledStartTime
|
||||
? videoroom.scheduledStartTime.slice(0, 16)
|
||||
: ''
|
||||
}
|
||||
onChange={(e) =>
|
||||
setVideoroom({
|
||||
...videoroom,
|
||||
scheduledStartTime: e.target.value,
|
||||
})
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Süre (dakika)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="15"
|
||||
max="480"
|
||||
value={videoroom.duration}
|
||||
onChange={(e) =>
|
||||
setVideoroom({
|
||||
...videoroom,
|
||||
duration: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Maksimum Katılımcı
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={videoroom.maxParticipants}
|
||||
onChange={(e) =>
|
||||
setVideoroom({
|
||||
...videoroom,
|
||||
maxParticipants: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sınıf Ayarları */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-2">Sınıf Ayarları</h3>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6">
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-medium text-gray-700">Katılımcı İzinleri</h4>{' '}
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={videoroom.settingsDto?.allowHandRaise}
|
||||
onChange={(e) =>
|
||||
setVideoroom({
|
||||
...videoroom,
|
||||
settingsDto: {
|
||||
...videoroom.settingsDto!,
|
||||
allowHandRaise: e.target.checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm">Parmak kaldırma izni</span>
|
||||
</label>
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={videoroom.settingsDto?.allowStudentChat}
|
||||
onChange={(e) =>
|
||||
setVideoroom({
|
||||
...videoroom,
|
||||
settingsDto: {
|
||||
...videoroom.settingsDto!,
|
||||
allowStudentChat: e.target.checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm">Öğrenci sohbet izni</span>
|
||||
</label>
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={videoroom.settingsDto?.allowPrivateMessages}
|
||||
onChange={(e) =>
|
||||
setVideoroom({
|
||||
...videoroom,
|
||||
settingsDto: {
|
||||
...videoroom.settingsDto!,
|
||||
allowPrivateMessages: e.target.checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm">Özel mesaj izni</span>
|
||||
</label>
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={videoroom.settingsDto?.allowStudentScreenShare}
|
||||
onChange={(e) =>
|
||||
setVideoroom({
|
||||
...videoroom,
|
||||
settingsDto: {
|
||||
...videoroom.settingsDto!,
|
||||
allowStudentScreenShare: e.target.checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm">Öğrenci ekran paylaşımı</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-medium text-gray-700">Varsayılan Ayarlar</h4>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Varsayılan mikrofon durumu
|
||||
</label>
|
||||
<select
|
||||
value={videoroom.settingsDto?.defaultMicrophoneState}
|
||||
onChange={(e) =>
|
||||
setVideoroom({
|
||||
...videoroom,
|
||||
settingsDto: {
|
||||
...videoroom.settingsDto!,
|
||||
defaultMicrophoneState: e.target.value as 'muted' | 'unmuted',
|
||||
},
|
||||
})
|
||||
}
|
||||
className="border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="muted">Kapalı</option>
|
||||
<option value="unmuted">Açık</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Varsayılan kamera durumu
|
||||
</label>
|
||||
<select
|
||||
value={videoroom.settingsDto?.defaultCameraState}
|
||||
onChange={(e) =>
|
||||
setVideoroom({
|
||||
...videoroom,
|
||||
settingsDto: {
|
||||
...videoroom.settingsDto!,
|
||||
defaultCameraState: e.target.value as 'on' | 'off',
|
||||
},
|
||||
})
|
||||
}
|
||||
className="border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="on">Açık</option>
|
||||
<option value="off">Kapalı</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Varsayılan layout
|
||||
</label>
|
||||
<select
|
||||
value={videoroom.settingsDto?.defaultLayout}
|
||||
onChange={(e) =>
|
||||
setVideoroom({
|
||||
...videoroom,
|
||||
settingsDto: {
|
||||
...videoroom.settingsDto!,
|
||||
defaultLayout: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="grid">Izgara Görünümü</option>
|
||||
<option value="teacher-focus">Öğretmen Odaklı</option>
|
||||
<option value="presentation">Sunum Modu</option>
|
||||
<option value="sidebar">Yan Panel</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={videoroom.settingsDto?.autoMuteNewParticipants}
|
||||
onChange={(e) =>
|
||||
setVideoroom({
|
||||
...videoroom,
|
||||
settingsDto: {
|
||||
...videoroom.settingsDto!,
|
||||
autoMuteNewParticipants: e.target.checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm">Yeni katılımcıları otomatik sustur</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end space-x-4 pt-6 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (showCreateModal) {
|
||||
setShowCreateModal(false)
|
||||
}
|
||||
|
||||
if (showEditModal) {
|
||||
setShowEditModal(false)
|
||||
setVideoroom(newClassEntity)
|
||||
resetForm()
|
||||
}
|
||||
}}
|
||||
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
{showCreateModal ? 'Sınıf Oluştur' : 'Değişiklikleri Kaydet'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{showDeleteModal && videoroom && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="bg-white rounded-lg max-w-md w-full mx-4"
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="p-3 bg-red-100 rounded-full mr-4">
|
||||
<FaTrash className="text-red-600" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">Sınıfı Sil</h3>
|
||||
<p className="text-sm text-gray-600">Bu işlem geri alınamaz</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-700 mb-6">
|
||||
<strong>"{videoroom.name}"</strong> adlı sınıfı silmek istediğinizden emin
|
||||
misiniz?
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-end space-x-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowDeleteModal(false)
|
||||
setVideoroom(newClassEntity)
|
||||
}}
|
||||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteClass}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Sil
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RoomList
|
||||
282
ui/src/views/admin/videoroom/RoomParticipant.tsx
Normal file
282
ui/src/views/admin/videoroom/RoomParticipant.tsx
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
import React from 'react'
|
||||
import { FaMicrophoneSlash, FaExpand, FaUserTimes } from 'react-icons/fa'
|
||||
import { VideoPlayer } from './VideoPlayer'
|
||||
import { VideoroomParticipantDto, VideoroomLayoutDto } from '@/proxy/videoroom/models'
|
||||
|
||||
interface RoomParticipantProps {
|
||||
participants: VideoroomParticipantDto[]
|
||||
localStream?: MediaStream | null
|
||||
currentUserId: string
|
||||
currentUserName: string
|
||||
isTeacher: boolean
|
||||
isAudioEnabled: boolean
|
||||
isVideoEnabled: boolean
|
||||
onToggleAudio: () => void
|
||||
onToggleVideo: () => void
|
||||
onLeaveCall: () => void
|
||||
onMuteParticipant?: (participantId: string, isMuted: boolean, isTeacher: boolean) => void
|
||||
layout: VideoroomLayoutDto
|
||||
focusedParticipant?: string
|
||||
onParticipantFocus?: (participantId: string | undefined) => void
|
||||
onKickParticipant?: (participantId: string) => void
|
||||
hasSidePanel?: boolean
|
||||
}
|
||||
|
||||
export const RoomParticipant: React.FC<RoomParticipantProps> = ({
|
||||
participants,
|
||||
localStream,
|
||||
currentUserId,
|
||||
currentUserName,
|
||||
isTeacher,
|
||||
isAudioEnabled,
|
||||
isVideoEnabled,
|
||||
onToggleAudio,
|
||||
onToggleVideo,
|
||||
onLeaveCall,
|
||||
onMuteParticipant,
|
||||
layout,
|
||||
focusedParticipant,
|
||||
onParticipantFocus,
|
||||
onKickParticipant,
|
||||
hasSidePanel = false,
|
||||
}) => {
|
||||
// Only show current user's video once
|
||||
const currentUserParticipant = {
|
||||
id: currentUserId,
|
||||
name: currentUserName,
|
||||
isTeacher,
|
||||
stream: localStream ?? undefined, // null yerine undefined
|
||||
} as unknown as VideoroomParticipantDto
|
||||
|
||||
// Eğer hiç katılımcı yoksa ve localStream de yoksa hiçbir şey render etme
|
||||
if (!localStream && (!participants || participants.length === 0)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const allParticipants = [currentUserParticipant, ...participants]
|
||||
|
||||
// Ortak ana video kutusu container class'ı
|
||||
const mainVideoContainerClass = 'w-full h-full flex flex-col justify-center'
|
||||
|
||||
const renderGridLayout = () => {
|
||||
const getGridClass = (participantCount: number) => {
|
||||
if (participantCount === 1) return 'grid-cols-1'
|
||||
if (participantCount <= 2) return 'grid-cols-1 sm:grid-cols-2'
|
||||
if (participantCount <= 4) return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-2'
|
||||
if (participantCount <= 6) return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'
|
||||
if (participantCount <= 9) return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'
|
||||
return 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'
|
||||
}
|
||||
|
||||
const getGridRows = (participantCount: number) => {
|
||||
if (participantCount === 1) return 'grid-rows-1'
|
||||
if (participantCount <= 2) return 'grid-rows-1 sm:grid-rows-1'
|
||||
if (participantCount <= 4) return 'grid-rows-2 lg:grid-rows-2'
|
||||
if (participantCount <= 6) return 'grid-rows-3 sm:grid-rows-2'
|
||||
if (participantCount <= 9) return 'grid-rows-3'
|
||||
return 'grid-rows-4 sm:grid-rows-3'
|
||||
}
|
||||
|
||||
const getPadding = (participantCount: number) => {
|
||||
if (participantCount === 1) return ''
|
||||
if (participantCount <= 4) return 'p-2 sm:p-4'
|
||||
return 'p-1 sm:p-2'
|
||||
}
|
||||
|
||||
const getGap = (participantCount: number) => {
|
||||
if (participantCount === 1) return 'gap-0'
|
||||
if (participantCount <= 4) return 'gap-2 sm:gap-3'
|
||||
return 'gap-1 sm:gap-2'
|
||||
}
|
||||
|
||||
// Mobilde: En üstte öğretmen, altında katılımcılar 2'li grid ve dikey scroll
|
||||
const mainParticipant = allParticipants[0]
|
||||
const otherParticipants = allParticipants.slice(1)
|
||||
return (
|
||||
<>
|
||||
{/* Mobil özel layout */}
|
||||
<div className="sm:hidden w-full h-full flex flex-col items-center overflow-hidden p-2">
|
||||
{/* Ana katılımcı */}
|
||||
<div className="w-full max-w-md mx-auto flex-none flex items-center justify-center mb-2">
|
||||
<div className="w-full aspect-video max-h-[40vh] rounded-xl overflow-hidden flex bg-white/10 shadow-md border border-white/10">
|
||||
{renderParticipant(mainParticipant, true)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Diğer katılımcılar 2'li grid ve dikey scroll */}
|
||||
{otherParticipants.length > 0 && (
|
||||
<div
|
||||
className="w-full max-w-md mx-auto flex-1 overflow-y-auto grid grid-cols-1 gap-2 pb-2 min-h-0"
|
||||
style={{ maxHeight: '55vh' }}
|
||||
>
|
||||
{Array.from({ length: Math.ceil(otherParticipants.length / 2) }).map((_, rowIdx) => (
|
||||
<div key={rowIdx} className="flex gap-2">
|
||||
{otherParticipants.slice(rowIdx * 2, rowIdx * 2 + 2).map((participant) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className="flex-1 aspect-video rounded-lg overflow-hidden flex bg-white/10 shadow border border-white/10"
|
||||
>
|
||||
{renderParticipant(participant, false, true)}
|
||||
</div>
|
||||
))}
|
||||
{otherParticipants.length % 2 === 1 &&
|
||||
rowIdx === Math.floor(otherParticipants.length / 2) ? (
|
||||
<div className="flex-1" />
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Masaüstü ve tablet için eski grid layout */}
|
||||
<div className="hidden sm:flex h-full items-center justify-center overflow-hidden">
|
||||
<div
|
||||
className={`w-full h-full flex flex-col justify-center ${getPadding(allParticipants.length)}`}
|
||||
>
|
||||
<div
|
||||
className={`h-full grid ${getGridClass(allParticipants.length)} ${getGridRows(allParticipants.length)} ${getGap(allParticipants.length)} place-items-stretch`}
|
||||
>
|
||||
{allParticipants.map((participant) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className="w-full h-full max-h-full flex items-stretch justify-stretch min-h-0"
|
||||
>
|
||||
<div className="w-full h-full rounded-lg sm:rounded-xl overflow-hidden flex">
|
||||
{renderParticipant(participant, false)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const renderSidebarLayout = () => {
|
||||
const mainParticipant = focusedParticipant
|
||||
? allParticipants.find((p) => p.id === focusedParticipant) || allParticipants[0]
|
||||
: allParticipants[0]
|
||||
const otherParticipants = allParticipants.filter((p) => p.id !== mainParticipant.id)
|
||||
|
||||
const sidebarWidth = hasSidePanel
|
||||
? 'w-20 sm:w-24 md:w-32 lg:w-40'
|
||||
: 'w-24 sm:w-32 md:w-40 lg:w-48'
|
||||
|
||||
// Eğer hiç katılımcı yoksa, video player öğretmen odaklı gibi ortalanır ve geniş olur
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center p-0">
|
||||
<div className={mainVideoContainerClass + ' h-full'}>
|
||||
<div className="flex h-full">
|
||||
<div className={`flex-1 min-w-0 flex items-center justify-center`}>
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="w-full h-full rounded-xl overflow-hidden transition-all duration-200">
|
||||
{renderParticipant(mainParticipant, true)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{otherParticipants.length > 0 && (
|
||||
<div className={`${sidebarWidth} p-2 overflow-y-auto rounded-l-lg h-full`}>
|
||||
<div className="flex flex-col gap-2 h-full min-w-0">
|
||||
{otherParticipants.map((participant) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className="rounded-lg border border-blue-300/40 shadow shadow-blue-200/20 backdrop-blur-sm transition-all duration-200"
|
||||
>
|
||||
{renderParticipant(participant, false, true)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderTeacherFocusLayout = () => {
|
||||
// Sadece öğretmen gösterilecek, katılımcılar asla gösterilmeyecek
|
||||
const teacher = allParticipants.find((p) => p.isTeacher) || allParticipants[0]
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center overflow-hidden">
|
||||
<div className="w-full h-full flex flex-col justify-center ">
|
||||
<div className="h-full w-full max-h-full flex items-center justify-center">
|
||||
<div className="w-full h-full rounded-lg sm:rounded-xl overflow-hidden">
|
||||
{renderParticipant(teacher, true)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderParticipant = (
|
||||
participant: VideoroomParticipantDto,
|
||||
isMain: boolean = false,
|
||||
isSmall: boolean = false,
|
||||
) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className={`relative w-full h-full ${isMain ? '' : isSmall ? 'aspect-video' : ''} ${!isMain && onParticipantFocus ? 'cursor-pointer' : ''}`}
|
||||
onClick={() => !isMain && onParticipantFocus?.(participant.id)}
|
||||
style={{ minHeight: 0, minWidth: 0 }}
|
||||
>
|
||||
<div className="absolute inset-0 w-full h-full">
|
||||
<VideoPlayer
|
||||
stream={participant.stream}
|
||||
isLocal={participant.id === currentUserId}
|
||||
userName={participant.name}
|
||||
isAudioEnabled={
|
||||
participant.id === currentUserId ? isAudioEnabled : !participant.isAudioMuted
|
||||
}
|
||||
isVideoEnabled={
|
||||
participant.id === currentUserId ? isVideoEnabled : !participant.isVideoMuted
|
||||
}
|
||||
onToggleAudio={participant.id === currentUserId ? onToggleAudio : undefined}
|
||||
onToggleVideo={participant.id === currentUserId ? onToggleVideo : undefined}
|
||||
onLeaveCall={participant.id === currentUserId ? onLeaveCall : undefined}
|
||||
/>
|
||||
</div>
|
||||
{/* Teacher controls for students */}
|
||||
{isTeacher && participant.id !== currentUserId && (
|
||||
<div className="absolute top-2 left-2 flex space-x-1 z-10">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onMuteParticipant?.(participant.id, !participant.isAudioMuted, isTeacher)
|
||||
}}
|
||||
className={`p-1 rounded-full text-white text-xs ${
|
||||
participant.isAudioMuted ? 'bg-red-600' : 'bg-gray-600 hover:bg-gray-700'
|
||||
} transition-colors`}
|
||||
title={participant.isAudioMuted ? 'Sesi Aç' : 'Sesi Kapat'}
|
||||
>
|
||||
<FaMicrophoneSlash size={12} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onKickParticipant?.(participant.id)
|
||||
}}
|
||||
className="p-1 rounded-full bg-red-600 hover:bg-red-700 text-white text-xs transition-colors"
|
||||
title="Sınıftan Çıkar"
|
||||
>
|
||||
<FaUserTimes size={12} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderLayout = () => {
|
||||
switch (layout.type) {
|
||||
case 'sidebar':
|
||||
return renderSidebarLayout()
|
||||
case 'teacher-focus':
|
||||
return renderTeacherFocusLayout()
|
||||
default:
|
||||
return renderGridLayout()
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="h-full min-h-0 flex flex-col">{renderLayout()}</div>
|
||||
}
|
||||
76
ui/src/views/admin/videoroom/ScreenSharePanel.tsx
Normal file
76
ui/src/views/admin/videoroom/ScreenSharePanel.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import React from 'react';
|
||||
import { FaDesktop, FaStop, FaPlay } from 'react-icons/fa';
|
||||
|
||||
interface ScreenSharePanelProps {
|
||||
isSharing: boolean;
|
||||
onStartShare: () => void;
|
||||
onStopShare: () => void;
|
||||
sharedScreen?: MediaStream;
|
||||
sharerName?: string;
|
||||
}
|
||||
|
||||
export const ScreenSharePanel: React.FC<ScreenSharePanelProps> = ({
|
||||
isSharing,
|
||||
onStartShare,
|
||||
onStopShare,
|
||||
sharedScreen,
|
||||
sharerName,
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-md p-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaDesktop className="text-blue-600" size={20} />
|
||||
<h3 className="font-semibold text-gray-800">Ekran Paylaşımı</h3>
|
||||
</div>
|
||||
|
||||
{isSharing ? (
|
||||
<button
|
||||
onClick={onStopShare}
|
||||
className="flex items-center space-x-2 px-3 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
||||
>
|
||||
<FaStop size={16} />
|
||||
<span>Paylaşımı Durdur</span>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={onStartShare}
|
||||
className="flex items-center space-x-2 px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<FaPlay size={16} />
|
||||
<span>Ekranı Paylaş</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{sharedScreen && (
|
||||
<div className="relative bg-gray-900 rounded-lg overflow-hidden aspect-video">
|
||||
<video
|
||||
autoPlay
|
||||
playsInline
|
||||
muted
|
||||
className="w-full h-full object-contain"
|
||||
ref={(video) => {
|
||||
if (video && sharedScreen) {
|
||||
video.srcObject = sharedScreen;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{sharerName && (
|
||||
<div className="absolute bottom-2 left-2 bg-black bg-opacity-50 text-white px-2 py-1 rounded text-sm">
|
||||
{sharerName} ekranını paylaşıyor
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSharing && !sharedScreen && (
|
||||
<div className="bg-gray-100 rounded-lg p-8 text-center">
|
||||
<FaDesktop size={48} className="mx-auto text-gray-400 mb-4" />
|
||||
<p className="text-gray-600">Ekran paylaşımı başlatılıyor...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
81
ui/src/views/admin/videoroom/VideoPlayer.tsx
Normal file
81
ui/src/views/admin/videoroom/VideoPlayer.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import React, { useRef, useEffect } from 'react'
|
||||
import { FaMicrophoneSlash, FaVideoSlash } from 'react-icons/fa'
|
||||
|
||||
const VideoOff: React.FC<{ size?: number; className?: string }> = ({
|
||||
size = 24,
|
||||
className = '',
|
||||
}) => <FaVideoSlash size={size} className={className} />
|
||||
|
||||
interface VideoPlayerProps {
|
||||
stream?: MediaStream
|
||||
isLocal?: boolean
|
||||
userName: string
|
||||
isAudioEnabled?: boolean
|
||||
isVideoEnabled?: boolean
|
||||
onToggleAudio?: () => void
|
||||
onToggleVideo?: () => void
|
||||
onLeaveCall?: () => void
|
||||
}
|
||||
|
||||
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
stream,
|
||||
isLocal = false,
|
||||
userName,
|
||||
isAudioEnabled = true,
|
||||
isVideoEnabled = true,
|
||||
}) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const videoEl = videoRef.current
|
||||
if (!videoEl) return
|
||||
|
||||
if (stream) {
|
||||
videoEl.srcObject = stream
|
||||
} else {
|
||||
videoEl.srcObject = null
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (videoEl) {
|
||||
videoEl.srcObject = null
|
||||
}
|
||||
}
|
||||
}, [stream])
|
||||
|
||||
return (
|
||||
<div className="relative bg-gray-900 rounded-md sm:rounded-lg overflow-hidden p-1 sm:p-2 h-full">
|
||||
{/* Video sadece kamera açıkken göster */}
|
||||
<video
|
||||
ref={videoRef}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted={isLocal}
|
||||
className="w-full h-full object-cover"
|
||||
style={{ display: isVideoEnabled ? 'block' : 'none' }}
|
||||
/>
|
||||
|
||||
{/* User name overlay */}
|
||||
<div className="absolute bottom-1 sm:bottom-2 left-1 sm:left-2 bg-black bg-opacity-50 text-white px-1 sm:px-2 py-0.5 sm:py-1 rounded text-xs sm:text-sm">
|
||||
{userName} {isLocal && '(You)'}
|
||||
</div>
|
||||
|
||||
{/* Video kapalıysa avatar/placeholder göster */}
|
||||
{!isVideoEnabled && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-800">
|
||||
<div className="text-center text-white">
|
||||
<VideoOff size={24} className="mx-auto mb-1 sm:mb-2 text-white sm:size-8" />
|
||||
<p className="text-xs sm:text-sm">{userName}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Audio indicator */}
|
||||
{!isAudioEnabled && (
|
||||
<div className="absolute top-1 sm:top-2 right-1 sm:right-2 bg-red-500 rounded-full p-0.5 sm:p-1">
|
||||
<FaMicrophoneSlash size={12} className="text-white sm:size-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -81,7 +81,7 @@ export function AdminView({
|
|||
const navigationItems = [
|
||||
{
|
||||
id: 'stats' as AdminSection,
|
||||
label: translate('::App.Coordinator.Classroom.Dashboard'),
|
||||
label: translate('::App.Videoroom.Dashboard'),
|
||||
icon: FaChartBar,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue