Question Option App Service
This commit is contained in:
parent
fa13d879ae
commit
d1a254495e
12 changed files with 102 additions and 592 deletions
|
|
@ -8,5 +8,6 @@ public class QuestionOptionDto : FullAuditedEntityDto<Guid>
|
||||||
public string Text { get; set; }
|
public string Text { get; set; }
|
||||||
public bool IsCorrect { get; set; }
|
public bool IsCorrect { get; set; }
|
||||||
|
|
||||||
|
public Guid QuestionPoolId { get; set; }
|
||||||
public Guid QuestionId { get; set; }
|
public Guid QuestionId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
@ -379,7 +379,6 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
|
||||||
var lookup = JsonSerializer.Deserialize<LookupDto>(field.LookupJson);
|
var lookup = JsonSerializer.Deserialize<LookupDto>(field.LookupJson);
|
||||||
if (!string.IsNullOrWhiteSpace(lookup?.LookupQuery))
|
if (!string.IsNullOrWhiteSpace(lookup?.LookupQuery))
|
||||||
{
|
{
|
||||||
var lookupQuery = defaultValueHelper.GetDefaultValue(lookup.LookupQuery);
|
|
||||||
var parameters = new Dictionary<string, object>();
|
var parameters = new Dictionary<string, object>();
|
||||||
if (input.Filters != null)
|
if (input.Filters != null)
|
||||||
{
|
{
|
||||||
|
|
@ -388,6 +387,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
|
||||||
parameters.Add("@param" + i, input.Filters[i]);
|
parameters.Add("@param" + i, input.Filters[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var lookupQuery = defaultValueHelper.GetDefaultValue(lookup.LookupQuery);
|
||||||
return await dynamicDataRepository.QueryAsync(lookupQuery, connectionString, parameters);
|
return await dynamicDataRepository.QueryAsync(lookupQuery, connectionString, parameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,57 +73,30 @@ public class QuestionAppService : CrudAppService<
|
||||||
entity.CorrectAnswer = input.CorrectAnswer;
|
entity.CorrectAnswer = input.CorrectAnswer;
|
||||||
entity.QuestionType = input.QuestionType;
|
entity.QuestionType = input.QuestionType;
|
||||||
|
|
||||||
// 🟨 Şık tiplerine göre davranış belirle
|
// 🔹 Çoktan seçmeli türlerde mevcut mantık devam eder
|
||||||
var multiOptionTypes = new[] { "multiple-choice", "multiple-answer", "true-false" };
|
var existingOptions = entity.Options.ToList();
|
||||||
|
var incomingOptions = input.Options ?? [];
|
||||||
|
|
||||||
// 🔸 Eğer çoklu şık tipi değilse, sadece 1 option olacak
|
// Silinecekleri bul
|
||||||
if (!multiOptionTypes.Contains(input.QuestionType))
|
var toDelete = existingOptions.Where(e => !incomingOptions.Any(i => i.Id == e.Id)).ToList();
|
||||||
|
foreach (var del in toDelete)
|
||||||
|
entity.Options.Remove(del);
|
||||||
|
|
||||||
|
// Güncelle / ekle
|
||||||
|
foreach (var optDto in incomingOptions)
|
||||||
{
|
{
|
||||||
entity.Options.Clear();
|
var existing = existingOptions.FirstOrDefault(o => o.Id == optDto.Id);
|
||||||
|
if (existing != null)
|
||||||
// Sadece CorrectAnswer bilgisinden tek option oluştur
|
|
||||||
if (!string.IsNullOrWhiteSpace(input.CorrectAnswer))
|
|
||||||
{
|
{
|
||||||
var option = new QuestionOption(
|
existing.Text = optDto.Text;
|
||||||
Guid.NewGuid(),
|
existing.IsCorrect = optDto.IsCorrect;
|
||||||
entity.Id,
|
|
||||||
input.CorrectAnswer,
|
|
||||||
true // tek seçenek, doğru kabul edilir
|
|
||||||
);
|
|
||||||
|
|
||||||
entity.Options.Add(option);
|
|
||||||
|
|
||||||
entity.CorrectAnswer = option.Id.ToString(); // CorrectAnswer alanına option Id'si yazılır
|
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
else
|
|
||||||
{
|
|
||||||
// 🔹 Çoktan seçmeli türlerde mevcut mantık devam eder
|
|
||||||
var existingOptions = entity.Options.ToList();
|
|
||||||
var incomingOptions = input.Options ?? new List<QuestionOptionDto>();
|
|
||||||
|
|
||||||
// Silinecekleri bul
|
|
||||||
var toDelete = existingOptions.Where(e => !incomingOptions.Any(i => i.Id == e.Id)).ToList();
|
|
||||||
foreach (var del in toDelete)
|
|
||||||
entity.Options.Remove(del);
|
|
||||||
|
|
||||||
// Güncelle / ekle
|
|
||||||
foreach (var optDto in incomingOptions)
|
|
||||||
{
|
{
|
||||||
var existing = existingOptions.FirstOrDefault(o => o.Id == optDto.Id);
|
entity.Options.Add(new QuestionOption(optDto.Id, optDto.QuestionPoolId, entity.Id, optDto.Text, optDto.IsCorrect));
|
||||||
if (existing != null)
|
|
||||||
{
|
|
||||||
existing.Text = optDto.Text;
|
|
||||||
existing.IsCorrect = optDto.IsCorrect;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
entity.Options.Add(new QuestionOption(optDto.Id, entity.Id, optDto.Text, optDto.IsCorrect));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🟢 Kaydet
|
|
||||||
await _questionRepository.UpdateAsync(entity, autoSave: true);
|
await _questionRepository.UpdateAsync(entity, autoSave: true);
|
||||||
|
|
||||||
return ObjectMapper.Map<Question, QuestionDto>(entity);
|
return ObjectMapper.Map<Question, QuestionDto>(entity);
|
||||||
|
|
|
||||||
|
|
@ -30900,11 +30900,11 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
new EditingFormItemDto { Order = 2, DataField = "QuestionType", ColSpan = 2, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox },
|
new EditingFormItemDto { Order = 2, DataField = "QuestionType", ColSpan = 2, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox },
|
||||||
new EditingFormItemDto { Order = 3, DataField = "Points", ColSpan = 2, IsRequired = true, EditorType2 = EditorTypes.dxNumberBox },
|
new EditingFormItemDto { Order = 3, DataField = "Points", ColSpan = 2, IsRequired = true, EditorType2 = EditorTypes.dxNumberBox },
|
||||||
new EditingFormItemDto { Order = 4, DataField = "Title", ColSpan = 2, IsRequired = true, EditorType2 = EditorTypes.dxTextBox },
|
new EditingFormItemDto { Order = 4, DataField = "Title", ColSpan = 2, IsRequired = true, EditorType2 = EditorTypes.dxTextBox },
|
||||||
new EditingFormItemDto { Order = 5, DataField = "Content", ColSpan = 2, EditorType2 = EditorTypes.dxTextArea },
|
new EditingFormItemDto { Order = 5, DataField = "Content", ColSpan = 2, IsRequired = true, EditorType2 = EditorTypes.dxTextArea },
|
||||||
new EditingFormItemDto { Order = 6, DataField = "MediaType", ColSpan = 2, EditorType2 = EditorTypes.dxSelectBox },
|
new EditingFormItemDto { Order = 6, DataField = "Difficulty", ColSpan = 2, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox },
|
||||||
new EditingFormItemDto { Order = 7, DataField = "MediaUrl", ColSpan = 2, EditorType2 = EditorTypes.dxTextBox },
|
new EditingFormItemDto { Order = 7, DataField = "MediaType", ColSpan = 2, EditorType2 = EditorTypes.dxSelectBox },
|
||||||
new EditingFormItemDto { Order = 8, DataField = "CorrectAnswer", ColSpan = 2, EditorType2 = EditorTypes.dxTextBox },
|
new EditingFormItemDto { Order = 8, DataField = "MediaUrl", ColSpan = 2, EditorType2 = EditorTypes.dxTextBox },
|
||||||
new EditingFormItemDto { Order = 9, DataField = "Difficulty", ColSpan = 2, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox },
|
new EditingFormItemDto { Order = 9, DataField = "CorrectAnswer", ColSpan = 2, EditorType2 = EditorTypes.dxTextBox, EditorOptions="{\"disabled\": true}" },
|
||||||
new EditingFormItemDto { Order = 10, DataField = "TimeLimit", ColSpan = 2, EditorType2 = EditorTypes.dxNumberBox },
|
new EditingFormItemDto { Order = 10, DataField = "TimeLimit", ColSpan = 2, EditorType2 = EditorTypes.dxNumberBox },
|
||||||
new EditingFormItemDto { Order = 11, DataField = "Explanation", ColSpan = 2, EditorType2 = EditorTypes.dxTextArea },
|
new EditingFormItemDto { Order = 11, DataField = "Explanation", ColSpan = 2, EditorType2 = EditorTypes.dxTextArea },
|
||||||
]
|
]
|
||||||
|
|
@ -31083,7 +31083,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
{
|
{
|
||||||
IsPivot = true
|
IsPivot = true
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
new() {
|
new() {
|
||||||
ListFormCode = listFormQuestion.ListFormCode,
|
ListFormCode = listFormQuestion.ListFormCode,
|
||||||
RoleId = null,
|
RoleId = null,
|
||||||
|
|
@ -31194,7 +31194,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "MediaUrl",
|
FieldName = "MediaUrl",
|
||||||
Width = 300,
|
Width = 170,
|
||||||
ListOrderNo = 8,
|
ListOrderNo = 8,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
|
|
@ -31225,7 +31225,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.String,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "CorrectAnswer",
|
FieldName = "CorrectAnswer",
|
||||||
Width = 100,
|
Width = 150,
|
||||||
ListOrderNo = 9,
|
ListOrderNo = 9,
|
||||||
Visible = true,
|
Visible = true,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
|
|
@ -31327,7 +31327,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
|
||||||
RoleId = null,
|
RoleId = null,
|
||||||
UserId = null,
|
UserId = null,
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
SourceDbType = DbType.Int32,
|
SourceDbType = DbType.String,
|
||||||
FieldName = "Explanation",
|
FieldName = "Explanation",
|
||||||
Width = 100,
|
Width = 100,
|
||||||
ListOrderNo = 12,
|
ListOrderNo = 12,
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,18 @@ public class QuestionOption : FullAuditedEntity<Guid>, IMultiTenant
|
||||||
public string Text { get; set; }
|
public string Text { get; set; }
|
||||||
public bool IsCorrect { get; set; }
|
public bool IsCorrect { get; set; }
|
||||||
|
|
||||||
// Foreign key
|
public Guid QuestionPoolId { get; set; }
|
||||||
public Guid QuestionId { get; set; }
|
public Guid QuestionId { get; set; }
|
||||||
public Question Question { get; set; }
|
public Question Question { get; set; }
|
||||||
|
|
||||||
Guid? IMultiTenant.TenantId => TenantId;
|
Guid? IMultiTenant.TenantId => TenantId;
|
||||||
|
|
||||||
// 🟢 EF Core ve ABP için parametresiz constructor ZORUNLU
|
|
||||||
protected QuestionOption() { }
|
protected QuestionOption() { }
|
||||||
|
|
||||||
// 🟢 Yeni kayıt oluşturmak için custom constructor
|
public QuestionOption(Guid id, Guid questionPoolId, Guid questionId, string text, bool isCorrect)
|
||||||
public QuestionOption(Guid id, Guid questionId, string text, bool isCorrect)
|
|
||||||
{
|
{
|
||||||
Id = id; // burada atayabilirsin çünkü ctor içinde protected set erişilebilir
|
Id = id;
|
||||||
|
QuestionPoolId = questionPoolId;
|
||||||
QuestionId = questionId;
|
QuestionId = questionId;
|
||||||
Text = text;
|
Text = text;
|
||||||
IsCorrect = isCorrect;
|
IsCorrect = isCorrect;
|
||||||
|
|
|
||||||
|
|
@ -1598,8 +1598,8 @@ public class PlatformDbContext :
|
||||||
b.Property(x => x.Title).IsRequired().HasMaxLength(500);
|
b.Property(x => x.Title).IsRequired().HasMaxLength(500);
|
||||||
b.Property(x => x.Content).HasMaxLength(500);
|
b.Property(x => x.Content).HasMaxLength(500);
|
||||||
b.Property(x => x.MediaUrl).HasMaxLength(500);
|
b.Property(x => x.MediaUrl).HasMaxLength(500);
|
||||||
b.Property(x => x.MediaType).HasMaxLength(500);
|
b.Property(x => x.MediaType).HasMaxLength(10);
|
||||||
b.Property(x => x.CorrectAnswer).HasMaxLength(50);
|
b.Property(x => x.CorrectAnswer).HasMaxLength(500);
|
||||||
b.Property(x => x.Difficulty).HasMaxLength(10);
|
b.Property(x => x.Difficulty).HasMaxLength(10);
|
||||||
b.Property(x => x.TimeLimit).HasDefaultValue(0);
|
b.Property(x => x.TimeLimit).HasDefaultValue(0);
|
||||||
b.Property(x => x.Explanation).HasMaxLength(500);
|
b.Property(x => x.Explanation).HasMaxLength(500);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
||||||
namespace Kurs.Platform.Migrations
|
namespace Kurs.Platform.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(PlatformDbContext))]
|
[DbContext(typeof(PlatformDbContext))]
|
||||||
[Migration("20251016120353_Initial")]
|
[Migration("20251016203825_Initial")]
|
||||||
partial class Initial
|
partial class Initial
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -5113,8 +5113,8 @@ namespace Kurs.Platform.Migrations
|
||||||
.HasColumnType("nvarchar(500)");
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
b.Property<string>("CorrectAnswer")
|
b.Property<string>("CorrectAnswer")
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(500)
|
||||||
.HasColumnType("nvarchar(50)");
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
b.Property<DateTime>("CreationTime")
|
b.Property<DateTime>("CreationTime")
|
||||||
.HasColumnType("datetime2")
|
.HasColumnType("datetime2")
|
||||||
|
|
@ -5155,8 +5155,8 @@ namespace Kurs.Platform.Migrations
|
||||||
.HasColumnName("LastModifierId");
|
.HasColumnName("LastModifierId");
|
||||||
|
|
||||||
b.Property<string>("MediaType")
|
b.Property<string>("MediaType")
|
||||||
.HasMaxLength(500)
|
.HasMaxLength(10)
|
||||||
.HasColumnType("nvarchar(500)");
|
.HasColumnType("nvarchar(10)");
|
||||||
|
|
||||||
b.Property<string>("MediaUrl")
|
b.Property<string>("MediaUrl")
|
||||||
.HasMaxLength(500)
|
.HasMaxLength(500)
|
||||||
|
|
@ -5239,6 +5239,9 @@ namespace Kurs.Platform.Migrations
|
||||||
b.Property<Guid>("QuestionId")
|
b.Property<Guid>("QuestionId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("QuestionPoolId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
b.Property<Guid?>("TenantId")
|
b.Property<Guid?>("TenantId")
|
||||||
.HasColumnType("uniqueidentifier")
|
.HasColumnType("uniqueidentifier")
|
||||||
.HasColumnName("TenantId");
|
.HasColumnName("TenantId");
|
||||||
|
|
@ -2461,8 +2461,8 @@ namespace Kurs.Platform.Migrations
|
||||||
Title = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
Title = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||||
Content = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
Content = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||||
MediaUrl = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
MediaUrl = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||||
MediaType = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
MediaType = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: true),
|
||||||
CorrectAnswer = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
CorrectAnswer = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||||
Difficulty = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: true),
|
Difficulty = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: true),
|
||||||
TimeLimit = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
TimeLimit = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||||
Explanation = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
Explanation = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||||
|
|
@ -3280,6 +3280,7 @@ namespace Kurs.Platform.Migrations
|
||||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
Text = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
Text = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||||
IsCorrect = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
IsCorrect = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||||
|
QuestionPoolId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
QuestionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
QuestionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
|
@ -5110,8 +5110,8 @@ namespace Kurs.Platform.Migrations
|
||||||
.HasColumnType("nvarchar(500)");
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
b.Property<string>("CorrectAnswer")
|
b.Property<string>("CorrectAnswer")
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(500)
|
||||||
.HasColumnType("nvarchar(50)");
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
b.Property<DateTime>("CreationTime")
|
b.Property<DateTime>("CreationTime")
|
||||||
.HasColumnType("datetime2")
|
.HasColumnType("datetime2")
|
||||||
|
|
@ -5152,8 +5152,8 @@ namespace Kurs.Platform.Migrations
|
||||||
.HasColumnName("LastModifierId");
|
.HasColumnName("LastModifierId");
|
||||||
|
|
||||||
b.Property<string>("MediaType")
|
b.Property<string>("MediaType")
|
||||||
.HasMaxLength(500)
|
.HasMaxLength(10)
|
||||||
.HasColumnType("nvarchar(500)");
|
.HasColumnType("nvarchar(10)");
|
||||||
|
|
||||||
b.Property<string>("MediaUrl")
|
b.Property<string>("MediaUrl")
|
||||||
.HasMaxLength(500)
|
.HasMaxLength(500)
|
||||||
|
|
@ -5236,6 +5236,9 @@ namespace Kurs.Platform.Migrations
|
||||||
b.Property<Guid>("QuestionId")
|
b.Property<Guid>("QuestionId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("QuestionPoolId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
b.Property<Guid?>("TenantId")
|
b.Property<Guid?>("TenantId")
|
||||||
.HasColumnType("uniqueidentifier")
|
.HasColumnType("uniqueidentifier")
|
||||||
.HasColumnName("TenantId");
|
.HasColumnName("TenantId");
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ export interface QuestionDto extends FullAuditedEntityDto {
|
||||||
export interface QuestionOptionDto extends FullAuditedEntityDto {
|
export interface QuestionOptionDto extends FullAuditedEntityDto {
|
||||||
text: string;
|
text: string;
|
||||||
isCorrect: boolean;
|
isCorrect: boolean;
|
||||||
order?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Exam {
|
export interface Exam {
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ function QuestionDialog({
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
text: '',
|
text: '',
|
||||||
isCorrect: false,
|
isCorrect: false,
|
||||||
order: question.options?.length || 0,
|
|
||||||
}
|
}
|
||||||
setQuestion((prev) =>
|
setQuestion((prev) =>
|
||||||
prev ? { ...prev, options: [...(prev.options || []), newOption] } : prev,
|
prev ? { ...prev, options: [...(prev.options || []), newOption] } : prev,
|
||||||
|
|
@ -79,8 +78,8 @@ function QuestionDialog({
|
||||||
|
|
||||||
// 💾 Kaydetme
|
// 💾 Kaydetme
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!question.title.trim() || !question.content.trim()) {
|
if (!question.title.trim()) {
|
||||||
alert('Please fill in the title and content fields.')
|
alert('Please fill in the title field.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,23 +121,20 @@ function QuestionDialog({
|
||||||
|
|
||||||
const getCorrectAnswer = (q: QuestionDto): string | string[] => {
|
const getCorrectAnswer = (q: QuestionDto): string | string[] => {
|
||||||
switch (q.questionType) {
|
switch (q.questionType) {
|
||||||
case 'multiple-choice':
|
|
||||||
return q.options?.find((opt) => opt.isCorrect)?.id || ''
|
|
||||||
case 'multiple-answer':
|
case 'multiple-answer':
|
||||||
return q.options?.filter((opt) => opt.isCorrect).map((opt) => opt.id!) || []
|
const result = q.options?.filter((opt) => opt.isCorrect).map((opt) => opt.text!) || []
|
||||||
|
return result.join(', ')
|
||||||
|
case 'multiple-choice':
|
||||||
case 'true-false':
|
case 'true-false':
|
||||||
case 'fill-blank':
|
case 'fill-blank':
|
||||||
case 'open-ended':
|
case 'open-ended':
|
||||||
case 'calculation':
|
case 'calculation':
|
||||||
return q.correctAnswer as string
|
return q.options?.find((opt) => opt.isCorrect)?.text || ''
|
||||||
case 'matching':
|
case 'matching':
|
||||||
return q.options?.map((opt) => opt.id!) || []
|
|
||||||
case 'ordering':
|
case 'ordering':
|
||||||
return (
|
return q.options?.map((opt) => opt.text!) || []
|
||||||
q.options?.sort((a, b) => (a.order || 0) - (b.order || 0)).map((opt) => opt.id!) || []
|
|
||||||
)
|
|
||||||
default:
|
default:
|
||||||
return q.correctAnswer as string
|
return q.options?.find((opt) => opt.isCorrect)?.text || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,51 +264,61 @@ function QuestionDialog({
|
||||||
)
|
)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 🔹 Diğer tüm tipler: tek radio + text gösterilir
|
|
||||||
// Eğer hiç option yoksa bir tane oluştur
|
|
||||||
if (!question.options || question.options.length === 0) {
|
|
||||||
setQuestion((prev) =>
|
|
||||||
prev
|
|
||||||
? {
|
|
||||||
...prev,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
text: prev.correctAnswer || 'Doğru cevap metni',
|
|
||||||
isCorrect: true,
|
|
||||||
order: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: prev,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<div className="flex items-center justify-between">
|
||||||
Doğru Cevap (Tek Seçenek)
|
{question?.options?.length! === 0 && (
|
||||||
</label>
|
<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) => (
|
{question.options?.map((option, index) => (
|
||||||
<div
|
<div
|
||||||
key={option.id}
|
key={option.id}
|
||||||
className="flex items-center space-x-3 p-2.5 border border-gray-200 rounded-lg"
|
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
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="single-correct"
|
name="correct-answer"
|
||||||
value={option.id}
|
checked={option.isCorrect}
|
||||||
checked={question.correctAnswer === option.id}
|
onChange={() =>
|
||||||
onChange={() => handleChange('correctAnswer', option.id)}
|
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"
|
className="h-4 w-4 text-blue-600 border-gray-300"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-800">{option.text}</span>
|
<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>
|
||||||
))}
|
))}
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
Bu soru tipinde yalnızca tek doğru cevap seçeneği bulunur.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,475 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import { FaPlus, FaSearch, FaFilter, FaEdit, FaTrash } from "react-icons/fa";
|
|
||||||
import { generateMockPools } from "@/mocks/mockPools";
|
|
||||||
import { QuestionPoolDto, QuestionDto } from "@/types/coordinator";
|
|
||||||
import QuestionDialog from "./QuestionDialog";
|
|
||||||
|
|
||||||
export const QuestionPoolManager: React.FC = () => {
|
|
||||||
const [pools, setPools] = useState<QuestionPoolDto[]>(generateMockPools());
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
|
||||||
const [selectedTag, setSelectedTag] = useState("");
|
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
|
||||||
const [editingPool, setEditingPool] = useState<QuestionPoolDto | null>(null);
|
|
||||||
const [editingQuestion, setEditingQuestion] = useState<QuestionDto | null>(null);
|
|
||||||
const [showQuestionEditor, setShowQuestionEditor] = useState(false);
|
|
||||||
const [selectedPoolForQuestion, setSelectedPoolForQuestion] =
|
|
||||||
useState<string>("");
|
|
||||||
|
|
||||||
const [newPool, setNewPool] = useState({
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
tags: [] as string[],
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredPools = pools.filter((pool) => {
|
|
||||||
const matchesSearch =
|
|
||||||
pool.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
pool.description.toLowerCase().includes(searchTerm.toLowerCase());
|
|
||||||
const matchesTag = !selectedTag || pool.tags.includes(selectedTag);
|
|
||||||
return matchesSearch && matchesTag;
|
|
||||||
});
|
|
||||||
|
|
||||||
const allTags = Array.from(new Set(pools.flatMap((pool) => pool.tags)));
|
|
||||||
|
|
||||||
const handleCreatePool = () => {
|
|
||||||
if (!newPool.name.trim()) return;
|
|
||||||
|
|
||||||
const newPoolData: QuestionPoolDto = {
|
|
||||||
id: `pool-${Date.now()}`,
|
|
||||||
name: newPool.name,
|
|
||||||
description: newPool.description,
|
|
||||||
tags: newPool.tags,
|
|
||||||
questions: [],
|
|
||||||
createdBy: "current-user",
|
|
||||||
creationTime: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
setPools((prev) => [...prev, newPoolData]);
|
|
||||||
setNewPool({ name: "", description: "", tags: [] });
|
|
||||||
setIsCreating(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdatePool = () => {
|
|
||||||
if (!editingPool) return;
|
|
||||||
setPools((prev) =>
|
|
||||||
prev.map((pool) => (pool.id === editingPool.id ? editingPool : pool))
|
|
||||||
);
|
|
||||||
setEditingPool(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateQuestion = (poolId: string) => {
|
|
||||||
setSelectedPoolForQuestion(poolId);
|
|
||||||
setEditingQuestion(null);
|
|
||||||
setShowQuestionEditor(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditQuestion = (question: QuestionDto, poolId: string) => {
|
|
||||||
setSelectedPoolForQuestion(poolId);
|
|
||||||
setEditingQuestion(question);
|
|
||||||
setShowQuestionEditor(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveQuestion = (
|
|
||||||
questionData: Omit<QuestionDto, "id" | "creationTime" | "lastModificationTime">
|
|
||||||
) => {
|
|
||||||
const pool = pools.find((p) => p.id === selectedPoolForQuestion);
|
|
||||||
if (!pool) return;
|
|
||||||
|
|
||||||
const newQuestion: QuestionDto = {
|
|
||||||
...questionData,
|
|
||||||
id: editingQuestion?.id || `q-${Date.now()}`,
|
|
||||||
creationTime: editingQuestion?.creationTime || new Date(),
|
|
||||||
lastModificationTime: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedQuestions = editingQuestion
|
|
||||||
? pool.questions.map((q) =>
|
|
||||||
q.id === editingQuestion.id ? newQuestion : q
|
|
||||||
)
|
|
||||||
: [...pool.questions, newQuestion];
|
|
||||||
|
|
||||||
const updatedPool = { ...pool, questions: updatedQuestions };
|
|
||||||
setPools((prev) =>
|
|
||||||
prev.map((p) => (p.id === updatedPool.id ? updatedPool : p))
|
|
||||||
);
|
|
||||||
setShowQuestionEditor(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteQuestion = (questionId: string, poolId: string) => {
|
|
||||||
const pool = pools.find((p) => p.id === poolId);
|
|
||||||
if (!pool) return;
|
|
||||||
|
|
||||||
if (window.confirm("Are you sure you want to delete this question?")) {
|
|
||||||
const updatedQuestions = pool.questions.filter(
|
|
||||||
(q) => q.id !== questionId
|
|
||||||
);
|
|
||||||
const updatedPool = { ...pool, questions: updatedQuestions };
|
|
||||||
setPools((prev) =>
|
|
||||||
prev.map((p) => (p.id === updatedPool.id ? updatedPool : p))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-bold text-gray-900">Soru Havuzları</h2>
|
|
||||||
<p className="text-sm text-gray-600 mt-0.5">
|
|
||||||
Soru havuzlarınızı 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 Havuz</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="Havuz ara..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="pl-8 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="relative">
|
|
||||||
<FaFilter className="absolute left-3 top-2 h-4 w-4 text-gray-400" />
|
|
||||||
<select
|
|
||||||
value={selectedTag}
|
|
||||||
onChange={(e) => setSelectedTag(e.target.value)}
|
|
||||||
className="pl-10 w-full text-sm 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 etiketler</option>
|
|
||||||
{allTags.map((tag) => (
|
|
||||||
<option key={tag} value={tag}>
|
|
||||||
{tag}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm text-gray-600 flex items-center">
|
|
||||||
Toplam: {filteredPools.length} havuz
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Create/Edit Pool Modal */}
|
|
||||||
{(isCreating || editingPool) && (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white rounded-lg p-5 w-full max-w-md">
|
|
||||||
<h3 className="text-base font-semibold mb-3">
|
|
||||||
{isCreating ? "Yeni Soru Havuzu" : "Havuzu Düzenle"}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Havuz Adı
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={isCreating ? newPool.name : editingPool?.name || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (isCreating) {
|
|
||||||
setNewPool((prev) => ({ ...prev, name: e.target.value }));
|
|
||||||
} else if (editingPool) {
|
|
||||||
setEditingPool({ ...editingPool, 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="Havuz adını girin"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Açıklama
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={
|
|
||||||
isCreating
|
|
||||||
? newPool.description
|
|
||||||
: editingPool?.description || ""
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (isCreating) {
|
|
||||||
setNewPool((prev) => ({
|
|
||||||
...prev,
|
|
||||||
description: e.target.value,
|
|
||||||
}));
|
|
||||||
} else if (editingPool) {
|
|
||||||
setEditingPool({
|
|
||||||
...editingPool,
|
|
||||||
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="Açıklama girin"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Etiketler (virgülle ayırın)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={
|
|
||||||
isCreating
|
|
||||||
? newPool.tags.join(", ")
|
|
||||||
: editingPool?.tags.join(", ") || ""
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
const tags = e.target.value
|
|
||||||
.split(",")
|
|
||||||
.map((tag) => tag.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
if (isCreating) {
|
|
||||||
setNewPool((prev) => ({ ...prev, tags }));
|
|
||||||
} else if (editingPool) {
|
|
||||||
setEditingPool({ ...editingPool, tags });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
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="matematik, geometri, cebir"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-2 mt-4">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (isCreating) {
|
|
||||||
setIsCreating(false);
|
|
||||||
setNewPool({ name: "", description: "", tags: [] });
|
|
||||||
} else {
|
|
||||||
setEditingPool(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-800 px-3 py-1.5 text-sm rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
İptal
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={isCreating ? handleCreatePool : handleUpdatePool}
|
|
||||||
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 text-sm rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
{isCreating ? "Oluştur" : "Güncelle"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Question Editor Modal */}
|
|
||||||
{showQuestionEditor && (
|
|
||||||
<QuestionDialog
|
|
||||||
question={editingQuestion || undefined}
|
|
||||||
onSave={handleSaveQuestion}
|
|
||||||
onCancel={() => setShowQuestionEditor(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pool List */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
{filteredPools.map((pool) => (
|
|
||||||
<div
|
|
||||||
key={pool.id}
|
|
||||||
className="bg-white border border-gray-200 rounded-lg"
|
|
||||||
>
|
|
||||||
{/* Pool Header */}
|
|
||||||
<div className="p-4 border-b border-gray-200">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900 mb-1">
|
|
||||||
{pool.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs text-gray-600 mb-1.5">
|
|
||||||
{pool.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center text-xs text-gray-500 mb-2">
|
|
||||||
<span>{pool.questions.length} questions</span>
|
|
||||||
<span className="mx-2">•</span>
|
|
||||||
<span>{pool.creationTime.toLocaleDateString("en-US")}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{pool.tags.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{pool.tags.map((tag) => (
|
|
||||||
<span
|
|
||||||
key={tag}
|
|
||||||
className="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 ml-2">
|
|
||||||
<button
|
|
||||||
onClick={() => handleCreateQuestion(pool.id)}
|
|
||||||
className="flex items-center space-x-1.5 bg-blue-600 hover:bg-blue-700 text-white px-2.5 py-1.5 text-xs rounded-lg font-medium transition-colors"
|
|
||||||
>
|
|
||||||
<FaPlus className="w-3 h-3" />
|
|
||||||
<span>Add Question</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setEditingPool(pool)}
|
|
||||||
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
title="Edit Pool"
|
|
||||||
>
|
|
||||||
<FaEdit className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
window.confirm(
|
|
||||||
"Are you sure you want to delete this pool?"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
setPools((prev) => prev.filter((p) => p.id !== pool.id));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
||||||
title="Delete Pool"
|
|
||||||
>
|
|
||||||
<FaTrash className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Questions List */}
|
|
||||||
<div className="p-4">
|
|
||||||
{pool.questions.length > 0 ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="text-sm font-medium text-gray-900 mb-2">
|
|
||||||
Questions ({pool.questions.length})
|
|
||||||
</h4>
|
|
||||||
{pool.questions.map((question) => (
|
|
||||||
<div
|
|
||||||
key={question.id}
|
|
||||||
className="flex items-center justify-between p-3 border border-gray-200 rounded-lg hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center space-x-1.5 mb-1">
|
|
||||||
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-1.5 py-0.5 rounded">
|
|
||||||
{getQuestionTypeLabel(question.type)}
|
|
||||||
</span>
|
|
||||||
<span className="bg-green-100 text-green-800 text-xs font-medium px-1.5 py-0.5 rounded">
|
|
||||||
{question.points} pts
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={`text-xs font-medium px-1.5 py-0.5 rounded ${
|
|
||||||
question.difficulty === "easy"
|
|
||||||
? "bg-green-100 text-green-800"
|
|
||||||
: question.difficulty === "medium"
|
|
||||||
? "bg-yellow-100 text-yellow-800"
|
|
||||||
: "bg-red-100 text-red-800"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{question.difficulty}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs font-medium text-gray-900 mb-0.5">
|
|
||||||
{question.title}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-600 truncate">
|
|
||||||
{question.content}
|
|
||||||
</p>
|
|
||||||
{question.tags.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-1 mt-1.5">
|
|
||||||
{question.tags.map((tag) => (
|
|
||||||
<span
|
|
||||||
key={tag}
|
|
||||||
className="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded"
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-1 ml-4">
|
|
||||||
<button
|
|
||||||
onClick={() => handleEditQuestion(question, pool.id)}
|
|
||||||
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
title="Edit Question"
|
|
||||||
>
|
|
||||||
<FaEdit className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
handleDeleteQuestion(question.id, pool.id)
|
|
||||||
}
|
|
||||||
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
||||||
title="Delete Question"
|
|
||||||
>
|
|
||||||
<FaTrash className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-6 text-gray-500">
|
|
||||||
<p className="text-sm">No questions in this pool yet</p>
|
|
||||||
<p className="text-xs">Click "Add Question" to get started</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{filteredPools.length === 0 && (
|
|
||||||
<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">
|
|
||||||
No pools found
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
{searchTerm || selectedTag
|
|
||||||
? "Try adjusting your search criteria or create a new pool."
|
|
||||||
: "Create your first question pool to get started."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getQuestionTypeLabel = (type: string): string => {
|
|
||||||
const labels: Record<string, string> = {
|
|
||||||
"multiple-choice": "Multiple Choice",
|
|
||||||
"fill-blank": "Fill Blank",
|
|
||||||
"true-false": "True/False",
|
|
||||||
"open-ended": "Open Ended",
|
|
||||||
"multiple-answer": "Multiple Answer",
|
|
||||||
matching: "Matching",
|
|
||||||
ordering: "Ordering",
|
|
||||||
calculation: "Calculation",
|
|
||||||
};
|
|
||||||
return labels[type] || type;
|
|
||||||
};
|
|
||||||
Loading…
Reference in a new issue