Kaldırılan Dto dosyaları kaldırıldı

This commit is contained in:
Sedat Öztürk 2026-02-27 20:12:12 +03:00
parent aac3f4aa80
commit 078ba898bd
117 changed files with 16 additions and 16456 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +0,0 @@
using Volo.Abp.Application.Dtos;
public class ClassroomFilterInputDto : PagedAndSortedResultRequestDto
{
public string Search { get; set; }
public string Status { get; set; }
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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; } = [];
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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; } = [];
}

View file

@ -1,11 +0,0 @@
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace Sozsoft.Platform.Intranet;
public interface IIntranetAppService : IApplicationService
{
Task<IntranetDashboardDto> GetIntranetDashboardAsync();
}

View file

@ -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; } = [];
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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ı."
]
}
]
} }

View file

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

View file

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

View file

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

View file

@ -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',
},
]

View file

@ -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',
},
]

View file

@ -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',
},
]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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">
ı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&#10;B&#10;C&#10;D&#10;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>
);
};

View file

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

View file

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

View file

@ -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">
ıklama:
</h4>
<p className="text-sm text-gray-700">
{question.explanation}
</p>
</div>
)}
</div>
);
};

View file

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

View file

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

View file

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

View file

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

View file

@ -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">
ıklama:
</h4>
<p className="text-sm text-blue-700">
{question.explanation}
</p>
</div>
)}
</div>
);
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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