Kaldırılan Dto dosyaları kaldırıldı
This commit is contained in:
parent
aac3f4aa80
commit
078ba898bd
117 changed files with 16 additions and 16456 deletions
|
|
@ -1,19 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Banks;
|
|
||||||
|
|
||||||
public class BankAccountDto : AuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public string AccountNumber { get; set; }
|
|
||||||
public Guid BankId { get; set; }
|
|
||||||
public string BankName { get; set; }
|
|
||||||
|
|
||||||
public string AccountOwner { get; set; }
|
|
||||||
|
|
||||||
public string Currency { get; set; }
|
|
||||||
|
|
||||||
public bool CanTransferMoney { get; set; }
|
|
||||||
|
|
||||||
public string Company { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Banks;
|
|
||||||
|
|
||||||
public class BankDto : AuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string IdentifierCode { get; set; }
|
|
||||||
|
|
||||||
public string Address1 { get; set; }
|
|
||||||
public string Address2 { get; set; }
|
|
||||||
public string District { get; set; }
|
|
||||||
public string City { get; set; }
|
|
||||||
public string PostalCode { get; set; }
|
|
||||||
public string Country { get; set; }
|
|
||||||
public string PhoneNumber { get; set; }
|
|
||||||
public string Email { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Classrooms;
|
|
||||||
|
|
||||||
public class ClassroomChatDto
|
|
||||||
{
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Classrooms;
|
|
||||||
|
|
||||||
public class ClassroomDto : 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 ClassroomSettingsDto SettingsDto
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(SettingsJson))
|
|
||||||
return JsonSerializer.Deserialize<ClassroomSettingsDto>(SettingsJson);
|
|
||||||
|
|
||||||
return new ClassroomSettingsDto();
|
|
||||||
}
|
|
||||||
set { SettingsJson = JsonSerializer.Serialize(value); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ClassroomSettingsDto
|
|
||||||
{
|
|
||||||
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 GetClassroomListDto : PagedAndSortedResultRequestDto
|
|
||||||
{
|
|
||||||
public bool? IsActive { get; set; }
|
|
||||||
public Guid? TeacherId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ClassroomAttendanceDto : 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; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
public class ClassroomFilterInputDto : PagedAndSortedResultRequestDto
|
|
||||||
{
|
|
||||||
public string Search { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Classrooms;
|
|
||||||
|
|
||||||
public class ClassroomParticipantDto
|
|
||||||
{
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
using Volo.Abp.Application.Services;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Classrooms;
|
|
||||||
|
|
||||||
public interface IClassroomAppService : IApplicationService
|
|
||||||
{
|
|
||||||
Task<ClassroomDto> GetAsync(Guid id);
|
|
||||||
Task<PagedResultDto<ClassroomDto>> GetListAsync(ClassroomFilterInputDto input);
|
|
||||||
Task<ClassroomDto> CreateAsync(ClassroomDto input);
|
|
||||||
Task<ClassroomDto> UpdateAsync(Guid id, ClassroomDto input);
|
|
||||||
Task DeleteAsync(Guid id);
|
|
||||||
Task<ClassroomDto> StartClassAsync(Guid id);
|
|
||||||
Task EndClassAsync(Guid id);
|
|
||||||
Task<ClassroomDto> JoinClassAsync(Guid id);
|
|
||||||
Task LeaveClassAsync(Guid id);
|
|
||||||
Task<List<ClassroomAttendanceDto>> GetAttendanceAsync(Guid sessionId);
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Currencies;
|
|
||||||
|
|
||||||
public class CurrencyDto : AuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public string Code { get; set; }
|
|
||||||
public string Symbol { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public decimal Rate { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
public DateTime? LastUpdated { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class AnnouncementDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string Excerpt { get; set; }
|
|
||||||
public string Content { get; set; }
|
|
||||||
public string ImageUrl { get; set; }
|
|
||||||
public string Category { get; set; }
|
|
||||||
public Guid? EmployeeId { get; set; }
|
|
||||||
public EmployeeDto Employee { get; set; }
|
|
||||||
public DateTime PublishDate { get; set; }
|
|
||||||
public DateTime? ExpiryDate { get; set; }
|
|
||||||
public bool IsPinned { get; set; }
|
|
||||||
public int ViewCount { get; set; }
|
|
||||||
public string[] Departments { get; set; }
|
|
||||||
public string Attachments { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class CurrencyDto : FullAuditedEntityDto<string>
|
|
||||||
{
|
|
||||||
public string Code { get; set; } // TRY, USD, EUR
|
|
||||||
public string Symbol { get; set; } // ₺, $, etc.
|
|
||||||
public string Name { get; set; } // Turkish lira, US dollar, ...
|
|
||||||
public decimal Rate { get; set; } // TRY başına değer
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
public DateTime? LastUpdated { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class DepartmentDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public Guid? ParentDepartmentId { get; set; }
|
|
||||||
public string ParentDepartmentName { get; set; }
|
|
||||||
public Guid? ManagerId { get; set; }
|
|
||||||
public string ManagerName { get; set; }
|
|
||||||
public Guid? CostCenterId { get; set; }
|
|
||||||
public string CostCenterName { get; set; }
|
|
||||||
public decimal Budget { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
public List<DepartmentDto> SubDepartments { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class EmployeeDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
|
|
||||||
public string Code { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Avatar { get; set; }
|
|
||||||
public string NationalId { get; set; }
|
|
||||||
public DateTime BirthDate { get; set; }
|
|
||||||
public string Gender { get; set; }
|
|
||||||
public string MaritalStatus { get; set; }
|
|
||||||
|
|
||||||
// Embedded Address
|
|
||||||
public string Country { get; set; }
|
|
||||||
public string City { get; set; }
|
|
||||||
public string District { get; set; }
|
|
||||||
public string Township { get; set; }
|
|
||||||
public string PostalCode { get; set; }
|
|
||||||
public string PhoneNumber { get; set; }
|
|
||||||
public string MobileNumber { get; set; }
|
|
||||||
public string Email { get; set; }
|
|
||||||
public string Address1 { get; set; }
|
|
||||||
public string Address2 { get; set; }
|
|
||||||
|
|
||||||
// Emergency contact
|
|
||||||
public string EmergencyContactName { get; set; }
|
|
||||||
public string EmergencyContactRelationship { get; set; }
|
|
||||||
public long EmergencyContactPhoneNumber { get; set; }
|
|
||||||
|
|
||||||
public DateTime HireDate { get; set; }
|
|
||||||
public DateTime? TerminationDate { get; set; }
|
|
||||||
|
|
||||||
public Guid? EmploymentTypeId { get; set; }
|
|
||||||
public EmploymentTypeDto EmploymentType { get; set; }
|
|
||||||
|
|
||||||
public string? JobPositionId { get; set; }
|
|
||||||
public JobPositionDto JobPosition { get; set; }
|
|
||||||
|
|
||||||
public Guid? DepartmentId { get; set; }
|
|
||||||
public DepartmentDto Department { get; set; }
|
|
||||||
|
|
||||||
public string WorkLocation { get; set; }
|
|
||||||
|
|
||||||
public Guid? ManagerId { get; set; }
|
|
||||||
public EmployeeDto Manager { get; set; }
|
|
||||||
|
|
||||||
public decimal BaseSalary { get; set; }
|
|
||||||
public string Currency { get; set; }
|
|
||||||
|
|
||||||
public string PayrollGroup { get; set; } // e.g., Monthly, Biweekly, Weekly
|
|
||||||
|
|
||||||
public Guid BankId { get; set; }
|
|
||||||
public string IbanNumber { get; set; }
|
|
||||||
public Guid? BadgeId { get; set; }
|
|
||||||
|
|
||||||
public string EmployeeStatus { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class EmploymentTypeDto : FullAuditedEntityDto<string>
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
|
|
||||||
public string Name { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class EventCommentDto
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
public EventOrganizerDto Employee { get; set; }
|
|
||||||
public string Content { get; set; }
|
|
||||||
public DateTime CreationTime { get; set; }
|
|
||||||
public int Likes { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class EventDto
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string CategoryName { get; set; }
|
|
||||||
public string TypeName { get; set; }
|
|
||||||
public DateTime Date { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public string Place { get; set; }
|
|
||||||
public EventOrganizerDto Organizer { get; set; }
|
|
||||||
public int Participants { get; set; }
|
|
||||||
public List<string> Photos { get; set; } = new();
|
|
||||||
public List<EventCommentDto> Comments { get; set; } = new();
|
|
||||||
public int Likes { get; set; }
|
|
||||||
public bool IsPublished { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class EventOrganizerDto
|
|
||||||
{
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Position { get; set; }
|
|
||||||
public string Avatar { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class ExpenseDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
|
|
||||||
public Guid? EmployeeId { get; set; }
|
|
||||||
public EmployeeDto Employee { get; set; }
|
|
||||||
|
|
||||||
public string Category { get; set; }
|
|
||||||
public decimal Amount { get; set; }
|
|
||||||
public string Currency { get; set; }
|
|
||||||
|
|
||||||
public DateTime RequestDate { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public string Project { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
public Guid? ApproverId { get; set; }
|
|
||||||
public EmployeeDto Approver { get; set; }
|
|
||||||
public DateTime? ApprovalDate { get; set; }
|
|
||||||
public string RejectionReason { get; set; }
|
|
||||||
public string Notes { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class ExpensesDto
|
|
||||||
{
|
|
||||||
public decimal TotalRequested { get; set; }
|
|
||||||
public decimal TotalApproved { get; set; }
|
|
||||||
public List<ExpenseDto> Last5Expenses { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Volo.Abp.Application.Services;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public interface IIntranetAppService : IApplicationService
|
|
||||||
{
|
|
||||||
Task<IntranetDashboardDto> GetIntranetDashboardAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using Sozsoft.Platform.FileManagement;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class IntranetDashboardDto
|
|
||||||
{
|
|
||||||
public List<EventDto> Events { get; set; } = [];
|
|
||||||
public List<EmployeeDto> Birthdays { get; set; } = [];
|
|
||||||
public List<VisitorDto> Visitors { get; set; } = [];
|
|
||||||
public List<ReservationDto> Reservations { get; set; } = [];
|
|
||||||
public List<TrainingDto> Trainings { get; set; } = [];
|
|
||||||
public ExpensesDto Expenses { get; set; } = new ExpensesDto();
|
|
||||||
public List<FileItemDto> Documents { get; set; } = [];
|
|
||||||
public List<AnnouncementDto> Announcements { get; set; } = [];
|
|
||||||
public List<ShuttleRouteDto> ShuttleRoutes { get; set; } = [];
|
|
||||||
public List<MealDto> Meals { get; set; } = [];
|
|
||||||
public List<LeaveDto> Leaves { get; set; } = [];
|
|
||||||
public List<OvertimeDto> Overtimes { get; set; } = [];
|
|
||||||
public List<SurveyDto> Surveys { get; set; } = [];
|
|
||||||
public List<SocialPostDto> SocialPosts { get; set; } = [];
|
|
||||||
public List<ProjectTaskDto> Tasks { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class JobPositionDto : FullAuditedEntityDto<string>
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
|
|
||||||
public Guid? DepartmentId { get; set; }
|
|
||||||
public string DepartmentName { get; set; }
|
|
||||||
|
|
||||||
public string Level { get; set; }
|
|
||||||
public decimal MinSalary { get; set; }
|
|
||||||
public decimal MaxSalary { get; set; }
|
|
||||||
public string Currency { get; set; }
|
|
||||||
public string RequiredSkills { get; set; }
|
|
||||||
public string Responsibilities { get; set; }
|
|
||||||
public string Qualifications { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class LeaveDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
|
|
||||||
public Guid EmployeeId { get; set; }
|
|
||||||
public EmployeeDto Employee { get; set; }
|
|
||||||
|
|
||||||
public string LeaveType { get; set; }
|
|
||||||
public DateTime StartDate { get; set; }
|
|
||||||
public DateTime EndDate { get; set; }
|
|
||||||
public decimal TotalDays { get; set; }
|
|
||||||
public bool IsHalfDay { get; set; }
|
|
||||||
public string Reason { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
public DateTime AppliedDate { get; set; }
|
|
||||||
public Guid? ApprovedById { get; set; }
|
|
||||||
public EmployeeDto ApprovedBy { get; set; }
|
|
||||||
public DateTime? ApprovedDate { get; set; }
|
|
||||||
public string RejectionReason { get; set; }
|
|
||||||
public string Attachments { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class MealDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
public Guid? BranchId { get; set; }
|
|
||||||
|
|
||||||
public DateTime Date { get; set; }
|
|
||||||
public string Type { get; set; }
|
|
||||||
public decimal TotalCalorie { get; set; }
|
|
||||||
public string[] Materials { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class OvertimeDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
|
|
||||||
public Guid EmployeeId { get; set; }
|
|
||||||
public EmployeeDto Employee { get; set; } // Navigation'dan doldurulabilir
|
|
||||||
|
|
||||||
public DateTime Date { get; set; } // Mesai tarihi
|
|
||||||
public DateTime StartTime { get; set; } // Başlangıç zamanı
|
|
||||||
public DateTime EndTime { get; set; } // Bitiş zamanı
|
|
||||||
public decimal TotalHours { get; set; } // Toplam fazla mesai süresi
|
|
||||||
public string Reason { get; set; } // Fazla mesai nedeni
|
|
||||||
public string Status { get; set; } // Durum: "Bekliyor", "Onaylandı", "Reddedildi"
|
|
||||||
public Guid? ApprovedById { get; set; } // Onaylayan kişi ID
|
|
||||||
public DateTime? ApprovedDate { get; set; } // Onay tarihi
|
|
||||||
public string RejectionReason { get; set; } // Reddetme nedeni
|
|
||||||
public decimal Rate { get; set; } // Fazla mesai oranı (ör. 1.5x)
|
|
||||||
public decimal? Amount { get; set; } // Hesaplanan ödeme tutarı
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class ProjectTaskDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
|
|
||||||
public Guid? ProjectId { get; set; }
|
|
||||||
public Guid? PhaseId { get; set; }
|
|
||||||
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
|
|
||||||
public string? TaskTypeId { get; set; }
|
|
||||||
|
|
||||||
public string Priority { get; set; }
|
|
||||||
public string? StatusId { get; set; }
|
|
||||||
|
|
||||||
public Guid? EmployeeId { get; set; }
|
|
||||||
public EmployeeDto Employee { get; set; }
|
|
||||||
|
|
||||||
public DateTime StartDate { get; set; }
|
|
||||||
public DateTime EndDate { get; set; }
|
|
||||||
|
|
||||||
public int Progress { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class ReservationDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
|
|
||||||
public string Type { get; set; } // room | vehicle | equipment
|
|
||||||
public string ResourceName { get; set; }
|
|
||||||
|
|
||||||
public Guid? EmployeeId { get; set; }
|
|
||||||
public string EmployeeName { get; set; } // Optional: ilişkili personel ismi
|
|
||||||
|
|
||||||
public DateTime StartDate { get; set; }
|
|
||||||
public DateTime EndDate { get; set; }
|
|
||||||
public string Purpose { get; set; } // Amaç
|
|
||||||
public int? Participants { get; set; } // Katılımcı Sayısı
|
|
||||||
public string Notes { get; set; }
|
|
||||||
public string Status { get; set; } // pending | approved | rejected | completed
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class ShuttleRouteDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
|
|
||||||
public string Type { get; set; } // Örn: "Servis", "Transfer", "Ring"
|
|
||||||
public string Name { get; set; } // Hat adı veya açıklaması
|
|
||||||
public string DepartureTime { get; set; } // Kalkış saati
|
|
||||||
public string ArrivalTime { get; set; } // Varış saati
|
|
||||||
public int Capacity { get; set; } // Toplam kapasite
|
|
||||||
public int Available { get; set; } // Mevcut boş koltuk sayısı
|
|
||||||
public string[] Route { get; set; } // JSON veya metin formatında güzergah bilgisi
|
|
||||||
}
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class SocialPostDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid? EmployeeId { get; set; }
|
|
||||||
public EmployeeDto? Employee { get; set; }
|
|
||||||
public string Content { get; set; }
|
|
||||||
|
|
||||||
public int LikeCount { get; set; }
|
|
||||||
public bool IsLiked { get; set; }
|
|
||||||
public bool IsOwnPost { get; set; }
|
|
||||||
|
|
||||||
public SocialLocationDto? Location { get; set; }
|
|
||||||
public SocialMediaDto? Media { get; set; }
|
|
||||||
public List<SocialCommentDto> Comments { get; set; }
|
|
||||||
public List<SocialLikeDto> Likes { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SocialLocationDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid SocialPostId { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string? Address { get; set; }
|
|
||||||
public double? Lat { get; set; }
|
|
||||||
public double? Lng { get; set; }
|
|
||||||
public string? PlaceId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SocialMediaDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid SocialPostId { get; set; }
|
|
||||||
public string Type { get; set; } // image | video | poll
|
|
||||||
public string[] Urls { get; set; }
|
|
||||||
|
|
||||||
// Poll Fields
|
|
||||||
public string? PollQuestion { get; set; }
|
|
||||||
public int? PollTotalVotes { get; set; }
|
|
||||||
public DateTime? PollEndsAt { get; set; }
|
|
||||||
public string? PollUserVoteId { get; set; }
|
|
||||||
|
|
||||||
public List<SocialPollOptionDto> PollOptions { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SocialPollOptionDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid SocialMediaId { get; set; }
|
|
||||||
public string Text { get; set; }
|
|
||||||
public int Votes { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SocialCommentDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid SocialPostId { get; set; }
|
|
||||||
public Guid? EmployeeId { get; set; }
|
|
||||||
public EmployeeDto? Employee { get; set; }
|
|
||||||
public string Content { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SocialLikeDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid SocialPostId { get; set; }
|
|
||||||
public Guid? EmployeeId { get; set; }
|
|
||||||
public EmployeeDto? Employee { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class SurveyDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public DateTime Deadline { get; set; }
|
|
||||||
public int Responses { get; set; }
|
|
||||||
public string Status { get; set; } // draft | active | closed
|
|
||||||
public bool IsAnonymous { get; set; }
|
|
||||||
|
|
||||||
public List<SurveyQuestionDto> Questions { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SurveyQuestionDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid SurveyId { get; set; }
|
|
||||||
public string QuestionText { get; set; }
|
|
||||||
public string Type { get; set; } // rating | multiple-choice | text | textarea | yes-no
|
|
||||||
public int Order { get; set; }
|
|
||||||
public bool IsRequired { get; set; }
|
|
||||||
|
|
||||||
public List<SurveyQuestionOptionDto> Options { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SurveyQuestionOptionDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid QuestionId { get; set; }
|
|
||||||
public string Text { get; set; }
|
|
||||||
public int Order { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SurveyResponseDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid SurveyId { get; set; }
|
|
||||||
public Guid? EmployeeId { get; set; }
|
|
||||||
public DateTime SubmissionTime { get; set; }
|
|
||||||
|
|
||||||
public List<SurveyAnswerDto> Answers { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SurveyAnswerDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid ResponseId { get; set; }
|
|
||||||
public Guid QuestionId { get; set; }
|
|
||||||
public string QuestionType { get; set; }
|
|
||||||
public string Value { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class TrainingDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public string Instructor { get; set; }
|
|
||||||
public string Category { get; set; } // technical | soft-skills | management | compliance | other
|
|
||||||
public string Type { get; set; } // online | classroom | hybrid
|
|
||||||
public int Duration { get; set; } // saat veya gün olarak
|
|
||||||
public DateTime StartDate { get; set; }
|
|
||||||
public DateTime EndDate { get; set; }
|
|
||||||
public int MaxParticipants { get; set; }
|
|
||||||
public int Enrolled { get; set; }
|
|
||||||
public string Status { get; set; } // upcoming | ongoing | completed
|
|
||||||
public string Location { get; set; }
|
|
||||||
public string Thumbnail { get; set; }
|
|
||||||
|
|
||||||
// İlişkili veriler
|
|
||||||
public int CertificateCount { get; set; } // optional: ilişkili sertifika sayısı
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Intranet;
|
|
||||||
|
|
||||||
public class VisitorDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string CompanyName { get; set; }
|
|
||||||
public string Email { get; set; }
|
|
||||||
public string PhoneNumber { get; set; }
|
|
||||||
public string Purpose { get; set; }
|
|
||||||
public DateTime VisitDate { get; set; }
|
|
||||||
public DateTime? CheckIn { get; set; }
|
|
||||||
public DateTime? CheckOut { get; set; }
|
|
||||||
public Guid? EmployeeId { get; set; }
|
|
||||||
public EmployeeDto Employee { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
public string BadgeNumber { get; set; }
|
|
||||||
public string Photo { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Sozsoft.Platform.Intranet;
|
using Sozsoft.Platform.Identity.Dto;
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Public;
|
namespace Sozsoft.Platform.Public;
|
||||||
|
|
@ -18,8 +18,8 @@ public class BlogPostDto : FullAuditedEntityDto<Guid>
|
||||||
public Guid CategoryId { get; set; }
|
public Guid CategoryId { get; set; }
|
||||||
public BlogCategoryDto Category { get; set; }
|
public BlogCategoryDto Category { get; set; }
|
||||||
|
|
||||||
public Guid EmployeeId { get; set; }
|
public Guid UserId { get; set; }
|
||||||
public EmployeeDto Employee { get; set; }
|
public UserInfoViewModel User { get; set; }
|
||||||
|
|
||||||
public int ViewCount { get; set; }
|
public int ViewCount { get; set; }
|
||||||
public int LikeCount { get; set; }
|
public int LikeCount { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Sozsoft.Platform.Identity.Dto;
|
using Sozsoft.Platform.Identity.Dto;
|
||||||
using Sozsoft.Platform.Intranet;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Public;
|
namespace Sozsoft.Platform.Public;
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Questions;
|
|
||||||
|
|
||||||
public class QuestionDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public string QuestionType { get; set; }
|
|
||||||
public int Points { get; set; }
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string Content { get; set; }
|
|
||||||
public string MediaUrl { get; set; }
|
|
||||||
public string MediaType { get; set; }
|
|
||||||
public string CorrectAnswer { get; set; }
|
|
||||||
public string Difficulty { get; set; }
|
|
||||||
public int TimeLimit { get; set; }
|
|
||||||
public string Explanation { get; set; }
|
|
||||||
|
|
||||||
public Guid QuestionPoolId { get; set; }
|
|
||||||
public List<QuestionOptionDto> Options { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
using System;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Questions;
|
|
||||||
|
|
||||||
public class QuestionOptionDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public string Text { get; set; }
|
|
||||||
public bool IsCorrect { get; set; }
|
|
||||||
|
|
||||||
public Guid QuestionPoolId { get; set; }
|
|
||||||
public Guid QuestionId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Questions;
|
|
||||||
|
|
||||||
public class QuestionPoolDto : FullAuditedEntityDto<Guid>
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public string Tags { get; set; }
|
|
||||||
|
|
||||||
public List<QuestionDto> Questions { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
@ -12,8 +12,6 @@ using Volo.Abp.Domain.Entities;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Sozsoft.Platform.Intranet;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Volo.Abp.Identity;
|
using Volo.Abp.Identity;
|
||||||
using Sozsoft.Platform.Identity.Dto;
|
using Sozsoft.Platform.Identity.Dto;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,10 @@
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using Sozsoft.Platform.Entities;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Branchs;
|
namespace Sozsoft.Platform.Branchs;
|
||||||
|
|
||||||
public class BranchSeederDto
|
public class BranchSeederDto
|
||||||
{
|
{
|
||||||
public List<RegistrationTypeSeedDto> RegistrationTypes { get; set; }
|
|
||||||
public List<RegistrationMethodSeedDto> RegistrationMethods { get; set; }
|
|
||||||
public List<ClassTypeSeedDto> ClassTypes { get; set; }
|
|
||||||
public List<ClassSeedDto> Classes { get; set; }
|
|
||||||
public List<LevelSeedDto> Levels { get; set; }
|
|
||||||
public List<LessonPeriodSeedDto> LessonPeriods { get; set; }
|
|
||||||
public List<ScheduleSeedDto> Schedules { get; set; }
|
|
||||||
public List<MealSeedDto> Meals { get; set; }
|
|
||||||
public List<BankSeedDto> Banks { get; set; }
|
public List<BankSeedDto> Banks { get; set; }
|
||||||
public List<CashSeedDto> Cashes { get; set; }
|
|
||||||
public List<CurrentAccountSeedDto> CurrentAccounts { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CurrentAccountSeedDto
|
|
||||||
{
|
|
||||||
public string Code { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public long? TaxNumber { get; set; }
|
|
||||||
public string TaxOffice { get; set; }
|
|
||||||
public decimal CreditLimit { get; set; }
|
|
||||||
public decimal Balance { get; set; }
|
|
||||||
public string Currency { get; set; }
|
|
||||||
public string Risk { get; set; } //Low, Medium, High, Blocked
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CashSeedDto
|
|
||||||
{
|
|
||||||
public string Code { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public string Currency { get; set; }
|
|
||||||
public decimal Balance { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BankSeedDto
|
public class BankSeedDto
|
||||||
|
|
@ -56,84 +20,3 @@ public class BankSeedDto
|
||||||
public string PhoneNumber { get; set; }
|
public string PhoneNumber { get; set; }
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MealSeedDto
|
|
||||||
{
|
|
||||||
public DateTime Date { get; set; }
|
|
||||||
public string Type { get; set; }
|
|
||||||
public decimal TotalCalorie { get; set; }
|
|
||||||
public string Materials { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RegistrationTypeSeedDto
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RegistrationMethodSeedDto
|
|
||||||
{
|
|
||||||
public string RegistrationTypeName { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ClassTypeSeedDto
|
|
||||||
{
|
|
||||||
public string RegistrationTypeName { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public int? MinStudentCount { get; set; }
|
|
||||||
public int? MaxStudentCount { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ClassSeedDto
|
|
||||||
{
|
|
||||||
public string ClassTypeName { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LevelSeedDto
|
|
||||||
{
|
|
||||||
public string ClassTypeName { get; set; }
|
|
||||||
public string LevelType { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public int Order { get; set; }
|
|
||||||
public int LessonCount { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
public int? LessonDuration { get; set; }
|
|
||||||
public decimal? MonthlyPaymentRate { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LessonPeriodSeedDto
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Day { get; set; }
|
|
||||||
|
|
||||||
public string Lesson1 { get; set; }
|
|
||||||
public string Lesson2 { get; set; }
|
|
||||||
public string Lesson3 { get; set; }
|
|
||||||
public string Lesson4 { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ScheduleSeedDto
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
public string StartTime { get; set; }
|
|
||||||
public string EndTime { get; set; }
|
|
||||||
public int LessonMinute { get; set; }
|
|
||||||
public int LessonBreakMinute { get; set; }
|
|
||||||
public int LessonCount { get; set; }
|
|
||||||
public string LunchTime { get; set; }
|
|
||||||
public int? LunchMinute { get; set; }
|
|
||||||
public bool? IncludeLunch { get; set; }
|
|
||||||
public bool? Monday { get; set; }
|
|
||||||
public bool? Tuesday { get; set; }
|
|
||||||
public bool? Wednesday { get; set; }
|
|
||||||
public bool? Thursday { get; set; }
|
|
||||||
public bool? Friday { get; set; }
|
|
||||||
public bool? Saturday { get; set; }
|
|
||||||
public bool? Sunday { get; set; }
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9,30 +9,32 @@ public class TenantSeederDto
|
||||||
{
|
{
|
||||||
//Saas
|
//Saas
|
||||||
public List<GlobalSearchSeedDto> GlobalSearch { get; set; }
|
public List<GlobalSearchSeedDto> GlobalSearch { get; set; }
|
||||||
public List<ForumCategorySeedDto> ForumCategories { get; set; }
|
|
||||||
public List<CustomEndpointSeedDto> CustomEndpoints { get; set; }
|
public List<CustomEndpointSeedDto> CustomEndpoints { get; set; }
|
||||||
public List<CustomComponentSeedDto> CustomComponents { get; set; }
|
public List<CustomComponentSeedDto> CustomComponents { get; set; }
|
||||||
public List<ReportCategorySeedDto> ReportCategories { get; set; }
|
|
||||||
|
//Tanımlamalar
|
||||||
|
public List<SectorSeedDto> Sectors { get; set; }
|
||||||
|
public List<WorkHourSeedDto> WorkHours { get; set; }
|
||||||
public List<UomCategorySeedDto> UomCategories { get; set; }
|
public List<UomCategorySeedDto> UomCategories { get; set; }
|
||||||
public List<UomSeedDto> Uoms { get; set; }
|
public List<UomSeedDto> Uoms { get; set; }
|
||||||
public List<SkillTypeSeedDto> SkillTypes { get; set; }
|
public List<SkillTypeSeedDto> SkillTypes { get; set; }
|
||||||
public List<SkillSeedDto> Skills { get; set; }
|
public List<SkillSeedDto> Skills { get; set; }
|
||||||
public List<SkillLevelSeedDto> SkillLevels { get; set; }
|
public List<SkillLevelSeedDto> SkillLevels { get; set; }
|
||||||
|
|
||||||
public List<AboutSeedDto> Abouts { get; set; }
|
public List<AboutSeedDto> Abouts { get; set; }
|
||||||
public List<ServiceSeedDto> Services { get; set; }
|
public List<ServiceSeedDto> Services { get; set; }
|
||||||
public List<PaymentMethodSeedDto> PaymentMethods { get; set; }
|
public List<PaymentMethodSeedDto> PaymentMethods { get; set; }
|
||||||
public List<InstallmentOptionSeedDto> InstallmentOptions { get; set; }
|
public List<InstallmentOptionSeedDto> InstallmentOptions { get; set; }
|
||||||
public List<BlogCategorySeedDto> BlogCategories { get; set; }
|
public List<BlogCategorySeedDto> BlogCategories { get; set; }
|
||||||
public List<BlogPostSeedDto> BlogPosts { get; set; }
|
public List<BlogPostSeedDto> BlogPosts { get; set; }
|
||||||
public List<ContactSeedDto> Contacts { get; set; }
|
|
||||||
public List<ProductSeedDto> Products { get; set; }
|
public List<ProductSeedDto> Products { get; set; }
|
||||||
|
public List<ContactSeedDto> Contacts { get; set; }
|
||||||
//Tanımlamalar
|
|
||||||
public List<SectorSeedDto> Sectors { get; set; }
|
|
||||||
public List<WorkHourSeedDto> WorkHours { get; set; }
|
|
||||||
|
|
||||||
//Report Templates
|
//Report Templates
|
||||||
|
public List<ReportCategorySeedDto> ReportCategories { get; set; }
|
||||||
public List<ReportTemplateSeedDto> ReportTemplates { get; set; }
|
public List<ReportTemplateSeedDto> ReportTemplates { get; set; }
|
||||||
|
|
||||||
|
public List<ForumCategorySeedDto> ForumCategories { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GlobalSearchSeedDto
|
public class GlobalSearchSeedDto
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ using Hangfire.PostgreSql;
|
||||||
using Sozsoft.Languages;
|
using Sozsoft.Languages;
|
||||||
using Sozsoft.MailQueue;
|
using Sozsoft.MailQueue;
|
||||||
using Sozsoft.Notifications.Application;
|
using Sozsoft.Notifications.Application;
|
||||||
using Sozsoft.Platform.Classrooms;
|
|
||||||
using Sozsoft.Platform.EntityFrameworkCore;
|
using Sozsoft.Platform.EntityFrameworkCore;
|
||||||
using Sozsoft.SqlQueryManager;
|
using Sozsoft.SqlQueryManager;
|
||||||
using Sozsoft.Platform.Extensions;
|
using Sozsoft.Platform.Extensions;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Sozsoft.Platform.Classrooms;
|
|
||||||
using Sozsoft.Platform.Enums;
|
using Sozsoft.Platform.Enums;
|
||||||
using Sozsoft.Platform.DynamicServices;
|
using Sozsoft.Platform.DynamicServices;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
|
|
||||||
|
|
@ -1,314 +1,4 @@
|
||||||
{
|
{
|
||||||
"commit": "809bfb71",
|
"commit": "aac3f4a",
|
||||||
"releases": [
|
"releases": []
|
||||||
{
|
|
||||||
"version": "1.0.40",
|
|
||||||
"buildDate": "2026-01-17",
|
|
||||||
"commit": "52c93ccbaf1be1c8365097e97276f48988864de4",
|
|
||||||
"changeLog": [
|
|
||||||
"- Grid üzerinden Dil desteği güncellemeleri yapıldı.",
|
|
||||||
"- Nodejs 24 versiyonuna geçildi.",
|
|
||||||
"- Grid ve Popup içerisinde Numeric formatlar geliştirildi.",
|
|
||||||
"- File Management geliştirildi.",
|
|
||||||
"- XtraReport viewer ve desing komponentleri geliştirildi.",
|
|
||||||
"- Devexpress Licenceı artırıldı."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.37",
|
|
||||||
"buildDate": "2025-12-07",
|
|
||||||
"commit": "8fd2d58c0f629af466df87dd8b03a17d9c44ceb2",
|
|
||||||
"changeLog": [
|
|
||||||
"- Before ve After Insert Command",
|
|
||||||
"- Before ve After Update Command",
|
|
||||||
"- Before ve After Delete Command",
|
|
||||||
"Bu tanımlamalar artık liste formlarda çalıştırıldı test edildi."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.36",
|
|
||||||
"buildDate": "2025-12-06",
|
|
||||||
"commit": "9938acb94c3758a54bcb8593690825b63a4ab54f",
|
|
||||||
"changeLog": [
|
|
||||||
"SQL Query Manager komponentin üretildi. Artık uygulama üzerinden veritabanına hükmedilebilecek."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.34",
|
|
||||||
"buildDate": "2025-11-18",
|
|
||||||
"commit": "07f03bdf3c2a6e57e80ee83cfaba45042abde994",
|
|
||||||
"changeLog": [
|
|
||||||
"- Subform içinde ilişkili sütun düzenlemesi",
|
|
||||||
"- Grid Popup Autonumber özelliği",
|
|
||||||
"- TreeView ve GridView düzeltmeleri"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.33",
|
|
||||||
"buildDate": "2025-10-26",
|
|
||||||
"commit": "78cce3d4fb126ccfaedff0f53da8661261e1ce44",
|
|
||||||
"changeLog": [
|
|
||||||
"Dosya Yönetimi ve Intranet menüsün hazılanması"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.32",
|
|
||||||
"buildDate": "2025-10-15",
|
|
||||||
"commit": "8564bff367eefb62b1cfd7ac5790097fcf8feaa7",
|
|
||||||
"changeLog": [
|
|
||||||
"Form View ve Form Edit ekranlarında Activity özelliği eklendi. 3 farklı Activity eklenebiliyor ayrıca dosya eklenebiliyor. Bu dosyalar diskte Blob olarak kaydediliyor."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.31",
|
|
||||||
"buildDate": "2025-10-09",
|
|
||||||
"commit": "035366ab7020dd77bfe2b5b66ea253e743526ea6",
|
|
||||||
"changeLog": [
|
|
||||||
"- Grid üzerinde Mask ve Format güncellemesi",
|
|
||||||
"- Allow Column Reordering uygulamasının çalıştırılması"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.30",
|
|
||||||
"buildDate": "2025-10-08",
|
|
||||||
"commit": "e45885f5693176257e12ecc05d4ed51f87ef0120",
|
|
||||||
"changeLog": [
|
|
||||||
"- Tenant ve Barch arasında ilişki kuruldu ve listelerde filtreli şekilde listeleniyor.",
|
|
||||||
"- Genel seederlar düzenlendi.",
|
|
||||||
"- Yeni tanımlamalar listeleri eklendi. Kayıt Tipi, Kayı Şekli, Program vs.",
|
|
||||||
"- Default Helper eklendi ve tüm Application Servisler o metoda yönlendirildi.",
|
|
||||||
"- Tanımlamalar menülere dağıtıldı."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.29",
|
|
||||||
"buildDate": "2025-09-28",
|
|
||||||
"commit": "565357ba3e3f90811758b9b070e7e29e2c40b855",
|
|
||||||
"changeLog": [
|
|
||||||
"Chart komponenti eklendi."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.28",
|
|
||||||
"buildDate": "2025-09-24",
|
|
||||||
"commit": "948925816f3fc5808a07bf0ce6fd86e0ac880ec7",
|
|
||||||
"changeLog": [
|
|
||||||
"Vite güncellemesi ile artık versiyon bilgileri düzelecek ve cache problemi düzelmeli"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.27",
|
|
||||||
"buildDate": "2025-09-24",
|
|
||||||
"commit": "6e3f58ce9d3e7bc79c74dfc33ec79d115b7160a2",
|
|
||||||
"changeLog": [
|
|
||||||
"FormView, FormNew, Grid Popup için Script özelliği eklendi. Ayrıca itemlara buton eklenebiliyor.",
|
|
||||||
"Sadece textbox olan inputlara ekleniyor. Diğer komponenler için render özelliği kullanılması gerekiyor."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.26",
|
|
||||||
"buildDate": "2025-09-23",
|
|
||||||
"commit": "ea5dbe91f5abd0a7408b75cc111c3119fbb8eb53",
|
|
||||||
"changeLog": [
|
|
||||||
"Liste, Pivot ve Card görünümü düzeltildi.",
|
|
||||||
"Menü ikonları Listelerde gösterildi.",
|
|
||||||
"Form detayında Subform filtreleri çalıştırıldı.",
|
|
||||||
"Grid proplarına gridDto gelmiyorsa kendi başının çaresine bakacak şekilde düzenlendi."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.25",
|
|
||||||
"buildDate": "2025-09-23",
|
|
||||||
"commit": "d4f994b7b17bd96c45ed868e4c94d18ca757e217",
|
|
||||||
"changeLog": [
|
|
||||||
"Genel düzenlemeler"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.24",
|
|
||||||
"buildDate": "2025-09-23",
|
|
||||||
"commit": "f62c35dec957dae225ab93811a0899c975cecf52",
|
|
||||||
"changeLog": [
|
|
||||||
"BaseModel LocalStorage açıldı.",
|
|
||||||
"ListeForm Layout ve CardColumnCount eklendi.",
|
|
||||||
"FormView, FormEdit ve FormNew den geri butonu ile Liste kaldığı yerden devam edebilecek."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.23",
|
|
||||||
"buildDate": "2025-09-23",
|
|
||||||
"commit": "e92143fd055b9db1a8932f2c4d0b81172b19ef00",
|
|
||||||
"changeLog": [
|
|
||||||
"Vite üzerinde html dosyaları cacheleme"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.22",
|
|
||||||
"buildDate": "2025-09-23",
|
|
||||||
"commit": "226410e7a928a54f01dd7f5305bf09fb7dcbc59d",
|
|
||||||
"changeLog": [
|
|
||||||
"MenuIcon için hook hazırladı. Liste, FormEdit, FormView ve FormNew de kullanılabilir."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.21",
|
|
||||||
"buildDate": "2025-09-23",
|
|
||||||
"commit": "fcd6547dcb85d46d90d78eaa24788d7cc079d1b3",
|
|
||||||
"changeLog": [
|
|
||||||
"Versiyon güncellemeleri sistemin son aşama; eğer versiyon bilgisi değişmiş ise ChangeLog sayfasına yönlendirildi."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.19",
|
|
||||||
"buildDate": "2025-09-23",
|
|
||||||
"commit": "43a945969174dd50b798d0d619e5fb7932402d04",
|
|
||||||
"changeLog": [
|
|
||||||
"Docker içerisinden diskclean script kaldırıldı. Ayrıca çalıştırılabilecek şekilde diskclean dizinin içerisinde sh dosyası mevcut."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.18",
|
|
||||||
"buildDate": "2025-09-23",
|
|
||||||
"commit": "09566cce3b6750ad1f6ef2faad24b81d21dfd668",
|
|
||||||
"changeLog": [
|
|
||||||
"\"build\": \"node scripts/generate-version.js && vite build\"",
|
|
||||||
"\"build:production\": \"vite build\",",
|
|
||||||
"Yukarıdaki şekilde hem developer hem de production ortamı build işlemi birbirinden ayrıldı."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.17",
|
|
||||||
"buildDate": "2025-09-23",
|
|
||||||
"commit": "5456486692eb070601313ab439de9c33a977b15b",
|
|
||||||
"changeLog": [
|
|
||||||
"Hem developer hem de production için versiyon güncellemesi"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.16",
|
|
||||||
"buildDate": "2025-09-23",
|
|
||||||
"commit": "08799590e2e3dcee039e88370540c9224420d17f",
|
|
||||||
"changeLog": [
|
|
||||||
"Genel versiyon düzeltmesi"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.15",
|
|
||||||
"buildDate": "2025-09-23",
|
|
||||||
"commit": "08799590e2e3dcee039e88370540c9224420d17f",
|
|
||||||
"changeLog": [
|
|
||||||
"Genel versiyon güncelleme hataları",
|
|
||||||
"- UI versiyon gösterilecek",
|
|
||||||
"- Deploy edilince otomatik sayfa tazelenecek"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.14",
|
|
||||||
"buildDate": "2025-09-22",
|
|
||||||
"commit": "1c4ab4f8232b4cd2a39fa66f8101664840113ce5",
|
|
||||||
"changeLog": [
|
|
||||||
"Yeni versiyon çıktı uyarı gelecek şekilde düzenlendi.",
|
|
||||||
"Sağ alt kısımda mesaj çıkacak ve yenile butonu ile uygulama yeni versiyona geçecektir."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.13",
|
|
||||||
"buildDate": "2025-09-22",
|
|
||||||
"commit": "c5f3a65304bc3c04d89ddf2f01d02563c656b911",
|
|
||||||
"changeLog": [
|
|
||||||
"nginx ayarları ve versiyon yenileme hakkında düzeltme"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.12",
|
|
||||||
"buildDate": "2025-09-22",
|
|
||||||
"commit": "f55b777a171ec2072999e204b8e1e818fd91d8a3",
|
|
||||||
"changeLog": [
|
|
||||||
"Versiyon yenileme sistemi güncellemesi"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.11",
|
|
||||||
"buildDate": "2025-09-22",
|
|
||||||
"commit": "b2e489d7051ca47a82561cbf2674ec49dc30ed92",
|
|
||||||
"changeLog": [
|
|
||||||
"Liste formlarda Layout görünümü düzenlemesi"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.10",
|
|
||||||
"buildDate": "2025-09-22",
|
|
||||||
"commit": "b75158bc018b4d5076e0208796d68f16975e77d8",
|
|
||||||
"changeLog": [
|
|
||||||
"EditorOptions içerisine DataSource özelliği eklendi"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.9",
|
|
||||||
"buildDate": "2025-09-21",
|
|
||||||
"commit": "e14d6930c21d2b1c108f16ce675dd05474a95d9e",
|
|
||||||
"changeLog": [
|
|
||||||
"Form komponentinin SelectBox -> lookup bilgisi varsa verileri dolduruyor"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.8",
|
|
||||||
"buildDate": "2025-09-21",
|
|
||||||
"commit": "3f69cc54e94cf40db87fb23ba4cf7b311cc1f77c",
|
|
||||||
"changeLog": [
|
|
||||||
"Listelere Grid ve Card görünümleri eklendi."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.7",
|
|
||||||
"buildDate": "2025-09-20",
|
|
||||||
"commit": "a01422ca600fbcbdf3f51bb9c91ad7dba46c98a2",
|
|
||||||
"changeLog": [
|
|
||||||
"Versiyon güncellemesi için geçiş uyarı sistemi"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.6",
|
|
||||||
"buildDate": "2025-09-19",
|
|
||||||
"commit": "9e85780623d940f43155474b66f2820d997abe3a",
|
|
||||||
"changeLog": [
|
|
||||||
"Versiyon güncelleme sistemi",
|
|
||||||
"Vite.Config dosyasında hızlandırma adına güncellemeler"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.5",
|
|
||||||
"buildDate": "2025-09-19",
|
|
||||||
"commit": "c947fb2a1c0979df3d7fd4dab47af7a2d370f622",
|
|
||||||
"changeLog": [
|
|
||||||
"Form ekranındaki Butonlar güncellemeleri yapıldı",
|
|
||||||
"Edit Form ekranındaki Info butonu eklendi.",
|
|
||||||
"New Form ekranındaki Geri butonu eklendi."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.4",
|
|
||||||
"buildDate": "2025-09-19",
|
|
||||||
"commit": "6766d1129d345e165282fc3ec198a168f188ab00",
|
|
||||||
"changeLog": [
|
|
||||||
"Subformlar üzerinde extra filters ve Widget çalışmaları yapıldı."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.3",
|
|
||||||
"buildDate": "2025-09-19",
|
|
||||||
"commit": "656d1626179733f8da56aa2268b852a79efe26d8",
|
|
||||||
"changeLog": [
|
|
||||||
"Manage Grid üzerinde Extra filtre tanımlaması yapıldı."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "1.0.2",
|
|
||||||
"buildDate": "2025-09-16",
|
|
||||||
"commit": "c6d2fbf30acae9c96502dfdd3846cbfbaf8af614",
|
|
||||||
"changeLog": [
|
|
||||||
"Genel Static olan Url bilgileri kaldırıldı."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,282 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { FaMicrophoneSlash, FaExpand, FaUserTimes } from 'react-icons/fa'
|
|
||||||
import { VideoPlayer } from './VideoPlayer'
|
|
||||||
import { ClassroomParticipantDto, VideoLayoutDto } from '@/proxy/classroom/models'
|
|
||||||
|
|
||||||
interface ParticipantGridProps {
|
|
||||||
participants: ClassroomParticipantDto[]
|
|
||||||
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: VideoLayoutDto
|
|
||||||
focusedParticipant?: string
|
|
||||||
onParticipantFocus?: (participantId: string | undefined) => void
|
|
||||||
onKickParticipant?: (participantId: string) => void
|
|
||||||
hasSidePanel?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ParticipantGrid: React.FC<ParticipantGridProps> = ({
|
|
||||||
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 ClassroomParticipantDto
|
|
||||||
|
|
||||||
// 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: ClassroomParticipantDto,
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import { Classroom } from '@/proxy/classroom/planning'
|
|
||||||
|
|
||||||
export const classrooms: Classroom[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'Theater Sınıfı',
|
|
||||||
layoutType: 'Theater',
|
|
||||||
rows: 6,
|
|
||||||
columns: 8,
|
|
||||||
capacity: 48,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'U-Shape Sınıfı',
|
|
||||||
layoutType: 'UShape',
|
|
||||||
rows: 5,
|
|
||||||
columns: 8,
|
|
||||||
capacity: 40,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'Bus Sınıfı',
|
|
||||||
layoutType: 'Bus',
|
|
||||||
rows: 10,
|
|
||||||
columns: 5,
|
|
||||||
capacity: 50,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
name: 'Lab Sınıfı',
|
|
||||||
layoutType: 'Lab',
|
|
||||||
rows: 8,
|
|
||||||
columns: 6,
|
|
||||||
capacity: 48,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
name: 'Exam Sınıfı',
|
|
||||||
layoutType: 'Exam',
|
|
||||||
rows: 10,
|
|
||||||
columns: 10,
|
|
||||||
capacity: 100,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
name: 'Grid Sınıfı',
|
|
||||||
layoutType: 'Grid',
|
|
||||||
rows: 8,
|
|
||||||
columns: 8,
|
|
||||||
capacity: 64,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
import {
|
|
||||||
FaBookOpen,
|
|
||||||
FaBus,
|
|
||||||
FaCircle,
|
|
||||||
FaFlask,
|
|
||||||
FaLayerGroup,
|
|
||||||
FaSquare,
|
|
||||||
FaThLarge,
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
|
|
||||||
export const layouts = [
|
|
||||||
{
|
|
||||||
value: 'Theater',
|
|
||||||
label: 'Theater',
|
|
||||||
icon: FaThLarge,
|
|
||||||
description: 'Tam grid düzen',
|
|
||||||
},
|
|
||||||
{ value: 'Bus', label: 'Bus', icon: FaBus, description: 'Ortada koridor' },
|
|
||||||
{
|
|
||||||
value: 'UShape',
|
|
||||||
label: 'U-Shape',
|
|
||||||
icon: FaSquare,
|
|
||||||
description: 'U şekli düzen',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'Grid',
|
|
||||||
label: 'Grid',
|
|
||||||
icon: FaLayerGroup,
|
|
||||||
description: 'Basit tablo',
|
|
||||||
},
|
|
||||||
{ value: 'Lab', label: 'Lab', icon: FaFlask, description: 'Masa grupları' },
|
|
||||||
{
|
|
||||||
value: 'Exam',
|
|
||||||
label: 'Exam',
|
|
||||||
icon: FaBookOpen,
|
|
||||||
description: 'Sınav düzeni',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'Circle',
|
|
||||||
label: 'Circle',
|
|
||||||
icon: FaCircle,
|
|
||||||
description: 'Yuvarlak düzen',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
@ -1,454 +0,0 @@
|
||||||
import { Student } from '@/proxy/classroom/planning'
|
|
||||||
|
|
||||||
export const mockStudents: Student[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
fullName: 'Ahmet Yılmaz',
|
|
||||||
photoUrl: '/img/planning/1.jpg',
|
|
||||||
tags: ['Matematik', 'Fizik'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
fullName: 'Ayşe Demir',
|
|
||||||
photoUrl: '/img/planning/2.jpg',
|
|
||||||
tags: ['Edebiyat', 'Tarih'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
fullName: 'Mehmet Kaya',
|
|
||||||
photoUrl: '/img/planning/3.jpg',
|
|
||||||
tags: ['Kimya', 'Biyoloji'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
fullName: 'Fatma Özkan',
|
|
||||||
photoUrl: '/img/planning/4.jpg',
|
|
||||||
tags: ['Geometri', 'Sanat'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
fullName: 'Ali Çelik',
|
|
||||||
photoUrl: '/img/planning/5.jpg',
|
|
||||||
tags: ['İngilizce', 'Müzik'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
fullName: 'Zeynep Arslan',
|
|
||||||
photoUrl: '/img/planning/6.jpg',
|
|
||||||
tags: ['Matematik', 'İngilizce'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '7',
|
|
||||||
fullName: 'Murat Doğan',
|
|
||||||
photoUrl: '/img/planning/7.jpg',
|
|
||||||
tags: ['Tarih', 'Coğrafya'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '8',
|
|
||||||
fullName: 'Elif Yıldız',
|
|
||||||
photoUrl: '/img/planning/8.jpg',
|
|
||||||
tags: ['Biyoloji', 'Edebiyat'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '9',
|
|
||||||
fullName: 'Osman Güler',
|
|
||||||
photoUrl: '/img/planning/9.jpg',
|
|
||||||
tags: ['Fizik', 'Kimya'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '10',
|
|
||||||
fullName: 'Hatice Aydın',
|
|
||||||
photoUrl: '/img/planning/10.jpg',
|
|
||||||
tags: ['Sanat', 'Müzik'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '11',
|
|
||||||
fullName: 'Emre Şahin',
|
|
||||||
photoUrl: '/img/planning/11.jpg',
|
|
||||||
tags: ['Matematik', 'Geometri'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '12',
|
|
||||||
fullName: 'Büşra Öztürk',
|
|
||||||
photoUrl: '/img/planning/12.jpg',
|
|
||||||
tags: ['İngilizce', 'Edebiyat'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '13',
|
|
||||||
fullName: 'Kemal Polat',
|
|
||||||
photoUrl: '/img/planning/13.jpg',
|
|
||||||
tags: ['Coğrafya', 'Tarih'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '14',
|
|
||||||
fullName: 'Selin Karaca',
|
|
||||||
photoUrl: '/img/planning/14.jpg',
|
|
||||||
tags: ['Biyoloji', 'Kimya'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '15',
|
|
||||||
fullName: 'Can Yavaş',
|
|
||||||
photoUrl: '/img/planning/15.jpg',
|
|
||||||
tags: ['Fizik', 'Matematik'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '16',
|
|
||||||
fullName: 'Deniz Mutlu',
|
|
||||||
photoUrl: '/img/planning/16.jpg',
|
|
||||||
tags: ['Müzik', 'Sanat'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '17',
|
|
||||||
fullName: 'Berk Koç',
|
|
||||||
photoUrl: '/img/planning/17.jpg',
|
|
||||||
tags: ['Geometri', 'Fizik'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '18',
|
|
||||||
fullName: 'Naz Aktaş',
|
|
||||||
photoUrl: '/img/planning/18.jpg',
|
|
||||||
tags: ['Edebiyat', 'İngilizce'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '19',
|
|
||||||
fullName: 'Arda Bulut',
|
|
||||||
photoUrl: '/img/planning/19.jpg',
|
|
||||||
tags: ['Tarih', 'Matematik'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '20',
|
|
||||||
fullName: 'İrem Tosun',
|
|
||||||
photoUrl: '/img/planning/20.jpg',
|
|
||||||
tags: ['Biyoloji', 'Sanat'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '21',
|
|
||||||
fullName: 'Kaan Erdoğan',
|
|
||||||
photoUrl: '/img/planning/21.jpg',
|
|
||||||
tags: ['Kimya', 'Coğrafya'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '22',
|
|
||||||
fullName: 'Lale Gündüz',
|
|
||||||
photoUrl: '/img/planning/22.jpg',
|
|
||||||
tags: ['Müzik', 'Edebiyat'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '23',
|
|
||||||
fullName: 'Rıza Özer',
|
|
||||||
photoUrl: '/img/planning/23.jpg',
|
|
||||||
tags: ['Fizik', 'Geometri'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '24',
|
|
||||||
fullName: 'Mine Akın',
|
|
||||||
photoUrl: '/img/planning/24.jpg',
|
|
||||||
tags: ['İngilizce', 'Tarih'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '25',
|
|
||||||
fullName: 'Tolga Şen',
|
|
||||||
photoUrl: '/img/planning/25.jpg',
|
|
||||||
tags: ['Matematik', 'Biyoloji'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '26',
|
|
||||||
fullName: 'Pınar Yıldırım',
|
|
||||||
photoUrl: '/img/planning/26.jpg',
|
|
||||||
tags: ['Sanat', 'Kimya'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '27',
|
|
||||||
fullName: 'Serkan Bozkurt',
|
|
||||||
photoUrl: '/img/planning/27.jpg',
|
|
||||||
tags: ['Coğrafya', 'Müzik'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '28',
|
|
||||||
fullName: 'Cansu Güven',
|
|
||||||
photoUrl: '/img/planning/28.jpg',
|
|
||||||
tags: ['Edebiyat', 'Fizik'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '29',
|
|
||||||
fullName: 'Barış Tekin',
|
|
||||||
photoUrl: '/img/planning/29.jpg',
|
|
||||||
tags: ['Matematik', 'Tarih'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '30',
|
|
||||||
fullName: 'Gizem Aslan',
|
|
||||||
photoUrl: '/img/planning/30.jpg',
|
|
||||||
tags: ['Biyoloji', 'İngilizce'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '31',
|
|
||||||
fullName: 'Cem Yılmaz',
|
|
||||||
photoUrl: '/img/planning/31.jpg',
|
|
||||||
tags: ['Fizik', 'Matematik'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '32',
|
|
||||||
fullName: 'Seda Kaya',
|
|
||||||
photoUrl: '/img/planning/32.jpg',
|
|
||||||
tags: ['Kimya', 'Biyoloji'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '33',
|
|
||||||
fullName: 'Burak Özkan',
|
|
||||||
photoUrl: '/img/planning/33.jpg',
|
|
||||||
tags: ['Tarih', 'Coğrafya'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '34',
|
|
||||||
fullName: 'Esra Demir',
|
|
||||||
photoUrl: '/img/planning/34.jpg',
|
|
||||||
tags: ['Edebiyat', 'Sanat'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '35',
|
|
||||||
fullName: 'Onur Çelik',
|
|
||||||
photoUrl: '/img/planning/35.jpg',
|
|
||||||
tags: ['İngilizce', 'Müzik'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '36',
|
|
||||||
fullName: 'Merve Arslan',
|
|
||||||
photoUrl: '/img/planning/36.jpg',
|
|
||||||
tags: ['Matematik', 'Geometri'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '37',
|
|
||||||
fullName: 'Hakan Doğan',
|
|
||||||
photoUrl: '/img/planning/37.jpg',
|
|
||||||
tags: ['Fizik', 'Kimya'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '38',
|
|
||||||
fullName: 'Aylin Yıldız',
|
|
||||||
photoUrl: '/img/planning/38.jpg',
|
|
||||||
tags: ['Biyoloji', 'Tarih'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '39',
|
|
||||||
fullName: 'Volkan Güler',
|
|
||||||
photoUrl: '/img/planning/39.jpg',
|
|
||||||
tags: ['Coğrafya', 'Edebiyat'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '40',
|
|
||||||
fullName: 'Sibel Aydın',
|
|
||||||
photoUrl: '/img/planning/40.jpg',
|
|
||||||
tags: ['Sanat', 'İngilizce'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '41',
|
|
||||||
fullName: 'Taner Şahin',
|
|
||||||
photoUrl: '/img/planning/41.jpg',
|
|
||||||
tags: ['Matematik', 'Müzik'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '42',
|
|
||||||
fullName: 'Gamze Öztürk',
|
|
||||||
photoUrl: '/img/planning/42.jpg',
|
|
||||||
tags: ['Fizik', 'Sanat'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '43',
|
|
||||||
fullName: 'Erhan Polat',
|
|
||||||
photoUrl: '/img/planning/43.jpg',
|
|
||||||
tags: ['Kimya', 'Geometri'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '44',
|
|
||||||
fullName: 'Dilek Karaca',
|
|
||||||
photoUrl: '/img/planning/44.jpg',
|
|
||||||
tags: ['Biyoloji', 'Edebiyat'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '45',
|
|
||||||
fullName: 'Mert Yavaş',
|
|
||||||
photoUrl: '/img/planning/45.jpg',
|
|
||||||
tags: ['Tarih', 'İngilizce'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '46',
|
|
||||||
fullName: 'Özge Mutlu',
|
|
||||||
photoUrl: '/img/planning/46.jpg',
|
|
||||||
tags: ['Coğrafya', 'Müzik'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '47',
|
|
||||||
fullName: 'Koray Koç',
|
|
||||||
photoUrl: '/img/planning/47.jpg',
|
|
||||||
tags: ['Matematik', 'Fizik'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '48',
|
|
||||||
fullName: 'Yeliz Aktaş',
|
|
||||||
photoUrl: '/img/planning/48.jpg',
|
|
||||||
tags: ['Sanat', 'Biyoloji'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '49',
|
|
||||||
fullName: 'Serdar Bulut',
|
|
||||||
photoUrl: '/img/planning/49.jpg',
|
|
||||||
tags: ['Kimya', 'Tarih'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '50',
|
|
||||||
fullName: 'Nilüfer Tosun',
|
|
||||||
photoUrl: '/img/planning/50.jpg',
|
|
||||||
tags: ['Edebiyat', 'Geometri'],
|
|
||||||
isActive: true,
|
|
||||||
creationTime: '2023-01-01T00:00:00Z',
|
|
||||||
lastModificationTime: '2023-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
@ -1,196 +0,0 @@
|
||||||
import React, { useRef, useEffect } from 'react'
|
|
||||||
import { FaTimes, FaUsers, FaUser, FaBullhorn, FaPaperPlane } from 'react-icons/fa'
|
|
||||||
import {
|
|
||||||
ClassroomChatDto,
|
|
||||||
ClassroomParticipantDto,
|
|
||||||
ClassroomSettingsDto,
|
|
||||||
MessageType,
|
|
||||||
} from '@/proxy/classroom/models'
|
|
||||||
|
|
||||||
interface ChatPanelProps {
|
|
||||||
user: { id: string; name: string; role: string }
|
|
||||||
participants: ClassroomParticipantDto[]
|
|
||||||
chatMessages: ClassroomChatDto[]
|
|
||||||
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: ClassroomSettingsDto
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
import React, { useRef, useState } from 'react'
|
|
||||||
import { FaTimes, FaFile, FaEye, FaDownload, FaTrash } from 'react-icons/fa'
|
|
||||||
import { ClassDocumentDto } from '@/proxy/classroom/models'
|
|
||||||
|
|
||||||
interface DocumentsPanelProps {
|
|
||||||
user: { role: string; name: string }
|
|
||||||
documents: ClassDocumentDto[]
|
|
||||||
onUpload: (file: File) => void
|
|
||||||
onDelete: (id: string) => void
|
|
||||||
onView: (doc: ClassDocumentDto) => 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
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { FaTimes, FaTh, FaExpand, FaDesktop, FaUsers } from 'react-icons/fa'
|
|
||||||
import { VideoLayoutDto } from '@/proxy/classroom/models'
|
|
||||||
|
|
||||||
interface LayoutPanelProps {
|
|
||||||
layouts: VideoLayoutDto[]
|
|
||||||
currentLayout: VideoLayoutDto
|
|
||||||
onChangeLayout: (layout: VideoLayoutDto) => 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
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
import React, { useState } from 'react'
|
|
||||||
import {
|
|
||||||
FaTimes,
|
|
||||||
FaUsers,
|
|
||||||
FaClipboardList,
|
|
||||||
FaHandPaper,
|
|
||||||
FaCheck,
|
|
||||||
FaMicrophone,
|
|
||||||
FaMicrophoneSlash,
|
|
||||||
FaVideoSlash,
|
|
||||||
FaUserTimes,
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
import { ClassroomParticipantDto, ClassroomAttendanceDto } from '@/proxy/classroom/models'
|
|
||||||
|
|
||||||
interface ParticipantsPanelProps {
|
|
||||||
user: { id: string; name: string; role: string }
|
|
||||||
participants: ClassroomParticipantDto[]
|
|
||||||
attendanceRecords: ClassroomAttendanceDto[]
|
|
||||||
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
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { Classroom } from '@/proxy/classroom/planning'
|
|
||||||
import React from 'react'
|
|
||||||
import { classrooms } from '../data/classroom'
|
|
||||||
|
|
||||||
interface ClassroomSelectorProps {
|
|
||||||
selectedClassroom: Classroom | null
|
|
||||||
onClassroomChange: (classroom: Classroom | null) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ClassroomSelector: React.FC<ClassroomSelectorProps> = ({
|
|
||||||
selectedClassroom,
|
|
||||||
onClassroomChange,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<select
|
|
||||||
id="classroom-selector"
|
|
||||||
value={selectedClassroom?.id || ''}
|
|
||||||
onChange={(e) => onClassroomChange(classrooms.find((c) => c.id === e.target.value) || null)}
|
|
||||||
className="w-full px-2 py-1 border border-gray-300 rounded"
|
|
||||||
>
|
|
||||||
<option value="">Sınıf seçin...</option>
|
|
||||||
{classrooms.map((classroom) => (
|
|
||||||
<option key={classroom.id} value={classroom.id}>
|
|
||||||
{classroom.name} • {classroom.capacity} koltuk
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { Card } from '@/components/ui/Card'
|
|
||||||
import { Button } from '@/components/ui/Button'
|
|
||||||
import { Avatar } from '@/components/ui/Avatar'
|
|
||||||
import { FaPhone, FaEnvelope, FaRegCommentDots, FaUserTimes } from 'react-icons/fa'
|
|
||||||
import { Seat, Student } from '@/proxy/classroom/planning'
|
|
||||||
|
|
||||||
interface QuickActionsProps {
|
|
||||||
selectedSeats: string[]
|
|
||||||
seats: Seat[]
|
|
||||||
students: Student[]
|
|
||||||
onRemoveSelectedStudents: () => void
|
|
||||||
onToggleSeatBlock: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const QuickActions: React.FC<QuickActionsProps> = ({
|
|
||||||
selectedSeats,
|
|
||||||
seats,
|
|
||||||
students,
|
|
||||||
onRemoveSelectedStudents,
|
|
||||||
}) => {
|
|
||||||
const selectedStudents = selectedSeats
|
|
||||||
.map((seatId) => {
|
|
||||||
const seat = seats.find((s) => s.id === seatId)
|
|
||||||
return seat?.studentId ? students.find((s) => s.id === seat.studentId) : null
|
|
||||||
})
|
|
||||||
.filter(Boolean) as Student[]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4 space-y-4">
|
|
||||||
{/* Statistics */}
|
|
||||||
<Card
|
|
||||||
bodyClass="md:p-3"
|
|
||||||
header={<h3 className="text-sm">İstatistikler</h3>}
|
|
||||||
headerClass="p-2"
|
|
||||||
>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-600">Toplam koltuk:</span>
|
|
||||||
<span className="font-medium">{seats.length}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-600">Dolu koltuk:</span>
|
|
||||||
<span className="font-medium text-green-600">
|
|
||||||
{seats.filter((s) => s.studentId).length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-600">Boş koltuk:</span>
|
|
||||||
<span className="font-medium text-blue-600">
|
|
||||||
{seats.filter((s) => !s.studentId && !s.isBlocked).length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="pt-2 border-t">
|
|
||||||
<div className="flex justify-between text-sm font-medium">
|
|
||||||
<span>Doluluk oranı:</span>
|
|
||||||
<span className="text-primary">
|
|
||||||
{Math.round((seats.filter((s) => s.studentId).length / seats.length) * 100)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Selection Info */}
|
|
||||||
<Card
|
|
||||||
bodyClass="md:p-3"
|
|
||||||
header={<h3 className="text-sm">Seçilen Koltuk ({selectedStudents.length})</h3>}
|
|
||||||
headerClass="p-2"
|
|
||||||
>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{/* Quick Actions */}
|
|
||||||
{selectedStudents.length > 0 && (
|
|
||||||
<Card bodyClass="md:p-3">
|
|
||||||
<div className="flex gap-2 justify-center">
|
|
||||||
{selectedStudents.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
title="Toplu Arama"
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="flex flex-row rounded-full justify-center items-center"
|
|
||||||
>
|
|
||||||
<FaPhone />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
title="E-posta Gönder"
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="flex flex-row rounded-full justify-center items-center"
|
|
||||||
>
|
|
||||||
<FaEnvelope />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
title="SMS Gönder"
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="flex flex-row rounded-full justify-center items-center"
|
|
||||||
>
|
|
||||||
<FaRegCommentDots />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedSeats.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
title="Atamaları Kaldır"
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="flex flex-row rounded-full justify-center items-center"
|
|
||||||
onClick={onRemoveSelectedStudents}
|
|
||||||
>
|
|
||||||
<FaUserTimes />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Selected Students */}
|
|
||||||
{selectedStudents.length > 0 && (
|
|
||||||
<Card bodyClass="md:p-3">
|
|
||||||
<div className="space-y-3">
|
|
||||||
{selectedStudents.map((student) => (
|
|
||||||
<div key={student.id} className="flex items-center space-x-3">
|
|
||||||
<Avatar
|
|
||||||
className="h-8 w-8"
|
|
||||||
shape="circle"
|
|
||||||
src={student.photoUrl || undefined}
|
|
||||||
/>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="text-sm font-medium truncate">{student.fullName}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { useDroppable } from '@dnd-kit/core'
|
|
||||||
import { Avatar } from '@/components/ui/Avatar'
|
|
||||||
import { FaTimes } from 'react-icons/fa'
|
|
||||||
import { Seat, SeatGridProps, Student } from '@/proxy/classroom/planning'
|
|
||||||
|
|
||||||
const DroppableSeat: React.FC<{
|
|
||||||
seat: Seat
|
|
||||||
student?: Student
|
|
||||||
isSelected: boolean
|
|
||||||
onSelect: () => void
|
|
||||||
onRemoveStudent: () => void
|
|
||||||
}> = ({ seat, student, isSelected, onSelect, onRemoveStudent }) => {
|
|
||||||
const { isOver, setNodeRef } = useDroppable({
|
|
||||||
id: seat.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
const isEmpty = !student
|
|
||||||
const isBlocked = seat.isBlocked
|
|
||||||
const canDrop = !isBlocked // Bloke olmayan tüm koltuklar drop edilebilir
|
|
||||||
const canSelect = !isEmpty // Sadece dolu koltuklar seçilebilir
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative group">
|
|
||||||
<div
|
|
||||||
ref={setNodeRef}
|
|
||||||
onClick={canSelect ? onSelect : undefined}
|
|
||||||
className={
|
|
||||||
'relative w-12 h-12 rounded-full border-2 transition-all duration-300 flex items-center justify-center transform ' +
|
|
||||||
(canSelect ? 'cursor-pointer ' : 'cursor-default ') +
|
|
||||||
(canDrop && !isOver
|
|
||||||
? 'bg-gray-100 border-gray-300 hover:border-primary hover:bg-gray-50 '
|
|
||||||
: '') +
|
|
||||||
(isBlocked
|
|
||||||
? 'bg-red-100 border-red-300 text-red-700 cursor-not-allowed opacity-75 '
|
|
||||||
: '') +
|
|
||||||
(isSelected && canSelect
|
|
||||||
? 'ring-2 ring-orange-400 ring-offset-2 bg-orange-50 border-orange-400 '
|
|
||||||
: '') +
|
|
||||||
(isOver && canDrop && isEmpty
|
|
||||||
? 'scale-125 ring-4 ring-green-400 ring-offset-4 bg-green-50 border-green-400 shadow-lg z-10 '
|
|
||||||
: '') +
|
|
||||||
(isOver && canDrop && !isEmpty
|
|
||||||
? 'scale-125 ring-4 ring-yellow-400 ring-offset-4 bg-yellow-50 border-yellow-400 shadow-lg z-10 '
|
|
||||||
: '') +
|
|
||||||
(isOver && !canDrop
|
|
||||||
? 'ring-4 ring-red-400 ring-offset-2 bg-red-50 border-red-400 shake '
|
|
||||||
: '')
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
zIndex: isOver ? 10 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{student ? (
|
|
||||||
<Avatar shape='circle'
|
|
||||||
className={
|
|
||||||
'w-full h-full rounded-full object-cover transition-all duration-200 ' +
|
|
||||||
'group-hover:ring-2 group-hover:ring-blue-500 group-hover:ring-offset-1'
|
|
||||||
}
|
|
||||||
src={student.photoUrl || undefined}
|
|
||||||
>
|
|
||||||
{student.fullName
|
|
||||||
.split(' ')
|
|
||||||
.map((n) => n[0])
|
|
||||||
.join('')}
|
|
||||||
</Avatar>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
'text-xs font-medium transition-all duration-300 ' +
|
|
||||||
(isOver && canDrop ? 'text-green-700 font-bold' : 'text-gray-600')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{seat.label}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Drop indicator */}
|
|
||||||
{isOver && canDrop && isEmpty && (
|
|
||||||
<div className="absolute inset-0 rounded-lg bg-green-400/20 border-2 border-green-400 border-dashed animate-pulse" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Drop indicator for occupied seats */}
|
|
||||||
{isOver && canDrop && !isEmpty && (
|
|
||||||
<div className="absolute inset-0 rounded-lg bg-yellow-400/20 border-2 border-yellow-400 border-dashed animate-pulse" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Invalid drop indicator */}
|
|
||||||
{isOver && !canDrop && (
|
|
||||||
<div className="absolute inset-0 rounded-lg bg-red-400/20 border-2 border-red-400 border-dashed" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Remove button - sadece dolu koltuklar için */}
|
|
||||||
{student && (
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
onRemoveStudent()
|
|
||||||
}}
|
|
||||||
className="absolute -top-2 -right-2 flex items-center justify-center
|
|
||||||
w-6 h-6 rounded-full bg-red-600 text-white
|
|
||||||
hover:bg-red-700 shadow-md
|
|
||||||
opacity-0 group-hover:opacity-100
|
|
||||||
transition-opacity duration-200 z-20"
|
|
||||||
>
|
|
||||||
<FaTimes className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tooltip on hover */}
|
|
||||||
{student && (
|
|
||||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-10">
|
|
||||||
{student.fullName}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SeatGrid: React.FC<SeatGridProps> = ({
|
|
||||||
classroom,
|
|
||||||
seats,
|
|
||||||
students,
|
|
||||||
selectedSeats,
|
|
||||||
onSeatSelect,
|
|
||||||
onRemoveStudent,
|
|
||||||
}) => {
|
|
||||||
const handleSeatSelect = (seatId: string) => {
|
|
||||||
if (selectedSeats.includes(seatId)) {
|
|
||||||
onSeatSelect(selectedSeats.filter((id) => id !== seatId))
|
|
||||||
} else {
|
|
||||||
onSeatSelect([...selectedSeats, seatId])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create grid layout
|
|
||||||
const grid = Array.from({ length: classroom.rows }, (_, row) =>
|
|
||||||
Array.from({ length: classroom.columns }, (_, col) => {
|
|
||||||
const seat = seats.find((s) => s.row === row && s.col === col)
|
|
||||||
const student = seat?.studentId ? students.find((s) => s.id === seat.studentId) : undefined
|
|
||||||
return { seat, student }
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center space-y-4">
|
|
||||||
<div className="w-64 mt-4 h-5 items-center bg-gray-800 rounded-sm">
|
|
||||||
<div className="text-xs text-white justify-center text-center">TAHTA</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Seat Grid */}
|
|
||||||
<div
|
|
||||||
className="grid gap-2"
|
|
||||||
style={{ gridTemplateColumns: `repeat(${classroom.columns}, 1fr)` }}
|
|
||||||
>
|
|
||||||
{grid.flat().map(({ seat, student }, index) => {
|
|
||||||
if (!seat) return <div key={index} className="w-12 h-12" />
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DroppableSeat
|
|
||||||
key={seat.id}
|
|
||||||
seat={seat}
|
|
||||||
student={student}
|
|
||||||
isSelected={selectedSeats.includes(seat.id)}
|
|
||||||
onSelect={() => handleSeatSelect(seat.id)}
|
|
||||||
onRemoveStudent={() => onRemoveStudent(seat.id)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Legend */}
|
|
||||||
<div className="flex items-center space-x-6 text-sm text-gray-600">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="w-4 h-4 bg-gray-100 border-2 border-gray-300 rounded"></div>
|
|
||||||
<span>Boş</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="w-4 h-4 bg-blue-500 border-2 border-blue-600 rounded"></div>
|
|
||||||
<span>Dolu</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { useDraggable } from '@dnd-kit/core'
|
|
||||||
import { Avatar } from '@/components/ui/Avatar'
|
|
||||||
import { Student, StudentListProps } from '@/proxy/classroom/planning'
|
|
||||||
|
|
||||||
const DraggableStudent: React.FC<{ student: Student }> = ({ student }) => {
|
|
||||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
|
|
||||||
id: student.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
const style = transform
|
|
||||||
? {
|
|
||||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
{...listeners}
|
|
||||||
{...attributes}
|
|
||||||
className={`transition-all duration-300 cursor-grab active:cursor-grabbing transform hover:scale-105 ${
|
|
||||||
isDragging ? 'opacity-30 scale-110 rotate-3 shadow-2xl z-50' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center space-y-2 group">
|
|
||||||
<Avatar shape='circle' className="h-10 w-10" src={student.photoUrl || undefined}>
|
|
||||||
{student.fullName
|
|
||||||
.split(' ')
|
|
||||||
.map((n) => n[0])
|
|
||||||
.join('')}
|
|
||||||
</Avatar>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="font-medium text-xs text-gray-900 truncate w-full leading-tight">
|
|
||||||
{student.fullName}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StudentList: React.FC<StudentListProps> = ({ students }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex-1 overflow-auto p-4">
|
|
||||||
<div className="grid grid-cols-3 gap-3">
|
|
||||||
{students.length === 0 ? (
|
|
||||||
<div className="col-span-3 text-center py-8 text-gray-500">
|
|
||||||
<div className="text-sm">Öğrenci bulunamadı</div>
|
|
||||||
<div className="text-xs mt-1">Arama kriterlerinizi değiştirin</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
students.map((student) => <DraggableStudent key={student.id} student={student} />)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
import { PagedAndSortedResultRequestDto } from '../abp'
|
|
||||||
|
|
||||||
export type RoleState = 'role-selection' | 'dashboard' | 'classroom'
|
|
||||||
|
|
||||||
export type Role = 'teacher' | 'student' | 'observer'
|
|
||||||
|
|
||||||
export type MessageType = 'public' | 'private' | 'announcement'
|
|
||||||
|
|
||||||
export type VideoLayoutType = 'grid' | 'sidebar' | 'teacher-focus'
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
email: string
|
|
||||||
role: Role
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClassroomDto {
|
|
||||||
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?: ClassroomSettingsDto
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClassroomSettingsDto {
|
|
||||||
allowHandRaise: boolean
|
|
||||||
allowStudentChat: boolean
|
|
||||||
allowPrivateMessages: boolean
|
|
||||||
allowStudentScreenShare: boolean
|
|
||||||
defaultMicrophoneState: 'muted' | 'unmuted'
|
|
||||||
defaultCameraState: 'on' | 'off'
|
|
||||||
defaultLayout: string
|
|
||||||
autoMuteNewParticipants: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClassroomAttendanceDto {
|
|
||||||
id: string
|
|
||||||
sessionId: string
|
|
||||||
studentId: string
|
|
||||||
studentName: string
|
|
||||||
joinTime: string
|
|
||||||
leaveTime?: string
|
|
||||||
totalDurationMinutes: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClassroomParticipantDto {
|
|
||||||
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 ClassroomChatDto {
|
|
||||||
id: string
|
|
||||||
sessionId: string
|
|
||||||
senderId: string
|
|
||||||
senderName: string
|
|
||||||
message: string
|
|
||||||
timestamp: string
|
|
||||||
isTeacher: boolean
|
|
||||||
recipientId?: string
|
|
||||||
recipientName?: string
|
|
||||||
messageType: MessageType
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VideoLayoutDto {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
type: VideoLayoutType
|
|
||||||
description: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TeacherLayoutDto extends VideoLayoutDto {
|
|
||||||
id: 'teacher-focus'
|
|
||||||
name: 'Öğretmen Odaklı'
|
|
||||||
type: 'teacher-focus'
|
|
||||||
description: 'Öğretmen tam ekranda, öğrenciler küçük panelde'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScheduledClassDto {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
scheduledTime: string
|
|
||||||
duration: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HandRaiseDto {
|
|
||||||
id: string
|
|
||||||
studentId: string
|
|
||||||
studentName: string
|
|
||||||
timestamp: string
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClassDocumentDto {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
url: string
|
|
||||||
type: string
|
|
||||||
size: number
|
|
||||||
uploadedAt: string
|
|
||||||
uploadedBy: string
|
|
||||||
isPresentation?: boolean
|
|
||||||
totalPages?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScreenShareRequestDto {
|
|
||||||
userId: string
|
|
||||||
userName: string
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClassroomFilterInputDto extends PagedAndSortedResultRequestDto {
|
|
||||||
search: string
|
|
||||||
status: string
|
|
||||||
}
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
export interface Student {
|
|
||||||
id: string
|
|
||||||
fullName: string
|
|
||||||
photoUrl: string | null
|
|
||||||
tags: string[]
|
|
||||||
isActive: boolean
|
|
||||||
creationTime: string
|
|
||||||
lastModificationTime: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SeatGridProps {
|
|
||||||
classroom: Classroom
|
|
||||||
seats: Seat[]
|
|
||||||
students: Student[]
|
|
||||||
selectedSeats: string[]
|
|
||||||
onSeatSelect: (seatIds: string[]) => void
|
|
||||||
onRemoveStudent: (seatId: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StudentListProps {
|
|
||||||
students: Student[]
|
|
||||||
searchQuery: string
|
|
||||||
selectedTags: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Seat {
|
|
||||||
id: string
|
|
||||||
row: number
|
|
||||||
col: number
|
|
||||||
label: string
|
|
||||||
isBlocked: boolean
|
|
||||||
studentId?: string
|
|
||||||
seatType: SeatType
|
|
||||||
concurrencyStamp: string
|
|
||||||
creationTime: string
|
|
||||||
lastModificationTime: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Classroom {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
layoutType: string
|
|
||||||
rows: number
|
|
||||||
columns: number
|
|
||||||
capacity: number
|
|
||||||
creationTime: string
|
|
||||||
lastModificationTime: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StudentQuery {
|
|
||||||
q?: string
|
|
||||||
tags?: string[]
|
|
||||||
page?: number
|
|
||||||
size?: number
|
|
||||||
isActive?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// Classroom types
|
|
||||||
export type LayoutType = 'Theater' | 'Bus' | 'UShape' | 'Grid' | 'Lab' | 'Exam' | 'Circle'
|
|
||||||
export type SeatType = 'Standard' | 'Table' | 'Wheelchair'
|
|
||||||
export type AssignmentStrategy = 'FillByOrder' | 'MatchByIndex'
|
|
||||||
|
|
||||||
export interface SeatAssignment {
|
|
||||||
id: string
|
|
||||||
classroomId: string
|
|
||||||
seatId: string
|
|
||||||
studentId: string
|
|
||||||
assignedAt: string
|
|
||||||
assignedBy: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SeatMap {
|
|
||||||
classroom: Classroom
|
|
||||||
seats: Seat[]
|
|
||||||
assignments: SeatAssignment[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assignment operations
|
|
||||||
export interface AssignSingleDto {
|
|
||||||
seatId: string
|
|
||||||
studentId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AssignBulkDto {
|
|
||||||
seatIds: string[]
|
|
||||||
studentIds: string[]
|
|
||||||
strategy: AssignmentStrategy
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AssignmentResult {
|
|
||||||
success: boolean
|
|
||||||
assignedCount: number
|
|
||||||
errors?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Real-time events
|
|
||||||
export interface SseEvent {
|
|
||||||
id: string
|
|
||||||
event: string
|
|
||||||
data: any
|
|
||||||
timestamp: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SeatAssignedEvent {
|
|
||||||
classroomId: string
|
|
||||||
seatId: string
|
|
||||||
studentId: string
|
|
||||||
ts: string
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SeatUnassignedEvent {
|
|
||||||
classroomId: string
|
|
||||||
seatId: string
|
|
||||||
ts: string
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LayoutChangedEvent {
|
|
||||||
classroomId: string
|
|
||||||
ts: string
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
@ -1,469 +0,0 @@
|
||||||
import {
|
|
||||||
Address,
|
|
||||||
EmployeeStatusEnum,
|
|
||||||
EmploymentTypeEnum,
|
|
||||||
GenderEnum,
|
|
||||||
HrCostCenter,
|
|
||||||
HrDisciplinaryAction,
|
|
||||||
HrEmergencyContact,
|
|
||||||
HrPerformanceEvaluation,
|
|
||||||
HrTraining,
|
|
||||||
HrWorkSchedule,
|
|
||||||
JobLevelEnum,
|
|
||||||
LeaveStatusEnum,
|
|
||||||
LeaveTypeEnum,
|
|
||||||
MaritalStatusEnum,
|
|
||||||
} from '@/types/hr'
|
|
||||||
import { PriorityEnum } from '@/types/intranet'
|
|
||||||
|
|
||||||
export interface IntranetDashboardDto {
|
|
||||||
events: EventDto[]
|
|
||||||
birthdays: EmployeeDto[]
|
|
||||||
visitors: VisitorDto[]
|
|
||||||
reservations: ReservationDto[]
|
|
||||||
trainings: TrainingDto[]
|
|
||||||
expenses: ExpensesDto
|
|
||||||
documents: DocumentDto[]
|
|
||||||
announcements: AnnouncementDto[]
|
|
||||||
shuttleRoutes: ShuttleRouteDto[]
|
|
||||||
meals: MealDto[]
|
|
||||||
leaves: LeaveDto[]
|
|
||||||
overtimes: OvertimeDto[]
|
|
||||||
surveys: SurveyDto[]
|
|
||||||
socialPosts: SocialPostDto[]
|
|
||||||
tasks: ProjectTaskDto[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Etkinlik
|
|
||||||
export interface EventDto {
|
|
||||||
id: string
|
|
||||||
categoryName: string
|
|
||||||
typeName: string
|
|
||||||
date: Date
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
place: string
|
|
||||||
organizer: EmployeeDto
|
|
||||||
participants: number
|
|
||||||
photos: string[]
|
|
||||||
comments: EventCommentDto[]
|
|
||||||
likes: number
|
|
||||||
isPublished: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// Etkinlik Yorumu
|
|
||||||
export interface EventCommentDto {
|
|
||||||
id: string
|
|
||||||
employee: EmployeeDto
|
|
||||||
content: string
|
|
||||||
creationTime: Date
|
|
||||||
likes: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum BankAccountTypeEnum {
|
|
||||||
Current = 'CURRENT', // Vadesiz
|
|
||||||
Deposit = 'DEPOSIT', // Vadeli
|
|
||||||
Credit = 'CREDIT', // Kredi
|
|
||||||
Foreign = 'FOREIGN', // Yabancı Para
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bank Management Types
|
|
||||||
export interface BankAccount {
|
|
||||||
// Banka Hesabı
|
|
||||||
id: string
|
|
||||||
accountCode: string
|
|
||||||
bankName: string
|
|
||||||
branchName: string
|
|
||||||
accountNumber: string
|
|
||||||
iban: string
|
|
||||||
accountType: BankAccountTypeEnum
|
|
||||||
currency: string
|
|
||||||
balance: number
|
|
||||||
overdraftLimit: number
|
|
||||||
dailyTransferLimit: number
|
|
||||||
isActive: boolean
|
|
||||||
contactPerson?: string
|
|
||||||
phoneNumber?: string
|
|
||||||
swiftCode?: string
|
|
||||||
isDefault: boolean
|
|
||||||
creationTime: Date
|
|
||||||
lastModificationTime: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EmployeeDto {
|
|
||||||
// İnsan Kaynakları Çalışanı
|
|
||||||
id: string
|
|
||||||
code: string
|
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
name: string
|
|
||||||
email: string
|
|
||||||
phoneNumber?: string
|
|
||||||
mobileNumber?: string
|
|
||||||
avatar?: string
|
|
||||||
nationalId: string
|
|
||||||
birthDate: Date
|
|
||||||
gender: GenderEnum
|
|
||||||
maritalStatus: MaritalStatusEnum
|
|
||||||
address: Address
|
|
||||||
emergencyContact: HrEmergencyContact
|
|
||||||
hireDate: Date
|
|
||||||
terminationDate?: Date
|
|
||||||
employmentType: EmploymentTypeEnum
|
|
||||||
jobPositionId: string
|
|
||||||
jobPosition?: JobPositionDto
|
|
||||||
departmentId: string
|
|
||||||
department?: DepartmentDto
|
|
||||||
managerId?: string
|
|
||||||
manager?: EmployeeDto
|
|
||||||
baseSalary: number
|
|
||||||
currency: string
|
|
||||||
payrollGroup: string
|
|
||||||
bankAccountId: string
|
|
||||||
bankAccount?: BankAccount
|
|
||||||
workLocation: string
|
|
||||||
workSchedule?: HrWorkSchedule
|
|
||||||
badgeNumber?: string
|
|
||||||
employeeStatus: EmployeeStatusEnum
|
|
||||||
isActive: boolean
|
|
||||||
leaves: LeaveDto[]
|
|
||||||
evaluations: HrPerformanceEvaluation[]
|
|
||||||
trainings: HrTraining[]
|
|
||||||
disciplinaryActions: HrDisciplinaryAction[]
|
|
||||||
creationTime: Date
|
|
||||||
lastModificationTime: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DepartmentDto {
|
|
||||||
// İnsan Kaynakları Departmanı
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
description?: string
|
|
||||||
parentDepartmentId?: string
|
|
||||||
parentDepartment?: DepartmentDto
|
|
||||||
subDepartments: DepartmentDto[]
|
|
||||||
managerId?: string
|
|
||||||
manager?: EmployeeDto
|
|
||||||
costCenterId?: string
|
|
||||||
costCenter?: HrCostCenter
|
|
||||||
budget: number
|
|
||||||
isActive: boolean
|
|
||||||
creationTime: Date
|
|
||||||
lastModificationTime: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface JobPositionDto {
|
|
||||||
// İnsan Kaynakları İş Pozisyonu
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
description?: string
|
|
||||||
departmentId: string
|
|
||||||
department?: DepartmentDto
|
|
||||||
level: JobLevelEnum
|
|
||||||
minSalary: number
|
|
||||||
maxSalary: number
|
|
||||||
currency: string
|
|
||||||
requiredSkills: string[]
|
|
||||||
responsibilities: string[]
|
|
||||||
qualifications: string[]
|
|
||||||
isActive: boolean
|
|
||||||
employees: EmployeeDto[]
|
|
||||||
creationTime: Date
|
|
||||||
lastModificationTime: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ziyaretçi
|
|
||||||
export interface VisitorDto {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
companyName: string
|
|
||||||
email: string
|
|
||||||
phoneNumber: string
|
|
||||||
purpose: string
|
|
||||||
visitDate: Date
|
|
||||||
checkIn?: Date
|
|
||||||
checkOut?: Date
|
|
||||||
employeeId: string
|
|
||||||
employee: EmployeeDto
|
|
||||||
status: 'Planlandı' | 'Giriş' | 'Çıkış' | 'İptal Edildi'
|
|
||||||
badgeNumber?: string
|
|
||||||
photo?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rezervasyon
|
|
||||||
export interface ReservationDto {
|
|
||||||
id: string
|
|
||||||
type: 'room' | 'vehicle' | 'equipment'
|
|
||||||
resourceName: string
|
|
||||||
bookedBy: EmployeeDto
|
|
||||||
startDate: Date
|
|
||||||
endDate: Date
|
|
||||||
purpose: string
|
|
||||||
status: 'pending' | 'approved' | 'rejected' | 'completed'
|
|
||||||
participants?: number
|
|
||||||
notes?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eğitim
|
|
||||||
export interface TrainingDto {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
instructor: string
|
|
||||||
category: 'technical' | 'soft-skills' | 'management' | 'compliance' | 'other'
|
|
||||||
type: 'online' | 'classroom' | 'hybrid'
|
|
||||||
duration: number // saat
|
|
||||||
startDate: Date
|
|
||||||
endDate: Date
|
|
||||||
maxParticipants: number
|
|
||||||
enrolled: number
|
|
||||||
status: 'upcoming' | 'ongoing' | 'completed'
|
|
||||||
location?: string
|
|
||||||
thumbnail?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Harcama
|
|
||||||
export interface ExpensesDto {
|
|
||||||
totalRequested: number
|
|
||||||
totalApproved: number
|
|
||||||
last5Expenses: ExpenseDto[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Harcama
|
|
||||||
export interface ExpenseDto {
|
|
||||||
id: string
|
|
||||||
employee: EmployeeDto
|
|
||||||
category: 'travel' | 'meal' | 'accommodation' | 'transport' | 'other'
|
|
||||||
amount: number
|
|
||||||
currency: string
|
|
||||||
date: Date
|
|
||||||
description: string
|
|
||||||
project?: string
|
|
||||||
receipts: { name: string; url: string; size: string }[]
|
|
||||||
status: 'pending' | 'approved' | 'rejected'
|
|
||||||
approver?: EmployeeDto
|
|
||||||
approvalDate?: Date
|
|
||||||
notes?: string
|
|
||||||
creationTime: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
// Doküman (FileItemDto ile uyumlu)
|
|
||||||
export interface DocumentDto {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
type: string // "file" or "folder"
|
|
||||||
size: number
|
|
||||||
extension: string
|
|
||||||
mimeType: string
|
|
||||||
createdAt: Date
|
|
||||||
modifiedAt: Date
|
|
||||||
path: string
|
|
||||||
parentId: string
|
|
||||||
isReadOnly: boolean
|
|
||||||
childCount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sertifika
|
|
||||||
export interface CertificateDto {
|
|
||||||
id: string
|
|
||||||
employee: EmployeeDto
|
|
||||||
trainingTitle: string
|
|
||||||
issueDate: Date
|
|
||||||
expiryDate?: Date
|
|
||||||
certificateUrl: string
|
|
||||||
score?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duyuru
|
|
||||||
export interface AnnouncementDto {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
excerpt: string
|
|
||||||
content: string
|
|
||||||
imageUrl?: string
|
|
||||||
category: string
|
|
||||||
employeeId: string
|
|
||||||
employee: EmployeeDto
|
|
||||||
publishDate: Date
|
|
||||||
expiryDate?: Date
|
|
||||||
isPinned: boolean
|
|
||||||
viewCount: number
|
|
||||||
departments?: string[]
|
|
||||||
attachments?: { name: string; url: string; size: string }[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Servis
|
|
||||||
export interface ShuttleRouteDto {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
route: string[]
|
|
||||||
departureTime: string
|
|
||||||
arrivalTime: string
|
|
||||||
capacity: number
|
|
||||||
available: number
|
|
||||||
type: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Yemek Menüsü
|
|
||||||
export interface MealDto {
|
|
||||||
id: string
|
|
||||||
branchId: string
|
|
||||||
date: Date
|
|
||||||
type: 'breakfast' | 'lunch' | 'dinner'
|
|
||||||
totalCalorie?: number
|
|
||||||
materials: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// İnsan Kaynakları Fazla Mesai
|
|
||||||
export interface ProjectTaskDto {
|
|
||||||
id: string
|
|
||||||
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
|
|
||||||
priority: PriorityEnum
|
|
||||||
status?: string
|
|
||||||
|
|
||||||
employeeId?: string
|
|
||||||
employee: EmployeeDto
|
|
||||||
|
|
||||||
startDate: Date
|
|
||||||
endDate: Date
|
|
||||||
|
|
||||||
progress: number
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// İnsan Kaynakları Fazla Mesai
|
|
||||||
export interface OvertimeDto {
|
|
||||||
id: string
|
|
||||||
employeeId: string
|
|
||||||
employee?: EmployeeDto
|
|
||||||
date: Date
|
|
||||||
startTime: string
|
|
||||||
endTime: string
|
|
||||||
totalHours: number
|
|
||||||
reason: string
|
|
||||||
status: LeaveStatusEnum
|
|
||||||
approvedBy?: string
|
|
||||||
approver?: EmployeeDto
|
|
||||||
rate?: number
|
|
||||||
amount?: number
|
|
||||||
creationTime: Date
|
|
||||||
lastModificationTime: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
// İnsan Kaynakları İzni
|
|
||||||
export interface LeaveDto {
|
|
||||||
id: string
|
|
||||||
employeeId: string
|
|
||||||
employee?: EmployeeDto
|
|
||||||
leaveType: LeaveTypeEnum
|
|
||||||
startDate: Date
|
|
||||||
endDate: Date
|
|
||||||
totalDays: number
|
|
||||||
reason?: string
|
|
||||||
status: LeaveStatusEnum
|
|
||||||
appliedDate: Date
|
|
||||||
approvedBy?: string
|
|
||||||
approvedDate?: Date
|
|
||||||
rejectionReason?: string
|
|
||||||
isHalfDay: boolean
|
|
||||||
attachments: string[]
|
|
||||||
creationTime: Date
|
|
||||||
lastModificationTime: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anket Cevap
|
|
||||||
export interface SurveyAnswerDto {
|
|
||||||
questionId: string
|
|
||||||
questionType: 'rating' | 'multiple-choice' | 'text' | 'textarea' | 'yes-no'
|
|
||||||
value: string | number | string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sosyal Duvar - Comment Interface
|
|
||||||
export interface SocialCommentDto {
|
|
||||||
id: string
|
|
||||||
creator: EmployeeDto
|
|
||||||
content: string
|
|
||||||
creationTime: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SocialPollOptionDto {
|
|
||||||
id: string
|
|
||||||
text: string
|
|
||||||
votes: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sosyal Duvar - Social Media Interface
|
|
||||||
export interface SocialMediaDto {
|
|
||||||
id?: string
|
|
||||||
type: 'image' | 'video' | 'poll'
|
|
||||||
|
|
||||||
// Ortak alanlar
|
|
||||||
urls?: string[]
|
|
||||||
|
|
||||||
// Anket (poll) ile ilgili alanlar doğrudan burada
|
|
||||||
pollQuestion?: string
|
|
||||||
pollOptions?: SocialPollOptionDto[]
|
|
||||||
pollTotalVotes?: number
|
|
||||||
pollEndsAt?: Date
|
|
||||||
pollUserVoteId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sosyal Duvar - Ana Interface
|
|
||||||
export interface SocialPostDto {
|
|
||||||
id: string
|
|
||||||
employee: EmployeeDto
|
|
||||||
content: string
|
|
||||||
locationJson?: string
|
|
||||||
media?: SocialMediaDto
|
|
||||||
likeCount: number
|
|
||||||
isLiked: boolean
|
|
||||||
likeUsers: EmployeeDto[]
|
|
||||||
comments: SocialCommentDto[]
|
|
||||||
isOwnPost: boolean
|
|
||||||
creationTime: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anket Cevabı
|
|
||||||
export interface SurveyResponseDto {
|
|
||||||
id: string
|
|
||||||
surveyId: string
|
|
||||||
respondentId?: string // Anonymous ise null
|
|
||||||
submissionTime: Date
|
|
||||||
answers: SurveyAnswerDto[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anket Sorusu Seçeneği
|
|
||||||
export interface SurveyQuestionOptionDto {
|
|
||||||
id: string
|
|
||||||
text: string
|
|
||||||
order: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anket Sorusu
|
|
||||||
export interface SurveyQuestionDto {
|
|
||||||
id: string
|
|
||||||
surveyId: string
|
|
||||||
questionText: string
|
|
||||||
type: 'rating' | 'multiple-choice' | 'text' | 'textarea' | 'yes-no'
|
|
||||||
order: number
|
|
||||||
isRequired: boolean
|
|
||||||
options?: SurveyQuestionOptionDto[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anket
|
|
||||||
export interface SurveyDto {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
creatorId: EmployeeDto
|
|
||||||
creationTime: Date
|
|
||||||
deadline: Date
|
|
||||||
questions: SurveyQuestionDto[]
|
|
||||||
responses: number
|
|
||||||
targetAudience: string[]
|
|
||||||
status: 'draft' | 'active' | 'closed'
|
|
||||||
isAnonymous: boolean
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
import {
|
|
||||||
FaFileAlt,
|
|
||||||
FaFilePdf,
|
|
||||||
FaFileWord,
|
|
||||||
FaFileExcel,
|
|
||||||
FaFilePowerpoint,
|
|
||||||
FaFileImage,
|
|
||||||
FaFileArchive,
|
|
||||||
FaFileCode,
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
|
|
||||||
export const getFileIcon = (extension: string) => {
|
|
||||||
switch (extension.toLowerCase()) {
|
|
||||||
case '.pdf':
|
|
||||||
return <FaFilePdf className="w-4 h-4 text-red-600 dark:text-red-400" />
|
|
||||||
case '.doc':
|
|
||||||
case '.docx':
|
|
||||||
return <FaFileWord className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
|
||||||
case '.xls':
|
|
||||||
case '.xlsx':
|
|
||||||
return <FaFileExcel className="w-4 h-4 text-green-600 dark:text-green-400" />
|
|
||||||
case '.ppt':
|
|
||||||
case '.pptx':
|
|
||||||
return <FaFilePowerpoint className="w-4 h-4 text-orange-600 dark:text-orange-400" />
|
|
||||||
case '.jpg':
|
|
||||||
case '.jpeg':
|
|
||||||
case '.png':
|
|
||||||
case '.gif':
|
|
||||||
return <FaFileImage className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
|
||||||
case '.zip':
|
|
||||||
case '.rar':
|
|
||||||
return <FaFileArchive className="w-4 h-4 text-yellow-600 dark:text-yellow-400" />
|
|
||||||
case '.txt':
|
|
||||||
return <FaFileCode className="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
|
||||||
default:
|
|
||||||
return <FaFileAlt className="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getFileType = (extension: string) => {
|
|
||||||
switch (extension.toLowerCase()) {
|
|
||||||
case '.pdf':
|
|
||||||
return '📄 PDF'
|
|
||||||
case '.doc':
|
|
||||||
case '.docx':
|
|
||||||
return '📝 Word'
|
|
||||||
case '.xls':
|
|
||||||
case '.xlsx':
|
|
||||||
return '📊 Excel'
|
|
||||||
case '.ppt':
|
|
||||||
case '.pptx':
|
|
||||||
return '📽️ PowerPoint'
|
|
||||||
case '.jpg':
|
|
||||||
case '.jpeg':
|
|
||||||
return '🖼️ JPEG'
|
|
||||||
case '.png':
|
|
||||||
return '🖼️ PNG'
|
|
||||||
case '.gif':
|
|
||||||
return '🖼️ GIF'
|
|
||||||
case '.zip':
|
|
||||||
return '🗜️ ZIP'
|
|
||||||
case '.rar':
|
|
||||||
return '🗜️ RAR'
|
|
||||||
case '.txt':
|
|
||||||
return '📝 Text'
|
|
||||||
default:
|
|
||||||
return '📄 Dosya'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import apiService, { Config } from './api.service'
|
|
||||||
import { IntranetDashboardDto } from '@/proxy/intranet/models'
|
|
||||||
|
|
||||||
export class IntranetService {
|
|
||||||
apiName = 'Default'
|
|
||||||
|
|
||||||
getDashboard = (config?: Partial<Config>) =>
|
|
||||||
apiService.fetchData<IntranetDashboardDto>(
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
url: '/api/app/intranet/intranet-dashboard',
|
|
||||||
},
|
|
||||||
{ apiName: this.apiName, ...config },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const intranetService = new IntranetService()
|
|
||||||
|
|
@ -1,250 +0,0 @@
|
||||||
import React, { useState } from 'react'
|
|
||||||
import {
|
|
||||||
FaPlus,
|
|
||||||
FaSearch,
|
|
||||||
FaFilter,
|
|
||||||
FaEdit,
|
|
||||||
FaTrash,
|
|
||||||
FaClock,
|
|
||||||
FaUsers,
|
|
||||||
FaCalendar,
|
|
||||||
FaPlay,
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { generateMockExam } from '@/mocks/mockExams'
|
|
||||||
import { generateMockPools } from '@/mocks/mockPools'
|
|
||||||
import { Exam, QuestionPoolDto } from '@/types/coordinator'
|
|
||||||
import { ExamCreator } from './ExamInterface/ExamCreator'
|
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
||||||
|
|
||||||
const Assignments: React.FC = () => {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const [assignments, setAssignments] = useState<Exam[]>(generateMockExam())
|
|
||||||
const [pools] = useState<QuestionPoolDto[]>(generateMockPools())
|
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
|
||||||
const [statusFilter, setStatusFilter] = useState('')
|
|
||||||
const [isCreating, setIsCreating] = useState(false)
|
|
||||||
const [editingAssignment, setEditingAssignment] = useState<Exam | null>(null)
|
|
||||||
|
|
||||||
const assignmentList = assignments.filter((assignment) => assignment.type === 'assignment')
|
|
||||||
|
|
||||||
const filteredAssignments = assignmentList.filter((assignment) => {
|
|
||||||
const matchesSearch =
|
|
||||||
assignment.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
assignment.description.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
const matchesStatus =
|
|
||||||
!statusFilter ||
|
|
||||||
(statusFilter === 'active' && assignment.isActive) ||
|
|
||||||
(statusFilter === 'inactive' && !assignment.isActive)
|
|
||||||
return matchesSearch && matchesStatus
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleCreateAssignment = (
|
|
||||||
assignmentData: Omit<Exam, 'id' | 'creationTime' | 'lastModificationTime'>,
|
|
||||||
) => {
|
|
||||||
const newAssignment: Exam = {
|
|
||||||
...assignmentData,
|
|
||||||
type: 'assignment',
|
|
||||||
id: `assignment-${Date.now()}`,
|
|
||||||
creationTime: new Date(),
|
|
||||||
lastModificationTime: new Date(),
|
|
||||||
}
|
|
||||||
setAssignments((prev) => [...prev, newAssignment])
|
|
||||||
setIsCreating(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdateAssignment = (
|
|
||||||
assignmentData: Omit<Exam, 'id' | 'creationTime' | 'lastModificationTime'>,
|
|
||||||
) => {
|
|
||||||
if (editingAssignment) {
|
|
||||||
const updatedAssignment: Exam = {
|
|
||||||
...assignmentData,
|
|
||||||
id: editingAssignment.id,
|
|
||||||
creationTime: editingAssignment.creationTime,
|
|
||||||
lastModificationTime: new Date(),
|
|
||||||
}
|
|
||||||
setAssignments((prev) =>
|
|
||||||
prev.map((a) => (a.id === updatedAssignment.id ? updatedAssignment : a)),
|
|
||||||
)
|
|
||||||
setEditingAssignment(null)
|
|
||||||
setIsCreating(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEdit = (assignment: Exam) => {
|
|
||||||
setEditingAssignment(assignment)
|
|
||||||
setIsCreating(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setIsCreating(false)
|
|
||||||
setEditingAssignment(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCreating) {
|
|
||||||
return (
|
|
||||||
<ExamCreator
|
|
||||||
pools={pools}
|
|
||||||
onCreateExam={editingAssignment ? handleUpdateAssignment : handleCreateAssignment}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
editingExam={editingAssignment || undefined}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-bold text-gray-900">Ödev Yönetimi</h1>
|
|
||||||
<p className="text-sm text-gray-600 mt-0.5">Ödevleri oluşturun ve yönetin</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setIsCreating(true)}
|
|
||||||
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<FaPlus className="w-3.5 h-3.5" />
|
|
||||||
<span>Yeni Ödev</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters */}
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
||||||
<div className="relative">
|
|
||||||
<FaSearch className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Ödev ara..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="pl-8 text-sm w-full border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<FaFilter className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
|
|
||||||
<select
|
|
||||||
value={statusFilter}
|
|
||||||
onChange={(e) => setStatusFilter(e.target.value)}
|
|
||||||
className="pl-8 text-sm w-full border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 appearance-none"
|
|
||||||
>
|
|
||||||
<option value="">Tüm durumlar</option>
|
|
||||||
<option value="active">Aktif</option>
|
|
||||||
<option value="inactive">Pasif</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm text-gray-600 flex items-center">
|
|
||||||
Toplam: {filteredAssignments.length} ödev
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Assignments List */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
{filteredAssignments.length > 0 ? (
|
|
||||||
filteredAssignments.map((assignment) => (
|
|
||||||
<div
|
|
||||||
key={assignment.id}
|
|
||||||
className="bg-white border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center space-x-2 mb-1.5">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900">{assignment.title}</h3>
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
||||||
assignment.isActive
|
|
||||||
? 'bg-green-100 text-green-800'
|
|
||||||
: 'bg-gray-100 text-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{assignment.isActive ? 'Aktif' : 'Pasif'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-gray-600 mb-2">{assignment.description}</p>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<FaClock className="w-4 h-4 text-gray-400" />
|
|
||||||
<span className="text-gray-600">{assignment.timeLimit} dakika</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<FaUsers className="w-4 h-4 text-gray-400" />
|
|
||||||
<span className="text-gray-600">{assignment.questions.length} soru</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<FaCalendar className="w-4 h-4 text-gray-400" />
|
|
||||||
<span className="text-gray-600">Toplam: {assignment.totalPoints} puan</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-gray-600">Geçme: %{assignment.passingScore}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{assignment.startTime && assignment.endTime && (
|
|
||||||
<div className="mt-2 p-2 bg-orange-50 rounded">
|
|
||||||
<div className="text-xs text-orange-700">
|
|
||||||
<strong>Başlangıç:</strong> {assignment.startTime.toLocaleString('tr-TR')} -
|
|
||||||
<strong> Bitiş:</strong> {assignment.endTime.toLocaleString('tr-TR')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-1.5 ml-4">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate(ROUTES_ENUM.protected.coordinator.assignmentDetail.replace(':id', assignment.id))}
|
|
||||||
className="flex items-center space-x-1 px-2.5 py-1.5 bg-green-600 hover:bg-green-700 text-white text-xs rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<FaPlay className="w-3 h-3" />
|
|
||||||
<span>Başlat</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleEdit(assignment)}
|
|
||||||
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
title="Düzenle"
|
|
||||||
>
|
|
||||||
<FaEdit className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (window.confirm('Bu ödevi silmek istediğinizden emin misiniz?')) {
|
|
||||||
setAssignments((prev) => prev.filter((a) => a.id !== assignment.id))
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
||||||
title="Sil"
|
|
||||||
>
|
|
||||||
<FaTrash className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<FaSearch className="w-6 h-6 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-base font-medium text-gray-900 mb-1">Ödev bulunamadı</h3>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
{searchTerm || statusFilter
|
|
||||||
? 'Arama kriterlerinizi değiştirin veya yeni ödev oluşturun.'
|
|
||||||
: 'İlk ödevinizi oluşturun.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Assignments
|
|
||||||
|
|
@ -1,919 +0,0 @@
|
||||||
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 { ClassroomDto } from '@/proxy/classroom/models'
|
|
||||||
import { useStoreState } from '@/store/store'
|
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
||||||
import { Container } from '@/components/shared'
|
|
||||||
import {
|
|
||||||
createClassroom,
|
|
||||||
deleteClassroom,
|
|
||||||
getClassrooms,
|
|
||||||
startClassroom,
|
|
||||||
updateClassroom,
|
|
||||||
} from '@/services/classroom.service'
|
|
||||||
import { Helmet } from 'react-helmet'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
|
||||||
|
|
||||||
export interface ClassProps {
|
|
||||||
status: string
|
|
||||||
className: string
|
|
||||||
showButtons: boolean
|
|
||||||
title: string
|
|
||||||
classes: string
|
|
||||||
event?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const ClassList: React.FC = () => {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const { user } = useStoreState((state) => state.auth)
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
|
|
||||||
const newClassEntity: ClassroomDto = {
|
|
||||||
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 [classList, setClassList] = useState<ClassroomDto[]>([])
|
|
||||||
const [classroom, setClassroom] = useState<ClassroomDto>(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 getClassroomList = async (
|
|
||||||
skipCount = 0,
|
|
||||||
maxResultCount = 1000,
|
|
||||||
sorting = '',
|
|
||||||
search = '',
|
|
||||||
status = '',
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const result = await getClassrooms({
|
|
||||||
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 ClassroomDto[]
|
|
||||||
|
|
||||||
setClassList(items)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching classrooms:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getClassroomList(0, 1000, '', searchTerm, statusFilter)
|
|
||||||
}, [searchTerm, statusFilter])
|
|
||||||
|
|
||||||
const handleCreateClass = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createClassroom(classroom)
|
|
||||||
|
|
||||||
getClassroomList()
|
|
||||||
setShowCreateModal(false)
|
|
||||||
setClassroom(newClassEntity)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Sınıf oluştururken hata oluştu:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEditClass = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (!classroom) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateClassroom(classroom)
|
|
||||||
|
|
||||||
getClassroomList()
|
|
||||||
setShowEditModal(false)
|
|
||||||
setClassroom(newClassEntity)
|
|
||||||
resetForm()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Sınıf oluştururken hata oluştu:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openEditModal = (classSession: ClassroomDto) => {
|
|
||||||
setClassroom(classSession)
|
|
||||||
setShowEditModal(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const openDeleteModal = (classSession: ClassroomDto) => {
|
|
||||||
setClassroom(classSession)
|
|
||||||
setShowDeleteModal(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteClass = async () => {
|
|
||||||
if (!classroom) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await deleteClassroom(classroom.id!)
|
|
||||||
getClassroomList()
|
|
||||||
setShowDeleteModal(false)
|
|
||||||
setClassroom(newClassEntity)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Sınıf silinirken hata oluştu:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
setClassroom(newClassEntity)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleStartClass = async (classSession: ClassroomDto) => {
|
|
||||||
await startClassroom(classSession.id!)
|
|
||||||
getClassroomList()
|
|
||||||
|
|
||||||
handleJoinClass(classSession)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleJoinClass = (classSession: ClassroomDto) => {
|
|
||||||
if (classSession.id) {
|
|
||||||
navigate(ROUTES_ENUM.protected.coordinator.classroom.roomDetail.replace(':id', classSession.id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePlanningClass = (classSession: ClassroomDto) => {
|
|
||||||
if (classSession.id) {
|
|
||||||
navigate(ROUTES_ENUM.protected.coordinator.classroom.planning.replace(':id', classSession.id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const widgets = () => {
|
|
||||||
return {
|
|
||||||
totalCount: classList.length,
|
|
||||||
activeCount: classList.filter((c) => !c.actualStartTime && !c.actualEndTime).length,
|
|
||||||
openCount: classList.filter(
|
|
||||||
(c) => c.actualStartTime && !c.actualEndTime, // && canJoinClass(c.actualStartTime),
|
|
||||||
).length,
|
|
||||||
passiveCount: classList.filter((c) => c.actualStartTime && c.actualEndTime).length,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getClassProps = (classSession: ClassroomDto): ClassProps => {
|
|
||||||
//Aktif -> boş boş
|
|
||||||
if (!classSession.actualStartTime && !classSession.actualEndTime) {
|
|
||||||
return {
|
|
||||||
status: 'Aktif',
|
|
||||||
className: 'bg-blue-100 text-blue-800',
|
|
||||||
showButtons: true,
|
|
||||||
title:
|
|
||||||
user.role === 'teacher' && classSession.teacherId === user.id ? 'Dersi Başlat' : 'Katıl',
|
|
||||||
classes:
|
|
||||||
user.role === 'teacher' && classSession.teacherId === user.id
|
|
||||||
? 'bg-green-600 text-white hover:bg-green-700'
|
|
||||||
: 'bg-blue-600 text-white hover:bg-blue-700',
|
|
||||||
event: () => {
|
|
||||||
user.role === 'teacher' && classSession.teacherId === user.id
|
|
||||||
? handleStartClass(classSession)
|
|
||||||
: handleJoinClass(classSession)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//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 | ${APP_NAME}`}
|
|
||||||
title={translate('::' + 'App.Coordinator.Classroom.List')}
|
|
||||||
defaultTitle={APP_NAME}
|
|
||||||
></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">
|
|
||||||
{classList.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">
|
|
||||||
{classList.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">
|
|
||||||
{classList.map((classSession, index) => {
|
|
||||||
const { status, className, showButtons, title, classes, event } =
|
|
||||||
getClassProps(classSession)
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
key={classSession.id}
|
|
||||||
initial={{ opacity: 0, x: -20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{ delay: index * 0.1 }}
|
|
||||||
className="border border-gray-200 rounded-lg p-4 sm:p-6 hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<h3 className="text-base sm:text-lg font-semibold text-gray-900 break-words">
|
|
||||||
{classSession.name}
|
|
||||||
</h3>
|
|
||||||
<span
|
|
||||||
className={`px-2 py-1 rounded-full text-xs font-medium ${className}`}
|
|
||||||
>
|
|
||||||
{status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sağ kısım: buton */}
|
|
||||||
{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 && classroom)) && (
|
|
||||||
<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={classroom.name}
|
|
||||||
onChange={(e) => setClassroom({ ...classroom, 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={classroom.description}
|
|
||||||
onChange={(e) => setClassroom({ ...classroom, 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={classroom.subject}
|
|
||||||
onChange={(e) => setClassroom({ ...classroom, 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={
|
|
||||||
classroom.scheduledStartTime
|
|
||||||
? classroom.scheduledStartTime.slice(0, 16)
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
setClassroom({
|
|
||||||
...classroom,
|
|
||||||
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={classroom.duration}
|
|
||||||
onChange={(e) =>
|
|
||||||
setClassroom({
|
|
||||||
...classroom,
|
|
||||||
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={classroom.maxParticipants}
|
|
||||||
onChange={(e) =>
|
|
||||||
setClassroom({
|
|
||||||
...classroom,
|
|
||||||
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={classroom.settingsDto?.allowHandRaise}
|
|
||||||
onChange={(e) =>
|
|
||||||
setClassroom({
|
|
||||||
...classroom,
|
|
||||||
settingsDto: {
|
|
||||||
...classroom.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={classroom.settingsDto?.allowStudentChat}
|
|
||||||
onChange={(e) =>
|
|
||||||
setClassroom({
|
|
||||||
...classroom,
|
|
||||||
settingsDto: {
|
|
||||||
...classroom.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={classroom.settingsDto?.allowPrivateMessages}
|
|
||||||
onChange={(e) =>
|
|
||||||
setClassroom({
|
|
||||||
...classroom,
|
|
||||||
settingsDto: {
|
|
||||||
...classroom.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={classroom.settingsDto?.allowStudentScreenShare}
|
|
||||||
onChange={(e) =>
|
|
||||||
setClassroom({
|
|
||||||
...classroom,
|
|
||||||
settingsDto: {
|
|
||||||
...classroom.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={classroom.settingsDto?.defaultMicrophoneState}
|
|
||||||
onChange={(e) =>
|
|
||||||
setClassroom({
|
|
||||||
...classroom,
|
|
||||||
settingsDto: {
|
|
||||||
...classroom.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={classroom.settingsDto?.defaultCameraState}
|
|
||||||
onChange={(e) =>
|
|
||||||
setClassroom({
|
|
||||||
...classroom,
|
|
||||||
settingsDto: {
|
|
||||||
...classroom.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={classroom.settingsDto?.defaultLayout}
|
|
||||||
onChange={(e) =>
|
|
||||||
setClassroom({
|
|
||||||
...classroom,
|
|
||||||
settingsDto: {
|
|
||||||
...classroom.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={classroom.settingsDto?.autoMuteNewParticipants}
|
|
||||||
onChange={(e) =>
|
|
||||||
setClassroom({
|
|
||||||
...classroom,
|
|
||||||
settingsDto: {
|
|
||||||
...classroom.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)
|
|
||||||
setClassroom(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 && classroom && (
|
|
||||||
<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>"{classroom.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)
|
|
||||||
setClassroom(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 ClassList
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
import { FaGraduationCap, FaUserCheck, FaEye } from 'react-icons/fa'
|
|
||||||
import { Role } from '@/proxy/classroom/models'
|
|
||||||
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 { APP_NAME } from '@/constants/app.constant'
|
|
||||||
|
|
||||||
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.coordinator.classroom.classes, { replace: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Helmet
|
|
||||||
titleTemplate={`%s | ${APP_NAME}`}
|
|
||||||
title={translate('::' + 'App.Coordinator.Classroom.Dashboard')}
|
|
||||||
defaultTitle={APP_NAME}
|
|
||||||
></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
|
|
||||||
|
|
@ -1,299 +0,0 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
|
||||||
import { DndContext, DragEndEvent, DragOverlay, DragStartEvent } from '@dnd-kit/core'
|
|
||||||
import { Button } from '@/components/ui/Button'
|
|
||||||
import { Avatar } from '@/components/ui/Avatar'
|
|
||||||
import { FaUsers, FaSearch, FaThLarge, FaUndo, FaSave, FaBolt } from 'react-icons/fa'
|
|
||||||
import { Classroom, Seat, Student } from '@/proxy/classroom/planning'
|
|
||||||
import { mockStudents } from '@/components/classroom/data/students'
|
|
||||||
import { StudentList } from '@/components/classroom/planning/StudentList'
|
|
||||||
import { SeatGrid } from '@/components/classroom/planning/SeatGrid'
|
|
||||||
import { ClassroomSelector } from '@/components/classroom/planning/ClassroomSelector'
|
|
||||||
import { QuickActions } from '@/components/classroom/planning/QuickActions'
|
|
||||||
import { Container } from '@/components/shared'
|
|
||||||
import { Helmet } from 'react-helmet'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
|
||||||
|
|
||||||
const PlanningPage: React.FC = () => {
|
|
||||||
const [students, setStudents] = useState<Student[]>([])
|
|
||||||
const [seats, setSeats] = useState<Seat[]>([])
|
|
||||||
const [selectedClassroom, setSelectedClassroom] = useState<Classroom | null>(null)
|
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>([])
|
|
||||||
const [draggedStudent, setDraggedStudent] = useState<Student | null>(null)
|
|
||||||
const [selectedSeats, setSelectedSeats] = useState<string[]>([])
|
|
||||||
|
|
||||||
// Mock data - gerçek API'den gelecek
|
|
||||||
useEffect(() => {
|
|
||||||
setStudents(mockStudents)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Sınıf değiştiğinde koltukları yeniden oluştur
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedClassroom) {
|
|
||||||
const newSeats: Seat[] = []
|
|
||||||
for (let row = 0; row < selectedClassroom.rows; row++) {
|
|
||||||
for (let col = 0; col < selectedClassroom.columns; col++) {
|
|
||||||
const label = String.fromCharCode(65 + row) + (col + 1)
|
|
||||||
newSeats.push({
|
|
||||||
id: `seat-${row}-${col}`,
|
|
||||||
row,
|
|
||||||
col,
|
|
||||||
label,
|
|
||||||
isBlocked: false,
|
|
||||||
seatType: 'Standard',
|
|
||||||
concurrencyStamp: '',
|
|
||||||
creationTime: new Date().toISOString(),
|
|
||||||
lastModificationTime: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setSeats(newSeats)
|
|
||||||
setSelectedSeats([]) // Seçili koltukları temizle
|
|
||||||
}
|
|
||||||
}, [selectedClassroom])
|
|
||||||
|
|
||||||
const handleDragStart = (event: DragStartEvent) => {
|
|
||||||
const student = students.find((s) => s.id === event.active.id)
|
|
||||||
setDraggedStudent(student || null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDragEnd = (event: DragEndEvent) => {
|
|
||||||
const { active, over } = event
|
|
||||||
|
|
||||||
if (over && over.id.toString().startsWith('seat-')) {
|
|
||||||
const seatId = over.id.toString()
|
|
||||||
const studentId = active.id.toString()
|
|
||||||
|
|
||||||
// Check if seat is blocked
|
|
||||||
const targetSeat = seats.find((s) => s.id === seatId)
|
|
||||||
if (!targetSeat || targetSeat.isBlocked) {
|
|
||||||
setDraggedStudent(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update seat assignment - eski öğrenciyi kaldır, yeni öğrenciyi yerleştir
|
|
||||||
setSeats((prev) =>
|
|
||||||
prev.map((seat) =>
|
|
||||||
seat.id === seatId
|
|
||||||
? { ...seat, studentId }
|
|
||||||
: seat.studentId === studentId
|
|
||||||
? { ...seat, studentId: undefined }
|
|
||||||
: seat,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setDraggedStudent(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemoveStudent = (seatId: string) => {
|
|
||||||
setSeats((prev) =>
|
|
||||||
prev.map((seat) => (seat.id === seatId ? { ...seat, studentId: undefined } : seat)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemoveSelectedStudents = () => {
|
|
||||||
setSeats((prev) =>
|
|
||||||
prev.map((seat) =>
|
|
||||||
selectedSeats.includes(seat.id) ? { ...seat, studentId: undefined } : seat,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
setSelectedSeats([]) // Seçimi temizle
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleToggleSeatBlock = () => {
|
|
||||||
setSeats((prev) =>
|
|
||||||
prev.map((seat) => {
|
|
||||||
if (selectedSeats.includes(seat.id)) {
|
|
||||||
return {
|
|
||||||
...seat,
|
|
||||||
isBlocked: !seat.isBlocked,
|
|
||||||
studentId: seat.isBlocked ? seat.studentId : undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return seat
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
setSelectedSeats([]) // Seçimi temizle
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSeatSelect = (seatIds: string[]) => {
|
|
||||||
// Sadece dolu koltukları seçilebilir yap
|
|
||||||
const validSeatIds = seatIds.filter((seatId) => {
|
|
||||||
const seat = seats.find((s) => s.id === seatId)
|
|
||||||
return seat && seat.studentId // Sadece öğrencisi olan koltuklar
|
|
||||||
})
|
|
||||||
setSelectedSeats(validSeatIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredStudents = students.filter((student) => {
|
|
||||||
const matchesSearch = student.fullName.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
const matchesTags =
|
|
||||||
selectedTags.length === 0 || selectedTags.some((tag) => student.tags.includes(tag))
|
|
||||||
return matchesSearch && matchesTags
|
|
||||||
})
|
|
||||||
|
|
||||||
const assignedStudentIds = seats.filter((seat) => seat.studentId).map((seat) => seat.studentId!)
|
|
||||||
|
|
||||||
const unassignedStudents = filteredStudents.filter(
|
|
||||||
(student) => !assignedStudentIds.includes(student.id),
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleClearAll = () => {
|
|
||||||
setSeats((prev) => prev.map((seat) => ({ ...seat, studentId: undefined })))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAutoAssign = () => {
|
|
||||||
const availableSeats = seats.filter((seat) => !seat.isBlocked && !seat.studentId)
|
|
||||||
const studentsToAssign = unassignedStudents.slice(0, availableSeats.length)
|
|
||||||
|
|
||||||
const newSeats = [...seats]
|
|
||||||
studentsToAssign.forEach((student, index) => {
|
|
||||||
const seatIndex = seats.findIndex((seat) => seat.id === availableSeats[index].id)
|
|
||||||
if (seatIndex !== -1) {
|
|
||||||
newSeats[seatIndex] = { ...newSeats[seatIndex], studentId: student.id }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setSeats(newSeats)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Helmet
|
|
||||||
titleTemplate={`%s | ${APP_NAME}`}
|
|
||||||
title={translate('::' + 'App.Coordinator.Classroom.Planning')}
|
|
||||||
defaultTitle={APP_NAME}
|
|
||||||
/>
|
|
||||||
<Container>
|
|
||||||
{/* Header */}
|
|
||||||
<header className="bg-white border-b border-gray-200 px-2 py-2 md:px-4 md:py-4">
|
|
||||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
|
||||||
<div className="flex items-center space-x-2 md:space-x-4">
|
|
||||||
<FaThLarge className="h-7 w-7 md:h-8 md:w-8 text-primary" />
|
|
||||||
<h1 className="text-xl md:text-2xl font-bold text-gray-900">Sınıf Planlama</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2 md:gap-3 items-center">
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="flex flex-row justify-center items-center"
|
|
||||||
onClick={handleClearAll}
|
|
||||||
>
|
|
||||||
<FaUndo className="h-4 w-4 mr-2" />
|
|
||||||
<span className="hidden sm:inline">Temizle</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="flex items-center px-3 whitespace-nowrap"
|
|
||||||
onClick={handleAutoAssign}
|
|
||||||
>
|
|
||||||
<FaBolt className="h-4 w-4 mr-2 text-yellow-400" />
|
|
||||||
<span className="hidden sm:inline">Otomatik Ata</span>
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" className="flex flex-row justify-center items-center">
|
|
||||||
<FaSave className="h-4 w-4 mr-2" />
|
|
||||||
<span className="hidden sm:inline">Kaydet</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<DndContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
|
||||||
<div className="flex flex-col md:flex-row h-auto md:h-[calc(100vh-80px)]">
|
|
||||||
{/* Left Sidebar - Student List */}
|
|
||||||
<div className="w-full md:w-80 bg-white border-b md:border-b-0 md:border-r border-gray-200 flex flex-col">
|
|
||||||
<div className="p-2 border-b border-gray-200">
|
|
||||||
<div className="flex items-center justify-between pb-2">
|
|
||||||
<h2 className="text-base md:text-lg font-semibold text-gray-900 flex items-center">
|
|
||||||
<FaUsers className="h-5 w-5 mr-2" />
|
|
||||||
Öğrenciler ({unassignedStudents.length})
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 md:space-y-3">
|
|
||||||
<div className="relative">
|
|
||||||
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 h-3 w-3 text-gray-400" />
|
|
||||||
<input
|
|
||||||
id="student-search"
|
|
||||||
placeholder="Öğrenci ara..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="w-full p-1 pl-8 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<StudentList
|
|
||||||
students={unassignedStudents}
|
|
||||||
searchQuery={searchQuery}
|
|
||||||
selectedTags={selectedTags}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content - Seat Grid */}
|
|
||||||
<div className="flex-1 flex flex-col">
|
|
||||||
<div className="p-2 border-b border-gray-200">
|
|
||||||
<div className="flex items-center justify-between pb-2">
|
|
||||||
<h2 className="text-base md:text-lg font-semibold text-gray-900 flex items-center">
|
|
||||||
<FaUsers className="h-5 w-5 mr-2" />
|
|
||||||
Sınıflar
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 md:space-y-3">
|
|
||||||
<div className="relative">
|
|
||||||
<ClassroomSelector
|
|
||||||
selectedClassroom={selectedClassroom}
|
|
||||||
onClassroomChange={setSelectedClassroom}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Seat Grid */}
|
|
||||||
<div className="flex-1 overflow-auto">
|
|
||||||
{selectedClassroom && (
|
|
||||||
<SeatGrid
|
|
||||||
classroom={selectedClassroom}
|
|
||||||
seats={seats}
|
|
||||||
students={students}
|
|
||||||
selectedSeats={selectedSeats}
|
|
||||||
onSeatSelect={handleSeatSelect}
|
|
||||||
onRemoveStudent={handleRemoveStudent}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Sidebar - Quick Actions */}
|
|
||||||
<div className="w-full md:w-80 bg-white border-t md:border-t-0 md:border-l border-gray-200">
|
|
||||||
<QuickActions
|
|
||||||
selectedSeats={selectedSeats}
|
|
||||||
seats={seats}
|
|
||||||
students={students}
|
|
||||||
onRemoveSelectedStudents={handleRemoveSelectedStudents}
|
|
||||||
onToggleSeatBlock={handleToggleSeatBlock}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DragOverlay>
|
|
||||||
{draggedStudent && (
|
|
||||||
<div className="transform rotate-3 scale-110">
|
|
||||||
<Avatar
|
|
||||||
className="h-12 w-12 border-4 border-primary shadow-2xl"
|
|
||||||
shape="circle"
|
|
||||||
src={draggedStudent.photoUrl || undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PlanningPage
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,496 +0,0 @@
|
||||||
import { QuestionPoolDto, Exam, QuestionDto } from "@/types/coordinator";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { FaPlus, FaClock, FaUsers, FaCog, FaSave } from "react-icons/fa";
|
|
||||||
|
|
||||||
interface ExamCreatorProps {
|
|
||||||
pools: QuestionPoolDto[];
|
|
||||||
onCreateExam: (exam: Omit<Exam, "id" | "creationTime" | "lastModificationTime">) => void;
|
|
||||||
onCancel?: () => void;
|
|
||||||
editingExam?: Exam;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExamCreator: React.FC<ExamCreatorProps> = ({
|
|
||||||
pools,
|
|
||||||
onCreateExam,
|
|
||||||
onCancel,
|
|
||||||
editingExam,
|
|
||||||
}) => {
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
title: editingExam?.title || "",
|
|
||||||
description: editingExam?.description || "",
|
|
||||||
type: editingExam?.type || ("exam" as "exam" | "assignment" | "test"),
|
|
||||||
timeLimit: editingExam?.timeLimit || 60,
|
|
||||||
passingScore: editingExam?.passingScore || 60,
|
|
||||||
maxAttempts: editingExam?.maxAttempts || 1,
|
|
||||||
allowReview: editingExam?.allowReview ?? true,
|
|
||||||
randomizeQuestions: editingExam?.randomizeQuestions ?? false,
|
|
||||||
showResults: editingExam?.showResults ?? true,
|
|
||||||
startTime: editingExam?.startTime?.toISOString().slice(0, 16) || "",
|
|
||||||
endTime: editingExam?.endTime?.toISOString().slice(0, 16) || "",
|
|
||||||
isActive: editingExam?.isActive ?? true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [selectedQuestions, setSelectedQuestions] = useState<QuestionDto[]>(
|
|
||||||
editingExam?.questions || []
|
|
||||||
);
|
|
||||||
|
|
||||||
const [selectedPool, setSelectedPool] = useState<string>("");
|
|
||||||
const [questionFilters, setQuestionFilters] = useState({
|
|
||||||
questionType: "",
|
|
||||||
difficulty: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: any) => {
|
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const addQuestionsFromPool = () => {
|
|
||||||
const pool = pools.find((p) => p.id === selectedPool);
|
|
||||||
if (!pool) return;
|
|
||||||
|
|
||||||
let questionsToAdd = pool.questions;
|
|
||||||
|
|
||||||
// Apply filters
|
|
||||||
if (questionFilters.questionType) {
|
|
||||||
questionsToAdd = questionsToAdd.filter(
|
|
||||||
(q) => q.questionType === questionFilters.questionType
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (questionFilters.difficulty) {
|
|
||||||
questionsToAdd = questionsToAdd.filter(
|
|
||||||
(q) => q.difficulty === questionFilters.difficulty
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add questions that aren't already selected
|
|
||||||
const newQuestions = questionsToAdd.filter(
|
|
||||||
(q) => !selectedQuestions.some((sq) => sq.id === q.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
setSelectedQuestions((prev) => [...prev, ...newQuestions]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeQuestion = (questionId: string) => {
|
|
||||||
setSelectedQuestions((prev) => prev.filter((q) => q.id !== questionId));
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveQuestion = (index: number, direction: "up" | "down") => {
|
|
||||||
const newQuestions = [...selectedQuestions];
|
|
||||||
const targetIndex = direction === "up" ? index - 1 : index + 1;
|
|
||||||
|
|
||||||
if (targetIndex >= 0 && targetIndex < newQuestions.length) {
|
|
||||||
[newQuestions[index], newQuestions[targetIndex]] = [
|
|
||||||
newQuestions[targetIndex],
|
|
||||||
newQuestions[index],
|
|
||||||
];
|
|
||||||
setSelectedQuestions(newQuestions);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (!formData.title.trim() || selectedQuestions.length === 0) {
|
|
||||||
alert("Please enter exam title and select at least one question.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalPoints = selectedQuestions.reduce((sum, q) => sum + q.points, 0);
|
|
||||||
|
|
||||||
const examData = {
|
|
||||||
...formData,
|
|
||||||
questions: selectedQuestions,
|
|
||||||
totalPoints,
|
|
||||||
startTime: formData.startTime ? new Date(formData.startTime) : undefined,
|
|
||||||
endTime: formData.endTime ? new Date(formData.endTime) : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
onCreateExam(examData);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-bold text-gray-900">
|
|
||||||
{editingExam ? "Sınavı Düzenle" : "Yeni Sınav Oluştur"}
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-gray-600 mt-0.5">
|
|
||||||
Sınav ayarlarını yapılandırın ve soruları seçin
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{onCancel && (
|
|
||||||
<button
|
|
||||||
onClick={onCancel}
|
|
||||||
className="flex items-center space-x-2 bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-1.5 text-sm rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<span>İptal</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-1.5 text-sm rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<FaSave className="w-3.5 h-3.5" />
|
|
||||||
<span>{editingExam ? "Güncelle" : "Oluştur"}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
{/* Basic Information */}
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center">
|
|
||||||
<FaCog className="w-4 h-4 mr-2" />
|
|
||||||
Temel Bilgiler
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Exam Title
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.title}
|
|
||||||
onChange={(e) => handleInputChange("title", e.target.value)}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="Enter exam title"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={formData.description}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("description", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
rows={3}
|
|
||||||
placeholder="Enter exam description"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Type
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={formData.type}
|
|
||||||
onChange={(e) => handleInputChange("type", e.target.value)}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="exam">Exam</option>
|
|
||||||
<option value="assignment">Assignment</option>
|
|
||||||
<option value="test">Test</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Settings */}
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center">
|
|
||||||
<FaClock className="w-4 h-4 mr-2" />
|
|
||||||
Ayarlar
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Time Limit (minutes)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={formData.timeLimit}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("timeLimit", parseInt(e.target.value))
|
|
||||||
}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
min="1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Passing Score (%)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={formData.passingScore}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("passingScore", parseInt(e.target.value))
|
|
||||||
}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Maximum Attempts
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={formData.maxAttempts}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("maxAttempts", parseInt(e.target.value))
|
|
||||||
}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
min="1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Start Date
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="datetime-local"
|
|
||||||
value={formData.startTime}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("startTime", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
End Date
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="datetime-local"
|
|
||||||
value={formData.endTime}
|
|
||||||
onChange={(e) => handleInputChange("endTime", e.target.value)}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2.5">
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.allowReview}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("allowReview", e.target.checked)
|
|
||||||
}
|
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<span className="ml-2 text-sm text-gray-700">
|
|
||||||
Allow answer review
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.randomizeQuestions}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("randomizeQuestions", e.target.checked)
|
|
||||||
}
|
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<span className="ml-2 text-sm text-gray-700">
|
|
||||||
Randomize questions
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.showResults}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("showResults", e.target.checked)
|
|
||||||
}
|
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<span className="ml-2 text-sm text-gray-700">Show results</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.isActive}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("isActive", e.target.checked)
|
|
||||||
}
|
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<span className="ml-2 text-sm text-gray-700">Active</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Question Selection */}
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center">
|
|
||||||
<FaUsers className="w-4 h-4 mr-2" />
|
|
||||||
Question Selection
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{/* Pool Selection */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 mb-3">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Question Pool
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={selectedPool}
|
|
||||||
onChange={(e) => setSelectedPool(e.target.value)}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="">Select pool</option>
|
|
||||||
{pools.map((pool) => (
|
|
||||||
<option key={pool.id} value={pool.id}>
|
|
||||||
{pool.name} ({pool.questions.length} questions)
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Question Type
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={questionFilters.questionType}
|
|
||||||
onChange={(e) =>
|
|
||||||
setQuestionFilters((prev) => ({
|
|
||||||
...prev,
|
|
||||||
questionType: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="">All types</option>
|
|
||||||
<option value="multiple-choice">Multiple Choice</option>
|
|
||||||
<option value="true-false">True/False</option>
|
|
||||||
<option value="fill-blank">Fill Blank</option>
|
|
||||||
<option value="open-ended">Open Ended</option>
|
|
||||||
<option value="multiple-answer">Multiple Answer</option>
|
|
||||||
<option value="matching">Matching</option>
|
|
||||||
<option value="ordering">Ordering</option>
|
|
||||||
<option value="calculation">Calculation</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Difficulty
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={questionFilters.difficulty}
|
|
||||||
onChange={(e) =>
|
|
||||||
setQuestionFilters((prev) => ({
|
|
||||||
...prev,
|
|
||||||
difficulty: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="">All levels</option>
|
|
||||||
<option value="easy">Easy</option>
|
|
||||||
<option value="medium">Medium</option>
|
|
||||||
<option value="hard">Hard</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-end">
|
|
||||||
<button
|
|
||||||
onClick={addQuestionsFromPool}
|
|
||||||
disabled={!selectedPool}
|
|
||||||
className="w-full flex items-center justify-center space-x-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-300 text-white py-2 rounded-lg text-xs transition-colors"
|
|
||||||
>
|
|
||||||
<FaPlus className="w-4 h-4" />
|
|
||||||
<span>Add Questions</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Selected Questions */}
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h4 className="text-md font-medium text-gray-900">
|
|
||||||
Selected Questions ({selectedQuestions.length})
|
|
||||||
</h4>
|
|
||||||
{selectedQuestions.length > 0 && (
|
|
||||||
<span className="text-sm text-gray-600">
|
|
||||||
Total Points:{" "}
|
|
||||||
{selectedQuestions.reduce((sum, q) => sum + q.points, 0)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedQuestions.length > 0 ? (
|
|
||||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
|
||||||
{selectedQuestions.map((question, index) => (
|
|
||||||
<div
|
|
||||||
key={question.id}
|
|
||||||
className="flex items-center justify-between p-4 border border-gray-200 rounded-lg"
|
|
||||||
>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center space-x-2 mb-1">
|
|
||||||
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-2 py-1 rounded">
|
|
||||||
{question.questionType}
|
|
||||||
</span>
|
|
||||||
<span className="bg-green-100 text-green-800 text-xs font-medium px-2 py-1 rounded">
|
|
||||||
{question.points} pts
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-gray-900">
|
|
||||||
{question.title}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-600 truncate">
|
|
||||||
{question.content}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-1 ml-4">
|
|
||||||
<button
|
|
||||||
onClick={() => moveQuestion(index, "up")}
|
|
||||||
disabled={index === 0}
|
|
||||||
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30 rounded"
|
|
||||||
>
|
|
||||||
↑
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => moveQuestion(index, "down")}
|
|
||||||
disabled={index === selectedQuestions.length - 1}
|
|
||||||
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30 rounded"
|
|
||||||
>
|
|
||||||
↓
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => removeQuestion(question.id!)}
|
|
||||||
className="p-1 text-red-400 hover:text-red-600 rounded ml-2"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8 text-gray-500">
|
|
||||||
<p>No questions selected yet</p>
|
|
||||||
<p className="text-sm">Select a pool above and add questions</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,301 +0,0 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
|
||||||
import { QuestionRenderer } from './QuestionRenderer'
|
|
||||||
import { ExamNavigation } from './ExamNavigation'
|
|
||||||
import { ExamTimer } from './ExamTimer'
|
|
||||||
import { SecurityWarning } from './SecurityWarning'
|
|
||||||
import { ExamSession, StudentAnswer } from '@/types/coordinator'
|
|
||||||
import { useExamSecurity } from '@/utils/hooks/useExamSecurity'
|
|
||||||
import { useStoreState } from '@/store/store'
|
|
||||||
import { generateMockExam } from '@/mocks/mockExams'
|
|
||||||
|
|
||||||
const ExamInterface: React.FC = () => {
|
|
||||||
const { id } = useParams<{ id: string }>()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const userId = useStoreState((state) => state.auth.user.id)
|
|
||||||
|
|
||||||
// TODO: Replace with actual API call to fetch exam by id
|
|
||||||
const exam = generateMockExam().find((e) => e.id === id)
|
|
||||||
|
|
||||||
if (!exam) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
|
||||||
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-6 text-center">
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Sınav Bulunamadı</h2>
|
|
||||||
<p className="text-gray-600 mb-6">İstenen sınav bulunamadı veya erişim yetkiniz yok.</p>
|
|
||||||
<button
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
Geri Dön
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const [session, setSession] = useState<ExamSession>({
|
|
||||||
id: `session-${Date.now()}`,
|
|
||||||
examId: exam.id,
|
|
||||||
studentId: userId,
|
|
||||||
startTime: new Date(),
|
|
||||||
answers: [],
|
|
||||||
status: 'in-progress',
|
|
||||||
timeRemaining: exam.timeLimit * 60,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
|
|
||||||
const [securityWarning, setSecurityWarning] = useState<{
|
|
||||||
show: boolean
|
|
||||||
message: string
|
|
||||||
type: 'warning' | 'error' | 'info'
|
|
||||||
}>({ show: false, message: '', type: 'warning' })
|
|
||||||
|
|
||||||
// Security configuration
|
|
||||||
const securityConfig = {
|
|
||||||
disableRightClick: true,
|
|
||||||
disableCopyPaste: true,
|
|
||||||
disableDevTools: true,
|
|
||||||
fullScreenMode: true,
|
|
||||||
preventTabSwitch: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
useExamSecurity(securityConfig, session.status === 'in-progress')
|
|
||||||
|
|
||||||
// Save exam session to backend/localStorage
|
|
||||||
const saveExamSession = (sessionData: ExamSession) => {
|
|
||||||
// TODO: Replace with actual API call
|
|
||||||
console.log('Saving exam session:', sessionData)
|
|
||||||
localStorage.setItem(`exam-session-${sessionData.examId}`, JSON.stringify(sessionData))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete exam and navigate
|
|
||||||
const handleExamComplete = (sessionData: ExamSession) => {
|
|
||||||
// TODO: Replace with actual API call
|
|
||||||
console.log('Exam completed:', sessionData)
|
|
||||||
saveExamSession(sessionData)
|
|
||||||
// Navigate to results or dashboard
|
|
||||||
// navigate('/admin/coordinator/exams')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-save mechanism
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if (session.status === 'in-progress') {
|
|
||||||
saveExamSession(session)
|
|
||||||
}
|
|
||||||
}, 30000) // Save every 30 seconds
|
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [session])
|
|
||||||
|
|
||||||
const handleAnswerChange = (questionId: string, answer: string | string[]) => {
|
|
||||||
setSession((prev) => {
|
|
||||||
const existingAnswerIndex = prev.answers.findIndex((a) => a.questionId === questionId)
|
|
||||||
const newAnswer: StudentAnswer = {
|
|
||||||
questionId,
|
|
||||||
answer,
|
|
||||||
timeSpent: 0, // In a real app, track time spent per question
|
|
||||||
}
|
|
||||||
|
|
||||||
const newAnswers =
|
|
||||||
existingAnswerIndex >= 0
|
|
||||||
? prev.answers.map((a, i) => (i === existingAnswerIndex ? newAnswer : a))
|
|
||||||
: [...prev.answers, newAnswer]
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
answers: newAnswers,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTimeUp = () => {
|
|
||||||
setSecurityWarning({
|
|
||||||
show: true,
|
|
||||||
message: 'Süre doldu! Sınav otomatik olarak teslim ediliyor...',
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
completeExam()
|
|
||||||
}, 3000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const completeExam = () => {
|
|
||||||
const completedSession: ExamSession = {
|
|
||||||
...session,
|
|
||||||
endTime: new Date(),
|
|
||||||
status: 'completed',
|
|
||||||
}
|
|
||||||
|
|
||||||
setSession(completedSession)
|
|
||||||
handleExamComplete(completedSession)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmitExam = () => {
|
|
||||||
const unanswered = exam.questions.filter(
|
|
||||||
(q) => !session.answers.find((a) => a.questionId === q.id && a.answer),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (unanswered.length > 0) {
|
|
||||||
const confirmSubmit = window.confirm(
|
|
||||||
`${unanswered.length} soru cevaplanmamış. Yine de sınavı teslim etmek istiyor musunuz?`,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!confirmSubmit) return
|
|
||||||
}
|
|
||||||
|
|
||||||
completeExam()
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentQuestion = exam.questions[currentQuestionIndex]
|
|
||||||
const currentAnswer = session.answers.find((a) => a.questionId === currentQuestion?.id)
|
|
||||||
|
|
||||||
const navigateQuestion = (direction: 'next' | 'prev' | number) => {
|
|
||||||
if (typeof direction === 'number') {
|
|
||||||
setCurrentQuestionIndex(Math.max(0, Math.min(exam.questions.length - 1, direction)))
|
|
||||||
} else if (direction === 'next') {
|
|
||||||
setCurrentQuestionIndex((prev) => Math.min(exam.questions.length - 1, prev + 1))
|
|
||||||
} else {
|
|
||||||
setCurrentQuestionIndex((prev) => Math.max(0, prev - 1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.status === 'completed') {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
|
||||||
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-6 text-center">
|
|
||||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<svg
|
|
||||||
className="w-8 h-8 text-green-600"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M5 13l4 4L19 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Sınav Tamamlandı!</h2>
|
|
||||||
<p className="text-gray-600 mb-6">
|
|
||||||
Sınavınız başarıyla teslim edildi. Sonuçlarınız değerlendirildikten sonra
|
|
||||||
bilgilendirileceksiniz.
|
|
||||||
</p>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
Başlama: {session.startTime.toLocaleString('tr-TR')}
|
|
||||||
<br />
|
|
||||||
Bitiş: {session.endTime?.toLocaleString('tr-TR')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50">
|
|
||||||
<SecurityWarning
|
|
||||||
isVisible={securityWarning.show}
|
|
||||||
onDismiss={() => setSecurityWarning((prev) => ({ ...prev, show: false }))}
|
|
||||||
message={securityWarning.message}
|
|
||||||
type={securityWarning.type}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<header className="bg-white border-b border-gray-200 px-4 py-3">
|
|
||||||
<div className="mx-auto flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-semibold text-gray-900">{exam.title}</h1>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Soru {currentQuestionIndex + 1} / {exam.questions.length}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<ExamTimer initialTime={exam.timeLimit * 60} onTimeUp={handleTimeUp} autoStart={true} />
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleSubmitExam}
|
|
||||||
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
Sınavı Teslim Et
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<div className="mx-auto p-4">
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
||||||
{/* Question Content */}
|
|
||||||
<div className="lg:col-span-3 space-y-6">
|
|
||||||
{currentQuestion && (
|
|
||||||
<QuestionRenderer
|
|
||||||
question={currentQuestion}
|
|
||||||
answer={currentAnswer}
|
|
||||||
onAnswerChange={handleAnswerChange}
|
|
||||||
disabled={session.status !== 'in-progress'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Navigation Controls */}
|
|
||||||
<div className="flex items-center justify-between bg-white border border-gray-200 rounded-lg p-4">
|
|
||||||
<button
|
|
||||||
onClick={() => navigateQuestion('prev')}
|
|
||||||
disabled={currentQuestionIndex === 0}
|
|
||||||
className="flex items-center space-x-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 disabled:bg-gray-50 disabled:text-gray-400 text-gray-700 rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M15 19l-7-7 7-7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>Önceki</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-sm text-gray-600">
|
|
||||||
{currentQuestionIndex + 1} / {exam.questions.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => navigateQuestion('next')}
|
|
||||||
disabled={currentQuestionIndex === exam.questions.length - 1}
|
|
||||||
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-50 disabled:text-gray-400 text-white rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<span>Sonraki</span>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation Sidebar */}
|
|
||||||
<div className="lg:col-span-1">
|
|
||||||
<ExamNavigation
|
|
||||||
questions={exam.questions}
|
|
||||||
answers={session.answers}
|
|
||||||
currentQuestionIndex={currentQuestionIndex}
|
|
||||||
onQuestionSelect={navigateQuestion}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ExamInterface
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
import { QuestionDto, StudentAnswer } from '@/types/coordinator';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface ExamNavigationProps {
|
|
||||||
questions: QuestionDto[];
|
|
||||||
answers: StudentAnswer[];
|
|
||||||
currentQuestionIndex: number;
|
|
||||||
onQuestionSelect: (index: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExamNavigation: React.FC<ExamNavigationProps> = ({
|
|
||||||
questions,
|
|
||||||
answers,
|
|
||||||
currentQuestionIndex,
|
|
||||||
onQuestionSelect
|
|
||||||
}) => {
|
|
||||||
const getQuestionStatus = (index: number) => {
|
|
||||||
const answer = answers.find(a => a.questionId === questions[index].id);
|
|
||||||
if (!answer || !answer.answer) return 'unanswered';
|
|
||||||
|
|
||||||
if (Array.isArray(answer.answer)) {
|
|
||||||
return answer.answer.some(a => a && a.toString().trim() !== '') ? 'answered' : 'unanswered';
|
|
||||||
}
|
|
||||||
|
|
||||||
return answer.answer.toString().trim() !== '' ? 'answered' : 'unanswered';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (index: number) => {
|
|
||||||
const status = getQuestionStatus(index);
|
|
||||||
const isCurrent = index === currentQuestionIndex;
|
|
||||||
|
|
||||||
if (isCurrent) {
|
|
||||||
return status === 'answered'
|
|
||||||
? 'bg-blue-600 text-white border-blue-600'
|
|
||||||
: 'bg-blue-100 text-blue-800 border-blue-300';
|
|
||||||
}
|
|
||||||
|
|
||||||
return status === 'answered'
|
|
||||||
? 'bg-green-100 text-green-800 border-green-300 hover:bg-green-200'
|
|
||||||
: 'bg-white text-gray-600 border-gray-300 hover:bg-gray-50';
|
|
||||||
};
|
|
||||||
|
|
||||||
const answeredCount = questions.filter((_, index) => getQuestionStatus(index) === 'answered').length;
|
|
||||||
const unansweredCount = questions.length - answeredCount;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-4 sticky top-4">
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Soru Haritası</h3>
|
|
||||||
<div className="flex items-center space-x-4 text-sm">
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<div className="w-3 h-3 bg-green-100 border border-green-300 rounded"></div>
|
|
||||||
<span className="text-gray-600">{answeredCount} Cevaplanmış</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<div className="w-3 h-3 bg-white border border-gray-300 rounded"></div>
|
|
||||||
<span className="text-gray-600">{unansweredCount} Cevaplanmamış</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-5 gap-2">
|
|
||||||
{questions.map((_, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
onClick={() => onQuestionSelect(index)}
|
|
||||||
className={`w-10 h-10 text-sm font-medium rounded-lg border-2 transition-all ${getStatusColor(index)}`}
|
|
||||||
title={`Soru ${index + 1} - ${getQuestionStatus(index) === 'answered' ? 'Cevaplanmış' : 'Cevaplanmamış'}`}
|
|
||||||
>
|
|
||||||
{index + 1}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 pt-4 border-t border-gray-200">
|
|
||||||
<div className="text-sm text-gray-600">
|
|
||||||
<div>Toplam: {questions.length} soru</div>
|
|
||||||
<div>Kalan: {unansweredCount} soru</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
import { useExamTimer } from '@/utils/hooks/useExamTimer';
|
|
||||||
import React from 'react';
|
|
||||||
import { FaPlay } from 'react-icons/fa';
|
|
||||||
|
|
||||||
interface ExamTimerProps {
|
|
||||||
initialTime: number;
|
|
||||||
onTimeUp: () => void;
|
|
||||||
autoStart?: boolean;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExamTimer: React.FC<ExamTimerProps> = ({
|
|
||||||
initialTime,
|
|
||||||
onTimeUp,
|
|
||||||
autoStart = true,
|
|
||||||
className = ''
|
|
||||||
}) => {
|
|
||||||
const { timeRemaining, formattedTime, isRunning, isPaused, start, pause, resume, progress } = useExamTimer({
|
|
||||||
initialTime,
|
|
||||||
onTimeUp,
|
|
||||||
autoStart
|
|
||||||
});
|
|
||||||
|
|
||||||
const getTimerColor = () => {
|
|
||||||
const percentage = (timeRemaining / initialTime) * 100;
|
|
||||||
if (percentage <= 10) return 'text-red-600 bg-red-50';
|
|
||||||
if (percentage <= 25) return 'text-orange-600 bg-orange-50';
|
|
||||||
return 'text-blue-600 bg-blue-50';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getProgressColor = () => {
|
|
||||||
const percentage = (timeRemaining / initialTime) * 100;
|
|
||||||
if (percentage <= 10) return 'bg-red-500';
|
|
||||||
if (percentage <= 25) return 'bg-orange-500';
|
|
||||||
return 'bg-blue-500';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${className}`}>
|
|
||||||
<div className={`inline-flex items-center px-4 py-2 rounded-lg border ${getTimerColor()} transition-colors`}>
|
|
||||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<span className="font-mono font-semibold text-lg">
|
|
||||||
{formattedTime}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{!isRunning && !isPaused && timeRemaining > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={start}
|
|
||||||
className="ml-2 p-1 hover:bg-gray-200 rounded"
|
|
||||||
title="Başlat"
|
|
||||||
>
|
|
||||||
<FaPlay className="w-4 h-4" />
|
|
||||||
<span>Başlat</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isRunning && !isPaused && (
|
|
||||||
<button
|
|
||||||
onClick={pause}
|
|
||||||
className="ml-2 p-1 hover:bg-gray-200 rounded"
|
|
||||||
title="Duraklat"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isPaused && (
|
|
||||||
<button
|
|
||||||
onClick={resume}
|
|
||||||
className="ml-2 p-1 hover:bg-gray-200 rounded"
|
|
||||||
title="Devam Et"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress bar */}
|
|
||||||
<div className="mt-2 w-full bg-gray-200 rounded-full h-1.5">
|
|
||||||
<div
|
|
||||||
className={`h-1.5 rounded-full transition-all duration-1000 ${getProgressColor()}`}
|
|
||||||
style={{ width: `${100 - progress}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,390 +0,0 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
|
||||||
import { ExamTimer } from './ExamTimer'
|
|
||||||
import { SecurityWarning } from './SecurityWarning'
|
|
||||||
import { FaFileAlt, FaImage, FaCheckCircle } from 'react-icons/fa'
|
|
||||||
import { Exam, ExamSession, StudentAnswer, AnswerKeyItem } from '@/types/coordinator'
|
|
||||||
import { useExamSecurity } from '@/utils/hooks/useExamSecurity'
|
|
||||||
import { useStoreState } from '@/store/store'
|
|
||||||
import { generateMockPDFTest } from '@/mocks/mockTests'
|
|
||||||
|
|
||||||
const PDFTestInterface: React.FC = () => {
|
|
||||||
const { id } = useParams<{ id: string }>()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const userId = useStoreState((state) => state.auth.user.id)
|
|
||||||
|
|
||||||
// TODO: Replace with actual API call to fetch test by id
|
|
||||||
const exam = generateMockPDFTest()
|
|
||||||
|
|
||||||
if (!exam || exam.id !== id) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
|
||||||
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-6 text-center">
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Test Bulunamadı</h2>
|
|
||||||
<p className="text-gray-600 mb-6">İstenen test bulunamadı veya erişim yetkiniz yok.</p>
|
|
||||||
<button
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
Geri Dön
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const [session, setSession] = useState<ExamSession>({
|
|
||||||
id: `session-${Date.now()}`,
|
|
||||||
examId: exam.id,
|
|
||||||
studentId: userId,
|
|
||||||
startTime: new Date(),
|
|
||||||
answers: [],
|
|
||||||
status: 'in-progress',
|
|
||||||
timeRemaining: exam.timeLimit * 60,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [answerKeyResponses, setAnswerKeyResponses] = useState<Record<string, string | string[]>>(
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
const [securityWarning, setSecurityWarning] = useState<{
|
|
||||||
show: boolean
|
|
||||||
message: string
|
|
||||||
type: 'warning' | 'error' | 'info'
|
|
||||||
}>({ show: false, message: '', type: 'warning' })
|
|
||||||
|
|
||||||
// Security configuration
|
|
||||||
const securityConfig = {
|
|
||||||
disableRightClick: true,
|
|
||||||
disableCopyPaste: true,
|
|
||||||
disableDevTools: true,
|
|
||||||
fullScreenMode: true,
|
|
||||||
preventTabSwitch: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
useExamSecurity(securityConfig, session.status === 'in-progress')
|
|
||||||
|
|
||||||
// Save test session to backend/localStorage
|
|
||||||
const saveTestSession = (sessionData: ExamSession) => {
|
|
||||||
// TODO: Replace with actual API call
|
|
||||||
console.log('Saving test session:', sessionData)
|
|
||||||
localStorage.setItem(`test-session-${sessionData.examId}`, JSON.stringify(sessionData))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete test and navigate
|
|
||||||
const handleTestComplete = (sessionData: ExamSession) => {
|
|
||||||
// TODO: Replace with actual API call
|
|
||||||
console.log('Test completed:', sessionData)
|
|
||||||
saveTestSession(sessionData)
|
|
||||||
// Navigate to results or dashboard
|
|
||||||
// navigate('/admin/coordinator/tests')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-save mechanism
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if (session.status === 'in-progress') {
|
|
||||||
saveTestSession(session)
|
|
||||||
}
|
|
||||||
}, 30000)
|
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [session])
|
|
||||||
|
|
||||||
const handleAnswerChange = (itemId: string, answer: string | string[]) => {
|
|
||||||
setAnswerKeyResponses((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[itemId]: answer,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Update session answers
|
|
||||||
setSession((prev) => {
|
|
||||||
const existingAnswerIndex = prev.answers.findIndex((a) => a.questionId === itemId)
|
|
||||||
const newAnswer: StudentAnswer = {
|
|
||||||
questionId: itemId,
|
|
||||||
answer,
|
|
||||||
timeSpent: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
const newAnswers =
|
|
||||||
existingAnswerIndex >= 0
|
|
||||||
? prev.answers.map((a, i) => (i === existingAnswerIndex ? newAnswer : a))
|
|
||||||
: [...prev.answers, newAnswer]
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
answers: newAnswers,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTimeUp = () => {
|
|
||||||
setSecurityWarning({
|
|
||||||
show: true,
|
|
||||||
message: 'Süre doldu! Test otomatik olarak teslim ediliyor...',
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
completeExam()
|
|
||||||
}, 3000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const completeExam = () => {
|
|
||||||
const completedSession: ExamSession = {
|
|
||||||
...session,
|
|
||||||
endTime: new Date(),
|
|
||||||
status: 'completed',
|
|
||||||
}
|
|
||||||
|
|
||||||
setSession(completedSession)
|
|
||||||
handleTestComplete(completedSession)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmitExam = () => {
|
|
||||||
const unanswered =
|
|
||||||
exam.answerKeyTemplate?.filter(
|
|
||||||
(item) =>
|
|
||||||
!answerKeyResponses[item.id] ||
|
|
||||||
(Array.isArray(answerKeyResponses[item.id]) &&
|
|
||||||
(answerKeyResponses[item.id] as string[]).length === 0) ||
|
|
||||||
(typeof answerKeyResponses[item.id] === 'string' && !answerKeyResponses[item.id]),
|
|
||||||
) || []
|
|
||||||
|
|
||||||
if (unanswered.length > 0) {
|
|
||||||
const confirmSubmit = window.confirm(
|
|
||||||
`${unanswered.length} soru cevaplanmamış. Yine de testi teslim etmek istiyor musunuz?`,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!confirmSubmit) return
|
|
||||||
}
|
|
||||||
|
|
||||||
completeExam()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAnsweredCount = () => {
|
|
||||||
return (
|
|
||||||
exam.answerKeyTemplate?.filter((item) => {
|
|
||||||
const answer = answerKeyResponses[item.id]
|
|
||||||
if (Array.isArray(answer)) {
|
|
||||||
return answer.length > 0 && answer.some((a) => a.trim() !== '')
|
|
||||||
}
|
|
||||||
return answer && answer.toString().trim() !== ''
|
|
||||||
}).length || 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderAnswerKeyItem = (item: AnswerKeyItem) => {
|
|
||||||
const currentAnswer = answerKeyResponses[item.id]
|
|
||||||
|
|
||||||
switch (item.type) {
|
|
||||||
case 'multiple-choice':
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{item.options?.map((option, index) => (
|
|
||||||
<label key={index} className="flex items-center space-x-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name={`question-${item.id}`}
|
|
||||||
value={option}
|
|
||||||
checked={currentAnswer === option}
|
|
||||||
onChange={(e) => handleAnswerChange(item.id, e.target.value)}
|
|
||||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700">{option}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'fill-blank':
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={(currentAnswer as string) || ''}
|
|
||||||
onChange={(e) => handleAnswerChange(item.id, e.target.value)}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="Cevabınızı yazın..."
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'true-false':
|
|
||||||
return (
|
|
||||||
<div className="flex space-x-4">
|
|
||||||
<label className="flex items-center space-x-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name={`question-${item.id}`}
|
|
||||||
value="true"
|
|
||||||
checked={currentAnswer === 'true'}
|
|
||||||
onChange={(e) => handleAnswerChange(item.id, e.target.value)}
|
|
||||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700">Doğru</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center space-x-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name={`question-${item.id}`}
|
|
||||||
value="false"
|
|
||||||
checked={currentAnswer === 'false'}
|
|
||||||
onChange={(e) => handleAnswerChange(item.id, e.target.value)}
|
|
||||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700">Yanlış</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.status === 'completed') {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
|
||||||
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-6 text-center">
|
|
||||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<FaCheckCircle className="w-8 h-8 text-green-600" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Test Tamamlandı!</h2>
|
|
||||||
<p className="text-gray-600 mb-6">
|
|
||||||
Testiniz başarıyla teslim edildi. Sonuçlarınız değerlendirildikten sonra
|
|
||||||
bilgilendirileceksiniz.
|
|
||||||
</p>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
Başlama: {session.startTime.toLocaleString('tr-TR')}
|
|
||||||
<br />
|
|
||||||
Bitiş: {session.endTime?.toLocaleString('tr-TR')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50">
|
|
||||||
<SecurityWarning
|
|
||||||
isVisible={securityWarning.show}
|
|
||||||
onDismiss={() => setSecurityWarning((prev) => ({ ...prev, show: false }))}
|
|
||||||
message={securityWarning.message}
|
|
||||||
type={securityWarning.type}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<header className="bg-white border-b border-gray-200 px-4 py-3 sticky top-0 z-40">
|
|
||||||
<div className="mx-auto flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-semibold text-gray-900">{exam.title}</h1>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
PDF Test - {getAnsweredCount()} / {exam.answerKeyTemplate?.length || 0} cevaplandı
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<ExamTimer initialTime={exam.timeLimit * 60} onTimeUp={handleTimeUp} autoStart={true} />
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleSubmitExam}
|
|
||||||
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
Testi Teslim Et
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<div className="mx-auto p-4">
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
|
||||||
{/* Document Viewer */}
|
|
||||||
<div className="lg:col-span-3 bg-white border border-gray-200 rounded-lg p-4">
|
|
||||||
<div className="flex items-center space-x-2 mb-4">
|
|
||||||
{exam.testDocument?.type === 'pdf' ? (
|
|
||||||
<FaFileAlt className="w-5 h-5 text-red-600" />
|
|
||||||
) : (
|
|
||||||
<FaImage className="w-5 h-5 text-blue-600" />
|
|
||||||
)}
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Test Dokümanı</h3>
|
|
||||||
<span className="text-sm text-gray-500">({exam.testDocument?.name})</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{exam.testDocument?.type === 'pdf' ? (
|
|
||||||
<div
|
|
||||||
className="border border-gray-300 rounded-lg overflow-hidden"
|
|
||||||
style={{ height: '600px' }}
|
|
||||||
>
|
|
||||||
<iframe src={exam.testDocument.url} className="w-full h-full" title="Test PDF" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="border border-gray-300 rounded-lg overflow-hidden">
|
|
||||||
<img
|
|
||||||
src={exam.testDocument?.url}
|
|
||||||
alt="Test Image"
|
|
||||||
className="w-full h-auto max-h-96 object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Answer Key */}
|
|
||||||
<div className="lg:col-span-2 bg-white border border-gray-200 rounded-lg p-4">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Cevap Anahtarı</h3>
|
|
||||||
|
|
||||||
<div className="space-y-6 max-h-96 overflow-y-auto">
|
|
||||||
{exam.answerKeyTemplate?.map((item) => {
|
|
||||||
const isAnswered =
|
|
||||||
answerKeyResponses[item.id] &&
|
|
||||||
(Array.isArray(answerKeyResponses[item.id])
|
|
||||||
? (answerKeyResponses[item.id] as string[]).length > 0
|
|
||||||
: answerKeyResponses[item.id].toString().trim() !== '')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className={`p-4 border-2 rounded-lg transition-all ${
|
|
||||||
isAnswered ? 'border-green-200 bg-green-50' : 'border-gray-200 bg-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="bg-blue-100 text-blue-800 text-sm font-medium px-2 py-1 rounded">
|
|
||||||
Soru {item.questionNumber}
|
|
||||||
</span>
|
|
||||||
<span className="bg-green-100 text-green-800 text-sm font-medium px-2 py-1 rounded">
|
|
||||||
{item.points} Puan
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{isAnswered && <FaCheckCircle className="w-5 h-5 text-green-600" />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{renderAnswerKeyItem(item)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress Summary */}
|
|
||||||
<div className="mt-6 pt-4 border-t border-gray-200">
|
|
||||||
<div className="flex items-center justify-between text-sm text-gray-600">
|
|
||||||
<span>
|
|
||||||
İlerleme: {getAnsweredCount()} / {exam.answerKeyTemplate?.length || 0}
|
|
||||||
</span>
|
|
||||||
<span>Toplam Puan: {exam.totalPoints}</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 w-full bg-gray-200 rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
|
||||||
style={{
|
|
||||||
width: `${(getAnsweredCount() / (exam.answerKeyTemplate?.length || 1)) * 100}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PDFTestInterface
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { CalculationQuestion } from '../QuestionTypes/CalculationQuestion';
|
|
||||||
import { FillBlankQuestion } from '../QuestionTypes/FillBlankQuestion';
|
|
||||||
import { MatchingQuestion } from '../QuestionTypes/MatchingQuestion';
|
|
||||||
import { MultipleAnswerQuestion } from '../QuestionTypes/MultipleAnswerQuestion';
|
|
||||||
import { MultipleChoiceQuestion } from '../QuestionTypes/MultipleChoiceQuestion';
|
|
||||||
import { OpenEndedQuestion } from '../QuestionTypes/OpenEndedQuestion';
|
|
||||||
import { OrderingQuestion } from '../QuestionTypes/OrderingQuestion';
|
|
||||||
import { TrueFalseQuestion } from '../QuestionTypes/TrueFalseQuestion';
|
|
||||||
import { QuestionDto, StudentAnswer } from '@/types/coordinator';
|
|
||||||
|
|
||||||
interface QuestionRendererProps {
|
|
||||||
question: QuestionDto;
|
|
||||||
answer?: StudentAnswer;
|
|
||||||
onAnswerChange: (questionId: string, answer: string | string[]) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
showCorrectAnswer?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const QuestionRenderer: React.FC<QuestionRendererProps> = ({
|
|
||||||
question,
|
|
||||||
answer,
|
|
||||||
onAnswerChange,
|
|
||||||
disabled = false,
|
|
||||||
showCorrectAnswer = false
|
|
||||||
}) => {
|
|
||||||
const renderQuestion = () => {
|
|
||||||
const commonProps = {
|
|
||||||
question,
|
|
||||||
answer,
|
|
||||||
onAnswerChange,
|
|
||||||
disabled,
|
|
||||||
showCorrectAnswer
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (question.questionType) {
|
|
||||||
case 'multiple-choice':
|
|
||||||
return <MultipleChoiceQuestion {...commonProps} />;
|
|
||||||
|
|
||||||
case 'fill-blank':
|
|
||||||
return <FillBlankQuestion {...commonProps} />;
|
|
||||||
|
|
||||||
case 'true-false':
|
|
||||||
return <TrueFalseQuestion {...commonProps} />;
|
|
||||||
|
|
||||||
case 'open-ended':
|
|
||||||
return <OpenEndedQuestion {...commonProps} />;
|
|
||||||
|
|
||||||
case 'multiple-answer':
|
|
||||||
return <MultipleAnswerQuestion {...commonProps} />;
|
|
||||||
|
|
||||||
case 'matching':
|
|
||||||
return <MatchingQuestion {...commonProps} />;
|
|
||||||
|
|
||||||
case 'ordering':
|
|
||||||
return <OrderingQuestion {...commonProps} />;
|
|
||||||
|
|
||||||
case 'calculation':
|
|
||||||
return <CalculationQuestion {...commonProps} />;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<div className="p-4 border border-gray-200 rounded-lg bg-gray-50">
|
|
||||||
<p className="text-gray-500">Desteklenmeyen soru tipi: {question.questionType}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-lg border border-gray-200 p-6">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
||||||
{getQuestionTypeLabel(question.questionType)}
|
|
||||||
</span>
|
|
||||||
{question.points && (
|
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
||||||
{question.points} Puan
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{question.difficulty && (
|
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
||||||
question.difficulty === 'easy'
|
|
||||||
? 'bg-green-100 text-green-800'
|
|
||||||
: question.difficulty === 'medium'
|
|
||||||
? 'bg-yellow-100 text-yellow-800'
|
|
||||||
: 'bg-red-100 text-red-800'
|
|
||||||
}`}>
|
|
||||||
{getDifficultyLabel(question.difficulty)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{question.timeLimit && (
|
|
||||||
<span className="text-sm text-gray-500">
|
|
||||||
Süre: {question.timeLimit} dk
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{renderQuestion()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getQuestionTypeLabel = (questionType: string): string => {
|
|
||||||
const labels: Record<string, string> = {
|
|
||||||
'multiple-choice': 'Çoktan Seçmeli',
|
|
||||||
'fill-blank': 'Boşluk Doldurma',
|
|
||||||
'true-false': 'Doğru-Yanlış',
|
|
||||||
'open-ended': 'Açık Uçlu',
|
|
||||||
'multiple-answer': 'Çok Yanıtlı',
|
|
||||||
'matching': 'Eşleştirme',
|
|
||||||
'ordering': 'Sıralama',
|
|
||||||
'calculation': 'Hesaplama'
|
|
||||||
};
|
|
||||||
return labels[questionType] || questionType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDifficultyLabel = (difficulty: string): string => {
|
|
||||||
const labels: Record<string, string> = {
|
|
||||||
'easy': 'Kolay',
|
|
||||||
'medium': 'Orta',
|
|
||||||
'hard': 'Zor'
|
|
||||||
};
|
|
||||||
return labels[difficulty] || difficulty;
|
|
||||||
};
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface SecurityWarningProps {
|
|
||||||
isVisible: boolean;
|
|
||||||
onDismiss: () => void;
|
|
||||||
message: string;
|
|
||||||
type: 'warning' | 'error' | 'info';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SecurityWarning: React.FC<SecurityWarningProps> = ({
|
|
||||||
isVisible,
|
|
||||||
onDismiss,
|
|
||||||
message,
|
|
||||||
type = 'warning'
|
|
||||||
}) => {
|
|
||||||
if (!isVisible) return null;
|
|
||||||
|
|
||||||
const getColors = () => {
|
|
||||||
switch (type) {
|
|
||||||
case 'error':
|
|
||||||
return 'bg-red-50 border-red-200 text-red-800';
|
|
||||||
case 'info':
|
|
||||||
return 'bg-blue-50 border-blue-200 text-blue-800';
|
|
||||||
default:
|
|
||||||
return 'bg-yellow-50 border-yellow-200 text-yellow-800';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getIcon = () => {
|
|
||||||
switch (type) {
|
|
||||||
case 'error':
|
|
||||||
return (
|
|
||||||
<svg className="w-5 h-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
case 'info':
|
|
||||||
return (
|
|
||||||
<svg className="w-5 h-5 text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<svg className="w-5 h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 z-50 max-w-md w-full mx-4">
|
|
||||||
<div className={`border rounded-lg p-4 ${getColors()}`}>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
{getIcon()}
|
|
||||||
</div>
|
|
||||||
<div className="ml-3">
|
|
||||||
<p className="text-sm font-medium">
|
|
||||||
{message}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="ml-auto pl-3">
|
|
||||||
<div className="-mx-1.5 -my-1.5">
|
|
||||||
<button
|
|
||||||
onClick={onDismiss}
|
|
||||||
className="inline-flex rounded-md p-1.5 hover:bg-yellow-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-yellow-50 focus:ring-yellow-600"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Dismiss</span>
|
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,601 +0,0 @@
|
||||||
import { Exam, AnswerKeyItem } from "@/types/coordinator";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import {
|
|
||||||
FaUpload,
|
|
||||||
FaFileAlt,
|
|
||||||
FaImage,
|
|
||||||
FaPlus,
|
|
||||||
FaTrash,
|
|
||||||
FaSave,
|
|
||||||
} from "react-icons/fa";
|
|
||||||
|
|
||||||
interface TestCreatorProps {
|
|
||||||
onCreateTest: (
|
|
||||||
test: Omit<Exam, "id" | "creationTime" | "lastModificationTime">
|
|
||||||
) => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
editingTest?: Exam;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TestCreator: React.FC<TestCreatorProps> = ({
|
|
||||||
onCreateTest,
|
|
||||||
onCancel,
|
|
||||||
editingTest,
|
|
||||||
}) => {
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
title: editingTest?.title || "",
|
|
||||||
description: editingTest?.description || "",
|
|
||||||
timeLimit: editingTest?.timeLimit || 60,
|
|
||||||
passingScore: editingTest?.passingScore || 60,
|
|
||||||
maxAttempts: editingTest?.maxAttempts || 1,
|
|
||||||
allowReview: editingTest?.allowReview ?? true,
|
|
||||||
showResults: editingTest?.showResults ?? true,
|
|
||||||
isActive: editingTest?.isActive ?? true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [testDocument, setTestDocument] = useState<{
|
|
||||||
url: string;
|
|
||||||
type: "pdf" | "image";
|
|
||||||
name: string;
|
|
||||||
}>(editingTest?.testDocument || { url: "", type: "pdf", name: "" });
|
|
||||||
|
|
||||||
const [answerKeyTemplate, setAnswerKeyTemplate] = useState<AnswerKeyItem[]>(
|
|
||||||
editingTest?.answerKeyTemplate || []
|
|
||||||
);
|
|
||||||
const [bulkAnswerMode, setBulkAnswerMode] = useState(false);
|
|
||||||
const [bulkAnswers, setBulkAnswers] = useState("");
|
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: any) => {
|
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDocumentChange = (field: string, value: any) => {
|
|
||||||
setTestDocument((prev) => ({ ...prev, [field]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const addAnswerKeyItem = () => {
|
|
||||||
const newItem: AnswerKeyItem = {
|
|
||||||
id: `item-${Date.now()}`,
|
|
||||||
questionNumber: answerKeyTemplate.length + 1,
|
|
||||||
type: "multiple-choice",
|
|
||||||
options: ["A", "B", "C", "D"],
|
|
||||||
points: 10,
|
|
||||||
};
|
|
||||||
setAnswerKeyTemplate((prev) => [...prev, newItem]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateAnswerKeyItem = (index: number, field: string, value: any) => {
|
|
||||||
setAnswerKeyTemplate((prev) =>
|
|
||||||
prev.map((item, i) => (i === index ? { ...item, [field]: value } : item))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeAnswerKeyItem = (index: number) => {
|
|
||||||
setAnswerKeyTemplate((prev) => prev.filter((_, i) => i !== index));
|
|
||||||
// Renumber remaining items
|
|
||||||
setAnswerKeyTemplate((prev) =>
|
|
||||||
prev.map((item, i) => ({
|
|
||||||
...item,
|
|
||||||
questionNumber: i + 1,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBulkAnswerSubmit = () => {
|
|
||||||
const answers = bulkAnswers.split("\n").filter((line) => line.trim());
|
|
||||||
const newItems: AnswerKeyItem[] = answers.map((answer, index) => ({
|
|
||||||
id: `item-${Date.now()}-${index}`,
|
|
||||||
questionNumber: index + 1,
|
|
||||||
type: "multiple-choice",
|
|
||||||
options: ["A", "B", "C", "D"],
|
|
||||||
points: 10,
|
|
||||||
correctAnswer: answer.trim().toUpperCase(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
setAnswerKeyTemplate(newItems);
|
|
||||||
setBulkAnswerMode(false);
|
|
||||||
setBulkAnswers("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (!formData.title.trim()) {
|
|
||||||
alert("Lütfen test başlığını girin.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!testDocument.url.trim()) {
|
|
||||||
alert("Lütfen test dokümanını yükleyin.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (answerKeyTemplate.length === 0) {
|
|
||||||
alert("Lütfen en az bir cevap anahtarı öğesi ekleyin.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalPoints = answerKeyTemplate.reduce(
|
|
||||||
(sum, item) => sum + item.points,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
const testData = {
|
|
||||||
...formData,
|
|
||||||
type: "test" as const,
|
|
||||||
testDocument,
|
|
||||||
answerKeyTemplate,
|
|
||||||
questions: [], // PDF testlerde soru listesi boş
|
|
||||||
totalPoints,
|
|
||||||
randomizeQuestions: false, // PDF testlerde sabit sıralama
|
|
||||||
};
|
|
||||||
|
|
||||||
onCreateTest(testData);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderAnswerKeyItemEditor = (item: AnswerKeyItem, index: number) => {
|
|
||||||
return (
|
|
||||||
<div key={item.id} className="border border-gray-200 rounded-lg p-3">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h4 className="text-sm font-medium text-gray-900">
|
|
||||||
<span className="inline-block bg-blue-100 text-blue-800 text-xs font-semibold px-2 py-1 rounded">
|
|
||||||
Soru {item.questionNumber}
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
<button
|
|
||||||
onClick={() => removeAnswerKeyItem(index)}
|
|
||||||
className="text-red-500 hover:text-red-700"
|
|
||||||
>
|
|
||||||
<FaTrash className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Soru Tipi
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={item.type}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateAnswerKeyItem(index, "type", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="multiple-choice">Çoktan Seçmeli</option>
|
|
||||||
<option value="fill-blank">Boşluk Doldurma</option>
|
|
||||||
<option value="true-false">Doğru-Yanlış</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Soru Numarası
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={item.questionNumber}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateAnswerKeyItem(
|
|
||||||
index,
|
|
||||||
"questionNumber",
|
|
||||||
parseInt(e.target.value)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
min="1"
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Puan
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={item.points}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateAnswerKeyItem(index, "points", parseInt(e.target.value))
|
|
||||||
}
|
|
||||||
min="1"
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{item.type === "multiple-choice" && (
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Seçenekler (virgülle ayırın)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={item.options?.join(", ") || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const options = e.target.value
|
|
||||||
.split(",")
|
|
||||||
.map((opt) => opt.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
updateAnswerKeyItem(index, "options", options);
|
|
||||||
}}
|
|
||||||
placeholder="A, B, C, D"
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Doğru Cevap (Opsiyonel - değerlendirme için)
|
|
||||||
</label>
|
|
||||||
{item.type === "multiple-choice" ? (
|
|
||||||
<select
|
|
||||||
value={(item.correctAnswer as string) || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateAnswerKeyItem(index, "correctAnswer", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="">Seçin...</option>
|
|
||||||
{item.options?.map((option) => (
|
|
||||||
<option key={option} value={option}>
|
|
||||||
{option}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
) : item.type === "true-false" ? (
|
|
||||||
<select
|
|
||||||
value={(item.correctAnswer as string) || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateAnswerKeyItem(index, "correctAnswer", e.target.value)
|
|
||||||
}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="">Seçin...</option>
|
|
||||||
<option value="true">Doğru</option>
|
|
||||||
<option value="false">Yanlış</option>
|
|
||||||
</select>
|
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={(item.correctAnswer as string) || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateAnswerKeyItem(index, "correctAnswer", e.target.value)
|
|
||||||
}
|
|
||||||
placeholder="Doğru cevabı girin..."
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-bold text-gray-900">
|
|
||||||
{editingTest ? "Testi Düzenle" : "Yeni PDF Test Oluştur"}
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-gray-600 mt-0.5">
|
|
||||||
PDF test belgesi yükleyin ve cevap anahtarını tanımlayın
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={onCancel}
|
|
||||||
className="flex items-center space-x-2 bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-1.5 text-sm rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<span>İptal</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-1.5 text-sm rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<FaSave className="w-3.5 h-3.5" />
|
|
||||||
<span>{editingTest ? "Güncelle" : "Oluştur"}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Basic Information */}
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
|
||||||
Test Başlığı
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.title}
|
|
||||||
onChange={(e) => handleInputChange("title", e.target.value)}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="Test başlığını girin"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
|
||||||
Süre (dakika)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={formData.timeLimit}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("timeLimit", parseInt(e.target.value))
|
|
||||||
}
|
|
||||||
min="1"
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
|
||||||
Açıklama
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={formData.description}
|
|
||||||
onChange={(e) => handleInputChange("description", e.target.value)}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
rows={3}
|
|
||||||
placeholder="Test açıklamasını girin"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Document Upload */}
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center">
|
|
||||||
<FaUpload className="w-4 h-4 mr-2" />
|
|
||||||
Test Dokümanı
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Dosya Tipi
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={testDocument.type}
|
|
||||||
onChange={(e) => handleDocumentChange("type", e.target.value)}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="pdf">PDF</option>
|
|
||||||
<option value="image">Resim</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Dosya Adı
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={testDocument.name}
|
|
||||||
onChange={(e) => handleDocumentChange("name", e.target.value)}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="Dosya adını girin"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Dosya URL'si
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
value={testDocument.url}
|
|
||||||
onChange={(e) => handleDocumentChange("url", e.target.value)}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="https://example.com/test.pdf"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{testDocument.url && (
|
|
||||||
<div className="mt-3 p-3 bg-gray-50 rounded-lg">
|
|
||||||
<div className="flex items-center space-x-2 text-xs text-gray-600">
|
|
||||||
{testDocument.type === "pdf" ? (
|
|
||||||
<FaFileAlt className="w-3.5 h-3.5 text-red-600" />
|
|
||||||
) : (
|
|
||||||
<FaImage className="w-3.5 h-3.5 text-blue-600" />
|
|
||||||
)}
|
|
||||||
<span>Önizleme: {testDocument.name || "Dosya"}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Answer Key Template */}
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900">
|
|
||||||
Cevap Anahtarı Şablonu
|
|
||||||
</h3>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setBulkAnswerMode(true)}
|
|
||||||
className="flex items-center space-x-2 bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 text-sm rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<span>Toplu Cevap</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={addAnswerKeyItem}
|
|
||||||
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 text-sm rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<FaPlus className="w-3.5 h-3.5" />
|
|
||||||
<span>Soru Ekle</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bulk Answer Mode */}
|
|
||||||
{bulkAnswerMode && (
|
|
||||||
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
|
||||||
<h4 className="text-sm font-medium text-blue-900 mb-2">
|
|
||||||
Toplu Cevap Girişi
|
|
||||||
</h4>
|
|
||||||
<p className="text-xs text-blue-700 mb-2">
|
|
||||||
Her satıra bir cevap yazın (A, B, C, D). Tüm sorular çoktan
|
|
||||||
seçmeli olarak oluşturulacak.
|
|
||||||
</p>
|
|
||||||
<textarea
|
|
||||||
value={bulkAnswers}
|
|
||||||
onChange={(e) => setBulkAnswers(e.target.value)}
|
|
||||||
placeholder="A B C D A"
|
|
||||||
className="w-full text-sm h-32 border border-blue-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<div className="flex space-x-2 mt-2">
|
|
||||||
<button
|
|
||||||
onClick={handleBulkAnswerSubmit}
|
|
||||||
className="px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
Uygula
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setBulkAnswerMode(false);
|
|
||||||
setBulkAnswers("");
|
|
||||||
}}
|
|
||||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 font-medium transition-colors"
|
|
||||||
>
|
|
||||||
İptal
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{answerKeyTemplate.length > 0 ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{answerKeyTemplate.map((item, index) =>
|
|
||||||
renderAnswerKeyItemEditor(item, index)
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Special Question Types Warning */}
|
|
||||||
{answerKeyTemplate.some(
|
|
||||||
(item) => item.type !== "multiple-choice"
|
|
||||||
) && (
|
|
||||||
<div className="mt-3 p-2.5 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4 text-yellow-600"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span className="text-xs font-medium text-yellow-800">
|
|
||||||
Bu testte çoktan seçmeli olmayan sorular bulunmaktadır
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
|
||||||
<div className="flex items-center justify-between text-xs">
|
|
||||||
<span className="text-blue-800 font-medium">
|
|
||||||
Toplam Soru: {answerKeyTemplate.length}
|
|
||||||
</span>
|
|
||||||
<span className="text-blue-800 font-medium">
|
|
||||||
Toplam Puan:{" "}
|
|
||||||
{answerKeyTemplate.reduce(
|
|
||||||
(sum, item) => sum + item.points,
|
|
||||||
0
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-6 text-gray-500">
|
|
||||||
<p className="text-sm">Henüz cevap anahtarı öğesi eklenmedi</p>
|
|
||||||
<p className="text-xs">
|
|
||||||
Yukarıdaki "Soru Ekle" butonuna tıklayarak başlayın
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Settings */}
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-3">Ayarlar</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
|
||||||
Geçme Notu (%)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={formData.passingScore}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("passingScore", parseInt(e.target.value))
|
|
||||||
}
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Maksimum Deneme
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={formData.maxAttempts}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("maxAttempts", parseInt(e.target.value))
|
|
||||||
}
|
|
||||||
min="1"
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2.5">
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.allowReview}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("allowReview", e.target.checked)
|
|
||||||
}
|
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<span className="ml-2 text-sm text-gray-700">
|
|
||||||
Cevap incelemeye izin ver
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.showResults}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("showResults", e.target.checked)
|
|
||||||
}
|
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<span className="ml-2 text-sm text-gray-700">Sonuçları göster</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.isActive}
|
|
||||||
onChange={(e) => handleInputChange("isActive", e.target.checked)}
|
|
||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<span className="ml-2 text-sm text-gray-700">Aktif</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,246 +0,0 @@
|
||||||
import React, { useState } from 'react'
|
|
||||||
import {
|
|
||||||
FaPlus,
|
|
||||||
FaSearch,
|
|
||||||
FaFilter,
|
|
||||||
FaEdit,
|
|
||||||
FaTrash,
|
|
||||||
FaClock,
|
|
||||||
FaUsers,
|
|
||||||
FaCalendar,
|
|
||||||
FaPlay,
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { generateMockExam } from '@/mocks/mockExams'
|
|
||||||
import { generateMockPools } from '@/mocks/mockPools'
|
|
||||||
import { Exam, QuestionPoolDto } from '@/types/coordinator'
|
|
||||||
import { ExamCreator } from './ExamInterface/ExamCreator'
|
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
||||||
|
|
||||||
const Exams: React.FC = () => {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const [exams, setExams] = useState<Exam[]>(generateMockExam())
|
|
||||||
const [pools] = useState<QuestionPoolDto[]>(generateMockPools())
|
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
|
||||||
const [statusFilter, setStatusFilter] = useState('')
|
|
||||||
const [isCreating, setIsCreating] = useState(false)
|
|
||||||
const [editingExam, setEditingExam] = useState<Exam | null>(null)
|
|
||||||
|
|
||||||
const examList = exams.filter((exam) => exam.type === 'exam')
|
|
||||||
|
|
||||||
const filteredExams = examList.filter((exam) => {
|
|
||||||
const matchesSearch =
|
|
||||||
exam.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
exam.description.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
const matchesStatus =
|
|
||||||
!statusFilter ||
|
|
||||||
(statusFilter === 'active' && exam.isActive) ||
|
|
||||||
(statusFilter === 'inactive' && !exam.isActive)
|
|
||||||
return matchesSearch && matchesStatus
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleCreateExam = (
|
|
||||||
examData: Omit<Exam, 'id' | 'creationTime' | 'lastModificationTime'>,
|
|
||||||
) => {
|
|
||||||
const newExam: Exam = {
|
|
||||||
...examData,
|
|
||||||
type: 'exam',
|
|
||||||
id: `exam-${Date.now()}`,
|
|
||||||
creationTime: new Date(),
|
|
||||||
lastModificationTime: new Date(),
|
|
||||||
}
|
|
||||||
setExams((prev) => [...prev, newExam])
|
|
||||||
setIsCreating(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdateExam = (
|
|
||||||
examData: Omit<Exam, 'id' | 'creationTime' | 'lastModificationTime'>,
|
|
||||||
) => {
|
|
||||||
if (editingExam) {
|
|
||||||
const updatedExam: Exam = {
|
|
||||||
...examData,
|
|
||||||
id: editingExam.id,
|
|
||||||
creationTime: editingExam.creationTime,
|
|
||||||
lastModificationTime: new Date(),
|
|
||||||
}
|
|
||||||
setExams((prev) => prev.map((exam) => (exam.id === updatedExam.id ? updatedExam : exam)))
|
|
||||||
setEditingExam(null)
|
|
||||||
setIsCreating(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEdit = (exam: Exam) => {
|
|
||||||
setEditingExam(exam)
|
|
||||||
setIsCreating(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setIsCreating(false)
|
|
||||||
setEditingExam(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCreating) {
|
|
||||||
return (
|
|
||||||
<ExamCreator
|
|
||||||
pools={pools}
|
|
||||||
onCreateExam={editingExam ? handleUpdateExam : handleCreateExam}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
editingExam={editingExam || undefined}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-bold text-gray-900">Sınav Yönetimi</h1>
|
|
||||||
<p className="text-sm text-gray-600 mt-0.5">Sınavları oluşturun ve yönetin</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setIsCreating(true)}
|
|
||||||
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<FaPlus className="w-3.5 h-3.5" />
|
|
||||||
<span>Yeni Sınav</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters */}
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
||||||
<div className="relative">
|
|
||||||
<FaSearch className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Sınav ara..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="pl-8 text-sm w-full border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<FaFilter className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
|
|
||||||
<select
|
|
||||||
value={statusFilter}
|
|
||||||
onChange={(e) => setStatusFilter(e.target.value)}
|
|
||||||
className="pl-8 text-sm w-full border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 appearance-none"
|
|
||||||
>
|
|
||||||
<option value="">Tüm durumlar</option>
|
|
||||||
<option value="active">Aktif</option>
|
|
||||||
<option value="inactive">Pasif</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm text-gray-600 flex items-center">
|
|
||||||
Toplam: {filteredExams.length} sınav
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Exams List */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
{filteredExams.length > 0 ? (
|
|
||||||
filteredExams.map((exam) => (
|
|
||||||
<div
|
|
||||||
key={exam.id}
|
|
||||||
className="bg-white border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center space-x-2 mb-1.5">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900">{exam.title}</h3>
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
||||||
exam.isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{exam.isActive ? 'Aktif' : 'Pasif'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-gray-600 mb-2">{exam.description}</p>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<FaClock className="w-4 h-4 text-gray-400" />
|
|
||||||
<span className="text-gray-600">{exam.timeLimit} dakika</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<FaUsers className="w-4 h-4 text-gray-400" />
|
|
||||||
<span className="text-gray-600">{exam.questions.length} soru</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<FaCalendar className="w-4 h-4 text-gray-400" />
|
|
||||||
<span className="text-gray-600">Toplam: {exam.totalPoints} puan</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-gray-600">Geçme: %{exam.passingScore}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{exam.startTime && exam.endTime && (
|
|
||||||
<div className="mt-2 p-2 bg-blue-50 rounded">
|
|
||||||
<div className="text-xs text-blue-700">
|
|
||||||
<strong>Başlangıç:</strong> {exam.startTime.toLocaleString('tr-TR')} -
|
|
||||||
<strong> Bitiş:</strong> {exam.endTime.toLocaleString('tr-TR')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-1.5 ml-4">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate(ROUTES_ENUM.protected.coordinator.examDetail.replace(':id', exam.id))}
|
|
||||||
className="flex items-center space-x-1 px-2.5 py-1.5 bg-green-600 hover:bg-green-700 text-white text-xs rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<FaPlay className="w-3 h-3" />
|
|
||||||
<span>Başlat</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleEdit(exam)}
|
|
||||||
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
title="Düzenle"
|
|
||||||
>
|
|
||||||
<FaEdit className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (window.confirm('Bu sınavı silmek istediğinizden emin misiniz?')) {
|
|
||||||
setExams((prev) => prev.filter((e) => e.id !== exam.id))
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
||||||
title="Sil"
|
|
||||||
>
|
|
||||||
<FaTrash className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<FaSearch className="w-6 h-6 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-base font-medium text-gray-900 mb-1">Sınav bulunamadı</h3>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
{searchTerm || statusFilter
|
|
||||||
? 'Arama kriterlerinizi değiştirin veya yeni sınav oluşturun.'
|
|
||||||
: 'İlk sınavınızı oluşturun.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Exams
|
|
||||||
|
|
@ -1,359 +0,0 @@
|
||||||
import { questionService } from '@/services/question.service'
|
|
||||||
import { QUESTION_TYPE_LABELS, QuestionDto, QuestionOptionDto } from '@/types/coordinator'
|
|
||||||
import React, { useState, useEffect } from 'react'
|
|
||||||
import { FaSave, FaPlus, FaTrash, FaTimes } from 'react-icons/fa'
|
|
||||||
|
|
||||||
function QuestionDialog({
|
|
||||||
open,
|
|
||||||
onDialogClose,
|
|
||||||
id,
|
|
||||||
}: {
|
|
||||||
open: boolean
|
|
||||||
onDialogClose: () => void
|
|
||||||
id?: string
|
|
||||||
}) {
|
|
||||||
const [question, setQuestion] = useState<QuestionDto | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchQuestion = async () => {
|
|
||||||
if (!open) return
|
|
||||||
if (id) {
|
|
||||||
const entity = await questionService.getQuestion(id)
|
|
||||||
setQuestion(entity)
|
|
||||||
} else {
|
|
||||||
setQuestion({
|
|
||||||
id: '',
|
|
||||||
questionType: 'multiple-choice',
|
|
||||||
points: 10,
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
mediaUrl: '',
|
|
||||||
mediaType: 'image',
|
|
||||||
correctAnswer: '',
|
|
||||||
difficulty: 'medium',
|
|
||||||
timeLimit: 0,
|
|
||||||
explanation: '',
|
|
||||||
options: [],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchQuestion()
|
|
||||||
}, [open, id])
|
|
||||||
|
|
||||||
if (!open || !question) return null
|
|
||||||
|
|
||||||
// 🔧 Ortak alan değişimi
|
|
||||||
const handleChange = (field: keyof QuestionDto, value: any) => {
|
|
||||||
setQuestion((prev) => (prev ? { ...prev, [field]: value } : prev))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔹 Option işlemleri
|
|
||||||
const addOption = (isCorrect: boolean = false) => {
|
|
||||||
const newOption: QuestionOptionDto = {
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
text: '',
|
|
||||||
isCorrect,
|
|
||||||
}
|
|
||||||
setQuestion((prev) =>
|
|
||||||
prev ? { ...prev, options: [...(prev.options || []), newOption] } : prev,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateOption = (index: number, field: keyof QuestionOptionDto, value: any) => {
|
|
||||||
setQuestion((prev) => {
|
|
||||||
if (!prev) return prev
|
|
||||||
const newOpts = [...(prev.options || [])]
|
|
||||||
newOpts[index] = { ...newOpts[index], [field]: value }
|
|
||||||
return { ...prev, options: newOpts }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeOption = (index: number) => {
|
|
||||||
setQuestion((prev) => {
|
|
||||||
if (!prev) return prev
|
|
||||||
return { ...prev, options: prev.options?.filter((_, i) => i !== index) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 💾 Kaydetme
|
|
||||||
const handleSave = async () => {
|
|
||||||
if (!question.title.trim()) {
|
|
||||||
alert('Please fill in the title field.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (question.points <= 0) {
|
|
||||||
alert('Points must be greater than 0.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
['multiple-choice', 'multiple-answer'].includes(question.questionType) &&
|
|
||||||
(question.options?.length || 0) < 2
|
|
||||||
) {
|
|
||||||
alert('Please add at least 2 options.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
question.questionType === 'multiple-choice' &&
|
|
||||||
!question.options?.some((opt) => opt.isCorrect)
|
|
||||||
) {
|
|
||||||
alert('Please mark the correct answer.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataToSave: QuestionDto = {
|
|
||||||
...question,
|
|
||||||
correctAnswer: getCorrectAnswer(question),
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (question.id) await questionService.updateQuestion(question.id, dataToSave)
|
|
||||||
else await questionService.createQuestion(dataToSave)
|
|
||||||
onDialogClose()
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
alert('Error while saving question.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCorrectAnswer = (q: QuestionDto): string | string[] => {
|
|
||||||
switch (q.questionType) {
|
|
||||||
case 'multiple-answer':
|
|
||||||
const result = q.options?.filter((opt) => opt.isCorrect).map((opt) => opt.text!) || []
|
|
||||||
return result.join(', ')
|
|
||||||
case 'multiple-choice':
|
|
||||||
case 'true-false':
|
|
||||||
case 'fill-blank':
|
|
||||||
case 'open-ended':
|
|
||||||
case 'calculation':
|
|
||||||
return q.options?.find((opt) => opt.isCorrect)?.text || ''
|
|
||||||
case 'matching':
|
|
||||||
case 'ordering':
|
|
||||||
return q.options?.map((opt) => opt.text!) || []
|
|
||||||
default:
|
|
||||||
return q.options?.find((opt) => opt.isCorrect)?.text || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderQuestionTypeSpecificFields = () => {
|
|
||||||
switch (question.questionType) {
|
|
||||||
case 'multiple-choice':
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => addOption()}
|
|
||||||
className="flex items-center space-x-1 text-sm text-blue-600 hover:text-blue-700"
|
|
||||||
>
|
|
||||||
<FaPlus className="w-3.5 h-3.5" />
|
|
||||||
<span>Seçenek Ekle</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{question.options?.map((option, index) => (
|
|
||||||
<div
|
|
||||||
key={option.id}
|
|
||||||
className="flex items-center space-x-2.5 p-2.5 border border-gray-200 rounded-lg"
|
|
||||||
>
|
|
||||||
<div className="bg-blue-100 text-blue-800 text-sm font-medium px-2 py-1 rounded min-w-8 text-center">
|
|
||||||
{String.fromCharCode(65 + index)}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="correct-answer"
|
|
||||||
checked={option.isCorrect}
|
|
||||||
onChange={() =>
|
|
||||||
setQuestion((prev) => {
|
|
||||||
if (!prev) return prev
|
|
||||||
const updated = prev.options?.map((opt, i) => ({
|
|
||||||
...opt,
|
|
||||||
isCorrect: i === index,
|
|
||||||
}))
|
|
||||||
return { ...prev, options: updated }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="h-4 w-4 text-blue-600 border-gray-300"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={option.text}
|
|
||||||
onChange={(e) => updateOption(index, 'text', e.target.value)}
|
|
||||||
placeholder={`${String.fromCharCode(65 + index)} şıkkı`}
|
|
||||||
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeOption(index)}
|
|
||||||
className="text-red-500 hover:text-red-700"
|
|
||||||
>
|
|
||||||
<FaTrash className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'multiple-answer':
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => addOption()}
|
|
||||||
className="flex items-center space-x-1 text-sm text-blue-600 hover:text-blue-700"
|
|
||||||
>
|
|
||||||
<FaPlus className="w-3.5 h-3.5" />
|
|
||||||
<span>Yanıt Ekle</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{question.options?.map((option, index) => (
|
|
||||||
<div
|
|
||||||
key={option.id}
|
|
||||||
className="flex items-center space-x-2.5 p-2.5 border border-gray-200 rounded-lg"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={option.isCorrect}
|
|
||||||
onChange={(e) => updateOption(index, 'isCorrect', e.target.checked)}
|
|
||||||
className="h-4 w-4 text-blue-600 border-gray-300"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={option.text}
|
|
||||||
onChange={(e) => updateOption(index, 'text', e.target.value)}
|
|
||||||
placeholder={`Yanıt ${index + 1}`}
|
|
||||||
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeOption(index)}
|
|
||||||
className="text-red-500 hover:text-red-700"
|
|
||||||
>
|
|
||||||
<FaTrash className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'true-false':
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Doğru Cevap</label>
|
|
||||||
<div className="flex space-x-4">
|
|
||||||
{['true', 'false'].map((val) => (
|
|
||||||
<label key={val} className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="true-false"
|
|
||||||
value={val}
|
|
||||||
checked={question.correctAnswer === val}
|
|
||||||
onChange={(e) => handleChange('correctAnswer', e.target.value)}
|
|
||||||
className="h-4 w-4 text-blue-600 border-gray-300"
|
|
||||||
/>
|
|
||||||
<span className="ml-2">{val === 'true' ? 'Doğru' : 'Yanlış'}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{question?.options?.length! === 0 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => addOption(true)}
|
|
||||||
className="flex items-center space-x-1 text-sm text-blue-600 hover:text-blue-700"
|
|
||||||
>
|
|
||||||
<FaPlus className="w-3.5 h-3.5" />
|
|
||||||
<span>Seçenek Ekle</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{question.options?.map((option, index) => (
|
|
||||||
<div
|
|
||||||
key={option.id}
|
|
||||||
className="flex items-center space-x-2.5 p-2.5 border border-gray-200 rounded-lg"
|
|
||||||
>
|
|
||||||
<div className="bg-blue-100 text-blue-800 text-sm font-medium px-2 py-1 rounded min-w-8 text-center">
|
|
||||||
{String.fromCharCode(65 + index)}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="correct-answer"
|
|
||||||
checked={option.isCorrect}
|
|
||||||
onChange={() =>
|
|
||||||
setQuestion((prev) => {
|
|
||||||
if (!prev) return prev
|
|
||||||
const updated = prev.options?.map((opt, i) => ({
|
|
||||||
...opt,
|
|
||||||
isCorrect: i === index,
|
|
||||||
}))
|
|
||||||
return { ...prev, options: updated }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="h-4 w-4 text-blue-600 border-gray-300"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={option.text}
|
|
||||||
onChange={(e) => updateOption(index, 'text', e.target.value)}
|
|
||||||
placeholder={`${String.fromCharCode(65 + index)} şıkkı`}
|
|
||||||
className="flex-1 text-sm border border-gray-300 rounded-lg px-2 py-1"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeOption(index)}
|
|
||||||
className="text-red-500 hover:text-red-700"
|
|
||||||
>
|
|
||||||
<FaTrash className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
|
||||||
<div className="sticky top-0 bg-white border-b border-gray-200 px-5 py-3 flex items-center justify-between">
|
|
||||||
<h2 className="text-base font-semibold text-gray-900">{question.title}</h2>
|
|
||||||
<button onClick={onDialogClose} className="text-gray-400 hover:text-gray-600">
|
|
||||||
<FaTimes className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-5 space-y-4">{renderQuestionTypeSpecificFields()}</div>
|
|
||||||
|
|
||||||
<div className="sticky bottom-0 bg-gray-50 border-t border-gray-200 px-5 py-3 flex justify-end space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={onDialogClose}
|
|
||||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleSave}
|
|
||||||
className="flex items-center space-x-2 px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-lg"
|
|
||||||
>
|
|
||||||
<FaSave className="w-3.5 h-3.5" />
|
|
||||||
<span>Save Question</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QuestionDialog
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
import { QuestionDto, StudentAnswer } from '@/types/coordinator';
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
interface CalculationQuestionProps {
|
|
||||||
question: QuestionDto;
|
|
||||||
answer?: StudentAnswer;
|
|
||||||
onAnswerChange: (questionId: string, answer: string) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
showCorrectAnswer?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CalculationQuestion: React.FC<CalculationQuestionProps> = ({
|
|
||||||
question,
|
|
||||||
answer,
|
|
||||||
onAnswerChange,
|
|
||||||
disabled = false,
|
|
||||||
showCorrectAnswer = false
|
|
||||||
}) => {
|
|
||||||
const [result, setResult] = useState<string>('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (answer?.answer) {
|
|
||||||
setResult(answer.answer as string);
|
|
||||||
}
|
|
||||||
}, [answer?.answer]);
|
|
||||||
|
|
||||||
const handleResultChange = (value: string) => {
|
|
||||||
if (!disabled) {
|
|
||||||
setResult(value);
|
|
||||||
onAnswerChange(question.id, value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const correctAnswer = question.correctAnswer as string || '';
|
|
||||||
|
|
||||||
const isResultCorrect = showCorrectAnswer &&
|
|
||||||
result.trim() === correctAnswer.trim();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="prose max-w-none">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
||||||
{question.title}
|
|
||||||
</h3>
|
|
||||||
{question.content && (
|
|
||||||
<p className="text-gray-700 mb-4">{question.content}</p>
|
|
||||||
)}
|
|
||||||
{question.mediaUrl && (
|
|
||||||
<div className="mb-4">
|
|
||||||
{question.mediaType === 'image' ? (
|
|
||||||
<img
|
|
||||||
src={question.mediaUrl}
|
|
||||||
alt="Question media"
|
|
||||||
className="max-w-full h-auto rounded-lg shadow-sm"
|
|
||||||
/>
|
|
||||||
) : question.mediaType === 'video' ? (
|
|
||||||
<video
|
|
||||||
src={question.mediaUrl}
|
|
||||||
controls
|
|
||||||
className="max-w-full h-auto rounded-lg shadow-sm"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Cevap (Sayısal):
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={result}
|
|
||||||
onChange={(e) => handleResultChange(e.target.value)}
|
|
||||||
disabled={disabled}
|
|
||||||
placeholder="Sayısal cevabınızı yazın (örn: 3, 15.5)"
|
|
||||||
className={`w-full text-sm p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
|
||||||
disabled ? 'bg-gray-50 cursor-not-allowed' : 'bg-white'
|
|
||||||
} ${
|
|
||||||
isResultCorrect
|
|
||||||
? 'border-green-500 bg-green-50'
|
|
||||||
: showCorrectAnswer && result
|
|
||||||
? 'border-red-500 bg-red-50'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showCorrectAnswer && correctAnswer && (
|
|
||||||
<div className="mt-4">
|
|
||||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
||||||
<h4 className="text-sm font-medium text-blue-800 mb-2">
|
|
||||||
Doğru Cevap:
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm text-blue-700 font-mono">
|
|
||||||
{correctAnswer}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{question.explanation && showCorrectAnswer && (
|
|
||||||
<div className="mt-4 p-4 bg-gray-50 border border-gray-200 rounded-lg">
|
|
||||||
<h4 className="text-sm font-medium text-gray-800 mb-2">
|
|
||||||
Açıklama:
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm text-gray-700">
|
|
||||||
{question.explanation}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
import { QuestionDto, StudentAnswer } from '@/types/coordinator';
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
interface FillBlankQuestionProps {
|
|
||||||
question: QuestionDto;
|
|
||||||
answer?: StudentAnswer;
|
|
||||||
onAnswerChange: (questionId: string, answer: string[]) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
showCorrectAnswer?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FillBlankQuestion: React.FC<FillBlankQuestionProps> = ({
|
|
||||||
question,
|
|
||||||
answer,
|
|
||||||
onAnswerChange,
|
|
||||||
disabled = false,
|
|
||||||
showCorrectAnswer = false
|
|
||||||
}) => {
|
|
||||||
const [blanks, setBlanks] = useState<string[]>([]);
|
|
||||||
|
|
||||||
// Parse content to find blanks marked with _____ or [blank]
|
|
||||||
const parseContent = (content: string): { parts: string[]; blankCount: number } => {
|
|
||||||
const parts = content.split(/(_____|\[blank\])/g);
|
|
||||||
const blankCount = parts.filter(part => part === '_____' || part === '[blank]').length;
|
|
||||||
return { parts, blankCount };
|
|
||||||
};
|
|
||||||
|
|
||||||
const { parts, blankCount } = parseContent(question.content);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (answer?.answer && Array.isArray(answer.answer)) {
|
|
||||||
setBlanks(answer.answer);
|
|
||||||
} else {
|
|
||||||
setBlanks(new Array(blankCount).fill(''));
|
|
||||||
}
|
|
||||||
}, [answer?.answer, blankCount]);
|
|
||||||
|
|
||||||
const handleBlankChange = (index: number, value: string) => {
|
|
||||||
if (!disabled) {
|
|
||||||
const newBlanks = [...blanks];
|
|
||||||
newBlanks[index] = value;
|
|
||||||
setBlanks(newBlanks);
|
|
||||||
onAnswerChange(question.id, newBlanks);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const correctAnswers = (question.correctAnswer as string || '').split('|').filter(Boolean);
|
|
||||||
let blankIndex = 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="prose max-w-none">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
||||||
{question.title}
|
|
||||||
</h3>
|
|
||||||
{question.mediaUrl && (
|
|
||||||
<div className="mb-4">
|
|
||||||
{question.mediaType === 'image' ? (
|
|
||||||
<img
|
|
||||||
src={question.mediaUrl}
|
|
||||||
alt="Question media"
|
|
||||||
className="max-w-full h-auto rounded-lg shadow-sm"
|
|
||||||
/>
|
|
||||||
) : question.mediaType === 'video' ? (
|
|
||||||
<video
|
|
||||||
src={question.mediaUrl}
|
|
||||||
controls
|
|
||||||
className="max-w-full h-auto rounded-lg shadow-sm"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-base leading-relaxed">
|
|
||||||
{parts.map((part, index) => {
|
|
||||||
if (part === '_____' || part === '[blank]') {
|
|
||||||
const currentBlankIndex = blankIndex++;
|
|
||||||
const currentAnswer = blanks[currentBlankIndex] || '';
|
|
||||||
const correctAnswer = correctAnswers[currentBlankIndex] || '';
|
|
||||||
const isCorrect = showCorrectAnswer && currentAnswer.toLowerCase().trim() === correctAnswer.toLowerCase().trim();
|
|
||||||
const isIncorrect = showCorrectAnswer && currentAnswer && !isCorrect;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span key={index} className="inline-block mx-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={currentAnswer}
|
|
||||||
onChange={(e) => handleBlankChange(currentBlankIndex, e.target.value)}
|
|
||||||
disabled={disabled}
|
|
||||||
className={`inline-block text-sm px-3 py-1 border-b-2 bg-transparent focus:outline-none focus:border-blue-500 min-w-24 text-center ${
|
|
||||||
disabled ? 'cursor-not-allowed opacity-50' : ''
|
|
||||||
} ${
|
|
||||||
isCorrect
|
|
||||||
? 'border-green-500 text-green-700'
|
|
||||||
: isIncorrect
|
|
||||||
? 'border-red-500 text-red-700'
|
|
||||||
: 'border-gray-300'
|
|
||||||
}`}
|
|
||||||
placeholder="____"
|
|
||||||
/>
|
|
||||||
{showCorrectAnswer && correctAnswer && (
|
|
||||||
<div className="text-xs text-gray-600 mt-1 text-center">
|
|
||||||
{isCorrect ? '✓' : `Doğru cevap: ${correctAnswer}`}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <span key={index}>{part}</span>;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
import { QuestionDto, StudentAnswer } from '@/types/coordinator';
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
interface MatchingQuestionProps {
|
|
||||||
question: QuestionDto;
|
|
||||||
answer?: StudentAnswer;
|
|
||||||
onAnswerChange: (questionId: string, answer: string[]) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
showCorrectAnswer?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MatchingPair {
|
|
||||||
id: string;
|
|
||||||
left: string;
|
|
||||||
right: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MatchingQuestion: React.FC<MatchingQuestionProps> = ({
|
|
||||||
question,
|
|
||||||
answer,
|
|
||||||
onAnswerChange,
|
|
||||||
disabled = false,
|
|
||||||
showCorrectAnswer = false
|
|
||||||
}) => {
|
|
||||||
const [matches, setMatches] = useState<Record<string, string>>({});
|
|
||||||
|
|
||||||
// Parse options into left and right items
|
|
||||||
const matchingPairs: MatchingPair[] = question.options?.map(option => ({
|
|
||||||
id: option.id,
|
|
||||||
left: option.text.split('|')[0] || option.text,
|
|
||||||
right: option.text.split('|')[1] || ''
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const leftItems = matchingPairs.map(pair => ({ id: pair.id, text: pair.left }));
|
|
||||||
const rightItems = matchingPairs
|
|
||||||
.map(pair => ({ id: pair.id, text: pair.right }))
|
|
||||||
.sort(() => Math.random() - 0.5); // Shuffle right items
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (answer?.answer && Array.isArray(answer.answer)) {
|
|
||||||
const matchObj: Record<string, string> = {};
|
|
||||||
answer.answer.forEach((match, index) => {
|
|
||||||
if (leftItems[index]) {
|
|
||||||
matchObj[leftItems[index].id] = match;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setMatches(matchObj);
|
|
||||||
}
|
|
||||||
}, [answer?.answer]);
|
|
||||||
|
|
||||||
const handleMatch = (leftId: string, rightId: string) => {
|
|
||||||
if (!disabled) {
|
|
||||||
const newMatches = { ...matches };
|
|
||||||
|
|
||||||
// Remove any existing match for this right item
|
|
||||||
Object.keys(newMatches).forEach(key => {
|
|
||||||
if (newMatches[key] === rightId) {
|
|
||||||
delete newMatches[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add new match
|
|
||||||
newMatches[leftId] = rightId;
|
|
||||||
setMatches(newMatches);
|
|
||||||
|
|
||||||
// Convert to array format
|
|
||||||
const answerArray = leftItems.map(item => newMatches[item.id] || '');
|
|
||||||
onAnswerChange(question.id, answerArray);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const correctMatches = question.correctAnswer as string[] || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="prose max-w-none">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
||||||
{question.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 mb-2">
|
|
||||||
Sol taraftaki öğeleri sağ taraftaki uygun öğelerle eşleştirin
|
|
||||||
</p>
|
|
||||||
{question.content && (
|
|
||||||
<p className="text-gray-700 mb-4">{question.content}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-6">
|
|
||||||
{/* Left Column */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h4 className="font-medium text-gray-700 mb-3">Sol Taraf</h4>
|
|
||||||
{leftItems.map((item, index) => {
|
|
||||||
const matchedRightId = matches[item.id];
|
|
||||||
const correctRightId = correctMatches[index];
|
|
||||||
const isCorrect = showCorrectAnswer && matchedRightId === correctRightId;
|
|
||||||
const isIncorrect = showCorrectAnswer && matchedRightId && matchedRightId !== correctRightId;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className={`p-3 rounded-lg border-2 ${
|
|
||||||
isCorrect
|
|
||||||
? 'border-green-500 bg-green-50'
|
|
||||||
: isIncorrect
|
|
||||||
? 'border-red-500 bg-red-50'
|
|
||||||
: matchedRightId
|
|
||||||
? 'border-blue-500 bg-blue-50'
|
|
||||||
: 'border-gray-200 bg-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-sm font-medium">{item.text}</span>
|
|
||||||
{matchedRightId && (
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
→ {rightItems.find(r => r.id === matchedRightId)?.text}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{showCorrectAnswer && correctRightId && (
|
|
||||||
<div className="text-xs mt-1 text-gray-600">
|
|
||||||
Doğru eşleşme: {rightItems.find(r => r.id === correctRightId)?.text}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h4 className="font-medium text-gray-700 mb-3">Sağ Taraf</h4>
|
|
||||||
{rightItems.map((item) => {
|
|
||||||
const isMatched = Object.values(matches).includes(item.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={item.id} className="space-y-2">
|
|
||||||
<div
|
|
||||||
className={`p-3 rounded-lg border-2 cursor-pointer transition-all ${
|
|
||||||
disabled ? 'cursor-not-allowed opacity-50' : ''
|
|
||||||
} ${
|
|
||||||
isMatched
|
|
||||||
? 'border-blue-500 bg-blue-50'
|
|
||||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="text-sm font-medium">{item.text}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Match buttons for each left item */}
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{leftItems.map((leftItem) => (
|
|
||||||
<button
|
|
||||||
key={leftItem.id}
|
|
||||||
onClick={() => handleMatch(leftItem.id, item.id)}
|
|
||||||
disabled={disabled}
|
|
||||||
className={`px-2 py-1 text-xs rounded transition-all ${
|
|
||||||
matches[leftItem.id] === item.id
|
|
||||||
? 'bg-blue-500 text-white'
|
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
|
||||||
} ${disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
|
|
||||||
>
|
|
||||||
{leftItem.text.substring(0, 10)}...
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
import { QuestionDto, StudentAnswer } from '@/types/coordinator';
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
interface MultipleAnswerQuestionProps {
|
|
||||||
question: QuestionDto;
|
|
||||||
answer?: StudentAnswer;
|
|
||||||
onAnswerChange: (questionId: string, answer: string[]) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
showCorrectAnswer?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MultipleAnswerQuestion: React.FC<MultipleAnswerQuestionProps> = ({
|
|
||||||
question,
|
|
||||||
answer,
|
|
||||||
onAnswerChange,
|
|
||||||
disabled = false,
|
|
||||||
showCorrectAnswer = false
|
|
||||||
}) => {
|
|
||||||
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (answer?.answer && Array.isArray(answer.answer)) {
|
|
||||||
setSelectedOptions(answer.answer);
|
|
||||||
}
|
|
||||||
}, [answer?.answer]);
|
|
||||||
|
|
||||||
const handleOptionToggle = (optionId: string) => {
|
|
||||||
if (!disabled) {
|
|
||||||
const newSelection = selectedOptions.includes(optionId)
|
|
||||||
? selectedOptions.filter(id => id !== optionId)
|
|
||||||
: [...selectedOptions, optionId];
|
|
||||||
|
|
||||||
setSelectedOptions(newSelection);
|
|
||||||
onAnswerChange(question.id, newSelection);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const correctAnswers = question.correctAnswer as string[] || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="prose max-w-none">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
||||||
{question.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 mb-2">
|
|
||||||
(Birden fazla seçenek işaretleyebilirsiniz)
|
|
||||||
</p>
|
|
||||||
{question.content && (
|
|
||||||
<p className="text-gray-700 mb-4">{question.content}</p>
|
|
||||||
)}
|
|
||||||
{question.mediaUrl && (
|
|
||||||
<div className="mb-4">
|
|
||||||
{question.mediaType === 'image' ? (
|
|
||||||
<img
|
|
||||||
src={question.mediaUrl}
|
|
||||||
alt="Question media"
|
|
||||||
className="max-w-full h-auto rounded-lg shadow-sm"
|
|
||||||
/>
|
|
||||||
) : question.mediaType === 'video' ? (
|
|
||||||
<video
|
|
||||||
src={question.mediaUrl}
|
|
||||||
controls
|
|
||||||
className="max-w-full h-auto rounded-lg shadow-sm"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
{question.options?.map((option) => {
|
|
||||||
const isSelected = selectedOptions.includes(option.id);
|
|
||||||
const isCorrect = showCorrectAnswer && correctAnswers.includes(option.id);
|
|
||||||
const isIncorrect = showCorrectAnswer && isSelected && !correctAnswers.includes(option.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={option.id}
|
|
||||||
className={`relative flex items-center p-4 rounded-lg border-2 cursor-pointer transition-all ${
|
|
||||||
disabled ? 'cursor-not-allowed opacity-50' : ''
|
|
||||||
} ${
|
|
||||||
isSelected
|
|
||||||
? isCorrect
|
|
||||||
? 'border-green-500 bg-green-50'
|
|
||||||
: isIncorrect
|
|
||||||
? 'border-red-500 bg-red-50'
|
|
||||||
: 'border-blue-500 bg-blue-50'
|
|
||||||
: isCorrect && showCorrectAnswer
|
|
||||||
? 'border-green-500 bg-green-50'
|
|
||||||
: 'border-gray-200 hover:border-gray-300 bg-white'
|
|
||||||
}`}
|
|
||||||
onClick={() => handleOptionToggle(option.id)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={isSelected}
|
|
||||||
onChange={() => handleOptionToggle(option.id)}
|
|
||||||
disabled={disabled}
|
|
||||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
||||||
/>
|
|
||||||
<label className="ml-3 text-sm font-medium text-gray-900 cursor-pointer">
|
|
||||||
{option.text}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{showCorrectAnswer && isCorrect && (
|
|
||||||
<div className="absolute right-4 text-green-600">
|
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
import { QuestionDto, StudentAnswer } from '@/types/coordinator';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface MultipleChoiceQuestionProps {
|
|
||||||
question: QuestionDto;
|
|
||||||
answer?: StudentAnswer;
|
|
||||||
onAnswerChange: (questionId: string, answer: string) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
showCorrectAnswer?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MultipleChoiceQuestion: React.FC<MultipleChoiceQuestionProps> = ({
|
|
||||||
question,
|
|
||||||
answer,
|
|
||||||
onAnswerChange,
|
|
||||||
disabled = false,
|
|
||||||
showCorrectAnswer = false
|
|
||||||
}) => {
|
|
||||||
const selectedAnswer = answer?.answer as string || '';
|
|
||||||
|
|
||||||
const handleOptionSelect = (optionId: string) => {
|
|
||||||
if (!disabled) {
|
|
||||||
onAnswerChange(question.id, optionId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="prose max-w-none">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
||||||
{question.title}
|
|
||||||
</h3>
|
|
||||||
{question.content && (
|
|
||||||
<p className="text-gray-700 mb-4">{question.content}</p>
|
|
||||||
)}
|
|
||||||
{question.mediaUrl && (
|
|
||||||
<div className="mb-4">
|
|
||||||
{question.mediaType === 'image' ? (
|
|
||||||
<img
|
|
||||||
src={question.mediaUrl}
|
|
||||||
alt="Question media"
|
|
||||||
className="max-w-full h-auto rounded-lg shadow-sm"
|
|
||||||
/>
|
|
||||||
) : question.mediaType === 'video' ? (
|
|
||||||
<video
|
|
||||||
src={question.mediaUrl}
|
|
||||||
controls
|
|
||||||
className="max-w-full h-auto rounded-lg shadow-sm"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
{question.options?.map((option) => {
|
|
||||||
const isSelected = selectedAnswer === option.id;
|
|
||||||
const isCorrect = showCorrectAnswer && option.isCorrect;
|
|
||||||
const isIncorrect = showCorrectAnswer && isSelected && !option.isCorrect;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={option.id}
|
|
||||||
className={`relative flex items-center p-4 rounded-lg border-2 cursor-pointer transition-all ${
|
|
||||||
disabled ? 'cursor-not-allowed opacity-50' : ''
|
|
||||||
} ${
|
|
||||||
isSelected
|
|
||||||
? isCorrect
|
|
||||||
? 'border-green-500 bg-green-50'
|
|
||||||
: isIncorrect
|
|
||||||
? 'border-red-500 bg-red-50'
|
|
||||||
: 'border-blue-500 bg-blue-50'
|
|
||||||
: isCorrect && showCorrectAnswer
|
|
||||||
? 'border-green-500 bg-green-50'
|
|
||||||
: 'border-gray-200 hover:border-gray-300 bg-white'
|
|
||||||
}`}
|
|
||||||
onClick={() => handleOptionSelect(option.id)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name={`question-${question.id}`}
|
|
||||||
checked={isSelected}
|
|
||||||
onChange={() => handleOptionSelect(option.id)}
|
|
||||||
disabled={disabled}
|
|
||||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
|
||||||
/>
|
|
||||||
<label className="ml-3 text-sm font-medium text-gray-900 cursor-pointer">
|
|
||||||
{option.text}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{showCorrectAnswer && isCorrect && (
|
|
||||||
<div className="absolute right-4 text-green-600">
|
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
import { QuestionDto, StudentAnswer } from '@/types/coordinator';
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
interface OpenEndedQuestionProps {
|
|
||||||
question: QuestionDto;
|
|
||||||
answer?: StudentAnswer;
|
|
||||||
onAnswerChange: (questionId: string, answer: string) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
showCorrectAnswer?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OpenEndedQuestion: React.FC<OpenEndedQuestionProps> = ({
|
|
||||||
question,
|
|
||||||
answer,
|
|
||||||
onAnswerChange,
|
|
||||||
disabled = false,
|
|
||||||
showCorrectAnswer = false
|
|
||||||
}) => {
|
|
||||||
const [text, setText] = useState<string>('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (answer?.answer && typeof answer.answer === 'string') {
|
|
||||||
setText(answer.answer);
|
|
||||||
}
|
|
||||||
}, [answer?.answer]);
|
|
||||||
|
|
||||||
const handleTextChange = (value: string) => {
|
|
||||||
if (!disabled) {
|
|
||||||
setText(value);
|
|
||||||
onAnswerChange(question.id, value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const wordCount = text.trim().split(/\s+/).filter(word => word.length > 0).length;
|
|
||||||
const charCount = text.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="prose max-w-none">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
||||||
{question.title}
|
|
||||||
</h3>
|
|
||||||
{question.content && (
|
|
||||||
<p className="text-gray-700 mb-4">{question.content}</p>
|
|
||||||
)}
|
|
||||||
{question.mediaUrl && (
|
|
||||||
<div className="mb-4">
|
|
||||||
{question.mediaType === 'image' ? (
|
|
||||||
<img
|
|
||||||
src={question.mediaUrl}
|
|
||||||
alt="Question media"
|
|
||||||
className="max-w-full h-auto rounded-lg shadow-sm"
|
|
||||||
/>
|
|
||||||
) : question.mediaType === 'video' ? (
|
|
||||||
<video
|
|
||||||
src={question.mediaUrl}
|
|
||||||
controls
|
|
||||||
className="max-w-full h-auto rounded-lg shadow-sm"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<textarea
|
|
||||||
value={text}
|
|
||||||
onChange={(e) => handleTextChange(e.target.value)}
|
|
||||||
disabled={disabled}
|
|
||||||
placeholder="Cevabınızı buraya yazın..."
|
|
||||||
className={`w-full text-sm p-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-vertical min-h-32 ${
|
|
||||||
disabled ? 'bg-gray-50 cursor-not-allowed' : 'bg-white'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex justify-between text-sm text-gray-500">
|
|
||||||
<span>{wordCount} kelime</span>
|
|
||||||
<span>{charCount} karakter</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showCorrectAnswer && question.correctAnswer && (
|
|
||||||
<div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
|
|
||||||
<h4 className="text-sm font-medium text-green-800 mb-2">
|
|
||||||
Örnek Cevap:
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm text-green-700">
|
|
||||||
{question.correctAnswer as string}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{question.explanation && showCorrectAnswer && (
|
|
||||||
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
||||||
<h4 className="text-sm font-medium text-blue-800 mb-2">
|
|
||||||
Açıklama:
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm text-blue-700">
|
|
||||||
{question.explanation}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,191 +0,0 @@
|
||||||
import { QuestionDto, StudentAnswer } from '@/types/coordinator';
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
interface OrderingQuestionProps {
|
|
||||||
question: QuestionDto;
|
|
||||||
answer?: StudentAnswer;
|
|
||||||
onAnswerChange: (questionId: string, answer: string[]) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
showCorrectAnswer?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OrderItem {
|
|
||||||
id: string;
|
|
||||||
text: string;
|
|
||||||
originalOrder: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OrderingQuestion: React.FC<OrderingQuestionProps> = ({
|
|
||||||
question,
|
|
||||||
answer,
|
|
||||||
onAnswerChange,
|
|
||||||
disabled = false,
|
|
||||||
showCorrectAnswer = false
|
|
||||||
}) => {
|
|
||||||
const [orderedItems, setOrderedItems] = useState<OrderItem[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (question.options) {
|
|
||||||
const items: OrderItem[] = question.options.map((option, index) => ({
|
|
||||||
id: option.id,
|
|
||||||
text: option.text,
|
|
||||||
originalOrder: option.order || index
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (answer?.answer && Array.isArray(answer.answer)) {
|
|
||||||
// Restore order from saved answer
|
|
||||||
const orderedIds = answer.answer;
|
|
||||||
const restored = orderedIds
|
|
||||||
.map(id => items.find(item => item.id === id))
|
|
||||||
.filter(Boolean) as OrderItem[];
|
|
||||||
setOrderedItems(restored);
|
|
||||||
} else {
|
|
||||||
// Shuffle items initially
|
|
||||||
const shuffled = [...items].sort(() => Math.random() - 0.5);
|
|
||||||
setOrderedItems(shuffled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [question.options, answer?.answer]);
|
|
||||||
|
|
||||||
const handleDragStart = (e: React.DragEvent, index: number) => {
|
|
||||||
if (!disabled) {
|
|
||||||
e.dataTransfer.setData('text/plain', index.toString());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent, dropIndex: number) => {
|
|
||||||
if (!disabled) {
|
|
||||||
e.preventDefault();
|
|
||||||
const dragIndex = parseInt(e.dataTransfer.getData('text/plain'));
|
|
||||||
|
|
||||||
if (dragIndex !== dropIndex) {
|
|
||||||
const newItems = [...orderedItems];
|
|
||||||
const draggedItem = newItems[dragIndex];
|
|
||||||
newItems.splice(dragIndex, 1);
|
|
||||||
newItems.splice(dropIndex, 0, draggedItem);
|
|
||||||
|
|
||||||
setOrderedItems(newItems);
|
|
||||||
onAnswerChange(question.id, newItems.map(item => item.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveItem = (fromIndex: number, direction: 'up' | 'down') => {
|
|
||||||
if (disabled) return;
|
|
||||||
|
|
||||||
const toIndex = direction === 'up' ? fromIndex - 1 : fromIndex + 1;
|
|
||||||
if (toIndex < 0 || toIndex >= orderedItems.length) return;
|
|
||||||
|
|
||||||
const newItems = [...orderedItems];
|
|
||||||
[newItems[fromIndex], newItems[toIndex]] = [newItems[toIndex], newItems[fromIndex]];
|
|
||||||
|
|
||||||
setOrderedItems(newItems);
|
|
||||||
onAnswerChange(question.id, newItems.map(item => item.id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const correctOrder = question.correctAnswer as string[] || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="prose max-w-none">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
||||||
{question.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 mb-2">
|
|
||||||
Aşağıdaki öğeleri doğru sıraya göre düzenleyin
|
|
||||||
</p>
|
|
||||||
{question.content && (
|
|
||||||
<p className="text-gray-700 mb-4">{question.content}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
{orderedItems.map((item, index) => {
|
|
||||||
const correctIndex = correctOrder.indexOf(item.id);
|
|
||||||
const isInCorrectPosition = showCorrectAnswer && correctIndex === index;
|
|
||||||
const isInWrongPosition = showCorrectAnswer && correctIndex !== -1 && correctIndex !== index;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
draggable={!disabled}
|
|
||||||
onDragStart={(e) => handleDragStart(e, index)}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDrop={(e) => handleDrop(e, index)}
|
|
||||||
className={`flex items-center p-4 rounded-lg border-2 transition-all ${
|
|
||||||
disabled ? 'cursor-not-allowed opacity-50' : 'cursor-move'
|
|
||||||
} ${
|
|
||||||
isInCorrectPosition
|
|
||||||
? 'border-green-500 bg-green-50'
|
|
||||||
: isInWrongPosition
|
|
||||||
? 'border-red-500 bg-red-50'
|
|
||||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-3 flex-1">
|
|
||||||
<div className="flex flex-col space-y-1">
|
|
||||||
<svg className="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
|
||||||
</svg>
|
|
||||||
<svg className="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-3 flex-1">
|
|
||||||
<div className="bg-blue-100 text-blue-800 text-sm font-medium px-2 py-1 rounded">
|
|
||||||
{index + 1}
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-medium">{item.text}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!disabled && (
|
|
||||||
<div className="flex flex-col space-y-1">
|
|
||||||
<button
|
|
||||||
onClick={() => moveItem(index, 'up')}
|
|
||||||
disabled={index === 0}
|
|
||||||
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => moveItem(index, 'down')}
|
|
||||||
disabled={index === orderedItems.length - 1}
|
|
||||||
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showCorrectAnswer && (
|
|
||||||
<div className="ml-4">
|
|
||||||
{isInCorrectPosition ? (
|
|
||||||
<div className="text-green-600">
|
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
) : correctIndex !== -1 ? (
|
|
||||||
<div className="text-red-600 text-xs">
|
|
||||||
Doğru sıra: {correctIndex + 1}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
import { QuestionDto, StudentAnswer } from '@/types/coordinator';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface TrueFalseQuestionProps {
|
|
||||||
question: QuestionDto;
|
|
||||||
answer?: StudentAnswer;
|
|
||||||
onAnswerChange: (questionId: string, answer: string) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
showCorrectAnswer?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TrueFalseQuestion: React.FC<TrueFalseQuestionProps> = ({
|
|
||||||
question,
|
|
||||||
answer,
|
|
||||||
onAnswerChange,
|
|
||||||
disabled = false,
|
|
||||||
showCorrectAnswer = false
|
|
||||||
}) => {
|
|
||||||
const selectedAnswer = answer?.answer as string || '';
|
|
||||||
const correctAnswer = question.correctAnswer as string;
|
|
||||||
|
|
||||||
const handleAnswerSelect = (value: string) => {
|
|
||||||
if (!disabled) {
|
|
||||||
onAnswerChange(question.id, value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
{ value: 'true', label: 'Doğru' },
|
|
||||||
{ value: 'false', label: 'Yanlış' }
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="prose max-w-none">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
||||||
{question.title}
|
|
||||||
</h3>
|
|
||||||
{question.content && (
|
|
||||||
<p className="text-gray-700 mb-4">{question.content}</p>
|
|
||||||
)}
|
|
||||||
{question.mediaUrl && (
|
|
||||||
<div className="mb-4">
|
|
||||||
{question.mediaType === 'image' ? (
|
|
||||||
<img
|
|
||||||
src={question.mediaUrl}
|
|
||||||
alt="Question media"
|
|
||||||
className="max-w-full h-auto rounded-lg shadow-sm"
|
|
||||||
/>
|
|
||||||
) : question.mediaType === 'video' ? (
|
|
||||||
<video
|
|
||||||
src={question.mediaUrl}
|
|
||||||
controls
|
|
||||||
className="max-w-full h-auto rounded-lg shadow-sm"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-4">
|
|
||||||
{options.map((option) => {
|
|
||||||
const isSelected = selectedAnswer === option.value;
|
|
||||||
const isCorrect = showCorrectAnswer && correctAnswer === option.value;
|
|
||||||
const isIncorrect = showCorrectAnswer && isSelected && correctAnswer !== option.value;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={option.value}
|
|
||||||
onClick={() => handleAnswerSelect(option.value)}
|
|
||||||
disabled={disabled}
|
|
||||||
className={`flex-1 flex items-center justify-center p-6 rounded-lg border-2 font-medium transition-all ${
|
|
||||||
disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:shadow-md'
|
|
||||||
} ${
|
|
||||||
isSelected
|
|
||||||
? isCorrect
|
|
||||||
? 'border-green-500 bg-green-50 text-green-700'
|
|
||||||
: isIncorrect
|
|
||||||
? 'border-red-500 bg-red-50 text-red-700'
|
|
||||||
: 'border-blue-500 bg-blue-50 text-blue-700'
|
|
||||||
: isCorrect && showCorrectAnswer
|
|
||||||
? 'border-green-500 bg-green-50 text-green-700'
|
|
||||||
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-2xl mb-2">
|
|
||||||
{option.value === 'true' ? '✓' : '✗'}
|
|
||||||
</div>
|
|
||||||
<div>{option.label}</div>
|
|
||||||
{showCorrectAnswer && isCorrect && (
|
|
||||||
<div className="text-sm mt-2 text-green-600">Doğru Cevap</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,223 +0,0 @@
|
||||||
import { generateMockTags } from '@/mocks/mockTags'
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { FaPlus, FaSearch, FaEdit, FaTrash, FaTag } from 'react-icons/fa'
|
|
||||||
|
|
||||||
interface TagItem {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
color: string
|
|
||||||
usageCount: number
|
|
||||||
creationTime: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
const Tags: React.FC = () => {
|
|
||||||
const [tags, setTags] = useState<TagItem[]>(generateMockTags())
|
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
|
||||||
const [isCreating, setIsCreating] = useState(false)
|
|
||||||
const [editingTag, setEditingTag] = useState<TagItem | null>(null)
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
color: '#3B82F6',
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredTags = tags.filter(
|
|
||||||
(tag) =>
|
|
||||||
tag.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
tag.description.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (!formData.name.trim()) return
|
|
||||||
|
|
||||||
if (editingTag) {
|
|
||||||
setTags((prev) =>
|
|
||||||
prev.map((tag) => (tag.id === editingTag.id ? { ...editingTag, ...formData } : tag)),
|
|
||||||
)
|
|
||||||
setEditingTag(null)
|
|
||||||
} else {
|
|
||||||
const newTag: TagItem = {
|
|
||||||
...formData,
|
|
||||||
id: `tag-${Date.now()}`,
|
|
||||||
usageCount: 0,
|
|
||||||
creationTime: new Date(),
|
|
||||||
}
|
|
||||||
setTags((prev) => [...prev, newTag])
|
|
||||||
setIsCreating(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
setFormData({ name: '', description: '', color: '#3B82F6' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEdit = (tag: TagItem) => {
|
|
||||||
setEditingTag(tag)
|
|
||||||
setFormData({
|
|
||||||
name: tag.name,
|
|
||||||
description: tag.description,
|
|
||||||
color: tag.color,
|
|
||||||
})
|
|
||||||
setIsCreating(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setIsCreating(false)
|
|
||||||
setEditingTag(null)
|
|
||||||
setFormData({ name: '', description: '', color: '#3B82F6' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-bold text-gray-900">Etiket Yönetimi</h1>
|
|
||||||
<p className="text-sm text-gray-600 mt-0.5">Soru ve içerik etiketlerini yönetin</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setIsCreating(true)}
|
|
||||||
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<FaPlus className="w-3.5 h-3.5" />
|
|
||||||
<span>Yeni Etiket</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search */}
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
|
||||||
<div className="relative">
|
|
||||||
<FaSearch className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Etiket ara..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="pl-8 text-sm w-full border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Create/Edit Form */}
|
|
||||||
{(isCreating || editingTag) && (
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
|
||||||
<h3 className="text-base font-semibold mb-3">
|
|
||||||
{editingTag ? 'Etiket Düzenle' : 'Yeni Etiket Oluştur'}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Etiket Adı</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="Etiket adını girin"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">Renk</label>
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={formData.color}
|
|
||||||
onChange={(e) => setFormData((prev) => ({ ...prev, color: e.target.value }))}
|
|
||||||
className="w-full text-sm h-9 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">Açıklama</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.description}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
description: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder="Açıklama girin"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={handleCancel}
|
|
||||||
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-700 hover:bg-gray-50 font-medium transition-colors"
|
|
||||||
>
|
|
||||||
İptal
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
{editingTag ? 'Güncelle' : 'Oluştur'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tags List */}
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg">
|
|
||||||
{filteredTags.length > 0 ? (
|
|
||||||
<div className="divide-y divide-gray-200">
|
|
||||||
{filteredTags.map((tag) => (
|
|
||||||
<div key={tag.id} className="p-3 hover:bg-gray-50">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-2.5">
|
|
||||||
<div
|
|
||||||
className="w-3.5 h-3.5 rounded-full"
|
|
||||||
style={{ backgroundColor: tag.color }}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-medium text-gray-900">{tag.name}</h3>
|
|
||||||
<p className="text-xs text-gray-600">{tag.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<span className="text-xs text-gray-500">{tag.usageCount} kullanım</span>
|
|
||||||
<div className="flex space-x-1">
|
|
||||||
<button
|
|
||||||
onClick={() => handleEdit(tag)}
|
|
||||||
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<FaEdit className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (window.confirm('Bu etiketi silmek istediğinizden emin misiniz?')) {
|
|
||||||
setTags((prev) => prev.filter((t) => t.id !== tag.id))
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<FaTrash className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<FaTag className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
|
||||||
<h3 className="text-base font-medium text-gray-900 mb-1">Etiket bulunamadı</h3>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
{searchTerm
|
|
||||||
? 'Arama kriterlerinizi değiştirin veya yeni etiket oluşturun.'
|
|
||||||
: 'İlk etiketinizi oluşturun.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Tags
|
|
||||||
|
|
@ -1,238 +0,0 @@
|
||||||
import React, { useState } from 'react'
|
|
||||||
import {
|
|
||||||
FaPlus,
|
|
||||||
FaSearch,
|
|
||||||
FaFilter,
|
|
||||||
FaEdit,
|
|
||||||
FaTrash,
|
|
||||||
FaFileAlt,
|
|
||||||
FaImage,
|
|
||||||
FaCalendar,
|
|
||||||
FaUsers,
|
|
||||||
FaPlay,
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { Exam } from '@/types/coordinator'
|
|
||||||
import { generateMockPDFTest } from '@/mocks/mockTests'
|
|
||||||
import { TestCreator } from './ExamInterface/TestCreator'
|
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
||||||
|
|
||||||
const Tests: React.FC = () => {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const [tests, setTests] = useState<Exam[]>([generateMockPDFTest()])
|
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
|
||||||
const [statusFilter, setStatusFilter] = useState('')
|
|
||||||
const [isCreating, setIsCreating] = useState(false)
|
|
||||||
const [editingTest, setEditingTest] = useState<Exam | null>(null)
|
|
||||||
|
|
||||||
const filteredTests = tests.filter((test) => {
|
|
||||||
const matchesSearch =
|
|
||||||
test.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
test.description.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
const matchesStatus =
|
|
||||||
!statusFilter ||
|
|
||||||
(statusFilter === 'active' && test.isActive) ||
|
|
||||||
(statusFilter === 'inactive' && !test.isActive)
|
|
||||||
return matchesSearch && matchesStatus && test.type === 'test'
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleCreateTest = (
|
|
||||||
testData: Omit<Exam, 'id' | 'creationTime' | 'lastModificationTime'>,
|
|
||||||
) => {
|
|
||||||
const newTest: Exam = {
|
|
||||||
...testData,
|
|
||||||
id: `test-${Date.now()}`,
|
|
||||||
creationTime: new Date(),
|
|
||||||
lastModificationTime: new Date(),
|
|
||||||
}
|
|
||||||
setTests((prev) => [...prev, newTest])
|
|
||||||
setIsCreating(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdateTest = (
|
|
||||||
testData: Omit<Exam, 'id' | 'creationTime' | 'lastModificationTime'>,
|
|
||||||
) => {
|
|
||||||
if (editingTest) {
|
|
||||||
const updatedTest: Exam = {
|
|
||||||
...testData,
|
|
||||||
id: editingTest.id,
|
|
||||||
creationTime: editingTest.creationTime,
|
|
||||||
lastModificationTime: new Date(),
|
|
||||||
}
|
|
||||||
setTests((prev) => prev.map((test) => (test.id === updatedTest.id ? updatedTest : test)))
|
|
||||||
setEditingTest(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEdit = (test: Exam) => {
|
|
||||||
setEditingTest(test)
|
|
||||||
setIsCreating(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setIsCreating(false)
|
|
||||||
setEditingTest(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCreating) {
|
|
||||||
return (
|
|
||||||
<TestCreator
|
|
||||||
onCreateTest={editingTest ? handleUpdateTest : handleCreateTest}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
editingTest={editingTest || undefined}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-bold text-gray-900">Test Yönetimi</h1>
|
|
||||||
<p className="text-sm text-gray-600 mt-0.5">PDF testler ve cevap anahtarlarını yönetin</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setIsCreating(true)}
|
|
||||||
className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<FaPlus className="w-3.5 h-3.5" />
|
|
||||||
<span>Yeni Test</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters */}
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
||||||
<div className="relative">
|
|
||||||
<FaSearch className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Test ara..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="pl-8 text-sm w-full border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<FaFilter className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
|
|
||||||
<select
|
|
||||||
value={statusFilter}
|
|
||||||
onChange={(e) => setStatusFilter(e.target.value)}
|
|
||||||
className="pl-8 text-sm w-full border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 appearance-none"
|
|
||||||
>
|
|
||||||
<option value="">Tüm durumlar</option>
|
|
||||||
<option value="active">Aktif</option>
|
|
||||||
<option value="inactive">Pasif</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm text-gray-600 flex items-center">
|
|
||||||
Toplam: {filteredTests.length} test
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tests List */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
{filteredTests.length > 0 ? (
|
|
||||||
filteredTests.map((test) => (
|
|
||||||
<div
|
|
||||||
key={test.id}
|
|
||||||
className="bg-white border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center space-x-2 mb-1.5">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900">{test.title}</h3>
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
||||||
test.isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{test.isActive ? 'Aktif' : 'Pasif'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-gray-600 mb-2">{test.description}</p>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{test.testDocument?.type === 'pdf' ? (
|
|
||||||
<FaFileAlt className="w-4 h-4 text-red-600" />
|
|
||||||
) : (
|
|
||||||
<FaImage className="w-4 h-4 text-blue-600" />
|
|
||||||
)}
|
|
||||||
<span className="text-gray-600">
|
|
||||||
{test.testDocument?.type === 'pdf' ? 'PDF' : 'Resim'}
|
|
||||||
<span>: {test.testDocument?.name}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<FaCalendar className="w-4 h-4 text-gray-400" />
|
|
||||||
<span className="text-gray-600">{test.timeLimit} dakika</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<FaUsers className="w-4 h-4 text-gray-400" />
|
|
||||||
<span className="text-gray-600">
|
|
||||||
{test.answerKeyTemplate?.length || 0} soru
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-gray-600">Toplam: {test.totalPoints} puan</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-1.5 ml-4">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate(ROUTES_ENUM.protected.coordinator.testDetail.replace(':id', test.id))}
|
|
||||||
className="flex items-center space-x-1 px-2.5 py-1.5 bg-green-600 hover:bg-green-700 text-white text-xs rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<FaPlay className="w-3 h-3" />
|
|
||||||
<span>Başlat</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleEdit(test)}
|
|
||||||
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
title="Düzenle"
|
|
||||||
>
|
|
||||||
<FaEdit className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (window.confirm('Bu testi silmek istediğinizden emin misiniz?')) {
|
|
||||||
setTests((prev) => prev.filter((t) => t.id !== test.id))
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
||||||
title="Sil"
|
|
||||||
>
|
|
||||||
<FaTrash className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<FaFileAlt className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
|
||||||
<h3 className="text-base font-medium text-gray-900 mb-1">Test bulunamadı</h3>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
{searchTerm || statusFilter
|
|
||||||
? 'Arama kriterlerinizi değiştirin veya yeni test oluşturun.'
|
|
||||||
: 'İlk testinizi oluşturun.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Tests
|
|
||||||
|
|
@ -1,750 +0,0 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
|
||||||
import { AnimatePresence } from 'framer-motion'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
||||||
import isBetween from 'dayjs/plugin/isBetween'
|
|
||||||
import localizedFormat from 'dayjs/plugin/localizedFormat'
|
|
||||||
|
|
||||||
// Widgets
|
|
||||||
import TodayBirthdays from './widgets/TodayBirthdays'
|
|
||||||
import UpcomingEvents from './widgets/UpcomingEvents'
|
|
||||||
import RecentDocuments from './widgets/RecentDocuments'
|
|
||||||
import PriorityTasks from './widgets/PriorityTasks'
|
|
||||||
import MealWeeklyMenu from './widgets/MealWeeklyMenu'
|
|
||||||
import LeaveManagement from './widgets/LeaveManagement'
|
|
||||||
import OvertimeManagement from './widgets/OvertimeManagement'
|
|
||||||
import ExpenseManagement from './widgets/ExpenseManagement'
|
|
||||||
import UpcomingTrainings from './widgets/UpcomingTrainings'
|
|
||||||
import ActiveReservations from './widgets/ActiveReservations'
|
|
||||||
import ActiveSurveys from './widgets/ActiveSurveys'
|
|
||||||
import Visitors from './widgets/Visitors'
|
|
||||||
|
|
||||||
// Modals
|
|
||||||
import SurveyModal from './modals/SurveyModal'
|
|
||||||
import LeaveRequestModal from './modals/LeaveRequestModal'
|
|
||||||
import OvertimeRequestModal from './modals/OvertimeRequestModal'
|
|
||||||
import ExpenseRequestModal from './modals/ExpenseRequestModal'
|
|
||||||
import ReservationRequestModal from './modals/ReservationRequestModal'
|
|
||||||
import AnnouncementDetailModal from './modals/AnnouncementDetailModal'
|
|
||||||
|
|
||||||
// Social Wall
|
|
||||||
import SocialWall from './SocialWall'
|
|
||||||
import { Container } from '@/components/shared'
|
|
||||||
import { usePermission } from '@/utils/hooks/usePermission'
|
|
||||||
import {
|
|
||||||
AnnouncementDto,
|
|
||||||
IntranetDashboardDto,
|
|
||||||
SurveyAnswerDto,
|
|
||||||
SurveyDto,
|
|
||||||
} from '@/proxy/intranet/models'
|
|
||||||
import { intranetService } from '@/services/intranet.service'
|
|
||||||
import Announcements from './widgets/Announcements'
|
|
||||||
import ShuttleRoute from './widgets/ShuttleRoute'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import useLocale from '@/utils/hooks/useLocale'
|
|
||||||
import { currentLocalDate } from '@/utils/dateUtils'
|
|
||||||
import { useStoreState } from '@/store/store'
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
|
||||||
dayjs.extend(isBetween)
|
|
||||||
dayjs.extend(localizedFormat)
|
|
||||||
|
|
||||||
const WIDGET_ORDER_KEY = 'dashboard-widget-order'
|
|
||||||
|
|
||||||
const IntranetDashboard: React.FC = () => {
|
|
||||||
const { checkPermission } = usePermission()
|
|
||||||
const [selectedAnnouncement, setSelectedAnnouncement] = useState<AnnouncementDto | null>(null)
|
|
||||||
const [selectedSurvey, setSelectedSurvey] = useState<SurveyDto | null>(null)
|
|
||||||
const [showSurveyModal, setShowSurveyModal] = useState(false)
|
|
||||||
const [showLeaveModal, setShowLeaveModal] = useState(false)
|
|
||||||
const [showOvertimeModal, setShowOvertimeModal] = useState(false)
|
|
||||||
const [showExpenseModal, setShowExpenseModal] = useState(false)
|
|
||||||
const [showReservationModal, setShowReservationModal] = useState(false)
|
|
||||||
const [isDesignMode, setIsDesignMode] = useState(false)
|
|
||||||
const [widgetOrder, setWidgetOrder] = useState<Record<string, string[]>>({
|
|
||||||
left: [],
|
|
||||||
center: [],
|
|
||||||
right: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
const [intranetDashboard, setIntranetDashboard] = useState<IntranetDashboardDto>()
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
const currentLocale = useLocale()
|
|
||||||
|
|
||||||
const fetchIntranetDashboard = async () => {
|
|
||||||
const dashboard = await intranetService.getDashboard()
|
|
||||||
if (dashboard.data) {
|
|
||||||
setIntranetDashboard(dashboard.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchIntranetDashboard()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Drag state'leri birleştirildi
|
|
||||||
const [dragState, setDragState] = useState<{
|
|
||||||
draggedId: string | null
|
|
||||||
targetColumn: string | null
|
|
||||||
targetIndex: number | null
|
|
||||||
}>({
|
|
||||||
draggedId: null,
|
|
||||||
targetColumn: null,
|
|
||||||
targetIndex: null,
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleTakeSurvey = (survey: SurveyDto) => {
|
|
||||||
setSelectedSurvey(survey)
|
|
||||||
setShowSurveyModal(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmitSurvey = (answers: SurveyAnswerDto[]) => {
|
|
||||||
setShowSurveyModal(false)
|
|
||||||
setSelectedSurvey(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmitLeave = () => {
|
|
||||||
setShowLeaveModal(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmitOvertime = () => {
|
|
||||||
setShowOvertimeModal(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmitExpense = () => {
|
|
||||||
setShowExpenseModal(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmitReservation = () => {
|
|
||||||
setShowReservationModal(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Widget metadata (component'lar yerine sadece meta bilgiler)
|
|
||||||
const widgetMetadata = [
|
|
||||||
{ id: 'upcoming-events', permission: 'App.Intranet.Events.Event.Widget', column: 'left' },
|
|
||||||
{ id: 'today-birthdays', permission: 'App.Hr.Employee.Widget', column: 'left' },
|
|
||||||
{ id: 'documents', permission: 'App.Files.Widget', column: 'left' },
|
|
||||||
{ id: 'upcoming-trainings', permission: 'App.Hr.Training.Widget', column: 'left' },
|
|
||||||
{ id: 'active-reservations', permission: 'App.Intranet.Reservation.Widget', column: 'left' },
|
|
||||||
{ id: 'active-surveys', permission: 'App.Hr.Survey.Widget', column: 'left' },
|
|
||||||
{ id: 'visitors', permission: 'App.Intranet.Visitor.Widget', column: 'left' },
|
|
||||||
{ id: 'expense-management', permission: 'App.Hr.Expense.Widget', column: 'left' },
|
|
||||||
{ id: 'social-wall', permission: 'App.Intranet.SocialPost.Widget', column: 'center' },
|
|
||||||
{
|
|
||||||
id: 'announcements',
|
|
||||||
permission: 'App.Intranet.Announcement.Widget',
|
|
||||||
column: 'right',
|
|
||||||
},
|
|
||||||
{ id: 'priority-tasks', permission: 'App.Project.ProjectTask.Widget', column: 'right' },
|
|
||||||
{ id: 'meal-weekly-menu', permission: 'App.Intranet.Meal.Widget', column: 'right' },
|
|
||||||
{ id: 'shuttle-route', permission: 'App.Intranet.ShuttleRoute.Widget', column: 'right' },
|
|
||||||
{ id: 'leave-management', permission: 'App.Hr.Leave.Widget', column: 'right' },
|
|
||||||
{ id: 'overtime-management', permission: 'App.Hr.Overtime.Widget', column: 'right' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// Widget sıralamasını yükle
|
|
||||||
useEffect(() => {
|
|
||||||
const savedOrder = localStorage.getItem(WIDGET_ORDER_KEY)
|
|
||||||
if (savedOrder) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(savedOrder) as Record<string, unknown[]>
|
|
||||||
// Duplicate key'leri temizle
|
|
||||||
const cleanedOrder: Record<string, string[]> = {
|
|
||||||
left: [...new Set((parsed.left || []) as string[])],
|
|
||||||
center: [...new Set((parsed.center || []) as string[])],
|
|
||||||
right: [...new Set((parsed.right || []) as string[])],
|
|
||||||
}
|
|
||||||
setWidgetOrder(cleanedOrder)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Widget order parse error:', error)
|
|
||||||
initializeDefaultOrder()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
initializeDefaultOrder()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// If permissions arrive after mount, initialize default order when needed
|
|
||||||
const grantedPolicies = useStoreState((state) => state.abpConfig?.config?.auth.grantedPolicies)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
grantedPolicies &&
|
|
||||||
(!widgetOrder.left.length && !widgetOrder.center.length && !widgetOrder.right.length)
|
|
||||||
) {
|
|
||||||
initializeDefaultOrder()
|
|
||||||
}
|
|
||||||
}, [grantedPolicies])
|
|
||||||
|
|
||||||
const initializeDefaultOrder = () => {
|
|
||||||
const defaultOrder = {
|
|
||||||
left: widgetMetadata
|
|
||||||
.filter((w) => w.column === 'left' && checkPermission(w.permission))
|
|
||||||
.map((w) => w.id),
|
|
||||||
center: widgetMetadata
|
|
||||||
.filter((w) => w.column === 'center' && checkPermission(w.permission))
|
|
||||||
.map((w) => w.id),
|
|
||||||
right: widgetMetadata
|
|
||||||
.filter((w) => w.column === 'right' && checkPermission(w.permission))
|
|
||||||
.map((w) => w.id),
|
|
||||||
}
|
|
||||||
setWidgetOrder(defaultOrder)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Widget sıralamasını kaydet
|
|
||||||
const saveWidgetOrder = (newOrder: Record<string, string[]>) => {
|
|
||||||
setWidgetOrder(newOrder)
|
|
||||||
localStorage.setItem(WIDGET_ORDER_KEY, JSON.stringify(newOrder))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDragStart = (e: React.DragEvent, widgetId: string, column: string) => {
|
|
||||||
setDragState({ draggedId: widgetId, targetColumn: null, targetIndex: null })
|
|
||||||
e.dataTransfer.effectAllowed = 'move'
|
|
||||||
e.dataTransfer.setData('widgetId', widgetId)
|
|
||||||
e.dataTransfer.setData('sourceColumn', column)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDragEnterWidget = (e: React.DragEvent, column: string, index: number) => {
|
|
||||||
// Sadece widget'ın üst kısmına yakınsa indicator göster
|
|
||||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
|
||||||
const mouseY = e.clientY
|
|
||||||
const widgetTop = rect.top
|
|
||||||
const widgetHeight = rect.height
|
|
||||||
const threshold = widgetHeight * 0.3 // Üst %30'luk alan
|
|
||||||
|
|
||||||
if (mouseY - widgetTop < threshold) {
|
|
||||||
// Üst kısma yakın - indicator göster
|
|
||||||
setDragState((prev) => ({ ...prev, targetColumn: column, targetIndex: index }))
|
|
||||||
} else {
|
|
||||||
// Widget'ın ortasında veya altında - indicator gösterme
|
|
||||||
setDragState((prev) => ({ ...prev, targetColumn: column, targetIndex: null }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDragEnterColumn = (column: string) => {
|
|
||||||
setDragState((prev) => ({ ...prev, targetColumn: column, targetIndex: null }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDragLeaveColumn = () => {
|
|
||||||
setDragState((prev) => ({ ...prev, targetColumn: null, targetIndex: null }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent, targetColumn: string, targetIndex?: number) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
const widgetId = e.dataTransfer.getData('widgetId')
|
|
||||||
const sourceColumn = e.dataTransfer.getData('sourceColumn')
|
|
||||||
|
|
||||||
if (!widgetId || !sourceColumn) return
|
|
||||||
|
|
||||||
const newOrder = { ...widgetOrder }
|
|
||||||
|
|
||||||
// ÖNCE tüm kolonlardan bu widget'ı kaldır (duplicate önleme)
|
|
||||||
Object.keys(newOrder).forEach((col) => {
|
|
||||||
newOrder[col] = newOrder[col].filter((id) => id !== widgetId)
|
|
||||||
})
|
|
||||||
|
|
||||||
// SONRA hedef kolona ekle
|
|
||||||
if (targetIndex !== undefined) {
|
|
||||||
newOrder[targetColumn].splice(targetIndex, 0, widgetId)
|
|
||||||
} else {
|
|
||||||
newOrder[targetColumn].push(widgetId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duplicate'leri temizle
|
|
||||||
Object.keys(newOrder).forEach((col) => {
|
|
||||||
newOrder[col] = [...new Set(newOrder[col])]
|
|
||||||
})
|
|
||||||
|
|
||||||
saveWidgetOrder(newOrder)
|
|
||||||
setDragState({ draggedId: null, targetColumn: null, targetIndex: null })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDragEnd = () => {
|
|
||||||
setDragState({ draggedId: null, targetColumn: null, targetIndex: null })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Widget component'ını render et
|
|
||||||
const renderWidgetComponent = (widgetId: string) => {
|
|
||||||
switch (widgetId) {
|
|
||||||
case 'upcoming-events':
|
|
||||||
return <UpcomingEvents events={intranetDashboard?.events || []} />
|
|
||||||
case 'today-birthdays':
|
|
||||||
return <TodayBirthdays employees={intranetDashboard?.birthdays || []} />
|
|
||||||
case 'visitors':
|
|
||||||
return <Visitors visitors={intranetDashboard?.visitors || []} />
|
|
||||||
case 'active-reservations':
|
|
||||||
return (
|
|
||||||
<ActiveReservations
|
|
||||||
reservations={intranetDashboard?.reservations || []}
|
|
||||||
onNewReservation={() => setShowReservationModal(true)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'upcoming-trainings':
|
|
||||||
return <UpcomingTrainings trainings={intranetDashboard?.trainings || []} />
|
|
||||||
case 'expense-management':
|
|
||||||
return (
|
|
||||||
<ExpenseManagement
|
|
||||||
expenses={
|
|
||||||
intranetDashboard?.expenses || {
|
|
||||||
totalRequested: 0,
|
|
||||||
totalApproved: 0,
|
|
||||||
last5Expenses: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onNewExpense={() => setShowExpenseModal(true)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'documents':
|
|
||||||
return <RecentDocuments documents={intranetDashboard?.documents || []} />
|
|
||||||
case 'announcements':
|
|
||||||
return (
|
|
||||||
<Announcements
|
|
||||||
announcements={intranetDashboard?.announcements || []}
|
|
||||||
onAnnouncementClick={setSelectedAnnouncement}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'shuttle-route':
|
|
||||||
return <ShuttleRoute shuttleRoutes={intranetDashboard?.shuttleRoutes || []} />
|
|
||||||
case 'meal-weekly-menu':
|
|
||||||
return <MealWeeklyMenu meals={intranetDashboard?.meals || []} />
|
|
||||||
case 'leave-management':
|
|
||||||
return (
|
|
||||||
<LeaveManagement
|
|
||||||
leaves={intranetDashboard?.leaves || []}
|
|
||||||
onNewLeave={() => setShowLeaveModal(true)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'overtime-management':
|
|
||||||
return (
|
|
||||||
<OvertimeManagement
|
|
||||||
overtimes={intranetDashboard?.overtimes || []}
|
|
||||||
onNewOvertime={() => setShowOvertimeModal(true)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'active-surveys':
|
|
||||||
return (
|
|
||||||
<ActiveSurveys
|
|
||||||
surveys={intranetDashboard?.surveys || []}
|
|
||||||
onTakeSurvey={handleTakeSurvey}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'social-wall':
|
|
||||||
return <SocialWall posts={intranetDashboard?.socialPosts || []} />
|
|
||||||
case 'priority-tasks':
|
|
||||||
return <PriorityTasks tasks={intranetDashboard?.tasks || []} />
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Widget'ları render et
|
|
||||||
const renderWidgets = (column: 'left' | 'center' | 'right') => {
|
|
||||||
const columnWidgets = widgetOrder[column] || []
|
|
||||||
|
|
||||||
// Duplicate'leri filtrele
|
|
||||||
const uniqueWidgets = [...new Set(columnWidgets)]
|
|
||||||
|
|
||||||
return uniqueWidgets
|
|
||||||
.map((widgetId, index) => {
|
|
||||||
const metadata = widgetMetadata.find((w) => w.id === widgetId)
|
|
||||||
if (!metadata || !checkPermission(metadata.permission)) return null
|
|
||||||
|
|
||||||
const isDragging = dragState.draggedId === widgetId
|
|
||||||
const isDropTarget = dragState.targetColumn === column && dragState.targetIndex === index
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={`${column}-${widgetId}-${index}`} className="relative group">
|
|
||||||
{/* Drop indicator - SADECE widget'ların arasına (üst %30'luk alana) gelince göster */}
|
|
||||||
{isDesignMode && isDropTarget && !isDragging && (
|
|
||||||
<div className="absolute -top-5 left-0 right-0 z-20 animate-in fade-in slide-in-from-top-2 duration-300">
|
|
||||||
{/* Çizgi */}
|
|
||||||
<div className="h-2 bg-gradient-to-r from-transparent via-blue-500 to-transparent rounded-full shadow-lg" />
|
|
||||||
{/* Badge */}
|
|
||||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2 bg-gradient-to-r from-blue-500 to-blue-600 text-white text-xs px-4 py-2 rounded-full whitespace-nowrap shadow-xl font-semibold flex items-center gap-2 border-2 border-white dark:border-gray-800">
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M19 14l-7 7m0 0l-7-7m7 7V3"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>Buraya Bırak</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
draggable={isDesignMode}
|
|
||||||
onDragStart={(e) => {
|
|
||||||
if (isDesignMode) {
|
|
||||||
handleDragStart(e, widgetId, column)
|
|
||||||
// Drag ghost image'i gizle
|
|
||||||
const ghost = document.createElement('div')
|
|
||||||
ghost.style.opacity = '0'
|
|
||||||
e.dataTransfer.setDragImage(ghost, 0, 0)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDragOver={(e) => {
|
|
||||||
if (!isDesignMode) return
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
// Throttle: Sadece düzenli aralıklarla güncelle
|
|
||||||
const now = Date.now()
|
|
||||||
if (
|
|
||||||
!e.currentTarget.dataset.lastUpdate ||
|
|
||||||
now - parseInt(e.currentTarget.dataset.lastUpdate) > 150
|
|
||||||
) {
|
|
||||||
e.currentTarget.dataset.lastUpdate = now.toString()
|
|
||||||
handleDragEnterWidget(e, column, index)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDragLeave={(e) => {
|
|
||||||
// Widget'tan çıkınca indicator'ı kaldır
|
|
||||||
if (isDesignMode) {
|
|
||||||
setDragState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
targetColumn: prev.targetColumn,
|
|
||||||
targetIndex: null,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDrop={(e) => {
|
|
||||||
if (!isDesignMode) return
|
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
// Drop pozisyonunu hesapla
|
|
||||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
|
||||||
const mouseY = e.clientY
|
|
||||||
const widgetTop = rect.top
|
|
||||||
const widgetHeight = rect.height
|
|
||||||
const threshold = widgetHeight * 0.3
|
|
||||||
|
|
||||||
if (mouseY - widgetTop < threshold) {
|
|
||||||
// Üst kısma bırak - mevcut index'e ekle
|
|
||||||
handleDrop(e, column, index)
|
|
||||||
} else {
|
|
||||||
// Alt kısma bırak - sonraki index'e ekle
|
|
||||||
handleDrop(e, column, index + 1)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
className={`
|
|
||||||
relative
|
|
||||||
${
|
|
||||||
isDesignMode
|
|
||||||
? `border-2 border-dashed rounded-lg cursor-move
|
|
||||||
${
|
|
||||||
isDragging
|
|
||||||
? 'border-blue-400 opacity-70 bg-blue-50/30 dark:bg-blue-900/10'
|
|
||||||
: 'border-gray-300 dark:border-gray-600 hover:border-blue-400 dark:hover:border-blue-500 hover:shadow-md'
|
|
||||||
}
|
|
||||||
transition-all duration-300 ease-out`
|
|
||||||
: 'border-0 transition-none'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
style={{
|
|
||||||
touchAction: 'none',
|
|
||||||
transition:
|
|
||||||
'border-color 0.3s ease-out, opacity 0.3s ease-out, box-shadow 0.3s ease-out',
|
|
||||||
willChange: isDragging ? 'opacity' : 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Dragging overlay - daha minimal */}
|
|
||||||
{isDesignMode && isDragging && (
|
|
||||||
<div className="absolute inset-0 bg-white/60 dark:bg-gray-900/40 rounded-lg z-10 flex items-center justify-center backdrop-blur-[1px] transition-opacity duration-300">
|
|
||||||
<div className="bg-gradient-to-r from-blue-500 to-blue-600 text-white px-4 py-2 rounded-lg font-medium shadow-lg flex items-center gap-2">
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>Taşınıyor</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={`${isDesignMode ? 'p-1.5' : ''} transition-all duration-500 ease-out`}
|
|
||||||
>
|
|
||||||
{renderWidgetComponent(widgetId)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<div className="mx-auto space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{/* Design Mode Toggle */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label
|
|
||||||
htmlFor="design-mode-toggle"
|
|
||||||
className="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
|
|
||||||
>
|
|
||||||
🎨 {translate('::App.Platform.Intranet.Dashboard.DesignMode')}
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
id="design-mode-toggle"
|
|
||||||
onClick={() => setIsDesignMode(!isDesignMode)}
|
|
||||||
className={`
|
|
||||||
relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
|
||||||
${isDesignMode ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-600'}
|
|
||||||
`}
|
|
||||||
role="switch"
|
|
||||||
aria-checked={isDesignMode}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`
|
|
||||||
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
|
|
||||||
${isDesignMode ? 'translate-x-6' : 'translate-x-1'}
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Reset Button - Sadece design mode aktifken görünsün */}
|
|
||||||
{isDesignMode && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
localStorage.removeItem(WIDGET_ORDER_KEY)
|
|
||||||
initializeDefaultOrder()
|
|
||||||
}}
|
|
||||||
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors flex items-center gap-1"
|
|
||||||
title="Widget düzenini varsayılana döndür"
|
|
||||||
>
|
|
||||||
🔄 {translate('::App.Platform.Intranet.Dashboard.Reset')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
<span className="font-medium">{translate('::AI.Welcome')},</span>{' '}
|
|
||||||
{currentLocalDate(new Date(), currentLocale || 'tr')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12">
|
|
||||||
<div
|
|
||||||
className={`lg:col-span-3 space-y-6 min-h-[100px] rounded-xl p-1
|
|
||||||
${
|
|
||||||
isDesignMode && dragState.targetColumn === 'left' && dragState.targetIndex === null
|
|
||||||
? 'bg-blue-50/80 dark:bg-blue-900/20 ring-2 ring-blue-300 dark:ring-blue-600 shadow-lg'
|
|
||||||
: 'bg-transparent'
|
|
||||||
}
|
|
||||||
transition-all duration-700 ease-out
|
|
||||||
`}
|
|
||||||
onDragOver={(e) => {
|
|
||||||
if (!isDesignMode) return
|
|
||||||
e.preventDefault()
|
|
||||||
// Throttle: Sadece her 150ms'de bir güncelle
|
|
||||||
const now = Date.now()
|
|
||||||
const target = e.currentTarget as HTMLElement
|
|
||||||
if (
|
|
||||||
!target.dataset.lastColumnUpdate ||
|
|
||||||
now - parseInt(target.dataset.lastColumnUpdate) > 150
|
|
||||||
) {
|
|
||||||
target.dataset.lastColumnUpdate = now.toString()
|
|
||||||
handleDragEnterColumn('left')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDragLeave={() => {
|
|
||||||
if (isDesignMode) handleDragLeaveColumn()
|
|
||||||
}}
|
|
||||||
onDrop={(e) => {
|
|
||||||
if (!isDesignMode) return
|
|
||||||
e.stopPropagation()
|
|
||||||
const columnWidgets = widgetOrder['left'] || []
|
|
||||||
handleDrop(e, 'left', columnWidgets.length)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{renderWidgets('left')}
|
|
||||||
{isDesignMode &&
|
|
||||||
dragState.targetColumn === 'left' &&
|
|
||||||
widgetOrder['left']?.length === 0 && (
|
|
||||||
<div className="flex items-center justify-center h-40 border-2 border-dashed border-blue-300 dark:border-blue-600 rounded-xl bg-blue-50/50 dark:bg-blue-900/10 transition-all duration-500 ease-out">
|
|
||||||
<p className="text-blue-600 dark:text-blue-400 font-medium flex items-center gap-2">
|
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>Widget'ı buraya bırakın</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`lg:col-span-6 space-y-6 min-h-[100px] rounded-xl p-1
|
|
||||||
${
|
|
||||||
isDesignMode &&
|
|
||||||
dragState.targetColumn === 'center' &&
|
|
||||||
dragState.targetIndex === null
|
|
||||||
? 'bg-blue-50/80 dark:bg-blue-900/20 ring-2 ring-blue-300 dark:ring-blue-600 shadow-lg'
|
|
||||||
: 'bg-transparent'
|
|
||||||
}
|
|
||||||
transition-all duration-700 ease-out
|
|
||||||
`}
|
|
||||||
onDragOver={(e) => {
|
|
||||||
if (!isDesignMode) return
|
|
||||||
e.preventDefault()
|
|
||||||
const now = Date.now()
|
|
||||||
const target = e.currentTarget as HTMLElement
|
|
||||||
if (
|
|
||||||
!target.dataset.lastColumnUpdate ||
|
|
||||||
now - parseInt(target.dataset.lastColumnUpdate) > 150
|
|
||||||
) {
|
|
||||||
target.dataset.lastColumnUpdate = now.toString()
|
|
||||||
handleDragEnterColumn('center')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDragLeave={() => {
|
|
||||||
if (isDesignMode) handleDragLeaveColumn()
|
|
||||||
}}
|
|
||||||
onDrop={(e) => {
|
|
||||||
if (!isDesignMode) return
|
|
||||||
e.stopPropagation()
|
|
||||||
const columnWidgets = widgetOrder['center'] || []
|
|
||||||
handleDrop(e, 'center', columnWidgets.length)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{renderWidgets('center')}
|
|
||||||
{isDesignMode &&
|
|
||||||
dragState.targetColumn === 'center' &&
|
|
||||||
widgetOrder['center']?.length === 0 && (
|
|
||||||
<div className="flex items-center justify-center rounded-xl bg-blue-50/50 dark:bg-blue-900/10 transition-all duration-500 ease-out">
|
|
||||||
<p className="text-blue-600 dark:text-blue-400 font-medium flex items-center gap-2">
|
|
||||||
<span>Widget'ı buraya bırakın</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`lg:col-span-3 space-y-6 min-h-[100px] rounded-xl p-1
|
|
||||||
${
|
|
||||||
isDesignMode && dragState.targetColumn === 'right' && dragState.targetIndex === null
|
|
||||||
? 'bg-blue-50/80 dark:bg-blue-900/20 ring-2 ring-blue-300 dark:ring-blue-600 shadow-lg'
|
|
||||||
: 'bg-transparent'
|
|
||||||
}
|
|
||||||
transition-all duration-700 ease-out
|
|
||||||
`}
|
|
||||||
onDragOver={(e) => {
|
|
||||||
if (!isDesignMode) return
|
|
||||||
e.preventDefault()
|
|
||||||
const now = Date.now()
|
|
||||||
const target = e.currentTarget as HTMLElement
|
|
||||||
if (
|
|
||||||
!target.dataset.lastColumnUpdate ||
|
|
||||||
now - parseInt(target.dataset.lastColumnUpdate) > 150
|
|
||||||
) {
|
|
||||||
target.dataset.lastColumnUpdate = now.toString()
|
|
||||||
handleDragEnterColumn('right')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDragLeave={() => {
|
|
||||||
if (isDesignMode) handleDragLeaveColumn()
|
|
||||||
}}
|
|
||||||
onDrop={(e) => {
|
|
||||||
if (!isDesignMode) return
|
|
||||||
e.stopPropagation()
|
|
||||||
const columnWidgets = widgetOrder['right'] || []
|
|
||||||
handleDrop(e, 'right', columnWidgets.length)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{renderWidgets('right')}
|
|
||||||
{isDesignMode &&
|
|
||||||
dragState.targetColumn === 'right' &&
|
|
||||||
widgetOrder['right']?.length === 0 && (
|
|
||||||
<div className="flex items-center justify-center h-40 border-2 border-dashed border-blue-300 dark:border-blue-600 rounded-xl bg-blue-50/50 dark:bg-blue-900/10 transition-all duration-500 ease-out">
|
|
||||||
<p className="text-blue-600 dark:text-blue-400 font-medium flex items-center gap-2">
|
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>Widget'ı buraya bırakın</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{showSurveyModal && selectedSurvey && (
|
|
||||||
<SurveyModal
|
|
||||||
survey={selectedSurvey}
|
|
||||||
onClose={() => setShowSurveyModal(false)}
|
|
||||||
onSubmit={handleSubmitSurvey}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{showLeaveModal && (
|
|
||||||
<LeaveRequestModal
|
|
||||||
onClose={() => setShowLeaveModal(false)}
|
|
||||||
onSubmit={handleSubmitLeave}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{showOvertimeModal && (
|
|
||||||
<OvertimeRequestModal
|
|
||||||
onClose={() => setShowOvertimeModal(false)}
|
|
||||||
onSubmit={handleSubmitOvertime}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{showExpenseModal && (
|
|
||||||
<ExpenseRequestModal
|
|
||||||
onClose={() => setShowExpenseModal(false)}
|
|
||||||
onSubmit={handleSubmitExpense}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{showReservationModal && (
|
|
||||||
<ReservationRequestModal
|
|
||||||
onClose={() => setShowReservationModal(false)}
|
|
||||||
onSubmit={handleSubmitReservation}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{selectedAnnouncement && (
|
|
||||||
<AnnouncementDetailModal
|
|
||||||
announcement={selectedAnnouncement}
|
|
||||||
onClose={() => setSelectedAnnouncement(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IntranetDashboard
|
|
||||||
|
|
@ -1,466 +0,0 @@
|
||||||
import React, { useState, useRef } from 'react'
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
|
||||||
import classNames from 'classnames'
|
|
||||||
import EmojiPicker, { EmojiClickData } from 'emoji-picker-react'
|
|
||||||
import {
|
|
||||||
FaChartBar,
|
|
||||||
FaSmile,
|
|
||||||
FaTimes,
|
|
||||||
FaImages,
|
|
||||||
FaMapMarkerAlt
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
import MediaManager from './MediaManager'
|
|
||||||
import LocationPicker from './LocationPicker'
|
|
||||||
import { SocialMediaDto } from '@/proxy/intranet/models'
|
|
||||||
|
|
||||||
interface CreatePostProps {
|
|
||||||
onCreatePost: (post: {
|
|
||||||
content: string
|
|
||||||
location?: string
|
|
||||||
media?: {
|
|
||||||
type: 'mixed' | 'poll'
|
|
||||||
mediaItems?: SocialMediaDto[]
|
|
||||||
poll?: {
|
|
||||||
question: string
|
|
||||||
options: Array<{ text: string }>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
|
||||||
const { translate } = useLocalization();
|
|
||||||
const [content, setContent] = useState('')
|
|
||||||
const [mediaType, setMediaType] = useState<'media' | 'poll' | null>(null)
|
|
||||||
const [mediaItems, setMediaItems] = useState<SocialMediaDto[]>([])
|
|
||||||
const [location, setLocation] = useState<string | null>(null)
|
|
||||||
const [pollQuestion, setPollQuestion] = useState('')
|
|
||||||
const [pollOptions, setPollOptions] = useState(['', ''])
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false)
|
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
|
||||||
const [showMediaManager, setShowMediaManager] = useState(false)
|
|
||||||
const [showLocationPicker, setShowLocationPicker] = useState(false)
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
||||||
const emojiPickerRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
if (!content.trim() && mediaItems.length === 0 && !mediaType) return
|
|
||||||
|
|
||||||
let media = undefined
|
|
||||||
|
|
||||||
if (mediaType === 'media' && mediaItems.length > 0) {
|
|
||||||
media = {
|
|
||||||
type: 'mixed' as const,
|
|
||||||
mediaItems
|
|
||||||
}
|
|
||||||
} else if (mediaType === 'poll' && pollQuestion && pollOptions.filter(o => o.trim()).length >= 2) {
|
|
||||||
media = {
|
|
||||||
type: 'poll' as const,
|
|
||||||
poll: {
|
|
||||||
question: pollQuestion,
|
|
||||||
options: pollOptions.filter(o => o.trim()).map(text => ({ text }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCreatePost({
|
|
||||||
content,
|
|
||||||
media,
|
|
||||||
location: location || undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
// Reset form
|
|
||||||
setContent('')
|
|
||||||
setMediaType(null)
|
|
||||||
setMediaItems([])
|
|
||||||
setLocation(null)
|
|
||||||
setPollQuestion('')
|
|
||||||
setPollOptions(['', ''])
|
|
||||||
setIsExpanded(false)
|
|
||||||
setShowEmojiPicker(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEmojiClick = (emojiData: EmojiClickData) => {
|
|
||||||
const emoji = emojiData.emoji
|
|
||||||
const textarea = textareaRef.current
|
|
||||||
if (!textarea) return
|
|
||||||
|
|
||||||
const start = textarea.selectionStart
|
|
||||||
const end = textarea.selectionEnd
|
|
||||||
const text = content
|
|
||||||
const before = text.substring(0, start)
|
|
||||||
const after = text.substring(end)
|
|
||||||
|
|
||||||
setContent(before + emoji + after)
|
|
||||||
|
|
||||||
// Set cursor position after emoji
|
|
||||||
setTimeout(() => {
|
|
||||||
textarea.selectionStart = textarea.selectionEnd = start + emoji.length
|
|
||||||
textarea.focus()
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addPollOption = () => {
|
|
||||||
if (pollOptions.length < 6) {
|
|
||||||
setPollOptions([...pollOptions, ''])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removePollOption = (index: number) => {
|
|
||||||
if (pollOptions.length > 2) {
|
|
||||||
setPollOptions(pollOptions.filter((_, i) => i !== index))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePollOption = (index: number, value: string) => {
|
|
||||||
const newOptions = [...pollOptions]
|
|
||||||
newOptions[index] = value
|
|
||||||
setPollOptions(newOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearMedia = () => {
|
|
||||||
setMediaType(null)
|
|
||||||
setMediaItems([])
|
|
||||||
setPollQuestion('')
|
|
||||||
setPollOptions(['', ''])
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeMediaItem = (id: string | undefined) => {
|
|
||||||
if (!id) return
|
|
||||||
setMediaItems(mediaItems.filter((m) => m.id !== id))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close emoji picker when clicking outside
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (emojiPickerRef.current && !emojiPickerRef.current.contains(event.target as Node)) {
|
|
||||||
setShowEmojiPicker(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showEmojiPicker) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside)
|
|
||||||
}
|
|
||||||
}, [showEmojiPicker])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 mb-6">
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
{/* Text Input */}
|
|
||||||
<div className="flex gap-3 mb-4">
|
|
||||||
<img
|
|
||||||
src="https://i.pravatar.cc/150?img=1"
|
|
||||||
alt="Your avatar"
|
|
||||||
className="w-12 h-12 rounded-full object-cover"
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<textarea
|
|
||||||
ref={textareaRef}
|
|
||||||
value={content}
|
|
||||||
onChange={(e) => setContent(e.target.value)}
|
|
||||||
onFocus={() => setIsExpanded(true)}
|
|
||||||
placeholder={translate('::App.Platform.Intranet.SocialWall.CreatePost.Placeholder')}
|
|
||||||
className={classNames(
|
|
||||||
'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none transition-all',
|
|
||||||
isExpanded ? 'min-h-[120px]' : 'min-h-[48px]'
|
|
||||||
)}
|
|
||||||
rows={isExpanded ? 4 : 1}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Media Preview */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{mediaType === 'media' && mediaItems.length > 0 && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, height: 0 }}
|
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
|
||||||
exit={{ opacity: 0, height: 0 }}
|
|
||||||
className="mb-4"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.CreatePost.MediaTitle')} ({mediaItems.length})
|
|
||||||
</h4>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
clearMedia()
|
|
||||||
}}
|
|
||||||
className="text-sm text-red-600 hover:text-red-700 font-medium"
|
|
||||||
title={translate('::App.Platform.Intranet.SocialWall.CreatePost.RemoveAllMediaTitle')}
|
|
||||||
>
|
|
||||||
{translate('::Cancel')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 gap-2">
|
|
||||||
{mediaItems.map((item) => (
|
|
||||||
<div key={item.id} className="relative group">
|
|
||||||
{item.type === 'image' ? (
|
|
||||||
<img
|
|
||||||
src={item.urls?.[0]}
|
|
||||||
alt="Preview"
|
|
||||||
className="w-full h-24 object-cover rounded-lg"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-24 bg-gray-900 rounded-lg relative">
|
|
||||||
<video src={item.urls?.[0]} className="w-full h-full object-cover rounded-lg" />
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<div className="w-10 h-10 bg-black bg-opacity-50 rounded-full flex items-center justify-center">
|
|
||||||
<div className="w-0 h-0 border-t-8 border-t-transparent border-l-12 border-l-white border-b-8 border-b-transparent ml-1"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeMediaItem(item.id)}
|
|
||||||
className="absolute -top-2 -right-2 p-1 bg-red-600 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
|
|
||||||
>
|
|
||||||
<FaTimes className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<div className="absolute bottom-1 left-1 px-2 py-0.5 bg-black bg-opacity-70 text-white text-xs rounded">
|
|
||||||
{item.type === 'image' ? '📷' : '🎥'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowMediaManager(true)}
|
|
||||||
className="h-24 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg flex items-center justify-center hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors"
|
|
||||||
>
|
|
||||||
<FaImages className="w-6 h-6 text-gray-400" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{location && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, height: 0 }}
|
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
|
||||||
exit={{ opacity: 0, height: 0 }}
|
|
||||||
className="mb-4"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">{translate('::App.Platform.Intranet.SocialWall.CreatePost.Location')}</h4>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setLocation(null)}
|
|
||||||
className="text-sm text-red-600 hover:text-red-700 font-medium"
|
|
||||||
title={translate('::App.Platform.Intranet.SocialWall.CreatePost.RemoveLocationTitle')}
|
|
||||||
>
|
|
||||||
{translate('::Cancel')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<FaMapMarkerAlt className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
|
||||||
{JSON.parse(location).name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
|
||||||
{JSON.parse(location).address}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{mediaType === 'poll' && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, height: 0 }}
|
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
|
||||||
exit={{ opacity: 0, height: 0 }}
|
|
||||||
className="mb-4"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">{translate('::App.Platform.Intranet.SocialWall.CreatePost.Poll')}</h4>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
clearMedia()
|
|
||||||
}}
|
|
||||||
className="text-sm text-red-600 hover:text-red-700 font-medium"
|
|
||||||
title={translate('::App.Platform.Intranet.SocialWall.CreatePost.RemovePollTitle')}
|
|
||||||
>
|
|
||||||
{translate('::Cancel')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={pollQuestion}
|
|
||||||
onChange={(e) => setPollQuestion(e.target.value)}
|
|
||||||
placeholder={translate('::App.Platform.Intranet.SocialWall.CreatePost.PollQuestionPlaceholder')}
|
|
||||||
className="w-full px-4 py-2 mb-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{pollOptions.map((option, index) => (
|
|
||||||
<div key={index} className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={option}
|
|
||||||
onChange={(e) => updatePollOption(index, e.target.value)}
|
|
||||||
placeholder={translate('::App.Platform.Intranet.SocialWall.CreatePost.PollOptionPlaceholder')}
|
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
{pollOptions.length > 2 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removePollOption(index)}
|
|
||||||
className="p-2 text-gray-500 hover:text-red-600"
|
|
||||||
>
|
|
||||||
<FaTimes className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{pollOptions.length < 6 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={addPollOption}
|
|
||||||
className="mt-2 text-sm text-blue-600 hover:text-blue-700 font-medium"
|
|
||||||
>
|
|
||||||
+ {translate('::App.Platform.Intranet.SocialWall.CreatePost.AddOption')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{isExpanded && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, height: 0 }}
|
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
|
||||||
exit={{ opacity: 0, height: 0 }}
|
|
||||||
className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-700"
|
|
||||||
>
|
|
||||||
<div className="flex gap-2 relative">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
if (mediaType === 'media' && mediaItems.length > 0) {
|
|
||||||
// Eğer zaten medya varsa, yöneticiyi aç
|
|
||||||
setShowMediaManager(true)
|
|
||||||
} else {
|
|
||||||
// Başka bir tip seçiliyse temizle ve medya modunu aç
|
|
||||||
clearMedia()
|
|
||||||
setMediaType('media')
|
|
||||||
setShowMediaManager(true)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={classNames(
|
|
||||||
'p-2 rounded-full transition-colors',
|
|
||||||
mediaType === 'media'
|
|
||||||
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
||||||
)}
|
|
||||||
title={mediaType === 'media' ? translate('::App.Platform.Intranet.SocialWall.CreatePost.EditMediaTitle') : translate('::App.Platform.Intranet.SocialWall.CreatePost.AddMediaTitle')}
|
|
||||||
>
|
|
||||||
<FaImages className="w-5 h-5" />
|
|
||||||
{mediaType === 'media' && mediaItems.length > 0 && (
|
|
||||||
<span className="absolute -top-1 -right-1 w-4 h-4 bg-blue-600 text-white text-xs rounded-full flex items-center justify-center">
|
|
||||||
{mediaItems.length}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
// Başka bir tip seçiliyse temizle
|
|
||||||
if (mediaType !== 'poll') {
|
|
||||||
clearMedia()
|
|
||||||
}
|
|
||||||
setMediaType(mediaType === 'poll' ? null : 'poll')
|
|
||||||
}}
|
|
||||||
className={classNames(
|
|
||||||
'p-2 rounded-full transition-colors',
|
|
||||||
mediaType === 'poll'
|
|
||||||
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
||||||
)}
|
|
||||||
title={mediaType === 'poll' ? translate('::App.Platform.Intranet.SocialWall.CreatePost.RemovePollTitle') : translate('::App.Platform.Intranet.SocialWall.CreatePost.AddPollTitle')}
|
|
||||||
>
|
|
||||||
<FaChartBar className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
|
||||||
className="p-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
|
|
||||||
title={translate('::App.Platform.Intranet.SocialWall.CreatePost.AddEmojiTitle')}
|
|
||||||
>
|
|
||||||
<FaSmile className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowLocationPicker(true)}
|
|
||||||
className={classNames(
|
|
||||||
'p-2 rounded-full transition-colors',
|
|
||||||
location
|
|
||||||
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
||||||
)}
|
|
||||||
title={location ? translate('::App.Platform.Intranet.SocialWall.CreatePost.EditLocationTitle') : translate('::App.Platform.Intranet.SocialWall.CreatePost.AddLocationTitle')}
|
|
||||||
>
|
|
||||||
<FaMapMarkerAlt className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Emoji Picker */}
|
|
||||||
{showEmojiPicker && (
|
|
||||||
<div ref={emojiPickerRef} className="absolute bottom-12 left-0 z-50">
|
|
||||||
<EmojiPicker
|
|
||||||
onEmojiClick={handleEmojiClick}
|
|
||||||
autoFocusSearch={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!content.trim() && mediaItems.length === 0 && !mediaType}
|
|
||||||
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-full hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.CreatePost.Submit')}
|
|
||||||
</button>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Media Manager Modal */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{showMediaManager && (
|
|
||||||
<MediaManager
|
|
||||||
media={mediaItems}
|
|
||||||
onChange={setMediaItems}
|
|
||||||
onClose={() => setShowMediaManager(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{/* Location Picker Modal */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{showLocationPicker && (
|
|
||||||
<LocationPicker
|
|
||||||
onSelect={setLocation}
|
|
||||||
onClose={() => setShowLocationPicker(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreatePost
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { FaExternalLinkAlt, FaMapMarkerAlt } from 'react-icons/fa'
|
|
||||||
|
|
||||||
interface LocationData {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
address: string
|
|
||||||
lat: number
|
|
||||||
lng: number
|
|
||||||
placeId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LocationMapProps {
|
|
||||||
location: string // JSON string
|
|
||||||
className?: string
|
|
||||||
showDirections?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const LocationMap: React.FC<LocationMapProps> = ({
|
|
||||||
location,
|
|
||||||
className = '',
|
|
||||||
showDirections = true
|
|
||||||
}) => {
|
|
||||||
const locationData: LocationData = JSON.parse(location)
|
|
||||||
|
|
||||||
const handleOpenGoogleMaps = () => {
|
|
||||||
const url = `https://www.google.com/maps/search/?api=1&query=${locationData.lat},${locationData.lng}&query_place_id=${locationData.placeId || ''}`
|
|
||||||
window.open(url, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Google Maps Static API URL (gerçek uygulamada API key eklenecek)
|
|
||||||
const getMapImageUrl = () => {
|
|
||||||
const { lat, lng } = locationData
|
|
||||||
const zoom = 15
|
|
||||||
const size = '600x300'
|
|
||||||
const marker = `color:red|${lat},${lng}`
|
|
||||||
|
|
||||||
// Production'da gerçek API key kullanılacak
|
|
||||||
// const apiKey = 'YOUR_GOOGLE_MAPS_API_KEY'
|
|
||||||
// return `https://maps.googleapis.com/maps/api/staticmap?center=${lat},${lng}&zoom=${zoom}&size=${size}&markers=${marker}&key=${apiKey}`
|
|
||||||
|
|
||||||
// Demo için OpenStreetMap kullanıyoruz
|
|
||||||
return `https://www.openstreetmap.org/export/embed.html?bbox=${lng - 0.01},${lat - 0.01},${lng + 0.01},${lat + 0.01}&layer=mapnik&marker=${lat},${lng}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const { translate } = useLocalization();
|
|
||||||
return (
|
|
||||||
<div className={`relative rounded-lg overflow-hidden bg-gray-200 dark:bg-gray-700 ${className}`}>
|
|
||||||
{/* Map Container */}
|
|
||||||
<div className="relative w-full h-64 group">
|
|
||||||
{/* OpenStreetMap iframe for demo */}
|
|
||||||
<iframe
|
|
||||||
title={`Map of ${locationData.name}`}
|
|
||||||
src={getMapImageUrl()}
|
|
||||||
className="w-full h-full border-0"
|
|
||||||
allowFullScreen
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Overlay with location info */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent pointer-events-none" />
|
|
||||||
|
|
||||||
{/* Location Info */}
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 p-4 text-white pointer-events-none">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<FaMapMarkerAlt className="w-5 h-5 mt-0.5 flex-shrink-0" />
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="font-bold text-lg mb-1 drop-shadow-lg">{locationData.name}</h3>
|
|
||||||
<p className="text-sm text-white/90 drop-shadow-md line-clamp-2">
|
|
||||||
{locationData.address}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Click to open overlay - invisible but clickable */}
|
|
||||||
<button
|
|
||||||
onClick={handleOpenGoogleMaps}
|
|
||||||
className="absolute inset-0 w-full h-full cursor-pointer group"
|
|
||||||
aria-label={translate('::App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps')}
|
|
||||||
>
|
|
||||||
<span className="sr-only">{translate('::App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps')}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Hover Effect */}
|
|
||||||
<div className="absolute inset-0 bg-blue-600/0 group-hover:bg-blue-600/10 transition-colors duration-200" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Directions Button */}
|
|
||||||
{showDirections && (
|
|
||||||
<div className="p-3 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<button
|
|
||||||
onClick={handleOpenGoogleMaps}
|
|
||||||
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<FaExternalLinkAlt className="w-5 h-5" />
|
|
||||||
<span>{translate('::App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps')}</span>
|
|
||||||
</button>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 text-center">
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.LocationMap.ClickForDirections')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LocationMap
|
|
||||||
|
|
@ -1,388 +0,0 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
import { FaTimes, FaSearch, FaMapMarkerAlt } from 'react-icons/fa'
|
|
||||||
import classNames from 'classnames'
|
|
||||||
|
|
||||||
interface LocationPickerProps {
|
|
||||||
onSelect: (location: string) => void
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LocationData {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
address: string
|
|
||||||
lat: number
|
|
||||||
lng: number
|
|
||||||
placeId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Google Maps API key - .env dosyasından alınmalı
|
|
||||||
const GOOGLE_API_KEY = import.meta.env.VITE_GOOGLE_MAPS_API_KEY || ''
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
google: any
|
|
||||||
initGoogleMaps?: () => void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const LocationPicker: React.FC<LocationPickerProps> = ({ onSelect, onClose }) => {
|
|
||||||
const { translate } = useLocalization();
|
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
|
||||||
const [locations, setLocations] = useState<LocationData[]>([])
|
|
||||||
const [selectedLocation, setSelectedLocation] = useState<LocationData | null>(null)
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [isGoogleLoaded, setIsGoogleLoaded] = useState(false)
|
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
||||||
const autocompleteServiceRef = useRef<any>(null)
|
|
||||||
const placesServiceRef = useRef<any>(null)
|
|
||||||
const debounceTimerRef = useRef<NodeJS.Timeout>()
|
|
||||||
const scriptLoadedRef = useRef(false)
|
|
||||||
|
|
||||||
// Google Maps SDK'yı yükle
|
|
||||||
useEffect(() => {
|
|
||||||
if (scriptLoadedRef.current) return
|
|
||||||
|
|
||||||
const loadGoogleMaps = () => {
|
|
||||||
if (window.google && window.google.maps && window.google.maps.places) {
|
|
||||||
setIsGoogleLoaded(true)
|
|
||||||
autocompleteServiceRef.current = new window.google.maps.places.AutocompleteService()
|
|
||||||
const mapDiv = document.createElement('div')
|
|
||||||
const map = new window.google.maps.Map(mapDiv)
|
|
||||||
placesServiceRef.current = new window.google.maps.places.PlacesService(map)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!GOOGLE_API_KEY) {
|
|
||||||
setError(translate('::App.Platform.Intranet.SocialWall.LocationPicker.ApiKeyError'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Script zaten yüklendiyse sadece bekle
|
|
||||||
const existingScript = document.querySelector('script[src*="maps.googleapis.com"]')
|
|
||||||
if (existingScript) {
|
|
||||||
const checkInterval = setInterval(() => {
|
|
||||||
if (window.google && window.google.maps && window.google.maps.places) {
|
|
||||||
clearInterval(checkInterval)
|
|
||||||
setIsGoogleLoaded(true)
|
|
||||||
autocompleteServiceRef.current = new window.google.maps.places.AutocompleteService()
|
|
||||||
const mapDiv = document.createElement('div')
|
|
||||||
const map = new window.google.maps.Map(mapDiv)
|
|
||||||
placesServiceRef.current = new window.google.maps.places.PlacesService(map)
|
|
||||||
}
|
|
||||||
}, 100)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Yeni script ekle
|
|
||||||
const script = document.createElement('script')
|
|
||||||
script.src = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_API_KEY}&libraries=places&language=tr`
|
|
||||||
script.async = true
|
|
||||||
script.defer = true
|
|
||||||
|
|
||||||
script.onload = () => {
|
|
||||||
if (window.google && window.google.maps && window.google.maps.places) {
|
|
||||||
setIsGoogleLoaded(true)
|
|
||||||
autocompleteServiceRef.current = new window.google.maps.places.AutocompleteService()
|
|
||||||
const mapDiv = document.createElement('div')
|
|
||||||
const map = new window.google.maps.Map(mapDiv)
|
|
||||||
placesServiceRef.current = new window.google.maps.places.PlacesService(map)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
script.onerror = () => {
|
|
||||||
setError(translate('::App.Platform.Intranet.SocialWall.LocationPicker.GoogleMapsLoadError'))
|
|
||||||
}
|
|
||||||
|
|
||||||
document.head.appendChild(script)
|
|
||||||
scriptLoadedRef.current = true
|
|
||||||
}
|
|
||||||
|
|
||||||
loadGoogleMaps()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
searchInputRef.current?.focus()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Google Places Autocomplete ile konum arama
|
|
||||||
useEffect(() => {
|
|
||||||
if (debounceTimerRef.current) {
|
|
||||||
clearTimeout(debounceTimerRef.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchQuery.trim() === '') {
|
|
||||||
setLocations([])
|
|
||||||
setError(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isGoogleLoaded) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
debounceTimerRef.current = setTimeout(async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Google Places Autocomplete Service kullan (CORS yok)
|
|
||||||
autocompleteServiceRef.current.getPlacePredictions(
|
|
||||||
{
|
|
||||||
input: searchQuery,
|
|
||||||
componentRestrictions: { country: 'tr' },
|
|
||||||
language: 'tr'
|
|
||||||
},
|
|
||||||
async (predictions: any, status: any) => {
|
|
||||||
if (status === window.google.maps.places.PlacesServiceStatus.ZERO_RESULTS) {
|
|
||||||
setLocations([])
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status !== window.google.maps.places.PlacesServiceStatus.OK) {
|
|
||||||
setError(translate('::App.Platform.Intranet.SocialWall.LocationPicker.SearchFailed'))
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!predictions || predictions.length === 0) {
|
|
||||||
setLocations([])
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Her bir prediction için detaylı bilgi al
|
|
||||||
const detailedLocations: LocationData[] = []
|
|
||||||
let completed = 0
|
|
||||||
|
|
||||||
predictions.forEach((prediction: any) => {
|
|
||||||
placesServiceRef.current.getDetails(
|
|
||||||
{
|
|
||||||
placeId: prediction.place_id,
|
|
||||||
fields: ['name', 'formatted_address', 'geometry', 'place_id']
|
|
||||||
},
|
|
||||||
(place: any, placeStatus: any) => {
|
|
||||||
completed++
|
|
||||||
|
|
||||||
if (placeStatus === window.google.maps.places.PlacesServiceStatus.OK && place) {
|
|
||||||
detailedLocations.push({
|
|
||||||
id: place.place_id,
|
|
||||||
name: place.name,
|
|
||||||
address: place.formatted_address,
|
|
||||||
lat: place.geometry.location.lat(),
|
|
||||||
lng: place.geometry.location.lng(),
|
|
||||||
placeId: place.place_id
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tüm istekler tamamlandıysa state'i güncelle
|
|
||||||
if (completed === predictions.length) {
|
|
||||||
setLocations(detailedLocations)
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Location search error:', err)
|
|
||||||
setError(translate('::App.Platform.Intranet.SocialWall.LocationPicker.SearchError'))
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}, 500) // 500ms debounce
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (debounceTimerRef.current) {
|
|
||||||
clearTimeout(debounceTimerRef.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [searchQuery, isGoogleLoaded])
|
|
||||||
|
|
||||||
const handleSelect = (location: LocationData) => {
|
|
||||||
setSelectedLocation(location)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleConfirm = () => {
|
|
||||||
if (selectedLocation) {
|
|
||||||
onSelect(JSON.stringify(selectedLocation))
|
|
||||||
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.9 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.9 }}
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col"
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">{translate('::App.Platform.Intranet.SocialWall.LocationPicker.AddLocation')}</h2>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
|
|
||||||
>
|
|
||||||
<FaTimes className="w-5 h-5 text-gray-500 dark:text-gray-400" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search */}
|
|
||||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="relative">
|
|
||||||
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
||||||
<input
|
|
||||||
ref={searchInputRef}
|
|
||||||
type="text"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
placeholder={translate('::App.Platform.Intranet.SocialWall.LocationPicker.SearchPlaceholder')}
|
|
||||||
disabled={!isGoogleLoaded}
|
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-600 disabled:cursor-not-allowed"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!isGoogleLoaded && (
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.LocationPicker.LoadingGoogleMaps')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Location List */}
|
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
|
||||||
{!isGoogleLoaded ? (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
||||||
<p className="text-gray-500 dark:text-gray-400">{translate('::App.Platform.Intranet.SocialWall.LocationPicker.LoadingGoogleMaps')}</p>
|
|
||||||
</div>
|
|
||||||
) : isLoading ? (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
||||||
<p className="text-gray-500 dark:text-gray-400">{translate('::App.Platform.Intranet.SocialWall.LocationPicker.SearchingLocations')}</p>
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<FaMapMarkerAlt className="w-16 h-16 mx-auto mb-4 text-red-400" />
|
|
||||||
<p className="text-red-500 dark:text-red-400">{error}</p>
|
|
||||||
</div>
|
|
||||||
) : searchQuery.trim() === '' ? (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<FaSearch className="w-16 h-16 mx-auto mb-4 text-gray-400" />
|
|
||||||
<p className="text-gray-500 dark:text-gray-400">
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.LocationPicker.TypeToSearch')}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400 dark:text-gray-500 mt-2">
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.LocationPicker.Example')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : locations.length === 0 ? (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<FaMapMarkerAlt className="w-16 h-16 mx-auto mb-4 text-gray-400" />
|
|
||||||
<p className="text-gray-500 dark:text-gray-400">
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.LocationPicker.NotFound')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{locations.map((location) => (
|
|
||||||
<button
|
|
||||||
key={location.id}
|
|
||||||
onClick={() => handleSelect(location)}
|
|
||||||
className={classNames(
|
|
||||||
'w-full text-left p-3 rounded-lg transition-all hover:bg-gray-50 dark:hover:bg-gray-700',
|
|
||||||
selectedLocation?.id === location.id
|
|
||||||
? 'bg-blue-50 dark:bg-blue-900/30 border-2 border-blue-500'
|
|
||||||
: 'border-2 border-transparent'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="mt-1">
|
|
||||||
<FaMapMarkerAlt
|
|
||||||
className={classNames(
|
|
||||||
'w-5 h-5',
|
|
||||||
selectedLocation?.id === location.id
|
|
||||||
? 'text-blue-600'
|
|
||||||
: 'text-gray-400'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3
|
|
||||||
className={classNames(
|
|
||||||
'font-semibold mb-1',
|
|
||||||
selectedLocation?.id === location.id
|
|
||||||
? 'text-blue-600 dark:text-blue-400'
|
|
||||||
: 'text-gray-900 dark:text-gray-100'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{location.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
|
||||||
{location.address}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
|
||||||
{location.lat.toFixed(4)}, {location.lng.toFixed(4)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{selectedLocation?.id === location.id && (
|
|
||||||
<div className="mt-1">
|
|
||||||
<div className="w-5 h-5 bg-blue-600 rounded-full flex items-center justify-center">
|
|
||||||
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-750">
|
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
{selectedLocation ? (
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<FaMapMarkerAlt className="w-4 h-4 text-blue-600" />
|
|
||||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
{selectedLocation.name}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>{translate('::App.Platform.Intranet.SocialWall.LocationPicker.SelectLocation')}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::Cancel')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleConfirm}
|
|
||||||
disabled={!selectedLocation}
|
|
||||||
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.LocationPicker.Add')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LocationPicker
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import Lightbox from 'yet-another-react-lightbox'
|
|
||||||
import 'yet-another-react-lightbox/styles.css'
|
|
||||||
import Video from 'yet-another-react-lightbox/plugins/video'
|
|
||||||
import Zoom from 'yet-another-react-lightbox/plugins/zoom'
|
|
||||||
import Counter from 'yet-another-react-lightbox/plugins/counter'
|
|
||||||
import 'yet-another-react-lightbox/plugins/counter.css'
|
|
||||||
import { SocialMediaDto } from '@/proxy/intranet/models'
|
|
||||||
|
|
||||||
interface MediaLightboxProps {
|
|
||||||
isOpen: boolean
|
|
||||||
onClose: () => void
|
|
||||||
media: SocialMediaDto
|
|
||||||
startIndex?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const MediaLightbox: React.FC<MediaLightboxProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
media,
|
|
||||||
startIndex = 0
|
|
||||||
}) => {
|
|
||||||
const slides = React.useMemo(() => {
|
|
||||||
if (media.type === 'video' && media.urls && media.urls.length > 0) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
type: 'video' as const,
|
|
||||||
sources: [
|
|
||||||
{
|
|
||||||
src: media.urls[0],
|
|
||||||
type: 'video/mp4'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const urls = media.urls || []
|
|
||||||
return urls.map((url) => ({
|
|
||||||
src: url
|
|
||||||
}))
|
|
||||||
}, [media])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Lightbox
|
|
||||||
open={isOpen}
|
|
||||||
close={onClose}
|
|
||||||
slides={slides}
|
|
||||||
index={startIndex}
|
|
||||||
plugins={[Video, Zoom, Counter]}
|
|
||||||
counter={{ container: { style: { top: 'unset', bottom: 0 } } }}
|
|
||||||
zoom={{
|
|
||||||
maxZoomPixelRatio: 3,
|
|
||||||
scrollToZoom: true
|
|
||||||
}}
|
|
||||||
video={{
|
|
||||||
controls: true,
|
|
||||||
autoPlay: false
|
|
||||||
}}
|
|
||||||
styles={{
|
|
||||||
container: { backgroundColor: 'rgba(0, 0, 0, 0.95)' }
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MediaLightbox
|
|
||||||
|
|
@ -1,240 +0,0 @@
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
import { FaTimes, FaLink, FaUpload } from 'react-icons/fa'
|
|
||||||
import classNames from 'classnames'
|
|
||||||
import { SocialMediaDto } from '@/proxy/intranet/models'
|
|
||||||
|
|
||||||
interface MediaManagerProps {
|
|
||||||
media: SocialMediaDto[]
|
|
||||||
onChange: (media: SocialMediaDto[]) => void
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose }) => {
|
|
||||||
const { translate } = useLocalization();
|
|
||||||
const [activeTab, setActiveTab] = useState<'upload' | 'url'>('upload')
|
|
||||||
const [urlInput, setUrlInput] = useState('')
|
|
||||||
const [mediaType, setMediaType] = useState<'image' | 'video'>('image')
|
|
||||||
|
|
||||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const files = e.target.files
|
|
||||||
if (!files) return
|
|
||||||
|
|
||||||
const newMedia: SocialMediaDto[] = Array.from(files).map((file) => ({
|
|
||||||
id: Math.random().toString(36).substr(2, 9),
|
|
||||||
type: file.type.startsWith('video/') ? 'video' : 'image',
|
|
||||||
urls: [URL.createObjectURL(file)],
|
|
||||||
file
|
|
||||||
}))
|
|
||||||
|
|
||||||
onChange([...media, ...newMedia])
|
|
||||||
e.target.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUrlAdd = () => {
|
|
||||||
if (!urlInput.trim()) return
|
|
||||||
|
|
||||||
const newMedia: SocialMediaDto = {
|
|
||||||
id: Math.random().toString(36).substr(2, 9),
|
|
||||||
type: mediaType,
|
|
||||||
urls: [urlInput]
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange([...media, newMedia])
|
|
||||||
setUrlInput('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeMedia = (id: string | undefined) => {
|
|
||||||
if (!id) return
|
|
||||||
onChange(media.filter((m) => m.id !== id))
|
|
||||||
}
|
|
||||||
|
|
||||||
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.9 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.9 }}
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-3xl max-h-[90vh] overflow-hidden"
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">{translate('::App.Platform.Intranet.SocialWall.MediaManager.AddMedia')}</h2>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
|
|
||||||
>
|
|
||||||
<FaTimes className="w-5 h-5 text-gray-500 dark:text-gray-400" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<div className="flex border-b border-gray-200 dark:border-gray-700 px-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('upload')}
|
|
||||||
className={classNames(
|
|
||||||
'px-4 py-3 font-medium border-b-2 transition-colors',
|
|
||||||
activeTab === 'upload'
|
|
||||||
? 'border-blue-600 text-blue-600'
|
|
||||||
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FaUpload className="w-5 h-5" />
|
|
||||||
<span>{translate('::App.Platform.Intranet.SocialWall.MediaManager.SelectFromComputer')}</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('url')}
|
|
||||||
className={classNames(
|
|
||||||
'px-4 py-3 font-medium border-b-2 transition-colors',
|
|
||||||
activeTab === 'url'
|
|
||||||
? 'border-blue-600 text-blue-600'
|
|
||||||
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FaLink className="w-5 h-5" />
|
|
||||||
<span>{translate('::App.Platform.Intranet.SocialWall.MediaManager.AddByUrl')}</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="p-4 overflow-y-auto max-h-[calc(90vh-240px)]">
|
|
||||||
{activeTab === 'upload' ? (
|
|
||||||
<div>
|
|
||||||
<label className="block">
|
|
||||||
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-8 text-center hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors cursor-pointer">
|
|
||||||
<FaUpload className="w-12 h-12 mx-auto mb-4 text-gray-400" />
|
|
||||||
<p className="text-gray-700 dark:text-gray-300 font-medium mb-1">
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.ClickToSelectFile')}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.ImageOrVideoFormats')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/*,video/*"
|
|
||||||
multiple
|
|
||||||
onChange={handleFileSelect}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.MediaType')}
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setMediaType('image')}
|
|
||||||
className={classNames(
|
|
||||||
'flex-1 py-2 px-4 rounded-lg font-medium transition-colors',
|
|
||||||
mediaType === 'image'
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.Image')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setMediaType('video')}
|
|
||||||
className={classNames(
|
|
||||||
'flex-1 py-2 px-4 rounded-lg font-medium transition-colors',
|
|
||||||
mediaType === 'video'
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.Video')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
value={urlInput}
|
|
||||||
onChange={(e) => setUrlInput(e.target.value)}
|
|
||||||
onKeyPress={(e) => e.key === 'Enter' && handleUrlAdd()}
|
|
||||||
placeholder={mediaType === 'image' ? translate('::App.Platform.Intranet.SocialWall.MediaManager.EnterImageUrl') : translate('::App.Platform.Intranet.SocialWall.MediaManager.EnterVideoUrl')}
|
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleUrlAdd}
|
|
||||||
disabled={!urlInput.trim()}
|
|
||||||
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.Add')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Media Preview */}
|
|
||||||
{media.length > 0 && (
|
|
||||||
<div className="mt-6">
|
|
||||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.AddedMedia')} ({media.length})
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-4 gap-3">
|
|
||||||
{media.map((item) => (
|
|
||||||
<div key={item.id} className="relative group">
|
|
||||||
{item.type === 'image' ? (
|
|
||||||
<img
|
|
||||||
src={item.urls?.[0]}
|
|
||||||
alt="Media preview"
|
|
||||||
className="w-full h-24 object-cover rounded-lg"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-24 bg-gray-900 rounded-lg flex items-center justify-center">
|
|
||||||
<video src={item.urls?.[0]} className="w-full h-full object-cover rounded-lg" />
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<div className="w-10 h-10 bg-black bg-opacity-50 rounded-full flex items-center justify-center">
|
|
||||||
<div className="w-0 h-0 border-t-8 border-t-transparent border-l-12 border-l-white border-b-8 border-b-transparent ml-1"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => removeMedia(item.id)}
|
|
||||||
className="absolute -top-2 -right-2 p-1 bg-red-600 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
|
|
||||||
>
|
|
||||||
<FaTimes className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<div className="absolute bottom-1 left-1 px-2 py-0.5 bg-black bg-opacity-70 text-white text-xs rounded">
|
|
||||||
{item.type === 'image' ? translate('::App.Platform.Intranet.SocialWall.MediaManager.ImageIcon') : translate('::App.Platform.Intranet.SocialWall.MediaManager.VideoIcon')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="flex items-center justify-end gap-2 p-4 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::Cancel')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
disabled={media.length === 0}
|
|
||||||
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MediaManager.Done', { count: media.length })}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MediaManager
|
|
||||||
|
|
@ -1,424 +0,0 @@
|
||||||
import React, { useState, useRef, useEffect } from 'react'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
|
||||||
import classNames from 'classnames'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
||||||
import 'dayjs/locale/tr'
|
|
||||||
import {
|
|
||||||
FaHeart,
|
|
||||||
FaRegHeart,
|
|
||||||
FaRegCommentAlt,
|
|
||||||
FaTrash,
|
|
||||||
FaPaperPlane,
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
import MediaLightbox from './MediaLightbox'
|
|
||||||
import LocationMap from './LocationMap'
|
|
||||||
import UserProfileCard from './UserProfileCard'
|
|
||||||
import { SocialPostDto } from '@/proxy/intranet/models'
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
|
||||||
dayjs.locale('tr')
|
|
||||||
|
|
||||||
interface PostItemProps {
|
|
||||||
post: SocialPostDto
|
|
||||||
onLike: (postId: string) => void
|
|
||||||
onComment: (postId: string, content: string) => void
|
|
||||||
onDelete: (postId: string) => void
|
|
||||||
onVote: (postId: string, optionId: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete, onVote }) => {
|
|
||||||
const { translate } = useLocalization();
|
|
||||||
const [showComments, setShowComments] = useState(false)
|
|
||||||
const [commentText, setCommentText] = useState('')
|
|
||||||
const [showAllImages, setShowAllImages] = useState(false)
|
|
||||||
const [lightboxOpen, setLightboxOpen] = useState(false)
|
|
||||||
const [lightboxIndex, setLightboxIndex] = useState(0)
|
|
||||||
const [showUserCard, setShowUserCard] = useState(false)
|
|
||||||
const [hoveredCommentAuthor, setHoveredCommentAuthor] = useState<string | null>(null)
|
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
|
||||||
|
|
||||||
// Intersection Observer for video autoplay/pause
|
|
||||||
useEffect(() => {
|
|
||||||
const video = videoRef.current
|
|
||||||
if (!video) return
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// Video ekranda görünür - oynat
|
|
||||||
video.play().catch(err => {
|
|
||||||
console.log('Video autoplay failed:', err)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Video ekrandan çıktı - durdur
|
|
||||||
video.pause()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
threshold: 0.5 // Video %50 görünür olduğunda oynat
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
observer.observe(video)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
}, [post.media?.type])
|
|
||||||
|
|
||||||
const handleSubmitComment = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (commentText.trim()) {
|
|
||||||
onComment(post.id, commentText)
|
|
||||||
setCommentText('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getImageLayout = (images: string[]) => {
|
|
||||||
const count = images.length
|
|
||||||
if (count === 1) return 'single'
|
|
||||||
if (count === 2) return 'double'
|
|
||||||
if (count === 3) return 'triple'
|
|
||||||
return 'multiple'
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderMedia = () => {
|
|
||||||
if (!post.media) return null
|
|
||||||
|
|
||||||
switch (post.media.type) {
|
|
||||||
case 'image':
|
|
||||||
if (post.media.urls && post.media.urls.length > 0) {
|
|
||||||
const layout = getImageLayout(post.media.urls)
|
|
||||||
const displayImages = showAllImages ? post.media.urls : post.media.urls.slice(0, 4)
|
|
||||||
const hasMore = post.media.urls.length > 4
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={classNames('mt-3 rounded-lg overflow-hidden', {
|
|
||||||
'grid gap-1': layout !== 'single',
|
|
||||||
'grid-cols-2': layout === 'double' || layout === 'multiple',
|
|
||||||
'grid-cols-3': layout === 'triple'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{displayImages.map((url, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={classNames('relative', {
|
|
||||||
'col-span-2': layout === 'triple' && index === 0,
|
|
||||||
'aspect-video': layout === 'single',
|
|
||||||
'aspect-square': layout !== 'single'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={url}
|
|
||||||
alt={`Post image ${index + 1}`}
|
|
||||||
className="w-full h-full object-cover cursor-pointer hover:opacity-90 transition-opacity"
|
|
||||||
onClick={() => {
|
|
||||||
setLightboxIndex(index)
|
|
||||||
setLightboxOpen(true)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{hasMore && index === 3 && !showAllImages && post.media?.urls && (
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-black bg-opacity-60 flex items-center justify-center cursor-pointer"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
setShowAllImages(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="text-white text-2xl font-bold">
|
|
||||||
+{post.media.urls.length - 4}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<MediaLightbox
|
|
||||||
isOpen={lightboxOpen}
|
|
||||||
onClose={() => setLightboxOpen(false)}
|
|
||||||
media={{ type: 'image', urls: post.media.urls }}
|
|
||||||
startIndex={lightboxIndex}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'video':
|
|
||||||
if (post.media.urls && post.media.urls.length > 0) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="mt-3 rounded-lg overflow-hidden cursor-pointer relative group"
|
|
||||||
onClick={() => setLightboxOpen(true)}
|
|
||||||
>
|
|
||||||
<video
|
|
||||||
ref={videoRef}
|
|
||||||
src={post.media.urls[0]}
|
|
||||||
className="w-full max-h-96 object-cover"
|
|
||||||
controls
|
|
||||||
playsInline
|
|
||||||
muted
|
|
||||||
loop
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<MediaLightbox
|
|
||||||
isOpen={lightboxOpen}
|
|
||||||
onClose={() => setLightboxOpen(false)}
|
|
||||||
media={{ type: 'video', urls: [post.media.urls[0]] }}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'poll':
|
|
||||||
if (post.media.pollQuestion && post.media.pollOptions) {
|
|
||||||
const isExpired = post.media.pollEndsAt ? new Date() > post.media.pollEndsAt : false
|
|
||||||
const hasVoted = !!post.media.pollUserVoteId
|
|
||||||
const totalVotes = post.media.pollTotalVotes || 0
|
|
||||||
const pollUserVoteId = post.media.pollUserVoteId
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-3 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-3">
|
|
||||||
{post.media.pollQuestion}
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{post.media.pollOptions.map((option) => {
|
|
||||||
const percentage = totalVotes > 0 ? (option.votes / totalVotes) * 100 : 0
|
|
||||||
const isSelected = pollUserVoteId === option.id
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={option.id}
|
|
||||||
onClick={() => !hasVoted && !isExpired && onVote(post.id, option.id)}
|
|
||||||
disabled={hasVoted || isExpired}
|
|
||||||
className={classNames(
|
|
||||||
'w-full text-left p-3 rounded-lg relative overflow-hidden transition-all',
|
|
||||||
{
|
|
||||||
'bg-blue-100 dark:bg-blue-900 border-2 border-blue-500':
|
|
||||||
isSelected,
|
|
||||||
'bg-white dark:bg-gray-600 hover:bg-gray-50 dark:hover:bg-gray-500':
|
|
||||||
!isSelected && !hasVoted && !isExpired,
|
|
||||||
'bg-white dark:bg-gray-600 cursor-not-allowed':
|
|
||||||
hasVoted || isExpired
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{hasVoted && (
|
|
||||||
<div
|
|
||||||
className="absolute inset-y-0 left-0 bg-blue-200 dark:bg-blue-800 transition-all"
|
|
||||||
style={{ width: `${percentage}%` }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="relative z-10 flex justify-between items-center">
|
|
||||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
{option.text}
|
|
||||||
</span>
|
|
||||||
{hasVoted && (
|
|
||||||
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
|
|
||||||
{percentage.toFixed(0)}%
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
{totalVotes} oy • {isExpired ? 'Sona erdi' : post.media.pollEndsAt ? dayjs(post.media.pollEndsAt).fromNow() + ' bitiyor' : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: -20 }}
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 mb-4"
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-start justify-between mb-3">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
className="relative"
|
|
||||||
onMouseEnter={() => setShowUserCard(true)}
|
|
||||||
onMouseLeave={() => setShowUserCard(false)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={post.employee.avatar || 'https://i.pravatar.cc/150?img=1'}
|
|
||||||
alt={post.employee.name}
|
|
||||||
className="w-12 h-12 rounded-full object-cover cursor-pointer ring-2 ring-transparent hover:ring-blue-500 transition-all"
|
|
||||||
/>
|
|
||||||
<AnimatePresence>
|
|
||||||
{showUserCard && (
|
|
||||||
<UserProfileCard
|
|
||||||
user={{
|
|
||||||
id: post.employee.id,
|
|
||||||
name: post.employee.name,
|
|
||||||
avatar: post.employee.avatar || 'https://i.pravatar.cc/150?img=1',
|
|
||||||
title: post.employee.jobPosition?.name || translate('::App.Platform.Intranet.SocialWall.PostItem.Employee'),
|
|
||||||
email: post.employee.email,
|
|
||||||
phoneNumber: post.employee.phoneNumber,
|
|
||||||
department: post.employee.department?.name,
|
|
||||||
location: post.employee.workLocation
|
|
||||||
}}
|
|
||||||
position="bottom"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{post.employee.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
{post.employee.jobPosition?.name || translate('::App.Platform.Intranet.SocialWall.PostItem.Employee')} • {dayjs(post.creationTime).fromNow()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{post.isOwnPost && (
|
|
||||||
<button
|
|
||||||
onClick={() => onDelete(post.id)}
|
|
||||||
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-full transition-colors"
|
|
||||||
title={translate('::App.Platform.Intranet.SocialWall.PostItem.DeletePost')}
|
|
||||||
>
|
|
||||||
<FaTrash className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="mb-3">
|
|
||||||
<p className="text-gray-800 dark:text-gray-200 whitespace-pre-wrap">{post.content}</p>
|
|
||||||
{renderMedia()}
|
|
||||||
|
|
||||||
{/* Location */}
|
|
||||||
{post.locationJson && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<LocationMap location={post.locationJson} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="flex items-center gap-6 pt-3 border-t border-gray-100 dark:border-gray-700">
|
|
||||||
<button
|
|
||||||
onClick={() => onLike(post.id)}
|
|
||||||
className={classNames(
|
|
||||||
'flex items-center gap-2 transition-colors',
|
|
||||||
post.isLiked
|
|
||||||
? 'text-red-600 hover:text-red-700'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:text-red-600'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{post.isLiked ? (
|
|
||||||
<FaHeart className="w-5 h-5" />
|
|
||||||
) : (
|
|
||||||
<FaRegHeart className="w-5 h-5" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-medium">{post.likeCount}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setShowComments(!showComments)}
|
|
||||||
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 transition-colors"
|
|
||||||
>
|
|
||||||
<FaRegCommentAlt className="w-5 h-5" />
|
|
||||||
<span className="text-sm font-medium">{post.comments.length}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Comments Section */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{showComments && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ height: 0, opacity: 0 }}
|
|
||||||
animate={{ height: 'auto', opacity: 1 }}
|
|
||||||
exit={{ height: 0, opacity: 0 }}
|
|
||||||
className="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700"
|
|
||||||
>
|
|
||||||
{/* Comment Form */}
|
|
||||||
<form onSubmit={handleSubmitComment} className="mb-4">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={commentText}
|
|
||||||
onChange={(e) => setCommentText(e.target.value)}
|
|
||||||
placeholder={translate('::App.Platform.Intranet.SocialWall.PostItem.CommentPlaceholder')}
|
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-full bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!commentText.trim()}
|
|
||||||
className="p-2 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
<FaPaperPlane className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Comments List */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
{post.comments.map((comment) => (
|
|
||||||
<div key={comment.id} className="flex gap-3">
|
|
||||||
<div
|
|
||||||
className="relative"
|
|
||||||
onMouseEnter={() => setHoveredCommentAuthor(comment.id)}
|
|
||||||
onMouseLeave={() => setHoveredCommentAuthor(null)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={comment.creator.avatar || 'https://i.pravatar.cc/150?img=1'}
|
|
||||||
alt={comment.creator.name}
|
|
||||||
className="w-8 h-8 rounded-full object-cover cursor-pointer ring-2 ring-transparent hover:ring-blue-500 transition-all"
|
|
||||||
/>
|
|
||||||
<AnimatePresence>
|
|
||||||
{hoveredCommentAuthor === comment.id && (
|
|
||||||
<UserProfileCard
|
|
||||||
user={{
|
|
||||||
id: comment.creator.id,
|
|
||||||
name: comment.creator.name,
|
|
||||||
avatar: comment.creator.avatar || 'https://i.pravatar.cc/150?img=1',
|
|
||||||
title: comment.creator.jobPosition?.name || translate('::App.Platform.Intranet.SocialWall.PostItem.Employee')
|
|
||||||
}}
|
|
||||||
position="bottom"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="bg-gray-100 dark:bg-gray-700 rounded-lg px-4 py-2">
|
|
||||||
<h4 className="font-semibold text-sm text-gray-900 dark:text-gray-100">
|
|
||||||
{comment.creator.name}
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm text-gray-800 dark:text-gray-200">{comment.content}</p>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 ml-4">
|
|
||||||
{dayjs(comment.creationTime).fromNow()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PostItem
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
import { FaEnvelope, FaPhone, FaBriefcase, FaMapMarkerAlt } from 'react-icons/fa'
|
|
||||||
|
|
||||||
interface UserProfileCardProps {
|
|
||||||
user: {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
avatar: string
|
|
||||||
title: string
|
|
||||||
email?: string
|
|
||||||
phoneNumber?: string
|
|
||||||
department?: string
|
|
||||||
location?: string
|
|
||||||
}
|
|
||||||
position?: 'top' | 'bottom'
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserProfileCard: React.FC<UserProfileCardProps> = ({ user, position = 'bottom' }) => {
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: position === 'bottom' ? -10 : 10 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: position === 'bottom' ? -10 : 10 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
className={`absolute left-0 ${
|
|
||||||
position === 'bottom' ? 'top-full mt-2' : 'bottom-full mb-2'
|
|
||||||
} z-50 w-72 bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 p-4`}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-start gap-3 mb-3 pb-3 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<img
|
|
||||||
src={user.avatar}
|
|
||||||
alt={user.name}
|
|
||||||
className="w-16 h-16 rounded-full object-cover ring-2 ring-blue-500"
|
|
||||||
/>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="font-bold text-gray-900 dark:text-gray-100 text-lg mb-1">
|
|
||||||
{user.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
|
|
||||||
<FaBriefcase className="w-4 h-4" />
|
|
||||||
{user.title}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Contact Info */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
{user.email && (
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<FaEnvelope className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
|
||||||
<a
|
|
||||||
href={`mailto:${user.email}`}
|
|
||||||
className="text-blue-600 dark:text-blue-400 hover:underline truncate"
|
|
||||||
>
|
|
||||||
{user.email}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{user.phoneNumber && (
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<FaPhone className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
|
||||||
<a
|
|
||||||
href={`tel:${user.phoneNumber}`}
|
|
||||||
className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400"
|
|
||||||
>
|
|
||||||
{user.phoneNumber}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{user.department && (
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<FaBriefcase className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
|
||||||
<span className="text-gray-700 dark:text-gray-300">{user.department}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{user.location && (
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<FaMapMarkerAlt className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
|
||||||
<span className="text-gray-700 dark:text-gray-300">{user.location}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Arrow indicator */}
|
|
||||||
<div
|
|
||||||
className={`absolute left-6 ${
|
|
||||||
position === 'bottom' ? '-top-2' : '-bottom-2'
|
|
||||||
} w-4 h-4 bg-white dark:bg-gray-800 border-l border-t border-gray-200 dark:border-gray-700 transform ${
|
|
||||||
position === 'bottom' ? 'rotate-45' : '-rotate-135'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UserProfileCard
|
|
||||||
|
|
@ -1,211 +0,0 @@
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { AnimatePresence } from 'framer-motion'
|
|
||||||
import PostItem from './PostItem'
|
|
||||||
import CreatePost from './CreatePost'
|
|
||||||
import { mockEmployees } from '@/mocks/mockEmployees'
|
|
||||||
import { EmployeeDto, SocialMediaDto, SocialPostDto } from '@/proxy/intranet/models'
|
|
||||||
|
|
||||||
const SocialWall: React.FC<{ posts: SocialPostDto[] }> = ({ posts }) => {
|
|
||||||
// const [posts, setPosts] = useState<SocialPost[]>(mockSocialPosts)
|
|
||||||
const [filter, setFilter] = useState<'all' | 'mine'>('all')
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
|
|
||||||
// Ali Öztürk'ü "Siz" kullanıcısı olarak kullan
|
|
||||||
const currentUserAuthor: EmployeeDto = { ...mockEmployees[0], name: translate('::App.Platform.Intranet.SocialWall.CurrentUser') }
|
|
||||||
|
|
||||||
const handleCreatePost = (postData: {
|
|
||||||
content: string
|
|
||||||
location?: string
|
|
||||||
media?: {
|
|
||||||
type: 'mixed' | 'poll'
|
|
||||||
mediaItems?: SocialMediaDto[]
|
|
||||||
poll?: {
|
|
||||||
question: string
|
|
||||||
options: Array<{ text: string }>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) => {
|
|
||||||
let mediaForPost = undefined
|
|
||||||
|
|
||||||
if (postData.media) {
|
|
||||||
if (postData.media.type === 'mixed' && postData.media.mediaItems) {
|
|
||||||
// Convert MediaItems to post format
|
|
||||||
const images = postData.media.mediaItems.filter((m) => m.type === 'image')
|
|
||||||
const videos = postData.media.mediaItems.filter((m) => m.type === 'video')
|
|
||||||
|
|
||||||
if (images.length > 0 && videos.length === 0) {
|
|
||||||
mediaForPost = {
|
|
||||||
type: 'image' as const,
|
|
||||||
urls: images.map((i) => i.urls?.[0]).filter((url) => url !== undefined) as string[],
|
|
||||||
}
|
|
||||||
} else if (videos.length > 0 && images.length === 0) {
|
|
||||||
mediaForPost = {
|
|
||||||
type: 'video' as const,
|
|
||||||
urls: videos[0].urls || [],
|
|
||||||
}
|
|
||||||
} else if (images.length > 0 || videos.length > 0) {
|
|
||||||
// Mixed media - use first image for now
|
|
||||||
mediaForPost = {
|
|
||||||
type: 'image' as const,
|
|
||||||
urls: images.map((i) => i.urls?.[0]).filter((url) => url !== undefined) as string[],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (postData.media.type === 'poll' && postData.media.poll) {
|
|
||||||
mediaForPost = {
|
|
||||||
type: 'poll' as const,
|
|
||||||
pollQuestion: postData.media.poll.question,
|
|
||||||
pollOptions: postData.media.poll.options.map((opt, index) => ({
|
|
||||||
id: `opt-${index}`,
|
|
||||||
text: opt.text,
|
|
||||||
votes: 0,
|
|
||||||
})),
|
|
||||||
pollTotalVotes: 0,
|
|
||||||
pollEndsAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPost: SocialPostDto = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
employee: currentUserAuthor,
|
|
||||||
content: postData.content,
|
|
||||||
creationTime: new Date(),
|
|
||||||
media: mediaForPost,
|
|
||||||
locationJson: postData.location,
|
|
||||||
likeCount: 0,
|
|
||||||
isLiked: false,
|
|
||||||
likeUsers: [],
|
|
||||||
comments: [],
|
|
||||||
isOwnPost: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// setPosts([newPost, ...posts])
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLike = (postId: string) => {
|
|
||||||
// setPosts(
|
|
||||||
// posts.map((post) => {
|
|
||||||
// if (post.id === postId) {
|
|
||||||
// return {
|
|
||||||
// ...post,
|
|
||||||
// likeCount: post.isLiked ? post.likeCount - 1 : post.likeCount + 1,
|
|
||||||
// isLiked: !post.isLiked
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return post
|
|
||||||
// })
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleComment = (postId: string, content: string) => {
|
|
||||||
// setPosts(
|
|
||||||
// posts.map((post) => {
|
|
||||||
// if (post.id === postId) {
|
|
||||||
// const commentAuthor = currentUserAuthor
|
|
||||||
// const newComment = {
|
|
||||||
// id: Date.now().toString(),
|
|
||||||
// creator: commentAuthor,
|
|
||||||
// content,
|
|
||||||
// creationTime: new Date()
|
|
||||||
// }
|
|
||||||
// return {
|
|
||||||
// ...post,
|
|
||||||
// comments: [...post.comments, newComment]
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return post
|
|
||||||
// })
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDelete = (postId: string) => {
|
|
||||||
if (window.confirm(translate('::App.Platform.Intranet.SocialWall.DeleteConfirm'))) {
|
|
||||||
// setPosts(posts.filter((post) => post.id !== postId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleVote = (postId: string, optionId: string) => {
|
|
||||||
// setPosts(
|
|
||||||
// posts.map((post) => {
|
|
||||||
// if (post.id === postId && post.media?.type === 'poll' && post.media.pollOptions) {
|
|
||||||
// // If user already voted, don't allow voting again
|
|
||||||
// if (post.media.pollUserVoteId) {
|
|
||||||
// return post
|
|
||||||
// }
|
|
||||||
// return {
|
|
||||||
// ...post,
|
|
||||||
// media: {
|
|
||||||
// ...post.media,
|
|
||||||
// pollOptions: post.media.pollOptions.map((opt) =>
|
|
||||||
// opt.id === optionId ? { ...opt, votes: opt.votes + 1 } : opt
|
|
||||||
// ),
|
|
||||||
// pollTotalVotes: (post.media.pollTotalVotes || 0) + 1,
|
|
||||||
// pollUserVoteId: optionId
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return post
|
|
||||||
// })
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredPosts = filter === 'mine' ? posts.filter((post) => post.isOwnPost) : posts
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto px-4">
|
|
||||||
{/* Filter Tabs */}
|
|
||||||
<div className="flex gap-4 mb-6 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<button
|
|
||||||
onClick={() => setFilter('all')}
|
|
||||||
className={`pb-3 px-1 border-b-2 transition-colors font-medium ${
|
|
||||||
filter === 'all'
|
|
||||||
? 'border-blue-600 text-blue-600'
|
|
||||||
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.AllPosts')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setFilter('mine')}
|
|
||||||
className={`pb-3 px-1 border-b-2 transition-colors font-medium ${
|
|
||||||
filter === 'mine'
|
|
||||||
? 'border-blue-600 text-blue-600'
|
|
||||||
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{translate('::App.Platform.Intranet.SocialWall.MyPosts')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Create Post */}
|
|
||||||
<CreatePost onCreatePost={handleCreatePost} />
|
|
||||||
|
|
||||||
{/* Posts Feed */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{filteredPosts.length > 0 ? (
|
|
||||||
filteredPosts.map((post) => (
|
|
||||||
<PostItem
|
|
||||||
key={post.id}
|
|
||||||
post={post}
|
|
||||||
onLike={handleLike}
|
|
||||||
onComment={handleComment}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
onVote={handleVote}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<p className="text-gray-500 dark:text-gray-400 text-lg">
|
|
||||||
{filter === 'mine'
|
|
||||||
? translate('::App.Platform.Intranet.SocialWall.NoMyPosts')
|
|
||||||
: translate('::App.Platform.Intranet.SocialWall.NoPosts')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SocialWall
|
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
import { FaTimes, FaEye, FaClipboard } from 'react-icons/fa'
|
|
||||||
import { AnnouncementDto } from '@/proxy/intranet/models'
|
|
||||||
import useLocale from '@/utils/hooks/useLocale'
|
|
||||||
import { currentLocalDate } from '@/utils/dateUtils'
|
|
||||||
|
|
||||||
interface AnnouncementDetailModalProps {
|
|
||||||
announcement: AnnouncementDto
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const AnnouncementDetailModal: React.FC<AnnouncementDetailModalProps> = ({ announcement, onClose }) => {
|
|
||||||
const { translate } = useLocalization();
|
|
||||||
const currentLocale = useLocale()
|
|
||||||
const getCategoryColor = (category: string) => {
|
|
||||||
const colors: Record<string, string> = {
|
|
||||||
general: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
|
||||||
hr: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300',
|
|
||||||
it: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300',
|
|
||||||
event: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
|
||||||
urgent: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300',
|
|
||||||
}
|
|
||||||
return colors[category] || colors.general
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
className="fixed inset-0 bg-black/50 z-40"
|
|
||||||
onClick={onClose}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 overflow-y-auto">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-3xl w-full"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-3 mb-3">
|
|
||||||
<span
|
|
||||||
className={`px-3 py-1 text-xs font-medium rounded-full ${getCategoryColor(announcement.category)}`}
|
|
||||||
>
|
|
||||||
{announcement.category === 'general' && `📢 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.General')}`}
|
|
||||||
{announcement.category === 'hr' && `👥 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.HR')}`}
|
|
||||||
{announcement.category === 'it' && `💻 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.IT')}`}
|
|
||||||
{announcement.category === 'event' && `🎉 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.Event')}`}
|
|
||||||
{announcement.category === 'urgent' && `🚨 ${translate('::App.Platform.Intranet.AnnouncementDetailModal.Category.Urgent')}`}
|
|
||||||
</span>
|
|
||||||
{announcement.isPinned && (
|
|
||||||
<span className="text-yellow-500 text-sm">📌 {translate('::App.Platform.Intranet.AnnouncementDetailModal.Pinned')}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
||||||
{announcement.title}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<FaTimes className="w-6 h-6 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Author Info */}
|
|
||||||
<div className="flex items-center gap-3 mt-4">
|
|
||||||
<img
|
|
||||||
src={announcement.employee.avatar}
|
|
||||||
alt={announcement.employee.name}
|
|
||||||
className="w-12 h-12 rounded-full"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold text-gray-900 dark:text-white">
|
|
||||||
{announcement.employee.name}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
<span>
|
|
||||||
{currentLocalDate(announcement.publishDate, currentLocale || 'tr')}
|
|
||||||
</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<FaEye className="w-4 h-4" />
|
|
||||||
{announcement.viewCount} {translate('::App.Platform.Intranet.AnnouncementDetailModal.Views')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
|
||||||
{/* Image if exists */}
|
|
||||||
{announcement.imageUrl && (
|
|
||||||
<img
|
|
||||||
src={announcement.imageUrl}
|
|
||||||
alt={announcement.title}
|
|
||||||
className="w-full rounded-lg mb-6"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Full Content */}
|
|
||||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
|
||||||
<p className="text-gray-700 dark:text-gray-300 whitespace-pre-line">
|
|
||||||
{announcement.content}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Attachments */}
|
|
||||||
{announcement.attachments &&
|
|
||||||
announcement.attachments.length > 0 && (
|
|
||||||
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
|
|
||||||
<FaClipboard className="w-5 h-5" />
|
|
||||||
{translate('::App.Platform.Intranet.AnnouncementDetailModal.Attachments')} ({announcement.attachments.length})
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{announcement.attachments.map((attachment, idx) => (
|
|
||||||
<a
|
|
||||||
key={idx}
|
|
||||||
href={attachment.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
||||||
>
|
|
||||||
<FaClipboard className="w-5 h-5 text-gray-400" />
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
|
||||||
{attachment.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{attachment.size}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-blue-600 dark:text-blue-400">
|
|
||||||
{translate('::App.Platform.Intranet.AnnouncementDetailModal.Download')}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Departments */}
|
|
||||||
{announcement.departments &&
|
|
||||||
announcement.departments.length > 0 && (
|
|
||||||
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
|
||||||
{translate('::App.Platform.Intranet.AnnouncementDetailModal.TargetDepartments')}
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{announcement.departments?.map((dept, idx) => (
|
|
||||||
<span
|
|
||||||
key={idx}
|
|
||||||
className="px-3 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm rounded-full"
|
|
||||||
>
|
|
||||||
{dept}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Expiry Date */}
|
|
||||||
{announcement.expiryDate && (
|
|
||||||
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
<span className="font-medium">{translate('::App.Platform.Intranet.AnnouncementDetailModal.ExpiryDate')}:</span>{' '}
|
|
||||||
{currentLocalDate(announcement.expiryDate, currentLocale || 'tr')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="p-6 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::App.Platform.Intranet.AnnouncementDetailModal.Close')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AnnouncementDetailModal
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
import { FaTimes } from 'react-icons/fa'
|
|
||||||
|
|
||||||
interface ExpenseRequestModalProps {
|
|
||||||
onClose: () => void
|
|
||||||
onSubmit: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExpenseRequestModal: React.FC<ExpenseRequestModalProps> = ({ onClose, onSubmit }) => {
|
|
||||||
const { translate } = useLocalization();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
className="fixed inset-0 bg-black/50 z-40"
|
|
||||||
onClick={onClose}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between sticky top-0 bg-white dark:bg-gray-800 z-10">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
{translate('::App.Platform.Intranet.ExpenseRequestModal.Title')}
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<FaTimes className="w-5 h-5 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
onSubmit()
|
|
||||||
}}
|
|
||||||
className="p-6 space-y-4"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{translate('::App.Platform.Intranet.ExpenseRequestModal.Category')}
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="">{translate('::App.Platform.Intranet.ExpenseRequestModal.Select')}</option>
|
|
||||||
<option value="travel">✈️ {translate('::App.Platform.Intranet.ExpenseRequestModal.Travel')}</option>
|
|
||||||
<option value="meal">🍽️ {translate('::App.Platform.Intranet.ExpenseRequestModal.Meal')}</option>
|
|
||||||
<option value="accommodation">🏨 {translate('::App.Platform.Intranet.ExpenseRequestModal.Accommodation')}</option>
|
|
||||||
<option value="transport">🚗 {translate('::App.Platform.Intranet.ExpenseRequestModal.Transport')}</option>
|
|
||||||
<option value="other">📋 {translate('::App.Platform.Intranet.ExpenseRequestModal.Other')}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{translate('::App.Platform.Intranet.ExpenseRequestModal.Description')}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
placeholder={translate('::App.Platform.Intranet.ExpenseRequestModal.DescriptionPlaceholder')}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{translate('::App.Platform.Intranet.ExpenseRequestModal.Amount')}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
required
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
placeholder="0.00"
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{translate('::App.Platform.Intranet.ExpenseRequestModal.Date')}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{translate('::App.Platform.Intranet.ExpenseRequestModal.Note')}
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
rows={3}
|
|
||||||
placeholder={translate('::App.Platform.Intranet.ExpenseRequestModal.NotePlaceholder')}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3 pt-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::Cancel')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="flex-1 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::App.Platform.Intranet.ExpenseRequestModal.Submit')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ExpenseRequestModal
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
import { FaTimes } from 'react-icons/fa'
|
|
||||||
|
|
||||||
interface LeaveRequestModalProps {
|
|
||||||
onClose: () => void
|
|
||||||
onSubmit: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const LeaveRequestModal: React.FC<LeaveRequestModalProps> = ({ onClose, onSubmit }) => {
|
|
||||||
const { translate } = useLocalization();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
className="fixed inset-0 bg-black/50 z-40"
|
|
||||||
onClick={onClose}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between sticky top-0 bg-white dark:bg-gray-800 z-10">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
{translate('::App.Platform.Intranet.LeaveRequestModal.Title')}
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<FaTimes className="w-5 h-5 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
onSubmit()
|
|
||||||
}}
|
|
||||||
className="p-6 space-y-4"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{translate('::App.Platform.Intranet.LeaveRequestModal.LeaveType')}
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="">{translate('::App.Platform.Intranet.LeaveRequestModal.Select')}</option>
|
|
||||||
<option value="annual">🏖️ {translate('::App.Platform.Intranet.LeaveRequestModal.AnnualLeave')}</option>
|
|
||||||
<option value="sick">🏥 {translate('::App.Platform.Intranet.LeaveRequestModal.SickLeave')}</option>
|
|
||||||
<option value="unpaid">💼 {translate('::App.Platform.Intranet.LeaveRequestModal.UnpaidLeave')}</option>
|
|
||||||
<option value="other">📋 {translate('::App.Platform.Intranet.LeaveRequestModal.Other')}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{translate('::App.Platform.Intranet.LeaveRequestModal.StartDate')}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{translate('::App.Platform.Intranet.LeaveRequestModal.EndDate')}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{translate('::App.Platform.Intranet.LeaveRequestModal.Description')}
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
required
|
|
||||||
rows={3}
|
|
||||||
placeholder={translate('::App.Platform.Intranet.LeaveRequestModal.ReasonPlaceholder')}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3 pt-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::Cancel')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::App.Platform.Intranet.LeaveRequestModal.Submit')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LeaveRequestModal
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
import { FaTimes } from 'react-icons/fa'
|
|
||||||
|
|
||||||
interface OvertimeRequestModalProps {
|
|
||||||
onClose: () => void
|
|
||||||
onSubmit: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const OvertimeRequestModal: React.FC<OvertimeRequestModalProps> = ({ onClose, onSubmit }) => {
|
|
||||||
const { translate } = useLocalization();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
className="fixed inset-0 bg-black/50 z-40"
|
|
||||||
onClick={onClose}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between sticky top-0 bg-white dark:bg-gray-800 z-10">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
{translate('::App.Platform.Intranet.OvertimeRequestModal.Title')}
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<FaTimes className="w-5 h-5 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
onSubmit()
|
|
||||||
}}
|
|
||||||
className="p-6 space-y-4"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{translate('::App.Platform.Intranet.OvertimeRequestModal.Date')}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{translate('::App.Platform.Intranet.OvertimeRequestModal.StartTime')}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{translate('::App.Platform.Intranet.OvertimeRequestModal.EndTime')}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
required
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{translate('::App.Platform.Intranet.OvertimeRequestModal.Description')}
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
required
|
|
||||||
rows={3}
|
|
||||||
placeholder={translate('::App.Platform.Intranet.OvertimeRequestModal.ReasonPlaceholder')}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3 pt-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::Cancel')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="flex-1 px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
{translate('::App.Platform.Intranet.OvertimeRequestModal.Submit')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OvertimeRequestModal
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue