Classroom

This commit is contained in:
Sedat ÖZTÜRK 2025-08-27 18:00:22 +03:00
parent 7e402d352c
commit 76615b074b
15 changed files with 133 additions and 143 deletions

View file

@ -13,8 +13,8 @@ public interface IClassroomAppService : IApplicationService
Task<ClassroomDto> GetAsync(Guid id);
Task<ClassroomDto> UpdateAsync(Guid id, ClassroomDto input);
Task DeleteAsync(Guid id);
Task<ClassroomDto> StartClassAsync(Guid id);
Task EndClassAsync(Guid id);
// Task<ClassroomDto> StartClassAsync(Guid id);
// Task EndClassAsync(Guid id);
Task<ClassroomDto> JoinClassAsync(Guid id);
Task LeaveClassAsync(Guid id);
Task<List<ClassAttendanceDto>> GetAttendanceAsync(Guid sessionId);

View file

@ -5,6 +5,7 @@ using System.Text.Json;
using System.Threading.Tasks;
using Kurs.Platform.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories;
@ -88,9 +89,15 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
classSession.Name = input.Name;
classSession.Description = input.Description;
classSession.Subject = input.Subject;
classSession.TeacherId = input.TeacherId;
classSession.TeacherName = input.TeacherName;
classSession.ScheduledStartTime = input.ScheduledStartTime;
classSession.ActualStartTime = input.ActualStartTime;
classSession.Duration = input.Duration;
classSession.MaxParticipants = input.MaxParticipants;
classSession.IsActive = input.IsActive;
classSession.IsScheduled = input.IsScheduled;
classSession.SettingsJson = JsonSerializer.Serialize(input.SettingsDto);
await _classSessionRepository.UpdateAsync(classSession);
return ObjectMapper.Map<Classroom, ClassroomDto>(classSession);
@ -113,7 +120,8 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
await _classSessionRepository.DeleteAsync(id);
}
public async Task<ClassroomDto> StartClassAsync(Guid id)
[HttpPut]
public async Task<ClassroomDto> StartClassroomAsync(Guid id)
{
var classSession = await _classSessionRepository.GetAsync(id);
@ -127,13 +135,19 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
throw new InvalidOperationException("Class cannot be started at this time");
}
classSession.StartClass();
if (classSession.IsActive)
throw new InvalidOperationException("Class is already active");
classSession.IsActive = true;
classSession.ActualStartTime = DateTime.Now;
await _classSessionRepository.UpdateAsync(classSession);
return ObjectMapper.Map<Classroom, ClassroomDto>(classSession);
}
public async Task EndClassAsync(Guid id)
[HttpPut]
public async Task EndClassroomAsync(Guid id)
{
var classSession = await _classSessionRepository.GetAsync(id);
@ -142,7 +156,12 @@ public class ClassroomAppService : PlatformAppService, IClassroomAppService
throw new UnauthorizedAccessException("Only the teacher can end this class");
}
classSession.EndClass();
if (!classSession.IsActive)
throw new InvalidOperationException("Class is not active");
classSession.IsActive = false;
classSession.EndTime = DateTime.Now;
await _classSessionRepository.UpdateAsync(classSession);
// Update attendance records

View file

@ -64,27 +64,9 @@ public class Classroom : FullAuditedEntity<Guid>
ChatMessages = new HashSet<ClassChat>();
}
public void StartClass()
{
if (IsActive)
throw new InvalidOperationException("Class is already active");
IsActive = true;
ActualStartTime = DateTime.UtcNow;
}
public void EndClass()
{
if (!IsActive)
throw new InvalidOperationException("Class is not active");
IsActive = false;
EndTime = DateTime.UtcNow;
}
public bool CanJoin()
{
var now = DateTime.UtcNow;
var now = DateTime.Now;
var tenMinutesBefore = ScheduledStartTime.AddMinutes(-10);
var twoHoursAfter = ScheduledStartTime.AddHours(2);

View file

@ -25,7 +25,7 @@ export interface ClassroomDto {
isScheduled: boolean
participantCount: number
canJoin: boolean
settings?: ClassroomSettingsDto
settingsDto?: ClassroomSettingsDto
}
export interface ClassroomSettingsDto {

View file

@ -35,3 +35,15 @@ export const deleteClassroom = (id: string) =>
method: 'DELETE',
url: `/api/app/classroom/${id}`,
})
export const startClassroom = (id: string) =>
apiService.fetchData({
method: 'PUT',
url: `/api/app/${id}/start-classroom`,
})
export const endClassroom = (id: string) =>
apiService.fetchData({
method: 'PUT',
url: `/api/app/${id}/end-classroom`,
})

View file

@ -0,0 +1,7 @@
// Veritabanındaki tarihi olduğu gibi göstermek için yardımcı fonksiyon
export function showDbDateAsIs(dateStr: string) {
if (!dateStr) return ''
// ISO formatı veya '2025-08-27 14:15:00.0000000' gibi formatlar için
// Sadece ilk 19 karakteri (YYYY-MM-DD HH:mm:ss) al
return dateStr.replace('T', ' ').substring(0, 19)
}

View file

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'
import { showDbDateAsIs } from '@/utils/dateUtils'
import { useNavigate } from 'react-router-dom'
import { motion } from 'framer-motion'
import {
@ -20,6 +21,7 @@ import {
createClassroom,
deleteClassroom,
getClassrooms,
startClassroom,
updateClassroom,
} from '@/services/classroom.service'
@ -41,7 +43,7 @@ const ClassList: React.FC = () => {
isScheduled: true,
participantCount: 0,
canJoin: false,
settings: {
settingsDto: {
allowHandRaise: true,
allowStudentChat: true,
allowPrivateMessages: true,
@ -67,7 +69,15 @@ const ClassList: React.FC = () => {
maxResultCount,
})
setClassList(result.data.items || [])
const items = (result.data.items || []).map((item) => ({
...item,
scheduledStartTime: item.scheduledStartTime
? showDbDateAsIs(item.scheduledStartTime)
: null,
actualStartTime: item.actualStartTime ? showDbDateAsIs(item.actualStartTime) : null,
})) as ClassroomDto[]
setClassList(items)
} catch (error) {
console.error('Error fetching classrooms:', error)
}
@ -138,17 +148,12 @@ const ClassList: React.FC = () => {
setClassroom(newClassEntity)
}
const handleStartClass = (classSession: ClassroomDto) => {
const updatedClass = {
...classSession,
isActive: true,
actualStartTime: new Date().toISOString(),
}
const handleStartClass = async (classSession: ClassroomDto) => {
await startClassroom(classSession.id!)
getClassroomList()
if (updatedClass.id) {
navigate(ROUTES_ENUM.protected.admin.classroom.classroom.replace(':id', updatedClass.id))
if (classSession.id) {
navigate(ROUTES_ENUM.protected.admin.classroom.classroom.replace(':id', classSession.id))
}
}
@ -186,16 +191,6 @@ const ClassList: React.FC = () => {
return `${minutes}d kaldı`
}
const formatDateTime = (dateString: string) => {
return new Date(dateString).toLocaleString('tr-TR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
return (
<Container>
{/* Main Content */}
@ -377,7 +372,7 @@ const ClassList: React.FC = () => {
<div className="col-span-1 flex items-center gap-2 px-3 py-2 rounded-lg">
<FaCalendarAlt size={14} className="text-gray-500" />
<span className="truncate">
{formatDateTime(classSession.scheduledStartTime)}
{showDbDateAsIs(classSession.scheduledStartTime)}
</span>
</div>
@ -536,12 +531,12 @@ const ClassList: React.FC = () => {
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={classroom.settings?.allowHandRaise}
checked={classroom.settingsDto?.allowHandRaise}
onChange={(e) =>
setClassroom({
...classroom,
settings: {
...classroom.settings!,
settingsDto: {
...classroom.settingsDto!,
allowHandRaise: e.target.checked,
},
})
@ -553,12 +548,12 @@ const ClassList: React.FC = () => {
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={classroom.settings?.allowStudentChat}
checked={classroom.settingsDto?.allowStudentChat}
onChange={(e) =>
setClassroom({
...classroom,
settings: {
...classroom.settings!,
settingsDto: {
...classroom.settingsDto!,
allowStudentChat: e.target.checked,
},
})
@ -570,12 +565,12 @@ const ClassList: React.FC = () => {
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={classroom.settings?.allowPrivateMessages}
checked={classroom.settingsDto?.allowPrivateMessages}
onChange={(e) =>
setClassroom({
...classroom,
settings: {
...classroom.settings!,
settingsDto: {
...classroom.settingsDto!,
allowPrivateMessages: e.target.checked,
},
})
@ -587,12 +582,12 @@ const ClassList: React.FC = () => {
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={classroom.settings?.allowStudentScreenShare}
checked={classroom.settingsDto?.allowStudentScreenShare}
onChange={(e) =>
setClassroom({
...classroom,
settings: {
...classroom.settings!,
settingsDto: {
...classroom.settingsDto!,
allowStudentScreenShare: e.target.checked,
},
})
@ -611,12 +606,12 @@ const ClassList: React.FC = () => {
Varsayılan mikrofon durumu
</label>
<select
value={classroom.settings?.defaultMicrophoneState}
value={classroom.settingsDto?.defaultMicrophoneState}
onChange={(e) =>
setClassroom({
...classroom,
settings: {
...classroom.settings!,
settingsDto: {
...classroom.settingsDto!,
defaultMicrophoneState: e.target.value as 'muted' | 'unmuted',
},
})
@ -633,12 +628,12 @@ const ClassList: React.FC = () => {
Varsayılan kamera durumu
</label>
<select
value={classroom.settings?.defaultCameraState}
value={classroom.settingsDto?.defaultCameraState}
onChange={(e) =>
setClassroom({
...classroom,
settings: {
...classroom.settings!,
settingsDto: {
...classroom.settingsDto!,
defaultCameraState: e.target.value as 'on' | 'off',
},
})
@ -655,12 +650,12 @@ const ClassList: React.FC = () => {
Varsayılan layout
</label>
<select
value={classroom.settings?.defaultLayout}
value={classroom.settingsDto?.defaultLayout}
onChange={(e) =>
setClassroom({
...classroom,
settings: {
...classroom.settings!,
settingsDto: {
...classroom.settingsDto!,
defaultLayout: e.target.value,
},
})
@ -677,12 +672,12 @@ const ClassList: React.FC = () => {
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={classroom.settings?.autoMuteNewParticipants}
checked={classroom.settingsDto?.autoMuteNewParticipants}
onChange={(e) =>
setClassroom({
...classroom,
settings: {
...classroom.settings!,
settingsDto: {
...classroom.settingsDto!,
autoMuteNewParticipants: e.target.checked,
},
})
@ -699,7 +694,10 @@ const ClassList: React.FC = () => {
<button
type="button"
onClick={() => {
if (showCreateModal) setShowCreateModal(false)
if (showCreateModal) {
setShowCreateModal(false)
}
if (showEditModal) {
setShowEditModal(false)
setClassroom(newClassEntity)

View file

@ -54,6 +54,7 @@ import { ScreenSharePanel } from '@/components/classroom/Panels/ScreenSharePanel
import { KickParticipantModal } from '@/components/classroom/KickParticipantModal'
import { useParams } from 'react-router-dom'
import { getClassroomById } from '@/services/classroom.service'
import { showDbDateAsIs } from '@/utils/dateUtils'
type SidePanelType =
| 'chat'
@ -129,7 +130,7 @@ const RoomDetail: React.FC = () => {
allowStudentScreenShare: false,
allowStudentChat: true,
allowPrivateMessages: true,
autoMuteNewParticipants: true
autoMuteNewParticipants: true,
})
const signalRServiceRef = useRef<SignalRService>()
@ -192,6 +193,7 @@ const RoomDetail: React.FC = () => {
//ClassEntity
const classEntity = await getClassroomById(params?.id ?? '')
if (classEntity) {
classEntity.data.scheduledStartTime = showDbDateAsIs(classEntity.data.scheduledStartTime)
setClassSession(classEntity.data)
}

View file

@ -1,6 +1,6 @@
import Card from '@/components/ui/Card'
import GrowShrinkTag from '@/components/shared/GrowShrinkTag'
import dayjs from 'dayjs'
import { showDbDateAsIs } from '@/utils/dateUtils'
const Widget = ({
label,
@ -11,25 +11,25 @@ const Widget = ({
}: {
label: string
datavalue: number
datagrowShrink: number,
datagrowShrink: number
valuePrefix: string
date: Date
date: string
}) => {
return (
<Card>
<h6 className="font-semibold mb-4 text-sm">{label}</h6>
<div className="flex justify-between items-center">
<div>
<h3 className="font-bold">{datavalue} {valuePrefix}</h3>
<p>
<span className="font-semibold">
{dayjs(date).format('DD MMM')}
</span>
</p>
</div>
<GrowShrinkTag value={datagrowShrink} suffix="%" />
</div>
</Card>
<Card>
<h6 className="font-semibold mb-4 text-sm">{label}</h6>
<div className="flex justify-between items-center">
<div>
<h3 className="font-bold">
{datavalue} {valuePrefix}
</h3>
<p>
<span className="font-semibold">{showDbDateAsIs(date)}</span>
</p>
</div>
<GrowShrinkTag value={datagrowShrink} suffix="%" />
</div>
</Card>
)
}

View file

@ -1,5 +1,13 @@
import React, { useState } from 'react'
import { FaPlus, FaEdit, FaTrashAlt, FaCheckCircle, FaCircle, FaHeart, FaSpinner } from 'react-icons/fa';
import {
FaPlus,
FaEdit,
FaTrashAlt,
FaCheckCircle,
FaCircle,
FaHeart,
FaSpinner,
} from 'react-icons/fa'
import { ForumPost, ForumTopic } from '@/proxy/forum/forum'
import { HtmlEditor, ImageUpload, Item, MediaResizing, Toolbar } from 'devextreme-react/html-editor'
import { useStoreState } from '@/store/store'
@ -8,6 +16,7 @@ import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup'
import { FormContainer, FormItem, Button } from '@/components/ui'
import { ConfirmDialog } from '@/components/shared'
import { showDbDateAsIs } from '@/utils/dateUtils'
import {
fontFamilyOptions,
fontSizeOptions,
@ -136,16 +145,7 @@ export function PostManagement({
}
const formatDate = (value: string | Date) => {
const date = value instanceof Date ? value : new Date(value)
if (isNaN(date.getTime())) return 'Invalid Date'
return new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date)
return showDbDateAsIs(typeof value === 'string' ? value : value.toISOString())
}
const postValidationSchema = Yup.object().shape({

View file

@ -19,6 +19,7 @@ import { Formik, Form, Field } from 'formik'
import * as Yup from 'yup'
import { FormContainer, FormItem, Button } from '@/components/ui'
import { ConfirmDialog } from '@/components/shared'
import { showDbDateAsIs } from '@/utils/dateUtils'
interface TopicManagementProps {
topics: ForumTopic[]
@ -180,16 +181,7 @@ export function TopicManagement({
}
const formatDate = (value: string | Date) => {
const date = value instanceof Date ? value : new Date(value)
if (isNaN(date.getTime())) return 'Invalid Date'
return new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date)
return showDbDateAsIs(typeof value === 'string' ? value : value.toISOString())
}
const topicInitialValues = {

View file

@ -1,8 +1,9 @@
import React from 'react'
import { FaHeart, FaCheckCircle, FaReply } from 'react-icons/fa';
import { FaHeart, FaCheckCircle, FaReply } from 'react-icons/fa'
import { ForumPost } from '@/proxy/forum/forum'
import { AVATAR_URL } from '@/constants/app.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { showDbDateAsIs } from '@/utils/dateUtils'
interface PostCardProps {
post: ForumPost
@ -21,16 +22,7 @@ export function ForumPostCard({
}: PostCardProps) {
const { translate } = useLocalization()
const formatDate = (value: string | Date) => {
const date = value instanceof Date ? value : new Date(value)
if (isNaN(date.getTime())) return 'Invalid Date'
return new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date)
return showDbDateAsIs(typeof value === 'string' ? value : value.toISOString())
}
return (

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'
import { FaTimes, FaSearch, FaFolder, FaRegComment, FaFileAlt } from 'react-icons/fa'
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
import { showDbDateAsIs } from '@/utils/dateUtils'
import { useForumSearch } from '@/utils/hooks/useForumSearch'
interface SearchModalProps {
@ -84,16 +85,7 @@ export function SearchModal({
}
const formatDate = (value: string | Date) => {
const date = value instanceof Date ? value : new Date(value)
if (isNaN(date.getTime())) return 'Invalid Date'
return new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date)
return showDbDateAsIs(typeof value === 'string' ? value : value.toISOString())
}
const getTopicTitle = (topicId: string) => {

View file

@ -1,8 +1,7 @@
import React, { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { FaCalendarAlt, FaUser, FaTag, FaSearch } from 'react-icons/fa'
import dayjs from 'dayjs'
import 'dayjs/locale/tr'
import { showDbDateAsIs } from '@/utils/dateUtils'
import { BlogCategory, BlogPost } from '@/proxy/blog/blog'
import { blogService } from '@/services/blog.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
@ -19,8 +18,6 @@ const Blog = () => {
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
dayjs.locale('tr')
useEffect(() => {
loadBlogData()
}, [currentPage, selectedCategory])
@ -200,7 +197,7 @@ const Blog = () => {
</div>
<div className="flex items-center">
<FaCalendarAlt size={16} className="mr-1" />
{dayjs(post.publishedAt || post.creationTime).format('DD MMM YYYY')}
{showDbDateAsIs(post.publishedAt || post.creationTime)}
</div>
</div>
</div>

View file

@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react'
import { Link, useParams } from 'react-router-dom'
import dayjs from 'dayjs'
import 'dayjs/locale/tr'
import { showDbDateAsIs } from '@/utils/dateUtils'
import { BlogPost } from '@/proxy/blog/blog'
import { blogService } from '@/services/blog.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
@ -28,8 +27,6 @@ const BlogDetail: React.FC = () => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
dayjs.locale('tr')
useEffect(() => {
const fetchBlogPost = async () => {
setLoading(true)
@ -111,7 +108,7 @@ const BlogDetail: React.FC = () => {
<span>{postData.author?.name}</span>
</div>
<div className="flex items-center">
{blogPost.publishedAt && dayjs(blogPost.publishedAt).format('DD MMM YYYY')}
{blogPost.publishedAt && showDbDateAsIs(blogPost.publishedAt)}
</div>
</div>
<div