From f64f13557ed3f3650d58c569e6b7d94e5a80f575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Wed, 15 Oct 2025 22:52:01 +0300 Subject: [PATCH] Classroom Exam Bilgileri --- .../Seeds/HostData.json | 80 ++ .../Seeds/ListFormsSeeder.cs | 269 +++++++ .../PlatformConsts.cs | 1 + .../Kurs.Platform.Domain/Data/SeedConsts.cs | 6 + .../Entities/Tenant/Tag.cs | 17 + .../EntityFrameworkCore/PlatformDbContext.cs | 15 + ....cs => 20251015192202_Initial.Designer.cs} | 65 +- ...6_Initial.cs => 20251015192202_Initial.cs} | 26 + .../PlatformDbContextModelSnapshot.cs | 63 ++ .../Tenants/Seeds/TenantData.json | 23 +- .../Tenants/TenantDataSeeder.cs | 20 +- .../Tenants/TenantSeederDto.cs | 8 + ui/src/mocks/mockCoordinator.ts | 79 ++ ui/src/mocks/mockExams.ts | 39 + ui/src/mocks/mockPools.ts | 46 ++ ui/src/mocks/mockTags.ts | 36 + ui/src/mocks/mockTests.ts | 48 ++ ui/src/types/coordinator.ts | 130 +++ ui/src/utils/hooks/useCoordinator.ts | 131 +++ ui/src/utils/hooks/useExamSecurity.ts | 95 +++ ui/src/utils/hooks/useExamTimer.ts | 94 +++ ui/src/views/classroom/ClassList.tsx | 5 +- ui/src/views/classroom/PlanningPage.tsx | 4 +- .../coordinator/AdminPanel/ExamCreator.tsx | 502 ++++++++++++ .../coordinator/AdminPanel/QuestionEditor.tsx | 749 ++++++++++++++++++ .../AdminPanel/QuestionPoolManager.tsx | 475 +++++++++++ .../coordinator/AdminPanel/TestCreator.tsx | 601 ++++++++++++++ ui/src/views/coordinator/Assignments.tsx | 249 ++++++ .../ExamInterface/ExamNavigation.tsx | 83 ++ .../coordinator/ExamInterface/ExamTimer.tsx | 93 +++ .../ExamInterface/PDFTestInterface.tsx | 396 +++++++++ .../ExamInterface/QuestionRenderer.tsx | 128 +++ .../ExamInterface/SecurityWarning.tsx | 81 ++ .../ExamInterface/StudentExamInterface.tsx | 250 ++++++ ui/src/views/coordinator/Exams.tsx | 245 ++++++ .../QuestionTypes/CalculationQuestion.tsx | 116 +++ .../QuestionTypes/FillBlankQuestion.tsx | 115 +++ .../QuestionTypes/MatchingQuestion.tsx | 173 ++++ .../QuestionTypes/MultipleAnswerQuestion.tsx | 120 +++ .../QuestionTypes/MultipleChoiceQuestion.tsx | 105 +++ .../QuestionTypes/OpenEndedQuestion.tsx | 105 +++ .../QuestionTypes/OrderingQuestion.tsx | 191 +++++ .../QuestionTypes/TrueFalseQuestion.tsx | 101 +++ ui/src/views/coordinator/Tags.tsx | 223 ++++++ ui/src/views/coordinator/Tests.tsx | 237 ++++++ 45 files changed, 6631 insertions(+), 7 deletions(-) create mode 100644 api/src/Kurs.Platform.Domain/Entities/Tenant/Tag.cs rename api/src/Kurs.Platform.EntityFrameworkCore/Migrations/{20251013214306_Initial.Designer.cs => 20251015192202_Initial.Designer.cs} (99%) rename api/src/Kurs.Platform.EntityFrameworkCore/Migrations/{20251013214306_Initial.cs => 20251015192202_Initial.cs} (99%) create mode 100644 ui/src/mocks/mockCoordinator.ts create mode 100644 ui/src/mocks/mockExams.ts create mode 100644 ui/src/mocks/mockPools.ts create mode 100644 ui/src/mocks/mockTags.ts create mode 100644 ui/src/mocks/mockTests.ts create mode 100644 ui/src/types/coordinator.ts create mode 100644 ui/src/utils/hooks/useCoordinator.ts create mode 100644 ui/src/utils/hooks/useExamSecurity.ts create mode 100644 ui/src/utils/hooks/useExamTimer.ts create mode 100644 ui/src/views/coordinator/AdminPanel/ExamCreator.tsx create mode 100644 ui/src/views/coordinator/AdminPanel/QuestionEditor.tsx create mode 100644 ui/src/views/coordinator/AdminPanel/QuestionPoolManager.tsx create mode 100644 ui/src/views/coordinator/AdminPanel/TestCreator.tsx create mode 100644 ui/src/views/coordinator/Assignments.tsx create mode 100644 ui/src/views/coordinator/ExamInterface/ExamNavigation.tsx create mode 100644 ui/src/views/coordinator/ExamInterface/ExamTimer.tsx create mode 100644 ui/src/views/coordinator/ExamInterface/PDFTestInterface.tsx create mode 100644 ui/src/views/coordinator/ExamInterface/QuestionRenderer.tsx create mode 100644 ui/src/views/coordinator/ExamInterface/SecurityWarning.tsx create mode 100644 ui/src/views/coordinator/ExamInterface/StudentExamInterface.tsx create mode 100644 ui/src/views/coordinator/Exams.tsx create mode 100644 ui/src/views/coordinator/QuestionTypes/CalculationQuestion.tsx create mode 100644 ui/src/views/coordinator/QuestionTypes/FillBlankQuestion.tsx create mode 100644 ui/src/views/coordinator/QuestionTypes/MatchingQuestion.tsx create mode 100644 ui/src/views/coordinator/QuestionTypes/MultipleAnswerQuestion.tsx create mode 100644 ui/src/views/coordinator/QuestionTypes/MultipleChoiceQuestion.tsx create mode 100644 ui/src/views/coordinator/QuestionTypes/OpenEndedQuestion.tsx create mode 100644 ui/src/views/coordinator/QuestionTypes/OrderingQuestion.tsx create mode 100644 ui/src/views/coordinator/QuestionTypes/TrueFalseQuestion.tsx create mode 100644 ui/src/views/coordinator/Tags.tsx create mode 100644 ui/src/views/coordinator/Tests.tsx diff --git a/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json b/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json index ec3a730c..1bdf937b 100644 --- a/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json +++ b/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json @@ -7879,6 +7879,12 @@ "tr": "Ders Dönemleri", "en": "Lesson Periods" }, + { + "resourceName": "Platform", + "key": "App.Classroom.Tag", + "tr": "Etiketler", + "en": "Tags" + }, { "resourceName": "Platform", "key": "App.Definitions.RegistrationType", @@ -14591,6 +14597,16 @@ "RequiredPermissionName": "App.Definitions.LessonPeriod", "IsDisabled": false }, + { + "ParentCode": "App.Coordinator.Definitions", + "Code": "App.Classroom.Tag", + "DisplayName": "App.Classroom.Tag", + "Order": 10, + "Url": "/admin/list/list-tag", + "Icon": "FcTags", + "RequiredPermissionName": "App.Classroom.Tag", + "IsDisabled": false + }, { "ParentCode": "App.Coordinator", "Code": "App.Classroom", @@ -21922,6 +21938,70 @@ "MultiTenancySide": 2, "MenuGroup": "Kurs" }, + { + "GroupName": "App.Coordinator", + "Name": "App.Classroom.Tag", + "ParentName": null, + "DisplayName": "App.Classroom.Tag", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Kurs" + }, + { + "GroupName": "App.Coordinator", + "Name": "App.Classroom.Tag.Create", + "ParentName": "App.Classroom.Tag", + "DisplayName": "Create", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Kurs" + }, + { + "GroupName": "App.Coordinator", + "Name": "App.Classroom.Tag.Update", + "ParentName": "App.Classroom.Tag", + "DisplayName": "Update", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Kurs" + }, + { + "GroupName": "App.Coordinator", + "Name": "App.Classroom.Tag.Delete", + "ParentName": "App.Classroom.Tag", + "DisplayName": "Delete", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Kurs" + }, + { + "GroupName": "App.Coordinator", + "Name": "App.Classroom.Tag.Export", + "ParentName": "App.Classroom.Tag", + "DisplayName": "Export", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Kurs" + }, + { + "GroupName": "App.Coordinator", + "Name": "App.Classroom.Tag.Import", + "ParentName": "App.Classroom.Tag", + "DisplayName": "Import", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Kurs" + }, + { + "GroupName": "App.Coordinator", + "Name": "App.Classroom.Tag.Activity", + "ParentName": "App.Classroom.Tag", + "DisplayName": "Activity", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Kurs" + }, + { "GroupName": "App.SupplyChain", "Name": "App.SupplyChain.MaterialTypes", diff --git a/api/src/Kurs.Platform.DbMigrator/Seeds/ListFormsSeeder.cs b/api/src/Kurs.Platform.DbMigrator/Seeds/ListFormsSeeder.cs index 8b3d4255..46c01465 100644 --- a/api/src/Kurs.Platform.DbMigrator/Seeds/ListFormsSeeder.cs +++ b/api/src/Kurs.Platform.DbMigrator/Seeds/ListFormsSeeder.cs @@ -29929,6 +29929,275 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency } #endregion + //Classroom + #region Tag + if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == ListFormCodes.Lists.Tag)) + { + var listFormTag = await _listFormRepository.InsertAsync( + new ListForm() + { + ListFormType = ListFormTypeEnum.List, + IsSubForm = false, + LayoutJson = JsonSerializer.Serialize(new LayoutDto() + { + Grid = true, + Card = true, + Pivot = true, + Chart = true, + DefaultLayout = "grid", + CardLayoutColumn = 3 + }), + CultureName = LanguageCodes.En, + ListFormCode = ListFormCodes.Lists.Tag, + Name = AppCodes.Classroom.Tag, + Title = AppCodes.Classroom.Tag, + DataSourceCode = SeedConsts.DataSources.DefaultCode, + IsTenant = true, + IsBranch = false, + IsOrganizationUnit = false, + Description = AppCodes.Classroom.Tag, + SelectCommandType = SelectCommandTypeEnum.Table, + SelectCommand = SelectCommandByTableName("Tag", Prefix.DbTableCoordinator), + KeyFieldName = "Id", + KeyFieldDbSourceType = DbType.Guid, + DefaultFilter = "\"IsDeleted\" = 'false'", + SortMode = GridOptions.SortModeSingle, + FilterRowJson = JsonSerializer.Serialize(new GridFilterRowDto { Visible = true }), + HeaderFilterJson = JsonSerializer.Serialize(new { Visible = true }), + SearchPanelJson = JsonSerializer.Serialize(new { Visible = true }), + GroupPanelJson = JsonSerializer.Serialize(new { Visible = true }), + SelectionJson = JsonSerializer.Serialize(new SelectionDto + { + Mode = GridOptions.SelectionModeSingle, + AllowSelectAll = false + }), + ColumnOptionJson = JsonSerializer.Serialize(new + { + ColumnFixingEnabled = true, + ColumnAutoWidth = true, + ColumnChooserEnabled = true, + AllowColumnResizing = true, + AllowColumnReordering = true, + ColumnResizingMode = "widget", + }), + PermissionJson = JsonSerializer.Serialize(new PermissionCrudDto + { + C = AppCodes.Classroom.Tag + ".Create", + R = AppCodes.Classroom.Tag, + U = AppCodes.Classroom.Tag + ".Update", + D = AppCodes.Classroom.Tag + ".Delete", + E = AppCodes.Classroom.Tag + ".Export", + I = AppCodes.Classroom.Tag + ".Import", + A = AppCodes.Classroom.Tag + ".Activity", + }), + DeleteCommand = $"UPDATE \"{SelectCommandByTableName("Tag", Prefix.DbTableCoordinator)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id", + DeleteFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { + new() { + FieldName = "DeleterId", + FieldDbType = DbType.Guid, + Value = "@USERID", + CustomValueType = FieldCustomValueTypeEnum.CustomKey }, + new() { + FieldName = "Id", + FieldDbType = DbType.Guid, + Value = "@ID", + CustomValueType = FieldCustomValueTypeEnum.CustomKey } + }), + PagerOptionJson = JsonSerializer.Serialize(new GridPagerOptionDto + { + Visible = true, + AllowedPageSizes = "10,20,50,100", + ShowPageSizeSelector = true, + ShowNavigationButtons = true, + ShowInfo = false, + InfoText = "Page {0} of {1} ({2} items)", + DisplayMode = GridColumnOptions.PagerDisplayModeAdaptive, + ScrollingMode = GridColumnOptions.ScrollingModeStandard, + LoadPanelEnabled = "auto", + LoadPanelText = "Loading..." + }), + EditingOptionJson = JsonSerializer.Serialize(new GridEditingDto + { + Popup = new GridEditingPopupDto() + { + Title = "Tag Form", + Width = 500, + Height = 250 + }, + AllowDeleting = true, + AllowAdding = true, + AllowUpdating = true, + SendOnlyChangedFormValuesUpdate = false, + }), + EditingFormJson = JsonSerializer.Serialize(new List() { + new() { Order=1,ColCount=1,ColSpan=2,ItemType="group", Items= + [ + new EditingFormItemDto { Order = 1, DataField = "Name", ColSpan = 2, IsRequired = true, EditorType2=EditorTypes.dxTextBox }, + new EditingFormItemDto { Order = 2, DataField = "Description", ColSpan = 2, EditorType2=EditorTypes.dxTextBox }, + new EditingFormItemDto { Order = 3, DataField = "Color", ColSpan = 2, IsRequired = true, EditorType2=EditorTypes.dxColorBox }, + ] + } + }), + InsertFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { + new() { + FieldName = "CreationTime", + FieldDbType = DbType.DateTime, + Value = "@NOW", + CustomValueType = FieldCustomValueTypeEnum.CustomKey }, + new() { + FieldName = "CreatorId", + FieldDbType = DbType.Guid, + Value = "@USERID", + CustomValueType = FieldCustomValueTypeEnum.CustomKey }, + new() { + FieldName = "IsDeleted", + FieldDbType = DbType.Boolean, + Value = "false", + CustomValueType = FieldCustomValueTypeEnum.Value } + }), + } + ); + + #region Tag Fields + await _listFormFieldRepository.InsertManyAsync(new ListFormField[] { + new() { + ListFormCode = listFormTag.ListFormCode, + RoleId = null, + UserId = null, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "Id", + Width = 100, + ListOrderNo = 1, + Visible = false, + IsActive = true, + IsDeleted = false, + SortIndex = 0, + ValidationRuleJson = JsonSerializer.Serialize(new ValidationRuleDto[] { + new ValidationRuleDto() { Type = Enum.GetName(UiColumnValidationRuleTypeEnum.required) } + }), + ColumnCustomizationJson = JsonSerializer.Serialize(new ColumnCustomizationDto + { + AllowReordering = true, + }), + PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto + { + C = AppCodes.Classroom.Tag + ".Create", + R = AppCodes.Classroom.Tag, + U = AppCodes.Classroom.Tag + ".Update", + E = true, + I = true, + Deny = false + }), + PivotSettingsJson = JsonSerializer.Serialize(new ListFormFieldPivotSettingsDto + { + IsPivot = true + }) + }, + new() { + ListFormCode = listFormTag.ListFormCode, + RoleId = null, + UserId = null, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Name", + Width = 250, + ListOrderNo = 2, + Visible = true, + IsActive = true, + IsDeleted = false, + SortIndex = 1, + SortDirection = GridColumnOptions.SortOrderAsc, + AllowSearch = true, + ValidationRuleJson = JsonSerializer.Serialize(new ValidationRuleDto[] { + new ValidationRuleDto() { Type = Enum.GetName(UiColumnValidationRuleTypeEnum.required) } + }), + ColumnCustomizationJson = JsonSerializer.Serialize(new ColumnCustomizationDto + { + AllowReordering = true, + }), + PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto + { + C = AppCodes.Classroom.Tag + ".Create", + R = AppCodes.Classroom.Tag, + U = AppCodes.Classroom.Tag + ".Update", + E = true, + I = true, + Deny = false + }), + PivotSettingsJson = JsonSerializer.Serialize(new ListFormFieldPivotSettingsDto + { + IsPivot = true + }) + }, + new() { + ListFormCode = listFormTag.ListFormCode, + RoleId = null, + UserId = null, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Description", + Width = 400, + ListOrderNo = 3, + Visible = true, + IsActive = true, + IsDeleted = false, + AllowSearch = true, + ColumnCustomizationJson = JsonSerializer.Serialize(new ColumnCustomizationDto + { + AllowReordering = true, + }), + PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto + { + C = AppCodes.Classroom.Tag + ".Create", + R = AppCodes.Classroom.Tag, + U = AppCodes.Classroom.Tag + ".Update", + E = true, + I = true, + Deny = false + }), + PivotSettingsJson = JsonSerializer.Serialize(new ListFormFieldPivotSettingsDto + { + IsPivot = true + }) + }, + new() { + ListFormCode = listFormTag.ListFormCode, + RoleId = null, + UserId = null, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Color", + Width = 100, + ListOrderNo = 4, + Visible = true, + IsActive = true, + IsDeleted = false, + AllowSearch = true, + ColumnCustomizationJson = JsonSerializer.Serialize(new ColumnCustomizationDto + { + AllowReordering = true, + }), + PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto + { + C = AppCodes.Classroom.Tag + ".Create", + R = AppCodes.Classroom.Tag, + U = AppCodes.Classroom.Tag + ".Update", + E = true, + I = true, + Deny = false + }), + PivotSettingsJson = JsonSerializer.Serialize(new ListFormFieldPivotSettingsDto + { + IsPivot = true + }) + } + }); + #endregion + } + #endregion + + #endregion } } diff --git a/api/src/Kurs.Platform.Domain.Shared/PlatformConsts.cs b/api/src/Kurs.Platform.Domain.Shared/PlatformConsts.cs index 8f37bb0f..0361419f 100644 --- a/api/src/Kurs.Platform.Domain.Shared/PlatformConsts.cs +++ b/api/src/Kurs.Platform.Domain.Shared/PlatformConsts.cs @@ -595,6 +595,7 @@ public static class PlatformConsts public const string ClassType = "list-classtype"; public const string Class = "list-class"; public const string Level = "list-level"; + public const string Tag = "list-tag"; } } diff --git a/api/src/Kurs.Platform.Domain/Data/SeedConsts.cs b/api/src/Kurs.Platform.Domain/Data/SeedConsts.cs index 3612098f..329d618a 100644 --- a/api/src/Kurs.Platform.Domain/Data/SeedConsts.cs +++ b/api/src/Kurs.Platform.Domain/Data/SeedConsts.cs @@ -440,6 +440,12 @@ public static class SeedConsts public const string Level = Default + ".Level"; } + public static class Classroom + { + public const string Default = Prefix.App + ".Classroom"; + public const string Tag = Default + ".Tag"; + } + public static class Accounting { public const string Default = Prefix.App + ".Accounting"; diff --git a/api/src/Kurs.Platform.Domain/Entities/Tenant/Tag.cs b/api/src/Kurs.Platform.Domain/Entities/Tenant/Tag.cs new file mode 100644 index 00000000..e15a4ad6 --- /dev/null +++ b/api/src/Kurs.Platform.Domain/Entities/Tenant/Tag.cs @@ -0,0 +1,17 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Kurs.Platform.Entities; + +public class Tag : FullAuditedEntity, IMultiTenant +{ + public Guid? TenantId; + + public string Name { get; set; } // Etiket adı (ör: Grammar) + public string Description { get; set; } // Açıklama + public string Color { get; set; } // Renk kodu (ör: #3B82F6) + public int UsageCount { get; set; } // Kullanım sayısı + + Guid? IMultiTenant.TenantId => TenantId; +} \ No newline at end of file diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Kurs.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index d1f53717..824937f4 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -139,6 +139,9 @@ public class PlatformDbContext : public DbSet Participants { get; set; } public DbSet AttendanceRecords { get; set; } public DbSet ChatMessages { get; set; } + + public DbSet Tags { get; set; } + #endregion #region Entities from the modules @@ -1561,5 +1564,17 @@ public class PlatformDbContext : b.Property(x => x.Subject).IsRequired().HasMaxLength(256); b.Property(x => x.Content).IsRequired().HasMaxLength(2000); }); + + // Classroom + builder.Entity(b => + { + b.ToTable(Prefix.DbTableCoordinator + nameof(Tag), Prefix.DbSchema); + b.ConfigureByConvention(); + + b.Property(x => x.Name).IsRequired().HasMaxLength(100); + b.Property(x => x.Description).HasMaxLength(500); + b.Property(x => x.Color).HasMaxLength(7); + b.Property(x => x.UsageCount).HasDefaultValue(0); + }); } } diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251013214306_Initial.Designer.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251015192202_Initial.Designer.cs similarity index 99% rename from api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251013214306_Initial.Designer.cs rename to api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251015192202_Initial.Designer.cs index f2333a77..b2ded844 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251013214306_Initial.Designer.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251015192202_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Kurs.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20251013214306_Initial")] + [Migration("20251015192202_Initial")] partial class Initial { /// @@ -6112,6 +6112,69 @@ namespace Kurs.Platform.Migrations b.ToTable("DSource", (string)null); }); + modelBuilder.Entity("Kurs.Platform.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Color") + .HasMaxLength(7) + .HasColumnType("nvarchar(7)"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UsageCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.ToTable("CTag", (string)null); + }); + modelBuilder.Entity("Kurs.Platform.Entities.Uom", b => { b.Property("Id") diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251013214306_Initial.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251015192202_Initial.cs similarity index 99% rename from api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251013214306_Initial.cs rename to api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251015192202_Initial.cs index 37931259..7cb3c933 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251013214306_Initial.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251015192202_Initial.cs @@ -434,6 +434,29 @@ namespace Kurs.Platform.Migrations table.PrimaryKey("PK_AbpUsers", x => x.Id); }); + migrationBuilder.CreateTable( + name: "CTag", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + Color = table.Column(type: "nvarchar(7)", maxLength: 7, nullable: true), + UsageCount = table.Column(type: "int", nullable: false, defaultValue: 0), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + CreationTime = table.Column(type: "datetime2", nullable: false), + CreatorId = table.Column(type: "uniqueidentifier", nullable: true), + LastModificationTime = table.Column(type: "datetime2", nullable: true), + LastModifierId = table.Column(type: "uniqueidentifier", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), + DeleterId = table.Column(type: "uniqueidentifier", nullable: true), + DeletionTime = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CTag", x => x.Id); + }); + migrationBuilder.CreateTable( name: "DBank", columns: table => new @@ -4142,6 +4165,9 @@ namespace Kurs.Platform.Migrations migrationBuilder.DropTable( name: "AbpUserTokens"); + migrationBuilder.DropTable( + name: "CTag"); + migrationBuilder.DropTable( name: "DBankAccount"); diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index 15d9cc65..a601097c 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -6109,6 +6109,69 @@ namespace Kurs.Platform.Migrations b.ToTable("DSource", (string)null); }); + modelBuilder.Entity("Kurs.Platform.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Color") + .HasMaxLength(7) + .HasColumnType("nvarchar(7)"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UsageCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.ToTable("CTag", (string)null); + }); + modelBuilder.Entity("Kurs.Platform.Entities.Uom", b => { b.Property("Id") diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/Seeds/TenantData.json b/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/Seeds/TenantData.json index e25865a1..2ffadc06 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/Seeds/TenantData.json +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/Seeds/TenantData.json @@ -1754,7 +1754,6 @@ "Sunday": true } ], - "Classrooms": [ { "name": "Matematik 101 - Diferansiyel Denklemler", @@ -1807,5 +1806,27 @@ "participantCount": 0, "settingsJson": "{\"AllowHandRaise\":true,\"AllowStudentChat\":true,\"AllowPrivateMessages\":true,\"AllowStudentScreenShare\":true,\"DefaultMicrophoneState\":\"muted\",\"DefaultCameraState\":\"off\",\"DefaultLayout\":\"grid\",\"AutoMuteNewParticipants\":true}" } + ], + "Tags": [ + { + "Name": "Grammar", + "Description": "Grammar related questions", + "Color": "#3B82F6" + }, + { + "Name": "Vocabulary", + "Description": "Vocabulary building exercises", + "Color": "#10B981" + }, + { + "Name": "Reading", + "Description": "Reading comprehension and analysis", + "Color": "#F59E0B" + }, + { + "Name": "Writing", + "Description": "Writing skills development", + "Color": "#EF4444" + } ] } diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantDataSeeder.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantDataSeeder.cs index 8e93d94d..8d6766ff 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantDataSeeder.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantDataSeeder.cs @@ -52,6 +52,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency private readonly IRepository _classCancellationReasonRepository; private readonly IRepository _workHourRepository; private readonly IRepository _classroomRepository; + private readonly IRepository _tagRepository; public TenantDataSeeder( IRepository globalSearch, @@ -90,7 +91,8 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency IRepository sourceRepository, IRepository interestingRepository, IRepository programRepository, - IRepository forumCategoryRepository + IRepository forumCategoryRepository, + IRepository tagRepository ) { _globalSearch = globalSearch; @@ -130,6 +132,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency _interestingRepository = interestingRepository; _programRepository = programRepository; _forumCategoryRepository = forumCategoryRepository; + _tagRepository = tagRepository; } private static IConfigurationRoot BuildConfiguration() @@ -725,5 +728,20 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency }); } } + + foreach (var item in items.Tags) + { + var exists = await _tagRepository.AnyAsync(x => x.Name == item.Name); + + if (!exists) + { + await _tagRepository.InsertAsync(new Tag + { + Name = item.Name, + Description = item.Description, + Color = item.Color + }); + } + } } } diff --git a/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantSeederDto.cs b/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantSeederDto.cs index 50a25db0..17544a20 100644 --- a/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantSeederDto.cs +++ b/api/src/Kurs.Platform.EntityFrameworkCore/Tenants/TenantSeederDto.cs @@ -26,6 +26,7 @@ public class TenantSeederDto public List BlogPosts { get; set; } public List Contacts { get; set; } public List Classrooms { get; set; } + public List Tags { get; set; } //Tanımlamalar public List Sectors { get; set; } @@ -343,4 +344,11 @@ public class ProgramSeedDto { public string Name { get; set; } public string Status { get; set; } +} + +public class TagSeedDto +{ + public string Name { get; set; } + public string Description { get; set; } + public string Color { get; set; } } \ No newline at end of file diff --git a/ui/src/mocks/mockCoordinator.ts b/ui/src/mocks/mockCoordinator.ts new file mode 100644 index 00000000..b0962f9a --- /dev/null +++ b/ui/src/mocks/mockCoordinator.ts @@ -0,0 +1,79 @@ +export const adminNavigationItems = [ + { + id: "dashboard", + title: "Dashboard", + icon: "LayoutDashboard", + path: "/admin/dashboard", + description: "Genel bakış ve istatistikler", + }, + { + id: "tags", + title: "Etiket Yönetimi", + icon: "Tags", + path: "/admin/tags", + description: "Soru ve içerik etiketleri", + }, + { + id: "question-pools", + title: "Soru Havuzları", + icon: "Database", + path: "/admin/question-pools", + description: "Soru havuzları ve kategoriler", + }, + { + id: "exams", + title: "Sınav Yönetimi", + icon: "FileText", + path: "/admin/exams", + description: "Sınavları yönetin", + }, + { + id: "assignments", + title: "Ödev Yönetimi", + icon: "BookOpen", + path: "/admin/assignments", + description: "Ödevleri yönetin", + }, + { + id: "tests", + title: "Test Yönetimi", + icon: "ClipboardList", + path: "/admin/tests", + description: "PDF testler ve cevap anahtarları", + }, +]; + +export const questionTypes = [ + { value: "multiple-choice", label: "Çoktan Seçmeli", icon: "CheckCircle" }, + { value: "fill-blank", label: "Boşluk Doldurma", icon: "Edit3" }, + { value: "multiple-answer", label: "Çok Yanıtlı", icon: "CheckSquare" }, + { value: "matching", label: "Eşleştirme", icon: "Link" }, + { value: "ordering", label: "Sıralama", icon: "ArrowUpDown" }, + { value: "open-ended", label: "Açık Uçlu", icon: "MessageSquare" }, + { value: "true-false", label: "Doğru-Yanlış", icon: "ToggleLeft" }, + { value: "calculation", label: "Hesaplama", icon: "Calculator" }, +]; + +export const difficultyLevels = [ + { value: "easy", label: "Kolay", color: "green" }, + { value: "medium", label: "Orta", color: "yellow" }, + { value: "hard", label: "Zor", color: "red" }, +]; + +export const examTypes = [ + { value: "exam", label: "Sınav", icon: "FileText" }, + { value: "assignment", label: "Ödev", icon: "BookOpen" }, + { value: "test", label: "Test", icon: "ClipboardList" }, +]; + +export const getBreadcrumb = (currentPath: string): string[] => { + const pathMap: Record = { + "/admin/dashboard": ["Admin", "Dashboard"], + "/admin/tags": ["Admin", "Etiket Yönetimi"], + "/admin/question-pools": ["Admin", "Soru Havuzları"], + "/admin/exams": ["Admin", "Sınav Yönetimi"], + "/admin/assignments": ["Admin", "Ödev Yönetimi"], + "/admin/tests": ["Admin", "Test Yönetimi"], + }; + return pathMap[currentPath] || ["Admin"]; +}; diff --git a/ui/src/mocks/mockExams.ts b/ui/src/mocks/mockExams.ts new file mode 100644 index 00000000..90f20339 --- /dev/null +++ b/ui/src/mocks/mockExams.ts @@ -0,0 +1,39 @@ +import { Exam } from "@/types/coordinator"; +import { generateMockPools } from "./mockPools"; + +export const generateMockExam = (): Exam[] => [ + { + id: "exam-1", + title: "Grammar Assessment", + description: "Comprehensive grammar evaluation", + type: "exam", + questions: generateMockPools()[0].questions, + timeLimit: 30, + totalPoints: 25, + passingScore: 60, + allowReview: true, + randomizeQuestions: false, + showResults: true, + maxAttempts: 1, + isActive: true, + creationTime: new Date(), + lastModificationTime: new Date(), + }, + { + id: "assignment-1", + title: "Assignment 1", + description: "First assignment on grammar topics", + type: "assignment", + questions: generateMockPools()[0].questions, + timeLimit: 30, + totalPoints: 25, + passingScore: 60, + allowReview: true, + randomizeQuestions: false, + showResults: true, + maxAttempts: 1, + isActive: true, + creationTime: new Date(), + lastModificationTime: new Date(), + }, +]; diff --git a/ui/src/mocks/mockPools.ts b/ui/src/mocks/mockPools.ts new file mode 100644 index 00000000..68741bbb --- /dev/null +++ b/ui/src/mocks/mockPools.ts @@ -0,0 +1,46 @@ +import { QuestionPool } from "@/types/coordinator"; + +// Dynamic data - no hardcoded content +export const generateMockPools = (): QuestionPool[] => [ + { + id: "pool-1", + name: "Grammar Fundamentals", + description: "Essential grammar concepts and structures", + questions: [ + { + id: "q1", + type: "multiple-choice", + title: "Verb Tenses", + content: "Select the appropriate tense form for the given context.", + options: [ + { id: "opt1", text: "Option A", isCorrect: false }, + { id: "opt2", text: "Option B", isCorrect: true }, + { id: "opt3", text: "Option C", isCorrect: false }, + { id: "opt4", text: "Option D", isCorrect: false }, + ], + correctAnswer: "opt2", + points: 10, + difficulty: "easy", + tags: ["grammar", "verbs"], + creationTime: new Date(), + lastModificationTime: new Date(), + }, + { + id: "q2", + type: "fill-blank", + title: "Articles", + content: + 'Fill in the blank: "I saw _____ elephant at the zoo yesterday."', + correctAnswer: "an", + points: 15, + difficulty: "medium", + tags: ["grammar", "articles"], + creationTime: new Date(), + lastModificationTime: new Date(), + }, + ], + tags: ["grammar", "fundamentals"], + createdBy: "admin", + creationTime: new Date(), + }, +]; diff --git a/ui/src/mocks/mockTags.ts b/ui/src/mocks/mockTags.ts new file mode 100644 index 00000000..df6d39eb --- /dev/null +++ b/ui/src/mocks/mockTags.ts @@ -0,0 +1,36 @@ +import { TagItem } from "@/types/coordinator"; + +export const generateMockTags = (): TagItem[] => [ + { + id: "tag-1", + name: "Grammar", + description: "Grammar related questions", + color: "#3B82F6", + usageCount: 25, + creationTime: new Date(), + }, + { + id: "tag-2", + name: "Vocabulary", + description: "Vocabulary building exercises", + color: "#10B981", + usageCount: 18, + creationTime: new Date(), + }, + { + id: "tag-3", + name: "Reading", + description: "Reading comprehension tasks", + color: "#F59E0B", + usageCount: 12, + creationTime: new Date(), + }, + { + id: "tag-4", + name: "Writing", + description: "Writing skill assessments", + color: "#EF4444", + usageCount: 8, + creationTime: new Date(), + }, +]; diff --git a/ui/src/mocks/mockTests.ts b/ui/src/mocks/mockTests.ts new file mode 100644 index 00000000..6b52abfe --- /dev/null +++ b/ui/src/mocks/mockTests.ts @@ -0,0 +1,48 @@ +import { Exam } from "@/types/coordinator"; + +export const generateMockPDFTest = (): Exam => ({ + id: "test-1", + title: "Reading Assessment", + description: "Document-based reading evaluation", + type: "test", + testDocument: { + url: "/sample-documents/reading-test.pdf", + type: "pdf", + name: "reading-assessment.pdf", + }, + answerKeyTemplate: [ + { + id: "ak1", + questionNumber: 1, + type: "multiple-choice", + options: ["A", "B", "C", "D"], + points: 10, + correctAnswer: "A", + }, + { + id: "ak2", + questionNumber: 2, + type: "true-false", + points: 5, + correctAnswer: "true", + }, + { + id: "ak3", + questionNumber: 3, + type: "fill-blank", + points: 15, + correctAnswer: "answer", + }, + ], + questions: [], + timeLimit: 45, + totalPoints: 30, + passingScore: 70, + allowReview: true, + randomizeQuestions: false, + showResults: true, + maxAttempts: 1, + isActive: true, + creationTime: new Date(), + lastModificationTime: new Date(), +}); diff --git a/ui/src/types/coordinator.ts b/ui/src/types/coordinator.ts new file mode 100644 index 00000000..cf8401c8 --- /dev/null +++ b/ui/src/types/coordinator.ts @@ -0,0 +1,130 @@ +export type QuestionType = + | "multiple-choice" + | "fill-blank" + | "multiple-answer" + | "matching" + | "ordering" + | "open-ended" + | "true-false" + | "calculation"; + +export type ExamType = "exam" | "assignment" | "test"; +export type TestType = 'pdf' | 'image'; +export type MediaType = 'image' | 'video'; +export type QuestionDifficulty = 'easy' | 'medium' | 'hard'; +export type ExamSessionStatus = 'in-progress' | 'completed' | 'submitted'; + +export interface QuestionPool { + id: string; + name: string; + description: string; + questions: Question[]; + tags: string[]; + createdBy: string; + creationTime: Date; +} + +export interface Question { + id: string; + type: QuestionType; + title: string; + content: string; + mediaUrl?: string; + mediaType?: MediaType; + options?: QuestionOption[]; + correctAnswer?: string | string[]; + points: number; + timeLimit?: number; + explanation?: string; + tags: string[]; + difficulty: QuestionDifficulty; + creationTime: Date; + lastModificationTime: Date; +} + +export interface QuestionOption { + id: string; + text: string; + isCorrect: boolean; + order?: number; +} + +export interface Exam { + id: string; + title: string; + description: string; + type: ExamType; + testDocument?: { + url: string; + type: TestType; + name: string; + }; + answerKeyTemplate?: AnswerKeyItem[]; + questions: Question[]; + timeLimit: number; + totalPoints: number; + passingScore: number; + allowReview: boolean; + randomizeQuestions: boolean; + showResults: boolean; + maxAttempts: number; + startTime?: Date; + endTime?: Date; + isActive: boolean; + creationTime: Date; + lastModificationTime: Date; +} + +export interface AnswerKeyItem { + id: string; + questionNumber: number; + type: QuestionType; + options?: string[]; + points: number; + correctAnswer?: string | string[]; +} + +export interface StudentAnswer { + questionId: string; + answer: string | string[]; + timeSpent: number; + isCorrect?: boolean; + points?: number; +} + +export interface ExamSession { + id: string; + examId: string; + studentId: string; + startTime: Date; + endTime?: Date; + answers: StudentAnswer[]; + totalScore?: number; + status: ExamSessionStatus; + timeRemaining: number; +} + +export interface TagItem { + id: string; + name: string; + description: string; + color: string; + usageCount: number; + creationTime: Date; +} + +export interface NavigationItem { + id: string; + title: string; + icon: string; + path: string; + description?: string; + badge?: number; +} + +export interface AdminRoute { + path: string; + component: string; + title: string; + breadcrumb: string[]; +} \ No newline at end of file diff --git a/ui/src/utils/hooks/useCoordinator.ts b/ui/src/utils/hooks/useCoordinator.ts new file mode 100644 index 00000000..1a73a519 --- /dev/null +++ b/ui/src/utils/hooks/useCoordinator.ts @@ -0,0 +1,131 @@ +import { QuestionPool, Exam, TagItem, ExamSession } from "@/types/coordinator"; +import { useState } from "react"; + +export function useCoordinator() { + const [currentPath, setCurrentPath] = useState("/admin/dashboard"); + const [pools, setPools] = useState([]); + const [exams, setExams] = useState([]); + const [tags, setTags] = useState([]); + const [currentExam, setCurrentExam] = useState(null); + + const handleUpdateTest = (updatedTest: Exam) => { + setExams((prev) => + prev.map((exam) => (exam.id === updatedTest.id ? updatedTest : exam)) + ); + }; + + const handleDeleteTest = (testId: string) => { + setExams((prev) => prev.filter((exam) => exam.id !== testId)); + }; + + const handleCreatePool = ( + poolData: Omit + ) => { + const newPool: QuestionPool = { + ...poolData, + id: `pool-${Date.now()}`, + creationTime: new Date(), + }; + setPools((prev) => [...prev, newPool]); + }; + + const handleUpdatePool = (updatedPool: QuestionPool) => { + setPools((prev) => + prev.map((pool) => (pool.id === updatedPool.id ? updatedPool : pool)) + ); + }; + + const handleDeletePool = (poolId: string) => { + setPools((prev) => prev.filter((pool) => pool.id !== poolId)); + }; + + const handleCreateExam = ( + examData: Omit + ) => { + const newExam: Exam = { + ...examData, + id: `exam-${Date.now()}`, + creationTime: new Date(), + lastModificationTime: new Date(), + }; + setExams((prev) => [...prev, newExam]); + setCurrentPath("/admin/exams"); + }; + + const handleSaveExam = (exam: Exam) => { + setExams((prev) => prev.map((e) => (e.id === exam.id ? exam : e))); + setCurrentPath("/admin/exams"); + }; + + const handleCreateTest = ( + testData: Omit + ) => { + const newTest: Exam = { + ...testData, + id: `test-${Date.now()}`, + creationTime: new Date(), + lastModificationTime: new Date(), + }; + setExams((prev) => [...prev, newTest]); + setCurrentPath("/admin/tests"); + }; + + const handleCreateTag = ( + tagData: Omit + ) => { + const newTag: TagItem = { + ...tagData, + id: `tag-${Date.now()}`, + usageCount: 0, + creationTime: new Date(), + }; + setTags((prev) => [...prev, newTag]); + }; + + const handleUpdateTag = (updatedTag: TagItem) => { + setTags((prev) => + prev.map((tag) => (tag.id === updatedTag.id ? updatedTag : tag)) + ); + }; + + const handleDeleteTag = (tagId: string) => { + setTags((prev) => prev.filter((tag) => tag.id !== tagId)); + }; + + const handleExamComplete = (session: ExamSession) => { + console.log("Exam completed:", session); + alert("Assessment completed successfully!"); + setCurrentPath("/admin/dashboard"); + setCurrentExam(null); + }; + + const startExam = (exam: Exam) => { + setCurrentExam(exam); + }; + + return { + currentPath, + setCurrentPath, + pools, + setPools, + exams, + setExams, + tags, + setTags, + currentExam, + setCurrentExam, + handleUpdateTest, + handleDeleteTest, + handleCreatePool, + handleUpdatePool, + handleDeletePool, + handleCreateExam, + handleSaveExam, + handleCreateTest, + handleCreateTag, + handleUpdateTag, + handleDeleteTag, + handleExamComplete, + startExam, + }; +} diff --git a/ui/src/utils/hooks/useExamSecurity.ts b/ui/src/utils/hooks/useExamSecurity.ts new file mode 100644 index 00000000..01e63155 --- /dev/null +++ b/ui/src/utils/hooks/useExamSecurity.ts @@ -0,0 +1,95 @@ +import { useEffect, useCallback } from 'react'; + +interface SecurityConfig { + disableRightClick: boolean; + disableCopyPaste: boolean; + disableDevTools: boolean; + fullScreenMode: boolean; + preventTabSwitch: boolean; +} + +export const useExamSecurity = (config: SecurityConfig, isActive: boolean = true) => { + const handleRightClick = useCallback((e: MouseEvent) => { + if (config.disableRightClick && isActive) { + e.preventDefault(); + return false; + } + }, [config.disableRightClick, isActive]); + + const handleKeyDown = useCallback((e: KeyboardEvent) => { + if (!isActive) return; + + // Disable copy/paste shortcuts + if (config.disableCopyPaste) { + if (e.ctrlKey && (e.key === 'c' || e.key === 'v' || e.key === 'x' || e.key === 'a')) { + e.preventDefault(); + return false; + } + } + + // Disable developer tools + if (config.disableDevTools) { + if (e.key === 'F12' || (e.ctrlKey && e.shiftKey && e.key === 'I')) { + e.preventDefault(); + return false; + } + } + + // Disable Alt+Tab for tab switching + if (config.preventTabSwitch && e.altKey && e.key === 'Tab') { + e.preventDefault(); + return false; + } + }, [config, isActive]); + + const handleVisibilityChange = useCallback(() => { + if (config.preventTabSwitch && isActive && document.hidden) { + // Log tab switch attempt - in real app, this would call an API + console.warn('Tab switch detected during exam'); + } + }, [config.preventTabSwitch, isActive]); + + const enterFullScreen = useCallback(async () => { + if (config.fullScreenMode && isActive) { + try { + await document.documentElement.requestFullscreen(); + } catch (error) { + console.error('Failed to enter fullscreen:', error); + } + } + }, [config.fullScreenMode, isActive]); + + const exitFullScreen = useCallback(async () => { + if (document.fullscreenElement) { + try { + await document.exitFullscreen(); + } catch (error) { + console.error('Failed to exit fullscreen:', error); + } + } + }, []); + + useEffect(() => { + if (isActive) { + document.addEventListener('contextmenu', handleRightClick); + document.addEventListener('keydown', handleKeyDown); + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + document.removeEventListener('contextmenu', handleRightClick); + document.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('visibilitychange', handleVisibilityChange); + + if (config.fullScreenMode) { + exitFullScreen(); + } + }; + } + }, [handleRightClick, handleKeyDown, handleVisibilityChange, exitFullScreen, isActive, config.fullScreenMode]); + + return { + enterFullScreen, + exitFullScreen, + isFullScreen: !!document.fullscreenElement + }; +}; \ No newline at end of file diff --git a/ui/src/utils/hooks/useExamTimer.ts b/ui/src/utils/hooks/useExamTimer.ts new file mode 100644 index 00000000..dd71e062 --- /dev/null +++ b/ui/src/utils/hooks/useExamTimer.ts @@ -0,0 +1,94 @@ +import { useState, useEffect, useCallback } from 'react'; + +interface UseExamTimerProps { + initialTime: number; // in seconds + onTimeUp?: () => void; + onTick?: (timeRemaining: number) => void; + autoStart?: boolean; +} + +export const useExamTimer = ({ + initialTime, + onTimeUp, + onTick, + autoStart = false +}: UseExamTimerProps) => { + const [timeRemaining, setTimeRemaining] = useState(initialTime); + const [isRunning, setIsRunning] = useState(autoStart); + const [isPaused, setIsPaused] = useState(false); + + const start = useCallback(() => { + setIsRunning(true); + setIsPaused(false); + }, []); + + const pause = useCallback(() => { + setIsPaused(true); + }, []); + + const resume = useCallback(() => { + setIsPaused(false); + }, []); + + const reset = useCallback(() => { + setTimeRemaining(initialTime); + setIsRunning(false); + setIsPaused(false); + }, [initialTime]); + + const stop = useCallback(() => { + setIsRunning(false); + setIsPaused(false); + }, []); + + useEffect(() => { + let interval: NodeJS.Timeout; + + if (isRunning && !isPaused && timeRemaining > 0) { + interval = setInterval(() => { + setTimeRemaining((prev) => { + const newTime = prev - 1; + onTick?.(newTime); + + if (newTime <= 0) { + setIsRunning(false); + onTimeUp?.(); + return 0; + } + + return newTime; + }); + }, 1000); + } + + return () => { + if (interval) { + clearInterval(interval); + } + }; + }, [isRunning, isPaused, timeRemaining, onTimeUp, onTick]); + + const formatTime = useCallback((seconds: number) => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + if (hours > 0) { + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } + return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + }, []); + + return { + timeRemaining, + formattedTime: formatTime(timeRemaining), + isRunning, + isPaused, + start, + pause, + resume, + reset, + stop, + progress: ((initialTime - timeRemaining) / initialTime) * 100 + }; +}; \ No newline at end of file diff --git a/ui/src/views/classroom/ClassList.tsx b/ui/src/views/classroom/ClassList.tsx index a12c82b6..9b3894bd 100644 --- a/ui/src/views/classroom/ClassList.tsx +++ b/ui/src/views/classroom/ClassList.tsx @@ -263,7 +263,7 @@ const ClassList: React.FC = () => { > {/* Stats Cards */} -
+
{ {/* Sağ kısım: buton */} {showButtons && (
- {user.role === 'teacher' && classSession.teacherId === user.id && ( + {/* {user.role === 'teacher' && classSession.teacherId === user.id && ( */} + {user.role === 'teacher' && ( <> + )} + +
+
+ +
+ {/* Basic Information */} +
+

+ + Temel Bilgiler +

+ +
+
+ + 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" + /> +
+ +
+ +